👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

👀 Report xmd plan authorship while it happens - #754

Merged
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability
Sep 3, 2026
Merged

👀 Report xmd plan authorship while it happens#754
taras merged 4 commits into
mainfrom
agent/issue-676-plan-observability

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#676.

Why

xmd plan takes minutes, asks you one question somewhere in the middle, and
until now said nothing at all about what it was doing. There was no way to tell
a slow Agent turn from a stuck one, no way to see the draft that was rejected,
and nothing to attach to a bug report when planning went wrong.

What changes

Before:

$ xmd plan "prepare the release program."
# … minutes of silence, then a browser form appears, thensource on stdout

After:

$ xmd plan "prepare the release program."> release.md## Preparing the PlanGetting the available XMD components and constructs and setting up the planning session.## Drafting the PlanThe coding agent is turning your request into an XMD program.This is the 1st of up to 10 attempts.## Checking the draftChecking that it is valid XMD and uses the available components and constructs correctly.## Waiting for your reviewReview the draft and choose what should happen next.## Finalizing the PlanClosing the planning session and producing the final Plan from the approved draft.

Every phase is announced before the work it names, so what you read is what
is happening rather than an account of what already finished. It all goes to
stderr; release.md above holds the approved source and nothing else.

Two long-only options come back, describing authorship and nothing after it:

  • --verbose adds every cleared draft and every failed check's structured
    findings;
  • --journal <path> exclusively creates a diagnostic JSONL record of the
    invocation, in the existing serializeDurableEvent() format.

-V and -j stay unsupported — they are xmd run's aliases for options about
a program's run — and are refused by naming the long spelling.

How it works

xmd plan → packaged command adapter → <Plan> → <PlanProgress> → DocumentOutput → stderr
↘ <Plan as="approved"> → stdout / --output

The phases are authored in packages/cli/src/documents/Plan.md, beside the
operations they precede, and written through a new private paired
<PlanProgress>. It renders its content, sends it through the current
DocumentOutput operation as ordinary prose, and returns the empty string.

Returning it instead would put a phase heading inside <Plan as="approved">'s
capture and inside the declaration's exact-source disposition — contaminating
the approved program and bypassing the presentation every other line of progress
gets. Which surface is asking and whether --verbose was written are sealed host
facts the declaration carries, so an ordinary <Plan> in someone's document
announces nothing and expands no progress body at all.

The command root's rendered transcript is that progress, so the adapter loses
its explanatory prose: its body is now two elements. The syntax catalog moves
behind <PlanInputs>, which is what lets a phase precede the preparation it
names — before this, no authored phase could run before the catalog was built.

Review guide

Start with:specs/plan-command-spec.md § "Watching a Plan being written"
and § "The --journal file"

Then review:

  1. packages/cli/src/documents/Plan.md — every phase a person reads, and the
    two bounds every loop, condition and ordinal is derived from
  2. packages/cli/src/plan-component.ts<PlanProgress>, the fifth private
    capability, and the sealed verbose fact
  3. packages/cli/src/authorship-profile.ts — the output lifecycle:
    normalization, conditional terminal formatting, the host-selected stream, and
    the in-scope drain
  4. packages/cli/src/plan-journal.ts — exclusive creation, the JSONL passthrough
    and the three diagnostics
  5. packages/cli/src/plan-args.ts, src/cli.ts — grammar, help, the stderr
    writer and the TTY fact

Look carefully at:

  • the drain in runPlanCommandDocument: consumption is inside the scope that
    owns the execution, so a stderr that stops accepting bytes cancels the live
    turn and waits for provider, Prompt, Elicitation, session-directory and
    execution teardown before anything is reported — and gets no stdout fallback
  • journalRefusal() in plan-journal.ts: a refused append is reported from the
    cause chain, because the durable runtime wraps it in its own message

What must stay true

  • Progress never enters either artifact sink. — enforced by
    <PlanProgress> returning "" and writing through DocumentOutput, checked
    by PO4 (both ordinary forms) and PO6 (stdout and --output byte-identical).
  • An ordinary <Plan> announces nothing. — enforced by the sealed surface
    discriminator, checked by PO4, which uses the command surface as its control.
  • Nothing displays before its durable event commits. — enforced by the
    existing serialized pre-append gate under secretDetection: true, checked by
    PO10/PO11, each with a clean-value control.
  • Make xmd plan produce approved XMD source only #724's three structural checks and their ordering. — one validate
    dependency asked by <CheckDraft>, <AdmitPlan> and the final host gate,
    checked by the unchanged PS6PS9 and by PO14.
  • No progress phase claims delivery. — checked by PO14, which asserts
    Finalizing is the last phase and that no delivery word appears.

