Skip to content

Commit f10c2c8

Browse files
authored
✨ Make <Plan> emit or capture approved XMD source (#722) (#739)
* ✨ Make `<Plan>` a text component that produces approved source (#722) `<Plan>` no longer declares a return. Its rendering is the approved program source: bare, it emits those exact bytes where the component is written; with `as`, ordinary text capture binds them and nothing is emitted. Neither form evaluates the source. Exact bytes are not prose, so a declaring host may say that what its component renders is source. Segments such a component produced carry that fact to the emission loop, which is outside every scope the invocation owned, and the Output Api carries it to the presentation middleware — whitespace normalization and terminal formatting both pass exact writes through untouched while the prose around them is presented as before. A tenth draft that still fails its check after its repairs no longer opens a review. There is no decision left to offer, so the workflow asks the coding agent once, automatically, why the attempts did not work, treats that answer as inert text, and ends without a Plan. The authored exhaustion ending went with the review it belonged to. One Plan-artifact record retains the invocation identity, the instruction identity, the approved source, its digest and the successful admission together, replacing the per-candidate admission record. The instruction identity is retained again in the frozen inputs, so a continuation whose instructions render differently is refused there — as stale input, before a directory, a provider, a turn or a review exists. Claude-Session: https://claude.ai/code/session_01DNPFLq52HVSu3NH6WczYLf * 🔒 Close the exact-source disposition and both Plan record protocols (#722) The trusted declaration's exact-source disposition is durable. It is retained in the declared-markdown selection as the closed shape `exact: true`, absence meaning ordinary prose, hostile-parsed as a fifth member the record may or may not carry, and compared against the current declaration including absence. A continuation whose origin, digest and bytes are unchanged but whose disposition moved is refused before it can publish those bytes — as prose where a program was recorded, or the reverse. Both Plan records are closed protocols. The frozen inputs hold exactly `syntax` and `instruction`; the artifact holds exactly `invocation`, `instruction`, `source`, `digest` and `admission`. A member missing, a member this version does not know, or a member of the wrong type is a record that cannot be read rather than one to fill a default into, and the invocation identity, the source digest and the successful admission are still checked on top of that shape. Both unreadable-record refusals are raised as durability failures, for the reason the changed-instruction refusal already was: they happen while the journal still holds entries the run will never reach, and an ordinary failure there is replaced by the completion's divergence report — so the person read a count of unreached entries instead of what was wrong with their history. Terminal formatting is held to the same bypass as whitespace normalization, and both directions are now proven: exact bytes keep their heading, fence, emphasis markers, trailing spaces and blank runs, while the prose on either side of the write is still formatted. Claude-Session: https://claude.ai/code/session_01DNPFLq52HVSu3NH6WczYLf * ✅ Hold the compiled binary to the text-mode `<Plan>` (#722) The compiled-binary probe still expected a declared return from the packaged Component, and the `smoke` job is where that surfaced: import-based test selection cannot see a subprocess boundary, so no local affected-test run reaches it. The binary reports what it embedded, and what it embedded is now a text component. The origin and digest assertions above the failure had already passed, which is how we know the compile carried the new Component rather than an older copy — the expectation was stale, not the build. The scripted-review type loses the decision that went with the review a tenth unrepairable draft no longer reaches. Nothing scripts it any more. Claude-Session: https://claude.ai/code/session_01DNPFLq52HVSu3NH6WczYLf * 🔒 Make exact source a provenance rather than a field (#722) Exact-source presentation was read from `ComponentDefinition.exact` and from `TextSegment.exact` — public data on objects an import can produce. Ordinary `Component.importComponent` middleware could therefore answer an open name with a definition of its own carrying the disposition, and core would publish that handler's bytes unpresented. It is now derived from two facts no answer can write: that canonical execution authorized the import for a name this execution closed, and that this host's own admitted declaration for that name states exact source. The admission holds the disposition, because a declaration crosses on an ExecutionInstallation by value before any middleware exists, while a definition is an object a handler can copy and answer with. Which segments an expansion produced as source is recorded against those objects' identity rather than on them. A segment scanned from a document, parsed from a journal or written by a handler is simply not in that record — where a field would have been supplied, membership cannot be. Both public opt-ins are gone rather than replaced: there is no `exact` on `ComponentDefinition` or `TextSegment`, and no new one anywhere a document, package, frontmatter or middleware can reach. The durable protocol is unchanged and now stated as the two closed shapes it is, with every other spelling refused. The specification and architecture say what the implementation does, and the acceptance tables gain the rows this work introduced: a Tier DM table for declared-Markdown selection and exact source, and the WN and TF rows for the two presentation bypasses. Claude-Session: https://claude.ai/code/session_01DNPFLq52HVSu3NH6WczYLf * 🐛 Give the exact-source record to the run that owns it (#722) The record of which segments an expansion produced as source was a module-scoped `WeakSet`, which `local/no-module-scoped-registry` refuses: one table shared by every execution the process performs, surviving the run that filled it and reachable from no caller's scope. CI caught it where a local lint run did not, because the failure was one error among a thousand warnings. It is now created inside the execution that owns it and handed down through context, beside the segment-cause table it sits next to — reclaimed with the run, answering nothing during the next. The security property is unchanged and is what it always was: membership is keyed by the identity of the segment objects canonical expansion marked, so nothing a document, a package or an import can supply is able to join it. The context is exported for the two core modules that produce and consume the mark and reaches neither public package entrypoint. The emission paths read that record once, where reading is an ordinary operation rather than a durable one. Claude-Session: https://claude.ai/code/session_01DNPFLq52HVSu3NH6WczYLf * 🔒 Pass the exact-source record by value rather than through a context (#722) An Effection context resolves by name, and a name is not a secret. Storing the exact-source record under `xmd.exact-source` therefore published it: an ordinary function component could build a context with that name, retrieve the execution's own WeakSet, replace the `has` it inherited, and have its prose emitted with no whitespace normalization and no terminal formatting. That is an authority escalation, and it is what AGENTS.md rule 15 means by refusing to let security enforcement trust replaceable context state. The record is now created inside the execution that owns it and handed down by value on the private `ExpansionAuthority` canonical core already carries its import authority and identity domains on. Marking and emission receive it explicitly. Nothing a document, a component, middleware or a separately loaded copy can name reaches that object, and it is reclaimed with the execution that made it. What the correction does not do is trade one hole for another: no module-scoped registry, no symbol or property brand on the segments, no secret context name — a name that must stay unguessed is still a name — and no new public opt-in. Eligibility is unchanged, and is still canonical authorization together with the host's admitted declaration. DM53 is the attack, end to end: middleware answers an open name with a function component that builds that context, corrupts whatever it finds, and returns prose carrying emphasis, trailing spaces and blank runs through both real presentation middlewares. It fails at 34e8c48 and passes here. Claude-Session: https://claude.ai/code/session_01DNPFLq52HVSu3NH6WczYLf
1 parent 257f8d4 commit f10c2c8

29 files changed

Lines changed: 1905 additions & 186 deletions

‎architecture.md‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Existing documents and code get aligned to this section retroactively.
4343
| Prompt | a person's original request, in ordinary natural language. `xmd plan` takes exactly one |
4444
| Plan | the executable program produced from a Prompt: an Executable Markdown document combining readable prose that expresses the Prompt's intent with the components that carry it out, each placed beside the prose describing what it does. It begins with one descriptive level-one heading. A Plan is what `xmd plan` approves and then delivers: printed to stdout by default, written to an `--output` file, and run only under `--run`. It is not a synonym for a workflow, a policy document or any executable Markdown file |
4545
| plan command document | the one exact checked-in first-party Markdown value root `xmd plan` executes. It is the command's adapter and nothing else: it projects the request into `<Plan>`, supplies the session, and returns the approved source. It is not itself a Plan. Internal: no command-line option selects another one, and no repository component search can answer for it |
46-
| packaged `<Plan>` Component | the one exact checked-in first-party Markdown value component that converts a Prompt into a Plan, `packages/cli/src/documents/Plan.md`, declared to every ordinary run as the public `<Plan>`. It owns and implements the Plan authorship workflow — the Prompt wording, the draft and repair loops, the `<CheckDraft>` branches, human review, revision, approval, stopping, exhaustion and the final explanation turn — and returns the exact approved Plan source. Every Plan-producing turn in it states the complete Plan requirements for itself, so a replacement may add or correct a title rather than only carry one forward. Both surfaces expand these exact bytes under one origin and one digest; there is no generated TypeScript copy and no second Markdown implementation. Its four phase components are private to it, and it is not itself a Plan |
46+
| packaged `<Plan>` Component | the one exact checked-in first-party Markdown text component that converts a prompt into a Plan, `packages/cli/src/documents/Plan.md`, declared to every ordinary run as the public `<Plan>`. It owns and implements the Plan authorship workflow — the Prompt wording, the draft and repair loops, the `<CheckDraft>` branches, human review, revision, approval, stopping, exhaustion and the automatic final explanation turn — and produces the exact approved Plan source as what it renders: written bare it emits those bytes, and `as` is ordinary text capture that binds them and emits nothing. Neither form evaluates the source. Every Plan-producing turn in it states the complete Plan requirements for itself, so a replacement may add or correct a title rather than only carry one forward. Both surfaces expand these exact bytes under one origin and one digest; there is no generated TypeScript copy and no second Markdown implementation. Its four phase components are private to it, and it is not itself a Plan |
4747
| authorship profile | the trusted-host assembly the packaged `<Plan>` Component runs its authored turns under, installed by its own `<PlanAuthorship>` inside the invocation that owns it rather than around an execution — which is what makes it the same frame whether `xmd plan`, an ordinary document, or a configured `<TestAgent>` run child asked. Which Agent context goes under it is a trusted-host capability the declaration carries — the production ACPX one built from the run's Agent stack, or the deterministic one a canonical `<TestAgent>` child declaration produced — and a host that supplies none states the sentence a `<Plan>` written there is refused with. The fixed policy is installed in one place for both, so a second provider cannot bring a weaker one: its fixed inputs, a constrained Agent provider, Elicitation, the fixed first-party components and the host-declared `<CheckDraft>`. It uses no repository component search and exposes no custom root, and the policy it installs is not readable from the command line. Its working directory is one host-owned directory dedicated to the logical session, keyed by the digest of that name, created empty and required to be empty on the way in. An explicitly named session's directory is durable, because continuation derives the same session identity from it; an invocation-unique default session's is scope-owned, claimed before it is created, and exactly one cleanup is attempted after profile teardown and before admission on every ending — the leaf removed non-recursively when it is still the empty directory that was handed over, and left as found with the command failing terminally when it has gained content or vanished. Where those directories live is a host dependency no caller or document selects |
4848
| upgrade command document | the one exact checked-in first-party Markdown streaming text root `xmd upgrade` executes to select and install a published release. It owns the exact-tag grammar, release selection, semantic-version comparison, consent, the status, already-current and installation branches, and the wording of every refusal and report; its rendered body is the command's output rather than a value it returns. Internal: no command-line option selects another one, and no repository component search can answer for it |
4949
| upgrade assembly | what one runtime-named entrypoint states about the `xmd` that is running: its provenance, reported version, invoked executable path, platform, architecture, release target when the release publishes one, and — for an eligible compiled macOS or Linux host alone — the factory for the four phases an installation needs. It describes how this `xmd` is running, never how its files arrived |
@@ -3561,15 +3561,35 @@ registration can answer for it — one decision, shared by execution, inspection
35613561
and validation, so a repository file under a private name is described by none
35623562
of them and runs nowhere.
35633563

3564-
**The journal records the asset, not a lookup.** A declared import records
3565-
exactly `{ kind: "declared-markdown", origin, digest, content }`, and a private
3566-
one records exactly `{ kind: "declared-private", origin }`. A continuation reads
3567-
both as hostile data, verifies the recorded origin, digest and bytes against
3568-
what this run declares, and reads no file: a host that no longer declares the
3569-
name, or that declares different bytes under it, refuses rather than continuing
3564+
**The journal records the asset, not a lookup.** A declared import records one
3565+
of two closed shapes — `{ kind, origin, digest, content }`, and the same with
3566+
`exact: true` for a component the host declared as rendering a program's source
3567+
— and a private one records exactly `{ kind: "declared-private", origin }`. A
3568+
continuation reads all of it as hostile data, verifies the recorded origin,
3569+
digest, bytes and disposition against what this run declares, and reads no file:
3570+
a host that no longer declares the name, that declares different bytes under it,
3571+
or that publishes those bytes a different way, refuses rather than continuing
35703572
somebody else's Markdown. A recorded private import refuses unless the element
35713573
asking for it is inside the same declaration.
35723574

3575+
**Exact source is a provenance, not a property.** Whether bytes are published as
3576+
a program's source or presented as prose is decided from two things no answer
3577+
can write: that canonical execution authorized the import for a name this
3578+
execution closed, and that the host's own admitted declaration for that name
3579+
states it. A definition carrying the claim, a segment arriving already marked, a
3580+
frontmatter key and a middleware answer are each data that reached the engine
3581+
from somewhere, so none of them decides it — an ordinary
3582+
`Component.importComponent` handler answering an open name with a definition
3583+
claiming the disposition publishes prose, exactly as a repository file of that
3584+
name does.
3585+
3586+
The record of which segments an expansion produced as source belongs to the
3587+
execution that made it and is handed down **by value** on canonical core's
3588+
private expansion authority. Not through a context: a context resolves by name,
3589+
and a name is not a secret, so a component could build one, reach the record and
3590+
answer that everything is exact. It is reclaimed with its execution, reaches no
3591+
public entrypoint, and marks nothing on the segments themselves.
3592+
35733593
## The syntax catalog boundary
35743594

35753595
`xmd syntax` answers what a document may write here, and answering must cost

‎packages/cli/src/documents/Plan.md‎

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ props:
44
properties:
55
session: { type: string, minLength: 1 }
66
additionalProperties: false
7-
returns:
8-
type: string
97
description: >-
10-
Create an XMD program from a Prompt. `<Plan as="program">Ask for the user's
11-
age.</Plan>` expands its content normally to form the complete Prompt.
12-
as: Required. The exact approved Plan source after teardown and structural admission.
13-
context: The complete Prompt, expanded once before authorship.
8+
Create an XMD program from a prompt. `<Plan>Ask for the user's age.</Plan>`
9+
emits the approved program source.
10+
as: Binds the exact approved program source and emits nothing. Omitted, that source is emitted where the component is written.
11+
context: The complete prompt, expanded once before authorship.
1412
---
1513

1614
# Turning a Prompt into a Plan
@@ -21,8 +19,9 @@ available to carry them out. A coding agent turns both into one document that
2119
explains and executes the sequence.
2220

2321
A draft remains text while this workflow reviews it. Nothing in it runs before
24-
you approve it. After approval, the exact source is checked once more as
25-
structure and handed back unchanged.
22+
you approve it, and nothing runs it afterwards either: what this workflow
23+
produces is the approved program's source, byte for byte, for you to keep,
24+
read, or hand to something that runs it.
2625

2726
## Read the Prompt
2827

@@ -37,7 +36,7 @@ nothing reaches no catalog, no session, no agent and no review.
3736
<Fail message="<Plan> requires its body to render a non-empty Prompt." />
3837
</If>
3938

40-
<PlanInputs session={props.session} as="inputs" />
39+
<PlanInputs session={props.session} instruction={prompt} as="inputs" />
4140

4241
## Say what this surface calls things
4342

@@ -48,13 +47,11 @@ of them is raised.
4847

4948
<If condition={inputs.surface === "command"}>
5049
<Let as="stopped" value="xmd plan stopped at your request. Nothing was output or run." />
51-
<Let as="exhausted" value="xmd plan reviewed ten drafts without an approved Plan. Nothing was output or run." />
5250
<Let as="unresolved" value="xmd plan ended without an approved Plan. Nothing was output or run." />
5351
<Let as="explained" value="xmd plan reviewed ten drafts without an approved Plan. The coding agent explained why:" />
5452
<Let as="closing" value="Nothing was output or run." />
5553
<Else>
5654
<Let as="stopped" value="Plan authorship stopped at your request. No Plan was returned." />
57-
<Let as="exhausted" value="Plan authorship reviewed ten drafts without an approved Plan. No Plan was returned." />
5855
<Let as="unresolved" value="Plan authorship ended without an approved Plan. No Plan was returned." />
5956
<Let as="explained" value="Plan authorship reviewed ten drafts without an approved Plan. The coding agent explained why:" />
6057
<Let as="closing" value="No Plan was returned." />
@@ -162,18 +159,44 @@ explanation before or after it.
162159
<CheckDraft source={draft} as="check" />
163160
</Loop>
164161

162+
## Explain a tenth draft that could not be repaired
163+
164+
Ten drafts is the limit, and the tenth cannot be revised into an eleventh. So a
165+
tenth draft that still has problems after its repair attempts leaves nothing to
166+
approve and nothing to ask for — there is no decision left for you to make, and
167+
this workflow does not ask you to make one.
168+
169+
Instead the coding agent is asked once, automatically, why the attempts did not
170+
work and what would make a future prompt more likely to succeed. That answer is
171+
explanation and nothing else: it creates no new draft, extends no limit, and
172+
ends this Plan without a program.
173+
174+
<If condition={round === 10 && !check.valid}>
175+
<Prompt as="explanation">
176+
The final Plan still has these problems:
177+
178+
<Json value={check.diagnostics} as="problems" />
179+
<CodeBlock value={problems} language="json" />
180+
181+
Explain briefly why the attempts did not resolve them and what the person should
182+
clarify in their next Prompt. Do not create another Plan.
183+
</Prompt>
184+
185+
<Fail message={`${explained}\n\n${explanation}\n\n${closing}`} />
186+
</If>
187+
165188
## Review the draft
166189

167-
The workflow shows you the complete draft after its repair attempts.
190+
Every draft you are shown has a decision left in it. The workflow shows you the
191+
complete draft after its repair attempts.
168192

169193
- Choose **Approve** to accept a draft that passed its check.
170194
- Choose **Request changes** to send feedback to the coding agent and create a
171195
new draft.
172196
- Choose **Stop** to end without returning anything.
173197

174-
A draft with remaining problems cannot be approved. You may review at most ten
175-
drafts, and the tenth cannot be revised. If the tenth draft still has problems,
176-
you may ask the coding agent to explain what went wrong or stop.
198+
A draft with remaining problems cannot be approved, and the tenth draft cannot
199+
be revised, so on either of those the choices you are offered are narrower.
177200

178201
<Elicit
179202
as="review"
@@ -184,9 +207,7 @@ you may ask the coding agent to explain what went wrong or stop.
184207
type: "string",
185208
enum: check.valid
186209
? (round === 10 ? ["Approve", "Stop"] : ["Approve", "Request changes", "Stop"])
187-
: (round === 10
188-
? ["Explain what went wrong", "Stop"]
189-
: ["Request changes", "Stop"]),
210+
: ["Request changes", "Stop"],
190211
},
191212
feedback: { type: "string" },
192213
},
@@ -226,36 +247,15 @@ problems:
226247
## Continue from your decision
227248

