Skip to content

✨ Make <Plan> expand inline by default (#711) - #720

Closed
taras wants to merge 2 commits into
mainfrom
agent/issue-711-plan-inline
Closed

✨ Make <Plan> expand inline by default (#711)#720
taras wants to merge 2 commits into
mainfrom
agent/issue-711-plan-inline

Conversation

@taras

@taras taras commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Closes #711.

Why

An author who writes <Plan> wants the program it approves to happen — right
there, in the document they are already running. Until now <Plan> was an
ordinary value component: it required as, bound the approved source as a
string, 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 the
CLI. An embedded <Plan> had no way to carry out its own program.

What changes

Before:

Prepare the release program and carry it out here.

<Plan as="program">
1. Read package.json and CHANGELOG.md.
2. Ask me to approve the next version.
3. Write RELEASE.md.
</Plan>

<!-- `program` holds the source. Nothing ran. -->

After:

Prepare the release program and carry it out here.

<Plan>
1. Read package.json and CHANGELOG.md.
2. Ask me to approve the next version.
3. Write RELEASE.md.
</Plan>

The release program completed, so continue with the result.

The <Plan> body still expands once to form the Prompt, drafts stay inert, and
review 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 as keeps the old behavior exactly: the same authorship and review, the
same byte-identical source bound, and none of the program's effects. That is why
xmd plan is still source-only — its adapter captures — and --run remains the
one 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

<Plan> invocation → authorship + review → invocation teardown
  → <AdmitPlan> retains exact bytes → <Return>
  → canonical core: embedded text root expanded at the authored site

A trusted host says the return is a program with new host-only metadata on the
declaration it hands the execution:

returnDisposition: { kind: "executable-source", sourceIdentity: "<plan>" }

Two things follow.

The as requirement lifts, for that declaration only.
returnCaptureViolation() gains one argument, so expansion and document
validation ask the same question and cannot disagree about which sites need
as. Nothing document-authored reaches the metadata: it lives only on
DeclaredMarkdownComponent, has no frontmatter field, and is authenticated at
the 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, after withInvocation has 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 returns is refused, a declared root props schema
validates the properties ambient at the site, and the source's own frontmatter
metadata and top-level <Output> apply. Everything else is the enclosing
expansion'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 a
declared component that stands where <Plan> stands and returns the caller's own
text, so every case is about what the engine does with approved bytes rather than
how they were approved.

Then review:

  1. packages/core/src/types.tsExecutableSourceDisposition, and the member
    added to ComponentSelection.
  2. packages/core/src/components/declared-markdown.tsadmitDisposition()
    and DeclaredImports.executableSourceFor().
  3. packages/core/src/expand.tsexpandExecutableSource() and its call site.
  4. packages/core/src/execute.ts — the durable declared-markdown selection:
    recording, parsing, the replay comparison, and the declaration capture.
  5. packages/cli/src/plan-component.ts and src/documents/Plan.md.

Look carefully at:

  • The projection runs after invocation teardown. Moving it inside
    withInvocation would run an approved program under the authorship ceiling.
  • sameDisposition() compares by absence as well as by value. A record carrying
    no 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

  • Only exact host-declared Markdown returning a string can carry the
    disposition
    — enforced by admitDisposition(), checked by ES11.
  • Nothing document-authored can opt in — enforced by the type living only on
    DeclaredMarkdownComponent with no frontmatter field, checked by ES9.
  • An ordinary value component still requires as — enforced by
    returnCaptureViolation(), checked by ES8 and ES12.
  • A changed or missing disposition refuses replay — enforced by
    sameDisposition() in durableImportComponent(), checked by ES10.
  • The authorship frame is gone before the program runs — enforced by the call
    site sitting after withInvocation returns, checked by PC19 and PC23.
  • as prevents every effect of the approved program — checked by PC20 and
    ES3, each with a negative-control effect in the captured source.
  • The program reaches no private capability and cannot re-enter its producer
    — 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.ts

Distribution, because the packaged <Plan> declaration changed:

deno task test scripts/tests/cli-npm-bin.test.ts
deno task build
deno task test scripts/tests/plan-component-compiled.test.ts

What the important cases prove:

  • PC19 proves one approved program runs once, between the two markers the
    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.
  • PC20 proves as binds byte-identical source while the exec block inside it
    never appends to the log. It fails if a captured Plan executes.
  • PC22b drives ten rounds with three repairs each — 10 reviews, 40 checks —
    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.
  • PC25 proves a partial journal restores authorship, the check, the approval
    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.
  • ES4 invokes a refusing component with as inside <PrintErrors>, so the
    document survives and can be asked what it holds, and reads the capture back
    through typeof. A positive control against a declaration that does return
    prints "the capture holds …", so the probe is proven to answer both ways.
  • ES6 proves a replayed embedded effect is restored rather than performed
    again; ES7 proves the journal records exactly one root import, which fails
    if the projection ever starts a second execution.
  • ES6b proves a stray <Break /> in a program is reported there. Removing
    the loop isolation makes the caller's <Loop> end after one iteration with no
    diagnostic, and the case fails.
  • ES10 proves both replay directions refuse: a continuation that drops the
    disposition, and one that adds it to a run that had captured an ordinary value.

Adversarial checks run while developing: disabling executableSourceFor() fails
7 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

  • The trusted returnDisposition on declared Markdown, end to end: admission,
    capture, selection, the durable record, replay comparison, validation and
    syntax inspection.
  • The embedded text-root projection in canonical core.
  • <Plan>'s declaration, its author-facing description and as documentation.
  • architecture.md, specs/executable-mdx-spec.md, specs/plan-command-spec.md
    and specs/release-process-spec.md.

Intentionally unchanged

  • The authorship workflow. No Prompt, review choice, bound, private phase or
    ending in Plan.md moved. Only the frontmatter description and as prose
    changed.
  • xmd plan. It stays source-only by default because plan-command.md
    captures with as, and --run remains its explicit independent-execution
    path.
  • <Evaluate source> and generated XMD. Untouched and not widened; Make <Evaluate> evaluate complete XMD programs #713 will
    consume this projection later.
  • Ordinary value components, string-returning ones included.

New abstractions

  • ExecutableSourceDisposition exists because a host needs to say something
    about 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 path
    with its own preflight, and inlining it would bury three root contracts inside
    an already long value branch.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included. returnDisposition has exactly
    one kind and one consumer.

Risks and limitations

  • The disposition is not in the declared bytes, so the digest cannot answer for
    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.
  • Workspace inheritance is structural — the projection opens no new one — and has
    no dedicated assertion of its own.
  • Recovery: the behavior is gated entirely on a declaration carrying
    returnDisposition. Removing it from planComponentDeclaration() restores the
    previous as-required contract without touching core.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified — there are none.
  • The description matches the final diff and test results.

Verification status. Focused evidence, both distribution probes,
deno task check and deno task lint all pass; the results are in the branch's
commit messages and the session handoff. deno task test --changed selects most
of the corpus because expand.ts and execute.ts moved; it was still running
with zero failures when this PR was opened, and CI owns the exhaustive matrices.

https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 redundant comments. Inline suggestions to remove them below.

// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// binds stays inside it.

});

