Skip to content

🔎 Let Plan ask for read-only XMD information (#762) - #792

Merged
taras merged 5 commits into
mainfrom
agent/issue-762-plan-loop
Sep 9, 2026
Merged

🔎 Let Plan ask for read-only XMD information (#762)#792
taras merged 5 commits into
mainfrom
agent/issue-762-plan-loop

Conversation

@taras

@taras taras commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Closes #762. Third and final PR of the frozen stack, after #789 (shared read
profile) and #790 (structural constructs in generated evaluation).

Why

An agent writing a Plan could not look at the project first. It had to guess at
file layout, available components and existing conventions, and the only way to
correct a wrong guess was to produce a draft and have the structural check
reject it.

What changes

An agent may now answer any Plan-producing turn with a read-only XMD program
instead of a draft.

Before:

Every response was treated as a draft candidate. A response that was not a
program failed the structural check and consumed a repair attempt.

After:

The response is classified first. A draft goes to the existing path unchanged. An
information request is evaluated under the host's own read authority, and the
rendered findings come back as the next turn's context.

How it works

Agent response → ClassifyPlanResponse → "draft"? existing path
                                      → "information"? PlanInformation
                                        → <Evaluate allow={["read"]}>
                                        → secret pre-disclosure check
                                        → findings as inert context → ask again

Plan.md binds requests = 8 beside its ten drafts and three repairs, and
applies one response-handling shape after each of the three Plan-producing
Prompt sites. The shape is inserted at their three identical endings so they
cannot drift.

Review guide

Start with: packages/core/src/plan-response.ts

Then review:

  1. packages/core/src/generated-request-refusal.ts — what core states it refused
  2. packages/core/src/components/Syntax.ts — the retained refusal, and why it
    is a value rather than a marked error
  3. packages/cli/src/plan-component.tsplanInformation() and the secret check
  4. packages/cli/src/documents/Plan.md — the loop, at three sites

Look carefully at:

  • The withholdSecrets() call site in planInformation(). It is placed where it
    is because the following Prompt is not durable.
  • Why the <Syntax> classification happens in the component rather than where
    the refusal is raised.

What must stay true

  • A draft is never evaluated. Enforced by classifying before the branch, on
    the lexical rule alone — checked by the seven PI3 rows and by
    PI3: a titled draft is never evaluated, which asserts zero reads.
  • Closed-but-invalid frontmatter is still a draft. Enforced by never parsing
    the frontmatter — checked by PI3: closed frontmatter is removed without being parsed. Parsing it would classify a malformed draft as a request and evaluate
    program text nobody approved.
  • Core states only what it refused; <Plan> alone decides a retry. Enforced per throw site
    rather than per error class, so GeneratedXmdError is not itself a marker —
    checked by FE34: a provider that throws is terminal, while its ordinary Err is not and FE34: a cleanup failure beats a refusal that was already classified.
  • A symbols provider cannot forge a refusal. Enforced by an instanceof
    check inside the durable executor on a private class, with the conclusion
    retained as a value rather than carried on an error — checked by the negative
    control in FE34: an unknown documented name is classified, where a provider
    throwing an error under the same declared name stays terminal.
  • A refusal means the same thing live and on replay, and an unrecorded one
    means nothing.
    Enforced by retaining { refused } in the record and
    interpreting it after createDurableOperation returns — checked by FE34: a refusal whose record cannot be published is terminal and unclassified and
    PI8: a retained selection refusal restores without asking the provider again.
  • A secret reaches no disclosure destination, the Agent included. Enforced by
    scanning complete settled text before <PlanInformation> binds — checked by
    PI12: a secret in the findings stops before they are disclosed.
  • Requests spend no drafting budget, and drafting spends no requests.
    Enforced by incrementing only requests_used — checked by the two PI7 rows.

How to verify it

  • PI12: a secret in the findings stops before they are disclosed proves a
    secret in a read file reaches neither stderr, the journal, nor the following
    Prompt, and fails if the implementation relies on the Prompt event's later
    append gate — which is what it did before this commit, and the row was written
    failing against that.
  • PI6: findings and the following turn wait for cleanup to finish holds cleanup
    open at the Plan boundary and proves no findings phase and no second turn have
    happened yet. It fails if disclosure runs beside cleanup rather than after it.
    Its own trap is real: written with the barrier first it passed vacuously, so it
    asserts the read had already run.
  • PI1: named documentation comes back, then an ordinary Plan proves a selection
    is a selection, and fails if the full catalog is injected — verified by asking
    for the third catalogued name and watching the row fail.
  • PI8: a changed request refuses rather than resuming proves a continuation
    whose instruction moved refuses in the frozen inputs before a turn, a review or
    a read.
  • smoke-test/plan-information/README.md runs a mixed refused-then-answered
    request-to-approved-Plan journey through source, npm and compiled
    installations. Its tripwire is in the scenario: the agent answers only prompts
    carrying the refusal and then the documentation, so a build that lost the
    packaged assets writes no Plan to assert about.

Manually:

deno task test packages/core/tests/evaluate-component.test.ts \
  packages/core/tests/capture-errors.test.ts \
  packages/core/tests/plan-response.test.ts
deno task test packages/cli/tests/plan-command-document.test.ts \
  packages/cli/tests/plan-component.test.ts packages/cli/tests/plan.test.ts \
  packages/cli/tests/plan-cli.test.ts packages/cli/tests/plan-host-acts.test.ts
deno task build   # the compiled row needs dist/xmd
deno task test scripts/tests/plan-component-compiled.test.ts \
  scripts/tests/cli-npm-bin.test.ts

Scope

Included

  • Lexical response classification, in core, exported through core/host alone.
  • One narrow generated-request-refusal classification and its core/host reader.
  • Two more private <Plan> capabilities: <ClassifyPlanResponse> and
    <PlanInformation>, taking the closure from five names to seven.
  • The eight-request loop at all three Plan-producing sites.
  • A pre-disclosure secret check on settled information text.
  • The PI1PI12 matrix, and the specification and architecture text for it.
  • The Plan writer rename: <PlanAuthorship><PlanWriter>,
    authorship-profile.tsplan-writer-profile.ts, and the profile, frame,
    placement, policy, observation, stack and root names that go with it. No
    compatibility aliases; Plan.md's digest moves with the two changed tag
    lines, and public <Plan> keeps its name, origin and identity.

Intentionally unchanged

  • Public <Evaluate> still throws. It gains no Result, no props, and no change
    to output or capture.
  • The serialized pre-append gate remains the authority for durable publication.
    The new check is additional and non-durable, and appends no event.
  • No Plan-specific durable record and no change to the generic durable-event
    envelope. Core's existing syntax_symbols payload gains the closed
    { refused: non-empty string } alternative beside the unchanged
    { symbols: string }; existing successful histories remain readable. A
    continuation restores through the ordinary Agent, admission, effect and Prompt
    events.
  • Request-aware <Syntax> rewriting and <Timeout> remain with Make <Syntax> find relevant component documentation from a question #787 and Add <Timeout> to bound an executable document region #788.

New abstractions

  • markGeneratedRequestRefusal / generatedRequestRefusal exist because a
    trusted host needs to tell one class of failure from every other across
    separately loaded package copies. They state a fact — core refused the
    generated request — and nothing about what a caller may do next. Consumers:
    the <Plan> loop, and core's own throw sites.

  • SyntaxSelectionRefusal exists because the durable boundary rebuilds a failure
    without its class, and the declared name that does survive is forgeable by a
    provider. It is internal — exported from neither mod.ts nor core/host.

  • classifyPlanResponse exists because a Plan response and the later structural
    check must not disagree about where a Markdown body begins.

  • Each new abstraction has multiple concrete uses or a clear justification.

  • No speculative functionality is included.

Risks and limitations

  • The refusal path's cleanup ordering is not proven by a held barrier at the Plan
    boundary, and cannot be: cleanup inside a fragment is per element, so it cannot
    straddle the failure that produces the refusal, and a provider handler cannot
    register scope-bound cleanup because its claim window is deliberately
    synchronous. The Architect accepted the composition instead — CE22 for
    teardown-before-recovery, a Plan-level row for recovery reaching the next turn,
    and FE34 for a cleanup failure winning and staying terminal.
  • PI9's command surface is proven in-process against the exact packaged command
    document rather than as a subprocess journey, because no seam points xmd plan
    at a deterministic agent and adding one would be a production surface change.
    The packaged command asset is covered instead by a compiled control that runs
    the command until it fails on an unresolvable agent.
  • The both-outcomes secret scan covers refusals, but that half has no reachable
    end-to-end demonstration: every candidate reason is built from the request text
    or a fixed sentence, so a secret in a refusal is already in the agent's
    response, which the pre-append response gate catches first.

Scope confirmation

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

An agent writing a Plan could not look at the project first. It can now answer
any Plan-producing turn with a read-only XMD program instead of a draft; the
workflow evaluates it under the host's own read authority, hands the rendered
findings back as inert context, and asks again.

Classification is lexical and happens before anything else touches the
response. A draft is a response whose first nonempty body block, after optional
lexically closed frontmatter, is a nonempty level-one heading; everything else
is an information candidate. The frontmatter is never parsed, so
closed-but-invalid YAML stays a draft and reaches the repair path that exists
to explain it — parsing it there would classify that draft as a request and
evaluate program text nobody approved.

The rule lives in core rather than beside `Plan.md`, exported through
`core/host` alone. It has to agree with core about where a Markdown body begins
and what a heading is, and both the `---` delimiters and the parser are there;
a classifier that drew the boundary differently from the structural check would
send a draft to evaluation. It is pure, carries no authority, and reaches no
document.

Recovery is one narrow class. `generated-candidate.ts` marks a failure at the
throw site that raises it and reads it back through a namespaced non-enumerable
property that survives a separately loaded copy. Marking per site rather than
per class is the point: `GeneratedXmdError` is raised for a refused construct
and for a retained admission whose ceilings moved, and recovering by
`instanceof` would recover stale history along with a typo. Anything unmarked is
terminal, so a failure added later is not recoverable until someone decides it
is.

An unknown name given to `<Syntax names={…}>` is one of those recoverable
mistakes, and it cannot be marked where it is raised: `<Syntax>` persists the
lookup, and a failure crossing that durable boundary is rebuilt without its
class or any non-enumerable property. Recognizing it by the declared name that
does survive is not enough either — a symbols provider that throws can use that
same name, and recovering one would hand a broken installation back to an agent
as retry context. So core raises a private `SyntaxSelectionRefusal` around the
selection it performs itself, after the provider has already returned, and the
component recognizes that class *inside* the executor by `instanceof`, where the
original is still in hand. Only the conclusion travels out, in a variable core's
own closure owns. A provider that throws stays terminal whatever it names its
error.

`<PlanInformation>` is the only place that turns a failure back into a turn. It
projects its child through `tryContent()`, which hands back the child's
*original* failure rather than a boundary's account of it — `content()` would
wrap it in a `ContentError` and collapse the distinction this depends on. It is
paired and value-returning, so `as` is mandatory and its closed
`{ status, text }` envelope is never rendered into a Plan. A refusal discards
partial output; every unmarked failure is rethrown unchanged.

Settled information text crosses a pre-disclosure secret check before that
envelope is bound. The following Prompt is a disclosure destination like the
progress stream and the journal, and unlike them it is not durable — it reaches
the agent before any event of it is appended, so the serialized pre-append gate
cannot be what protects it. Canonical execution owns the policy and the scanner;
`<PlanInformation>` asks them and appends nothing. The policy is resolved live,
so an absent or unauthentic one fails rather than reporting "off", and a host
that explicitly disabled detection is followed exactly rather than given a
second always-on policy. Both outcomes are scanned, a finding raises the
existing `SecretDetectedError`, and because it throws, nothing binds: no
findings progress, no following Prompt, no review, no artifact.

`Plan.md` binds `requests = 8` beside its ten drafts and three repairs and
applies one response-handling shape after each of the three Plan-producing
Prompt sites, inserted at their three identical endings so they cannot drift.
Successes and refusals each spend one request; drafts and repairs are untouched
in both directions; the ninth candidate is not evaluated and starts no turn.
The explanation turn stays outside the loop. The command host installs
`ordinaryEvaluationProfile()`; `<Plan>` installs nothing.

The fake agent now fails loudly on a turn nobody scripted, naming the turn and
quoting the prompt, instead of synthesizing an empty reply. That fiction let a
case whose script and expectations disagreed pass anyway — a document with two
`<Plan>` sites silently gave its second site nothing — and removing it
immediately found four more fixtures relying on untitled Plans.

Ten fixtures across five suites replied with untitled programs, which are
information requests by this contract. Each received the level-one title every
Plan is required to have, so each row still tests what it tested. One row's
premise genuinely changed: a fully fenced reply has no heading and is now a
request, so "nothing is stripped" is re-expressed as a draft carrying a fence
reaching review with that fence intact. The tenth was in a suite none of the
focused commands run, and only `--changed=origin/main` found it.

The disclosure-waits-for-cleanup evidence is a held barrier rather than a
recorder: an admitted `component-answer` — the same profile arm canonical
protected `<Syntax />` is admitted through — whose body registers cleanup that
blocks. It is written last in the request on purpose. Cleanup inside a fragment
is per element, so a barrier written first is released before the read even
happens and proves nothing; the first version of that control was vacuous for
exactly that reason, and the assertion that the read had already run is what
catches it. The refusal path cannot hold a barrier at all — element cleanup
cannot straddle the failure that produces the refusal, and a provider handler
cannot register scope-bound cleanup because its claim window is deliberately
synchronous. Its ordering is settled instead by `CE22`, which proves
projection-owned work is still live where a failure is reported and already torn
down by the time recovery runs, plus a Plan-level row for recovery reaching the
following turn and `FE34` for a cleanup failure winning and staying terminal.

The distribution evidence is an executable journey rather than asset hashes.
`smoke-test/plan-information/` runs a mixed request-to-approved-Plan exchange —
one request refused, one answered, then a Plan — through the source CLI, the
emitted npm bin and the compiled binary. The tripwire is in the scenario rather
than the assertions: its agent answers only prompts carrying the refusal and
then the selected documentation, so a build that lost the packaged assets or the
protected tier writes no Plan to assert about. A compiled control covers
`plan-command.md`, which has no catalog entry to digest, by running the command
until it fails on an unresolvable agent — reaching that point at all proves the
document, the declaration and the protected tier are embedded.

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

function* PlanInformation(): Operation<Json> {
// The child and its structured teardown have both settled by the time
// this returns, so what is scanned below is complete text and not a
// fragment still able to produce more.

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
// fragment still able to produce more.

Comment thread packages/core/src/components/Syntax.ts Outdated
// rather than the rebuilt error that comes out the other side. A provider
// cannot reach this closure, and cannot become an instance of that class by
// naming its own error the same thing — which is the hole a name comparison
// would leave open.

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
// would leave open.

return lines.slice(line + 1).join("\n");
}
}
// Opened and never closed: not an envelope, so nothing is removed.

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
// Opened and never closed: not an envelope, so nothing is removed.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

