✨ Make <Plan> expand inline by default (#711) - #720
Conversation
An approved Plan now runs where the author wrote the element. `<Plan>` without
`as` completes authorship, tears the constrained authorship frame down,
structurally admits the exact approved source, and expands those bytes at the
site as an embedded text root. `<Plan as="program">` binds the same bytes and
performs none of the program's effects, which is what keeps `xmd plan`
source-only: its adapter captures.
A trusted host says so with new host-only metadata on a declared Markdown
component, `returnDisposition: { kind: "executable-source", sourceIdentity }`.
It is admitted against the source — a declaration whose Markdown returns
anything but a string is refused — has no frontmatter field, travels in the
durable declared-component selection, and is compared whole and by absence on
replay, so a record that carries none is never read as executable source.
The projection is one expansion path in canonical core, after the invocation and
its teardown. It refuses a root `returns` and validates a declared root props
schema against the properties ambient at the site, both before the first effect;
applies the source's own frontmatter metadata and top-level `<Output>`; and
inherits the component selection, import authority, Workspace, error mode,
checked-failure ledger, block counter, output owner, cancellation scope and
journal. Its identities extend the authored element's, so a partial journal
resumes inside the program without repeating a completed effect. It is its own
flow: a stray `<Break>` is reported there rather than ending the caller's loop,
and a `<Return>` is reserved.
Ordinary value components are unchanged — a string-valued one still requires
`as`, and a repository component cannot opt in through frontmatter.
Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
PC22's "repeated rejection" row proved only that a Stop ends the run, which the row above it already proved. It is replaced by PC22b: every draft and repair answers with the same candidate, the host finds each unsound, nine rounds request changes and the tenth takes the exhaustion ending. The case asserts the exhaustion sentence exactly — stopping a round earlier produces "stopped at your request" instead — together with ten reviews, forty checks, and a candidate that carried a program effect through all of it and never performed it. ES4 asserted no expansion but never looked at the binding. It now invokes the refusing component with `as` inside `<PrintErrors>`, so the document survives the refusal and can be asked what it holds, and reads the capture back through `typeof`. A positive control against a declaration that does return proves the probe answers both ways. The authored-refusal half is kept: that failure is the run's own outcome, and it expands nothing either. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
| // canonical resolution retained rather than from the name, so a repository | ||
| // file, a bundle member or a registration answering for a declared name | ||
| // carries none of it — and an execution that declares nothing has none to | ||
| // read at all. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // read at all. |
| // The caller's bindings, with the root's own validated props over them. The | ||
| // layered environment is derived from the caller's, so the source reads what | ||
| // the document has bound; its own values object is fresh, so what the source | ||
| // binds stays inside it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // binds stays inside it. |
| }); | ||
|
|
||
| // The owner already holds what the source selected, exactly as a rendering | ||
| // body's does. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // body's does. |
PR #720: ✨ Make
|
|
Closing without merge because #725 supersedes #711's product contract. The completed focused and |
Closes #711.
Why
An author who writes
<Plan>wants the program it approves to happen — rightthere, in the document they are already running. Until now
<Plan>was anordinary value component: it required
as, bound the approved source as astring, and never ran a byte of it. The only way to carry a Plan out was
xmd plan --run, which ends authorship and starts a second root through theCLI. An embedded
<Plan>had no way to carry out its own program.What changes
Before:
After:
The
<Plan>body still expands once to form the Prompt, drafts stay inert, andreview is unchanged. After approval the constrained authorship frame tears down,
the exact approved source is structurally admitted, and that source expands
between the surrounding prose. What the reader sees is the program's selected
output, never its source.
Writing
askeeps the old behavior exactly: the same authorship and review, thesame byte-identical source bound, and none of the program's effects. That is why
xmd planis still source-only — its adapter captures — and--runremains theone path that starts an execution over those bytes.
Every unsuccessful ending — stopping, exhausting ten drafts, a failed Agent turn,
a structural refusal, cancellation — leaves the calling document with no program
effect and no binding.
How it works
A trusted host says the return is a program with new host-only metadata on the
declaration it hands the execution:
Two things follow.
The
asrequirement lifts, for that declaration only.returnCaptureViolation()gains one argument, so expansion and documentvalidation ask the same question and cannot disagree about which sites need
as. Nothing document-authored reaches the metadata: it lives only onDeclaredMarkdownComponent, has no frontmatter field, and is authenticated atthe call site against the definition canonical resolution retained — the same
name-plus-origin check the private closure uses — so a repository file, a bundle
member or a registration answering for a declared name carries none of it.
The return is projected as an embedded text root. This happens in
expandComponent's Markdown value branch, afterwithInvocationhas returned,so the source runs under the authority the authored site already had rather than
under the authorship ceiling. It is a root in three respects, each decided before
the first effect — a root
returnsis refused, a declared root props schemavalidates the properties ambient at the site, and the source's own frontmatter
metadata and top-level
<Output>apply. Everything else is the enclosingexpansion's: component selection, import authority, Workspace, error mode,
checked-failure ledger, block counter, output owner, cancellation scope and
journal. Identities extend the authored element's path, so a partial journal
re-enters the same expansion.
Review guide
Start with:
packages/core/tests/executable-source.test.ts— Tier ES uses adeclared component that stands where
<Plan>stands and returns the caller's owntext, so every case is about what the engine does with approved bytes rather than
how they were approved.
Then review:
packages/core/src/types.ts—ExecutableSourceDisposition, and the memberadded to
ComponentSelection.packages/core/src/components/declared-markdown.ts—admitDisposition()and
DeclaredImports.executableSourceFor().packages/core/src/expand.ts—expandExecutableSource()and its call site.packages/core/src/execute.ts— the durabledeclared-markdownselection:recording, parsing, the replay comparison, and the declaration capture.
packages/cli/src/plan-component.tsandsrc/documents/Plan.md.Look carefully at:
withInvocationwould run an approved program under the authorship ceiling.sameDisposition()compares by absence as well as by value. A record carryingno disposition must never be read as executable source.
ActiveLoop.set(undefined)inside the projection. Without it a stray<Break>in a generated program silently ends the caller's loop.
What must stay true
disposition — enforced by
admitDisposition(), checked by ES11.DeclaredMarkdownComponentwith no frontmatter field, checked by ES9.as— enforced byreturnCaptureViolation(), checked by ES8 and ES12.sameDisposition()indurableImportComponent(), checked by ES10.site sitting after
withInvocationreturns, checked by PC19 and PC23.asprevents every effect of the approved program — checked by PC20 andES3, each with a negative-control effect in the captured source.
— enforced by passing the site's authority and the component's own hide set,
checked by PC22's private-name row.
How to verify it
Focused evidence:
deno task test \ packages/core/tests/executable-source.test.ts \ packages/core/tests/declared-markdown-component.test.ts \ packages/core/tests/component-returns.test.ts \ packages/cli/tests/plan-component.test.ts \ packages/cli/tests/syntax-cli.test.ts \ packages/cli/tests/packaged-document.test.tsDistribution, because the packaged
<Plan>declaration changed:What the important cases prove:
author wrote, and that its source is not printed in its place. It fails if the
projection is skipped, runs twice, or emits raw source.
asbinds byte-identical source while the exec block inside itnever appends to the log. It fails if a captured Plan executes.
and asserts the exhaustion sentence exactly. Stopping one round earlier
yields "stopped at your request" and fails the case, so the ending is genuinely
tenth-round-specific rather than any
Stop.and the admission, and does not re-run the program's completed exec block. It
fails if replay authors again or repeats a completed embedded effect.
asinside<PrintErrors>, so thedocument survives and can be asked what it holds, and reads the capture back
through
typeof. A positive control against a declaration that does returnprints "the capture holds …", so the probe is proven to answer both ways.
again; ES7 proves the journal records exactly one root import, which fails
if the projection ever starts a second execution.
<Break />in a program is reported there. Removingthe loop isolation makes the caller's
<Loop>end after one iteration with nodiagnostic, and the case fails.
disposition, and one that adds it to a run that had captured an ordinary value.
Adversarial checks run while developing: disabling
executableSourceFor()fails7 of 13 ES cases, so the tier is not vacuous. Two cases in the first draft of
this branch were passing for the wrong reason — PC22's structural-refusal rows
failed on "the case scripted no answer for this review" rather than on admission
— and were tightened.
Scope
Included
returnDispositionon declared Markdown, end to end: admission,capture, selection, the durable record, replay comparison, validation and
syntax inspection.
<Plan>'s declaration, its author-facing description andasdocumentation.architecture.md,specs/executable-mdx-spec.md,specs/plan-command-spec.mdand
specs/release-process-spec.md.Intentionally unchanged
ending in
Plan.mdmoved. Only the frontmatterdescriptionandasprosechanged.
xmd plan. It stays source-only by default becauseplan-command.mdcaptures with
as, and--runremains its explicit independent-executionpath.
<Evaluate source>and generated XMD. Untouched and not widened; Make<Evaluate>evaluate complete XMD programs #713 willconsume this projection later.
New abstractions
ExecutableSourceDispositionexists because a host needs to say somethingabout its own Markdown that the bytes cannot say about themselves. Consumed by
admission, selection, the durable record, expansion, validation and inspection.
expandExecutableSource()exists because the projection is one expansion pathwith its own preflight, and inlining it would bury three root contracts inside
an already long value branch.
returnDispositionhas exactlyone kind and one consumer.
Risks and limitations
it. That is why it is recorded in the durable selection and asserted by both
distribution probes — a build shipping the Component without it would let a
document write
<Plan>and then refuse it for having captured nothing.no dedicated assertion of its own.
returnDisposition. Removing it fromplanComponentDeclaration()restores theprevious
as-required contract without touching core.Scope confirmation
Verification status. Focused evidence, both distribution probes,
deno task checkanddeno task lintall pass; the results are in the branch'scommit messages and the session handoff.
deno task test --changedselects mostof the corpus because
expand.tsandexecute.tsmoved; it was still runningwith zero failures when this PR was opened, and CI owns the exhaustive matrices.
https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7