228249
Approve keeps this exact draft and leaves the review. Request changes sends your
229-
feedback to the coding agent and starts a new draft. Stop ends here. On a tenth
230-
draft that still has problems there is nothing left to revise into, so the two
231-
remaining choices are to ask the coding agent what went wrong, or to stop.
250+
feedback to the coding agent and starts a new draft. Stop ends here.
232251

233252
<If condition={review.decision === "Approve"}>
234253
<Let as="approved" value={draft} />
235254
<Break />
236255
</If>
237256

238257
<If condition={review.decision === "Stop"}>
239-
<If condition={round === 10 && !check.valid}>
240-
<Fail message={exhausted} />
241-
<Else>
242258
<Fail message={stopped} />
243-
</Else>
244-
</If>
245-
</If>
246-
247-
<If condition={review.decision === "Explain what went wrong"}>
248-
<Prompt as="explanation">
249-
The final Plan still has these problems:
250-
251-
<Json value={check.diagnostics} as="problems" />
252-
<CodeBlock value={problems} language="json" />
253-
254-
Explain briefly why the attempts did not resolve them and what the person should
255-
clarify in their next Prompt. Do not create another Plan.
256-
</Prompt>
257-
258-
<Fail message={`${explained}\n\n${explanation}\n\n${closing}`} />
259259
</If>
260260

261261
<Prompt as="draft">
@@ -286,17 +286,21 @@ explanation before or after it.
286286
</Session>
287287
</PlanAuthorship>
288288

289-
## Return the approved Plan
289+
## Produce the approved Plan source
290290

291291
Only an approved Plan leaves this workflow, and only after the whole authorship
292292
frame above has been taken down. Its exact source is checked once more as
293-
structure — every declaration, resolution and form it uses — and then handed
294-
back byte for byte.
293+
structure — every declaration, resolution and form it uses — and retained
294+
together with the prompt it was written for, so asking again restores these
295+
exact bytes instead of writing a second Plan.
295296

296297
<If condition={approved !== null}>
297-
<AdmitPlan source={approved} as="admitted" />
298-
<Return value={admitted} />
298+
<AdmitPlan source={approved} instruction={prompt} as="admitted" />
299299
<Else>
300300
<Fail message={unresolved} />
301301
</Else>
302302
</If>
303+
304+
The source is what this workflow produces, byte for byte. Nothing here runs it.
305+
306+
<Output>{admitted}</Output>

0 commit comments

Comments
 (0)