PR #792: 🔎 Let Plan ask for read-only XMD information (#762)

39 files, +3181 / -536

Scope

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

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

🟡 39 files changed. Are all changes related?

🟡 Changes span 10 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×8: packages/cli/src/plan-writer-profile.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+2)
  • no-empty-function ×2: packages/cli/src/testing-host.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-assertion ×1: packages/cli/tests/support/fake-acp.ts

Slop

  • packages/cli/src/plan-component.ts:818// fragment still able to produce more.
  • packages/core/src/plan-response.ts:81// Opened and never closed: not an envelope, so nothing is removed.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts

Static Analysis

Oxlint: 59 diagnostics across 12 files (15 rules)
Density: 0.019 violations/added-line

unbound-method (13): packages/cli/tests/support/fake-acp.ts, packages/cli/tests/support/run-markdown-tier.ts, packages/cli/src/testing-host.ts (+4)
no-base-to-string (11): packages/core/src/fragment-capabilities.ts, packages/cli/src/plan-component.ts
no-unused-vars (8): packages/cli/src/plan-writer-profile.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+2)
no-shadow (5): packages/core/src/components/Syntax.ts, packages/core/src/generated-xmd.ts
no-unnecessary-type-conversion (4): packages/core/src/syntax-reference.ts
no-useless-spread (3): packages/cli/tests/support/fake-acp.ts
no-console (3): packages/cli/src/cli.ts
no-unsafe-type-assertion (3): packages/cli/src/plan-component.ts, packages/cli/src/cli.ts
consistent-function-scoping (2): packages/cli/src/plan-writer-profile.ts, packages/cli/src/plan-component.ts
no-empty-function (2): packages/cli/src/testing-host.ts, packages/cli/src/cli.ts
no-array-sort (1): packages/core/src/syntax-reference.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.ts
no-unnecessary-type-assertion (1): packages/cli/tests/support/fake-acp.ts
no-floating-promises (1): packages/cli/src/cli.ts
consistent-return (1): packages/cli/src/cli.ts