// The owner already holds what the source selected, exactly as a rendering
// body's does.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// body's does.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

PR #720: ✨ Make <Plan> expand inline by default (#711)

21 files, +1802 / -66

Scope

🔴 PR has 1868 lines changed. Split into focused PRs.

🟡 1868 lines changed. PRs under 400 receive more thorough review.

🟡 21 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×8: packages/core/src/expand.ts, packages/core/src/execute.ts
  • no-unnecessary-type-assertion ×6: packages/core/src/expand.ts, packages/core/src/execute.ts

Slop

  • packages/core/src/expand.ts:2486// read at all.
  • packages/core/src/expand.ts:2883// binds stays inside it.
  • packages/core/src/expand.ts:2916// body's does.

Oxlint slop signals:

  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 5 files (14 rules)
Density: 0.032 violations/added-line

no-unsafe-type-assertion (10): packages/core/src/expand.ts, packages/cli/src/plan-component.ts, packages/core/src/execute.ts
no-unused-vars (8): packages/core/src/expand.ts, packages/core/src/execute.ts
no-base-to-string (7): packages/cli/src/plan-component.ts, packages/core/src/expand.ts
unbound-method (6): packages/cli/src/plan-component.ts, packages/core/src/expand.ts, packages/core/src/execute.ts
no-unnecessary-type-assertion (6): packages/core/src/expand.ts, packages/core/src/execute.ts
no-array-sort (4): packages/core/src/document-validation.ts, packages/core/src/inspect.ts, packages/core/src/execute.ts
no-shadow (4): packages/core/src/expand.ts, packages/core/src/execute.ts
consistent-return (4): packages/core/src/inspect.ts, packages/core/src/execute.ts, packages/core/src/document-validation.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-misused-spread (2): packages/core/src/inspect.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts
no-floating-promises (1): packages/core/src/execute.ts
no-duplicate-type-constituents (1): packages/core/src/execute.ts

Correctness

No extraneous code patterns detected.

@taras

taras commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

Closing without merge because #725 supersedes #711's product contract.
#722 makes <Plan> emit or capture exact approved source without executing
it, while #713 owns explicit complete-program evaluation. This also removes the
host-only executable-source return disposition rather than repairing the two
architecture blockers found at the reviewed head: returned source inheriting a
declaration-private closure and returned executable source not being retained
generally for replay.

The completed focused and deno task test --changed results remain useful
historical evidence for the authorship, teardown, structural-admission, and
negative-control cases carried forward by the replacement issues.

@taras taras closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make <Plan> expand inline by default

1 participant