How to verify it

  • PO1 proves phases precede their work — including an early phase observed
    while an Agent turn is still blocked, so it fails if the transcript is
    buffered into an end-of-run summary.
  • PO2 proves the ordinals come from the loop bounds; it fails if a counter is
    off by one or a sentence carries its own number.
  • PO5 proves default progress excludes the request, drafts, diagnostics and
    approved source, and that verbose adds exactly two blocks in phase order.
  • PO8/PO9/PO12/PO16 cover the journal: created before any authorship,
    the existing JSONL in commit order, both creation refusals byte-exact, an
    append failure preserving its committed prefix, and an ordinary failure
    leaving a wholly parseable file.
  • PO13 proves a failed stderr cancels the live turn and completes teardown; it
    fails if consumption is detached or if a stdout fallback appears.

I checked these are not vacuous by breaking the behavior they cover: making
<PlanProgress> return its content turns PO4 red; an eager catalog turns
PO15 red; secretDetection: false turns PO10/PO11 red; dropping three
bytes per journal record turns PO8/PO12/PO16 red.

Every phase block and both terminal diagnostics were also compared byte for byte
against the rendered transcript of the packaged document.

Scope

Included

  • Progressive stderr reporting of every authorship phase, with counters derived
    from the two bounds Plan.md binds once.
  • --verbose and --journal <path>, long spellings only, with help copy and
    the journal warning.
  • Lazy catalog construction behind <PlanInputs>, and an output-free command
    adapter.
  • architecture.md, specs/plan-command-spec.md and
    specs/executable-mdx-spec.md updated for all of it.

Intentionally unchanged

  • Shared core output and secret middleware. The existing boundary supports this
    integration; nothing outside packages/cli, architecture.md and specs/
    is touched.
  • Every option xmd plan removed in Make xmd plan produce approved XMD source only #724--run, the root properties,
    --raw, the exec and fetch deadlines, the permission flags and both
    secret-detection spellings — stays removed, with its exact refusal.
  • --session --verbose has the same value-swallowing shape as the --journal
    case fixed here. It predates this work and belongs to Make xmd plan produce approved XMD source only #724's grammar, so it is
    left alone rather than widening this change.
  • The unexpected-no-Plan fallback keeps its packaged-source assertion rather
    than gaining an artificial runtime path; reaching it needs a review answer the
    schema forbids.

New abstractions

  • <PlanProgress> exists because progress must be a side effect of the command's
    own output rather than something <Plan> produces — its content is used at
    eleven authored sites in Plan.md.
  • ProgressOutput exists because only the entrypoint knows whether its stderr
    is a terminal and whether a write landed; it is implemented once in cli.ts
    and once in the test harness.
  • packages/cli/src/plan-journal.ts exists because the journal diagnostics are
    this command's own words, while the bytes stay xmd run's format.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • A journal can contain prompts, drafts and review answers. Secret detection
    checks every entry before it is recorded, but it may not catch every sensitive
    detail. Help says so, in the exact words the contract settled.
  • After a failed journal append, the guaranteed evidence is the sequence
    committed before it.
    An appending write is not a transaction, so a
    filesystem failing partway can leave part of a record. The specification says
    this plainly rather than promising atomicity; making it stronger would take a
    protocol Make xmd plan authorship observable during and after an invocation #676 does not need.
  • Colour on a terminal is chalk's decision about the stream it writes to, so
    it is absent in a test process. PO6 therefore asserts the colour-independent
    half — a fenced block rendered as indented text — rather than escape codes.
  • Recovery or rollback: the change is additive to xmd plan; reverting the
    three commits restores Make xmd plan produce approved XMD source only #724's behavior exactly.

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.