Correctness

No extraneous code patterns detected.

)

The classification was applied to whichever error left the durable operation,
decided by a variable set inside it. That is wrong in two directions, and the
second one only appears on a continuation.

If the selection refused and *publication* of the result then failed, the error
leaving the operation was the persistence, journal or secret failure — and it
left wearing the classification. `<PlanInformation>` would have offered that
back as retry context and started another Agent turn on a run whose journal had
already stopped accepting entries.

On replay the executor is skipped, so the variable was never set. The retained
refusal was rebuilt without its class and without the non-enumerable mark, and
came back unclassified. The same request was recoverable live and terminal after
a partial continuation.

Both follow from signalling a durable fact out of band. The refusal is now
retained: core's own `SyntaxSelectionRefusal` becomes `{ refused }` in the
record, beside the unchanged `{ symbols }`, and the closed value is interpreted
after `createDurableOperation` returns — the same code on a live run and on a
replay. A publication failure throws before that interpretation, so a result
that was never recorded is never acted on. A provider that throws is not caught
at all and fails the operation, which is what keeps infrastructure terminal
whatever it names its error.

Existing successful records are untouched, so a history written before this
still reads as exactly what it meant.

`markGeneratedCandidate` and `generatedCandidateReason` become
`markGeneratedRequestRefusal` and `generatedRequestRefusal`, in
`generated-request-refusal.ts`. The old names read as core granting a Plan
permission to retry, which is not core's decision to make: core states the fact
that it refused the generated request, and `<Plan>` alone decides that a refused
request earns another turn. `<PlanInformation>` translates the fact into its
existing refused result, unchanged.

