✨ Add xmd prompt, the authorship command (#260) - #657
Conversation
`xmd prompt "<request>"` asks the configured ACP agent for a complete executable Markdown root, validates it without executing any of it, repairs its definite defects by asking again, shows a person the exact bytes, and runs the approved ones through the ordinary supplied-source path under the `<prompt>` identity. It adds authorship around one normal document; it adds no second execution model, props model or journal. Authorship sits outside durability. The catalog, the fresh generator session, every repair and the review create no journal and replay nothing, and the generator's scope closes before the optional exclusive `--save` and before execution. A defect the draft authored is repairable and earns one of three turns; a defect the command line authored terminates without spending a turn on something the agent cannot fix. `xmd run` and `xmd prompt` now share one execution field set, one resolved Agent configuration, and one props source resolution. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
Three review findings on the xmd prompt command. An inline document reached the generator. `preparePropsPhase` skips the parse that carries the other commands' `-e` refusal, so a supplied document was dropped in silence and the command generated a different one. Refused in prompt's own branch, before the catalog, the provider, the review, the save, the journal or any execution. The agent configuration was resolved twice, once for generation and again for the document, so one command line had two chances to read DEFAULT_AGENT_NAME differently. A dispatch now settles it once and hands the resolved value to both consumers; DocumentMode carries the settled stack rather than the flags. The executed program still gets a fresh ordinary provider and inherits neither the generator session nor its system prompt. A document failing its own <Testing> boundary printed a bare message where a run prints a `tests failed:` heading. reportFailure moves to a module both commands share. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
|
|
||
| // The trusted host selects its own root provider by name. Document-level | ||
| // selection goes through the installation protocol; this is the host saying | ||
| // what it configured, which no document is composing around. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what it configured, which no document is composing around. |
| yield* installPermissionMode(permissionMode); | ||
| // `xmd run` is the one command that has a terminal to give away. Help, | ||
| // document inspection and `xmd test` install no launcher, so a document that | ||
| // reaches <Session.Launch> under any of them refuses instead of spawning. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reaches <Session.Launch> under any of them refuses instead of spawning. |
| }, | ||
| // Declared so `xmd run --help` lists it with every other option. The value is | ||
| // lifted out of argv by readEvalFlags before parsing — see eval-source.ts — | ||
| // so this field is never the source of the document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // so this field is never the source of the document. |
| // The same object generation was configured from. The provider it | ||
| // installs is a fresh ordinary one — the generator's scope is already | ||
| // gone — but which agent it defaults to and what it may do were | ||
| // decided once, for the whole invocation. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // decided once, for the whole invocation. |
| // Refused here rather than with the other commands' inline refusal below, | ||
| // because that one is reached through the parse this branch exists to skip. | ||
| // An inline document is what `xmd prompt` sets out to write, so a caller who | ||
| // supplied one would otherwise watch it generate a different one instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // supplied one would otherwise watch it generate a different one instead. |
| yield* deps.installElicitation(); | ||
| // The same vocabulary the catalog just described. Validation and the | ||
| // catalog read one registry, so a component the generator was told about | ||
| // is one validation resolves. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is one validation resolves. |
| *installElicitation() { | ||
| yield* Elicitation.around( | ||
| { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| { at: "min" }, | ||
| ); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| return yield* scoped(function* () { | ||
| yield* ensure(() => rm(dir, { recursive: true, force: true })); | ||
| yield* API.Env.around({ | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| }); | ||
| // The provider `API.Files` has no host default for, installed exactly where | ||
| // the runtime entrypoint installs it: a document that reaches the | ||
| // filesystem must reach the caller's, or fail. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // filesystem must reach the caller's, or fail. |
PR #657: ✨ Add xmd prompt, the authorship command (#260)18 files, +3550 / -151 Scope🔴 PR has 3701 lines changed. Split into focused PRs. 🟡 3701 lines changed. PRs under 400 receive more thorough review. 🟡 Changes span 6 directories. StructuralOxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 28 diagnostics across 5 files (12 rules) no-unnecessary-type-arguments (10): packages/cli/src/props.ts CorrectnessNo extraneous code patterns detected. |
Closes #260.
Why
xmdcould run a document and write one, but not ask for one. Getting a workingexecutable Markdown root out of an agent meant pasting a request into a chat,
copying the reply into a file, running it, and repeating that by hand until it
worked — with nothing checking that the reply was a valid document before it ran.
This adds the authorship loop as a command. It wraps one ordinary document; it
does not add a second execution model.
What changes
Before:
After:
The command asks the configured ACP agent for a complete root, validates it
without executing it, repairs definite defects automatically (at most three
turns per draft), presents the exact bytes for a live approve / revise / abort
decision, and executes the approved source through the ordinary supplied-root
path under the
<prompt>identity.--savewrites the approved bytesexclusively before execution.
How it works
One ordered ownership chain. No phase after the first failure begins, and only
the final execution owns
-j:Two kinds of failure are told apart throughout, because they have different
remedies. A candidate failure is something the agent wrote, so the agent is
asked again with the structured validation facts. A caller failure is
something the command line or environment said, so it terminates — teaching the
agent about it would spend a repair turn on a defect the agent cannot fix.
Review guide
Start with:
specs/prompt-command-spec.md— the complete observable contract.Then review:
packages/cli/src/prompt-args.ts— pure argv grammar; the request/propertyboundary and option-signature freezing, decidable before any document exists.
packages/cli/src/prompt.ts— generation, validation, repair budget, review.packages/cli/src/agent-stack.ts— the one resolved Agent configuration andits two consumers.
packages/cli/src/cli.ts— dispatch, deadline, save and execution wiring.Look carefully at:
prompt.ts(CandidateOutcome). Gettingit backwards either teaches the agent about the caller's mistakes or blames the
caller for the agent's.
prompt-args.ts. The comparison happens beforetoken extraction, so a boolean that a later draft redeclares as a value option
cannot reach forward and consume the
--rawwritten after it.runPrompt: the generator lives and dies inside onescoped, so a teardown failure is raised before the save and execution thatwould otherwise already have happened.
What must stay true
enter
-j— enforced by creating the journal only when execution starts, andchecked by P13, which asserts an approved run's journal holds
__root__andthe document body and none of the request,
agent_promptorelicit.stripping, no substring extraction — checked by P12, which fences a candidate
containing a five-backtick run with six and asserts execution receives the
close value byte for byte.
turn discards partial text — checked by P9.
DEFAULT_AGENT_NAMEis read onceand the settled value serves both generation and the executed document —
enforced by
DocumentModecarrying the resolvedAgentStackrather than theflags, and checked by a read-counting environment.
generator provider receives
newSessionOptions.systemPrompt, and its scope isclosed before execution begins.
How to verify it
Frozen acceptance is the P1–P16 table in
specs/prompt-command-spec.md. Everyrow is driven through deterministic in-process seams — a scriptable ACP runtime,
a scripted
Elicitationhandler, a temporary contextual cwd. No live agent,browser or network is in the evidence. Refusals are proven by tripwires that
stayed at zero, never by output nobody produced: a command that printed nothing
and still opened a session would pass such a check.
packages/cli/tests/prompt-args.test.ts— P1–P6, pure grammar and signatures.packages/cli/tests/prompt.test.ts— P5–P12, generation, validation, review.packages/cli/tests/prompt-cli.test.ts— P2, P4, P13–P16, command lifecycle,filesystem, journal and execution.
Run:
Results at
edb680ff: three prompt files3 passed (25 steps); branch-levelselection
28 passed (154 steps); the seven existing boundary files(
agent-config,agent-cli,run-timeouts,props-cli,props-sources,cli-help,syntax-cli)9 passed (87 steps).deno task lintanddeno task checkare clean. All zero failures.Three regressions in the second commit were each verified to fail without their
fix, since a regression that passes either way proves nothing:
- tests failed: … / + 1 test(s) failed ….-eguard fails the boundary test — before the fix the commandreached the ACP provider and reported an agent as unavailable.
resolveAgentStackcall fails the read-count assertion.Scope
Included
run -eexecution flag set, and--save.structured syntax catalog as its system instruction layer.
validation.
<prompt>.Intentionally unchanged
<Prompt>,<Elicit>,<Loop>,<Return>and Session semantics. Nothing incore changed to accommodate this command.
generation or review.
--savehas already completed and is there to hand-edit.-e/--evalstays exclusive toxmd run.New abstractions
packages/cli/src/prompt-args.tsexists because the request/property boundaryis decidable from fixed grammar alone, and proving every refusal is effect-free
requires a pure function over argv that reads no Context Apis and touches no disk.
packages/cli/src/agent-stack.tsexists becauserunandprompttake thesame Agent, permission and provider options and must not reach different
conclusions from one command line. Two concrete consumers: run installation and
prompt generation.
packages/cli/src/report.tsexists because both commands end in one ordinarydocument execution, and a person reading either has no reason to see the same
failure worded two ways. Extracted rather than exported from
cli.ts, whichprompt.tsimporting would have made a cycle.Risks and limitations
phase sits behind a returned value rather than a flag another phase reads, and
tests assert later-phase tripwires stay untouched on every refusal.
xmd promptmust be the first argument, exactly asxmd workflowmust —configliere reads
xmd --raw prompt "x"as the defaultruncommand with pathprompt. The dispatch carries an explicit refusal for that case.xmd runflag errors now surface slightly earlier. Because both commandsshare
DocumentMode, the Agent stack resolves at dispatch rather than insidethe document scope. The message text is unchanged and
agent-cli.test.tspasses, but this is a timing change on
run, not only onprompt.one session key across an invocation's turns, a different key per invocation.
The ACP fake cannot observe the JavaScript identity of the
Sessionvalue.Scope confirmation
https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6