`xmd plan` took minutes, asked one question in the middle and said nothing
about what it was doing. Now the packaged command root reports itself: one
Markdown phase announced on stderr before each piece of work — preparing,
drafting, checking, repairing, waiting for review, revising, finalizing,
stopping, and the ending a tenth unrepaired draft gets.
The phases are authored in `Plan.md`, beside the operations they precede, and
written through a new private paired `<PlanProgress>`. It renders its content,
sends it through the current document-output operation, and returns the empty
string — returning it would put a phase heading inside `<Plan as>`'s capture and
inside the declaration's exact-source disposition. Which surface is asking and
whether `--verbose` was written are sealed host facts, so an ordinary `<Plan>`
announces nothing and expands no progress body at all.
`Plan.md` now binds its two bounds once and derives every loop bound, condition
and sentence from them, so the ordinals a person reads cannot drift from the
counters the workflow keeps.
The catalog moves behind `<PlanInputs>`, which is what lets a phase precede the
preparation it names; the command adapter loses its explanatory prose and its
redundant `syntax` prop, because that root's transcript is now what an operator
reads.
Two long-only options come back, describing authorship and nothing after it:
`--verbose` adds every cleared draft and every failed check's structured
findings, and `--journal <path>` exclusively creates a diagnostic JSONL record
of the invocation. `-V` and `-j` stay unsupported and are refused by naming the
long spelling. Every event crosses the existing serialized pre-append secret
gate, so a rejected draft or diagnostic reaches neither the progress nor the
file, and the prefix before it stays readable.
The host owns the stream and the terminal: normalization always, terminal
formatting only when the entrypoint states its own stderr is one, and the
transcript drained inside the scope that owns the execution — so a destination
that stops accepting bytes cancels the live turn, waits for every owned
teardown, and gets no stdout fallback. Stdout and `--output` are untouched by
all of it.
Closes#676
`xmd plan "do it" --journal --verbose` passed fixed grammar: `--verbose` was
consumed as the path, so the command exclusively created a file called
`--verbose` and dropped the verbosity that was asked for. A token naming an
option this command defines is now that option rather than a filename, and the
value is refused with the sentence `--journal` already had. Only that position
changes — `--journal <path> --verbose`, `--verbose --journal <path>` and the
inline `--journal=` form all parse as before, and a removed spelling written
there keeps its own more specific refusal.
Beside it, PO16 pins what the file holds when an invocation ends for a reason of
its own: a failed turn after an entry has committed exits non-zero, delivers no
source and no artifact, completes teardown, and leaves a file whose bytes are
exactly the entries it took, re-serialized — so a truncated or unterminated
trailing record is caught rather than assumed absent. The specification now
states readable-prefix retention for all four endings: approval, an ordinary
failure, a refused entry and a secret rejection.
Two claims were wider than what the product does.
PO7 asserted that every retained option written where the `--journal` path goes
refuses. `--help` and `-h` are lifted out of the command line before any
command's own grammar runs, so `xmd plan "do it" --journal --help` prints help
and exits successfully — and it should: pre-help refusal is reserved for the
options this command removed, and a caller who asked for help beside an
incomplete option is asking for help. The missing-value check stays where it is.
The unit list now holds only the spellings that reach this grammar, and the
command-level row proves both halves: each of those spellings refuses before any
catalog, session, provider, filesystem or artifact work, and help answers instead
while still creating no journal and beginning no authorship.
The durability prose claimed a complete, wholly parseable file after every
ending, including a failed append. An appending write is not a transaction, so
that is not this command's to promise. Success, an ordinary failure and a secret
rejection each leave a wholly parseable file because no append failed; after a
failed append the guaranteed evidence is the sequence committed before it, and
the specification now says so and says why making it stronger would take an
atomic protocol #676 does not need. PO12 reads terminated records accordingly,
and keeps its subject: the preserved prefix and the approved diagnostic. PO16 is
unchanged and still proves byte-exact reserialization with no partial trailing
record, where no append failed.
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #754: 👀 Report xmd plan authorship while it happens

23 files, +2630 / -261

Scope

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

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

🟡 23 files changed. Are all changes related?

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 31 diagnostics across 5 files (10 rules)
Density: 0.012 violations/added-line

unbound-method (8): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+1)
no-unused-vars (7): packages/cli/src/plan.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts (+2)
no-base-to-string (5): packages/cli/src/plan-component.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 (1): packages/cli/src/authorship-profile.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.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 `smoke` job asserts what `xmd plan --help` says from `dist/xmd`, and its list
of options that must not appear still held `--journal` and `--verbose`. This
stack gives the command both, so the check was pinning the grammar it replaced.
It shells out to a binary, so no module graph connects it to the change and
affected-test selection could not have found it — which is what the run
demonstrated: every deno, node and bun shard passed while this one step failed.
The list now names `--run`, `--props`, `--raw`, `--deny-all` and the two short
aliases, matched as whole tokens because `-j` is a substring of the `--journal`
this command does define. Beside it the step now asserts the two options and the
journal warning *are* in the compiled help, so the boundary that would drop them
from a release is the one that reports it. `PlanProgress` joins the private names
no build lets a document write, here and in the npm bin's equivalent check.
@taras
taras merged commit 42732dd into mainSep 3, 2026
38 checks passed
@taras
taras deleted the agent/issue-676-plan-observability branch September 3, 2026 17:43
Sign up for freeto 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 xmd plan authorship observable during and after an invocation

1 participant

@taras