One existing case changed its premise rather than its expectation. `SYN31` held
that a refusal retains no record. It now retains one, deliberately — that is
what makes a replayed refusal mean what the live one meant — so the row asserts
the record is exactly `{ refused }` and that a continuation reaches the same
refusal. Its five malformed-list cases are unchanged and still retain nothing:
those refuse before the occurrence is claimed. `PC27` gained the shapes the
second alternative makes possible — both members present, a non-string refusal,
and an empty reason.

Both new controls were checked against the defect they exist for: with the
closure restored, the publication row reports the classification it must not
carry, and the continuation row calls the live provider it must not reach.
The classification was renamed but its prose was not. Core still described
itself as granting permission — "a failure a trusted host may offer the
candidate another chance at", "a mistake the candidate can correct", a private
helper called `candidate()` — which is the framing the rename was meant to
remove. Core knows one thing and should say only that: the generated request
itself was refused.

The `core/host` export documentation now states the fact and names its limits
explicitly: core says nothing about whether a refusal is correctable, whether a
host may ask again, or whether another turn should happen. `fragment-
capabilities.ts` renames `candidate()` to `refusedRequest()`, because what it
builds is a refusal of what the fragment asked for and not a verdict about who
can fix it. The two `generated-xmd.ts` comments that read as retry policy now
say which thing was refused instead.

