Compiling a sentence into a contract
Everything downstream of createGoalContract() depends on one
decision: which of nine domains the goal text belongs to. That decision is made by counting
substring hits from a hand-written keyword table, and it is the single most consequential — and
most fragile — eleven lines in the repository.
The compiler is 24 lines long
There is no model call, no embedding, no parsing. createGoalContract()
classifies the domain, looks up two fixed lists, attaches four fixed constraints and stamps a
threshold.
5export function createGoalContract(goal: string): GoalContract { 6 const domain = classifyDomain(goal); 7 const requiredProofs = defaultProofsForDomain(domain); 8 9 return { 10 id: `goal_${nanoid(10)}`, 11 goal, 12 domain, 13 deliverables: defaultDeliverablesForDomain(domain), 14 requiredProofs, 15 constraints: [ 16 "artifact must run from a clean checkout", 17 "verification must be reproducible from command line", 18 "do not count placeholder-only behavior as complete", 19 "store eval artifacts for later audit" 20 ], 21 doneWhen: { 22 minScore: domain === "unknown" ? 0.75 : 0.85, 23 requiredGatesPass: true, 24 noRegressions: true 25 }, 26 createdAt: new Date().toISOString() 27 }; 28}
The constraints array is identical for every goal in every domain. The
doneWhen.minScore takes one of two values. requiredGatesPass and
noRegressions are hard-coded true and, as sheet 04 shows,
requiredGatesPass is never read by the scorer.
Fig. 2.1 Goal compilation — elevation
domain.ts:38–48) and has no fallback beyond "unknown". Callout 2
is not hypothetical: the string "build" contains "ui", which is a
web_app keyword at domain.ts:10, so any goal beginning with the word
“build” carries a permanent web_app point.What the tie-break actually does
The interaction between callouts 1 and 2 is easy to demonstrate. Running the classifier over three goals gives:
| Goal text | Keyword hits | Result |
|---|---|---|
| build me Doom | browser_game: doomweb_app: ui (inside “build”) |
browser_game |
| build a CLI that renames files | web_app: ui (inside “build”)cli: cli |
web_app |
| make a command-line tool that renames files | cli: command-line |
cli |
Row two mis-classifies. Both domains score 1; web_app is declared at
domain.ts:9 and cli at domain.ts:17, so the stable sort
leaves web_app first. The consequence is not cosmetic: the goal then requires
user_interaction, visual_nontrivial and
responsive_layout instead of cli_golden_output, and gets a
Playwright evaluator instead of a golden-output evaluator. The smoke test does not cover this
case — it only asserts the browser_game path, where
doom plus ui happens to tie in the right order.
The word “Doom” is not incidental. RESEARCH.md opens by naming
build me Doom as the motivating request, and domain.ts:6 lists
"doom" as a literal browser_game keyword alongside
"three.js" and "phaser". The classifier is tuned to its own demo.
Fig. 2.2 Domain / required-proof matrix
domain.ts:50–81. The single red cell is the only combination in the whole default
matrix that produces an uncovered proof; it is traced on sheet 03. Note the absence of
tests_pass from the browser_game row — a game goal never asks for a
passing test suite, only for gameplay evidence.Milestones: a list, not a graph
README.md and docs/ARCHITECTURE.md both describe a “milestone DAG”.
The implementation, planMilestones(), returns Milestone[], and the
Milestone interface (types.ts:46–53) has no dependsOn,
parents or edges field. There are four hard-coded plans: one each for
browser_game, web_app and api, plus a three-milestone
fallback used by the other six domains.
| Domain | ID | Title | Required proofs | Weight |
|---|---|---|---|---|
| browser_game | m0 | Project boots | boots, builds | 0.15 |
| m1 | Scene renders | visual_nontrivial | 0.15 | |
| m2 | Player control | user_interaction, game_controls | 0.15 | |
| m3 | World rules | game_collision | 0.15 | |
| m4 | Combat loop | game_combat | 0.20 | |
| m5 | Objective and polish | game_win_loss, performance_budget | 0.20 | |
| web_app | m0 | Project boots | boots, builds | 0.25 |
| m1 | Core workflow | user_interaction | 0.35 | |
| m2 | Visual quality | visual_nontrivial, responsive_layout | 0.25 | |
| m3 | Regression checks | tests_pass | 0.15 | |
| api | m0 | Server boots | boots, builds | 0.25 |
| m1 | Contract | api_contract | 0.35 | |
| m2 | Behavior | auth_behavior, tests_pass | 0.30 | |
| m3 | Hardening | security_baseline | 0.10 | |
| all other domains | m0 | Runnable baseline | builds | 0.35 |
| m1 | Core behavior | goal.requiredProofs (spliced in) | 0.45 | |
| m2 | Regression guard | tests_pass | 0.20 |
The weight column is written and never read. Grepping the TypeScript sources
for weight returns exactly three hits: the field declaration at
types.ts:51, the helper parameter at milestones.ts:3, and the object
literal at milestones.ts:4. scoring.ts computes an unweighted mean
over required proofs and never inspects the milestone list.
The fallback plan has a subtle property worth noting. Its m1 takes
goal.requiredProofs wholesale, so for a cli goal m1
requires builds, tests_pass, cli_golden_output — the same
tests_pass that m2 also requires, and the same builds that
m0 already required. Milestones in the fallback path overlap rather than
partition.
Fig. 2.3 browser_game milestone chain
"passed". scoreRunAttempt() sets targetMilestone.status =
"passed" when the decision is keep (daedalus.ts:133–137) and
never writes any other status value, so a milestone that has failed six attempts is
indistinguishable from one nobody has started.Compiling the demo goal
daedalus plan runs all three compile steps and prints the combined result as
JSON without touching disk. Below is the head of that output for the goal named in
RESEARCH.md; the identifier and timestamp vary per invocation.
$ daedalus plan "build me Doom"
{
"goal": {
"id": "goal_V1StGXR8_Z",
"goal": "build me Doom",
"domain": "browser_game",
"deliverables": [
"runnable game app",
"local start command",
"gameplay tests",
"screenshots or video artifacts"
],
"requiredProofs": [
"boots", "builds", "visual_nontrivial", "user_interaction",
"game_controls", "game_collision", "game_combat",
"game_win_loss", "performance_budget"
],
"constraints": [ /* 4 fixed strings */ ],
"doneWhen": {
"minScore": 0.85,
"requiredGatesPass": true,
"noRegressions": true
},
"createdAt": "2026-01-01T00:00:00.000Z"
},
"milestones": [ /* m0 … m5, see Fig. 2.3 */ ],
"evalSelection": {
"domain": "browser_game",
"requiredProofs": [ /* same 9 */ ],
"evaluators": [ /* shell-core, playwright-web, browser-game */ ],
"uncoveredProofs": []
}
}
Structure and values transcribed from the source;
identifier and timestamp shown as placeholders because nanoid(10) and
new Date() differ per run. plan is read-only —
create-run is the command that persists.
Sheet notes
- Two YAML templates in
templates/describe the same demo goal by hand:goal.yaml(12 lines, matching the compiler output) anddoom.goal.yaml(24 lines, with a richer ten-item requirements list). Neither is read by any code — there is no YAML parser in the dependency list. Proofis a closed union of 17 string literals (types.ts:12–29). A goal cannot introduce a new proof kind without editing the type, which also means the evaluator registry can be checked exhaustively at compile time.deliverablesis free prose consumed by nothing. It exists to be shown to a builder agent.- The classifier is case-insensitive but whitespace- and punctuation-blind: it never tokenises, so hyphenation and word boundaries are irrelevant to matching.