`syntax-refusal.ts` was also describing a mechanism that no longer exists. It
still explained the closure variable the previous commit replaced, so it now
describes what actually happens: the refusal is recognized inside the durable
executor, retained as `{ refused }`, and re-raised when that value is read back
— which is why a refusal that could not be published is never interpreted.

Plan keeps its own voice, because the decision genuinely is Plan's.
`<PlanInformation>` remains "the only place in the workflow that turns a failure
back into another turn"; what changed is the one clause that had core answering
that question. It now asks core the factual question and states, in the same
breath, that turning a refused request into another turn is this workflow's
policy and this is where it is made.

Behavior, durable representation and record schema are unchanged; every edit
here is a comment, a doc block, or the name of one private helper and its two
local call sites.
…762)

"Authorship profile" and `<PlanAuthorship>` were generic names for one specific
thing: the constrained Agent and execution frame a Plan is written inside. The
generic name made every reader work out which authorship was meant — the frame,
or the drafting-and-review process that happens within it — and the two are not
the same subject.

The abstraction is now called the Plan writer throughout: the module, the
private component, its origin, the profile, frame, placement, policy,
observation, provider inputs, stack, roots, and the constants and helpers that
name any of them. No compatibility alias is left behind for any of the former
private names, so nothing can go on referring to the old abstraction by
accident.

Ordinary prose still says "authorship" where it means the process — a turn that
fails "ends authorship", the command "reports authorship as it happens", the
title requirement is "an authorship and human-review requirement". Those are
about drafting, repair, review and approval, which is what the word is for.

Two things were deliberately left alone and are worth naming. `AuthorshipFlags`
is the base of `AgentFlags`, which `xmd run` uses, so it describes agent
selection rather than the Plan writer; renaming it would leave `xmd run`
extending a plan-writer type. And `git-host`'s own `authorship()` is a commit's
author and committer, an unrelated word.

`<PlanAuthorship>` becoming `<PlanWriter>` changes two lines of `Plan.md` and
therefore its declared digest. That is accepted rather than worked around: this
branch already changes those bytes, so a continuation from previously shipped
source already crosses the stale-source boundary, and adding an alias, a dual
registration or a replay exception would create a second way to be that
Component. Every digest expectation recomputes the hash from the source file at
test time, so what needed updating was the private-name inventories rather than
any recorded constant. Public `<Plan>` keeps its name, origin and identity.

The retained-Syntax documentation was also still describing the payload as it
was before the durability correction. Four generic descriptions said an
occurrence retains exactly `{ symbols }`; the value has carried a second
alternative since a refusal became something the record distinguishes. They now
state the closed protocol — `{ symbols: string } | { refused: non-empty
string }`, exactly one member — along with what makes it closed: both
alternatives are durable values, the component interprets the value only after
publication succeeds, a retained refusal therefore means the same thing live and
on replay, a publication failure prevents interpretation and stays terminal, a
missing, additional, mistyped, empty or simultaneous member is stale input, and
existing `{ symbols }` histories remain readable.

Two statements that are specifically about `Plan.md`'s own bare `<Syntax />`
still say `{ symbols }`, because that occurrence names no component and cannot
produce a named-selection refusal. Both were reworded so they read as being
about that occurrence rather than as the protocol.

This extends one effect's closed payload description. It adds no durable record
and does not touch the generic durable-event envelope.
…load (#762)

Five identifiers still called the Plan writer "authorship". The frame's field
held the capability that installs a provider, and two commands held the settled
stack in a local of the same name, so reading any of them meant deciding which
authorship was meant — the writer, or the drafting the writer does.

`PlanWriterFrame.authorship` is now `PlanWriterFrame.planWriter`, with the
construction in `plan-component.ts` and the `frame.planWriter.installProvider`
call that reads it, plus the two locals in `agent-stack.ts` and `cli.ts`.
`planWriter` rather than `profile`, because the value is the capability that
installs a provider; `PlanWriterProfile` is the constrained execution profile
that results from it.

The two legitimate uses stay: `AuthorshipFlags` describes Agent selection shared
with `xmd run`, and `git-host`'s `authorship()` is a commit's author and
committer. So does every sentence using the word for the drafting, repair,
review and approval activity — including the `--agent-provider` help text, which
says "agent provider for Plan authorship" and is describing exactly that.

The Syntax suite's own documentation was also still promising more than the
payload delivers. It introduced the behavior as recording "exactly
`{ symbols }`", which stopped being true when a named selection refusal became
something the record carries. It now states the closed payload — one member,
either the rendered symbols or a refused selection — and says why the refusal is
a value rather than a failure: a continuation has to reach the same refusal it
reached live. `SYN19` exercises a bare occurrence that rendered, so its title now
says so instead of claiming to close the protocol.

No behavior, authority, classification or durable representation changes here,
and no compatibility alias is added.

@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 2 redundant comments. Inline suggestions to remove them below.

function* PlanInformation(): Operation<Json> {
// The child and its structured teardown have both settled by the time
// this returns, so what is scanned below is complete text and not a
// fragment still able to produce more.

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
// fragment still able to produce more.

return lines.slice(line + 1).join("\n");
}
}
// Opened and never closed: not an envelope, so nothing is removed.

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
// Opened and never closed: not an envelope, so nothing is removed.

@taras
taras merged commit 14c2307 into main Sep 9, 2026
38 checks passed
@taras
taras deleted the agent/issue-762-plan-loop branch September 9, 2026 22:30
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.

Add read-only XMD information requests to xmd plan

1 participant