Skip to content

✨ Add xmd prompt, the authorship command (#260) - #657

Draft
taras wants to merge 2 commits into
mainfrom
agent/issue-260-prompt-command
Draft

✨ Add xmd prompt, the authorship command (#260)#657
taras wants to merge 2 commits into
mainfrom
agent/issue-260-prompt-command

Conversation

@taras

@taras taras commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Closes #260.

Why

xmd could run a document and write one, but not ask for one. Getting a working
executable 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:

$ xmd prompt "ask me for my age and write the result to a file"
error: unrecognized command

After:

$ xmd prompt "ask me for my age and write the result to a file"
# asks the configured agent, validates the reply without running it,
# repairs definite defects, shows you the exact source, and runs what you approve

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. --save writes the approved bytes
exclusively before execution.

How it works

One ordered ownership chain. No phase after the first failure begins, and only
the final execution owns -j:

fixed CLI preflight → structured syntax catalog → fresh generator session
  → candidate props + non-executing validation → automatic repair (≤3)
  → live approval or human revision → generator teardown
  → optional exclusive save → ordinary in-memory document execution

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:

  1. packages/cli/src/prompt-args.ts — pure argv grammar; the request/property
    boundary and option-signature freezing, decidable before any document exists.
  2. packages/cli/src/prompt.ts — generation, validation, repair budget, review.
  3. packages/cli/src/agent-stack.ts — the one resolved Agent configuration and
    its two consumers.
  4. packages/cli/src/cli.ts — dispatch, deadline, save and execution wiring.

Look carefully at:

  • The candidate-versus-caller split in prompt.ts (CandidateOutcome). Getting
    it backwards either teaches the agent about the caller's mistakes or blames the
    caller for the agent's.
  • Option signature freezing in prompt-args.ts. The comparison happens before
    token extraction, so a boolean that a later draft redeclares as a value option
    cannot reach forward and consume the --raw written after it.
  • Scope ownership in runPrompt: the generator lives and dies inside one
    scoped, so a teardown failure is raised before the save and execution that
    would otherwise already have happened.

What must stay true

  • Authorship creates no journal. Generation, repairs, review and abort never
    enter -j — enforced by creating the journal only when execution starts, and
    checked by P13, which asserts an approved run's journal holds __root__ and
    the document body and none of the request, agent_prompt or elicit.
  • The approved bytes are the agent's close value, unmodified. No fence
    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.
  • Only a completed terminal turn yields a candidate. A failed or cancelled
    turn discards partial text — checked by P9.
  • One Agent configuration per invocation. DEFAULT_AGENT_NAME is read once
    and the settled value serves both generation and the executed document —
    enforced by DocumentMode carrying the resolved AgentStack rather than the
    flags, and checked by a read-counting environment.
  • The executed program inherits nothing from the generator. Only the
    generator provider receives newSessionOptions.systemPrompt, and its scope is
    closed before execution begins.

How to verify it

Frozen acceptance is the P1–P16 table in specs/prompt-command-spec.md. Every
row is driven through deterministic in-process seams — a scriptable ACP runtime,
a scripted Elicitation handler, 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:

deno task test packages/cli/tests/prompt-args.test.ts packages/cli/tests/prompt.test.ts packages/cli/tests/prompt-cli.test.ts
deno task test --changed=origin/main

Results at edb680ff: three prompt files 3 passed (25 steps); branch-level
selection 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 lint and
deno task check are 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:

  • Reverting the failure reporter gives - tests failed: … / + 1 test(s) failed ….
  • Removing the -e guard fails the boundary test — before the fix the command
    reached the ACP provider and reported an agent as unavailable.
  • Adding a second resolveAgentStack call fails the read-count assertion.

Scope

Included

  • One positional request, the complete run -e execution flag set, and --save.
  • One fresh configured ACP generator session with the run profile's complete
    structured syntax catalog as its system instruction layer.
  • Per-candidate root property binding, signature freezing, and non-executing
    validation.
  • Three automatic repair turns per initial or human-revised candidate.
  • Live approve / revise / abort, with unbounded human revision rounds.
  • Exclusive save and ordinary supplied-source execution under <prompt>.

Intentionally unchanged

  • <Prompt>, <Elicit>, <Loop>, <Return> and Session semantics. Nothing in
    core changed to accommodate this command.
  • No stdin or editor input, and no terminal Elicitation provider.
  • No runtime-failure repair: a document that fails at run time does not return to
    generation or review. --save has already completed and is there to hand-edit.
  • No generation or approval journaling, and no replay of authorship.
  • No fence stripping or response cleanup of any kind.
  • -e/--eval stays exclusive to xmd run.

New abstractions

  • packages/cli/src/prompt-args.ts exists because the request/property boundary
    is 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.ts exists because run and prompt take the
    same 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.ts exists because both commands end in one ordinary
    document execution, and a person reading either has no reason to see the same
    failure worded two ways. Extracted rather than exported from cli.ts, which
    prompt.ts importing would have made a cycle.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • Phase leakage and argv reinterpretation are the two largest risks. Each
    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 prompt must be the first argument, exactly as xmd workflow must —
    configliere reads xmd --raw prompt "x" as the default run command with path
    prompt. The dispatch carries an explicit refusal for that case.
  • xmd run flag errors now surface slightly earlier. Because both commands
    share DocumentMode, the Agent stack resolves at dispatch rather than inside
    the document scope. The message text is unchanged and agent-cli.test.ts
    passes, but this is a timing change on run, not only on prompt.
  • P8's "one exact session object" is proven by consequence, not identity
    one session key across an invocation's turns, a different key per invocation.
    The ACP fake cannot observe the JavaScript identity of the Session value.

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.

https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6

taras added 2 commits August 30, 2026 06:28
`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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// reaches <Session.Launch> under any of them refuses instead of spawning.

Comment thread packages/cli/src/cli.ts
},
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// so this field is never the source of the document.

Comment thread packages/cli/src/cli.ts
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// decided once, for the whole invocation.

Comment thread packages/cli/src/cli.ts
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// is one validation resolves.

*installElicitation() {
yield* Elicitation.around(
{
// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

{ at: "min" },
);
},
// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// filesystem must reach the caller's, or fail.

@github-actions

Copy link
Copy Markdown

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.

Structural

Oxlint structural signals:

  • no-unnecessary-type-arguments ×10: packages/cli/src/props.ts
  • no-unused-vars ×2: packages/cli/src/cli.ts
  • no-empty-function ×1: packages/cli/src/cli.ts
  • no-unnecessary-type-assertion ×1: packages/cli/tests/support/fake-acp.ts

Slop

  • packages/cli/src/agent-stack.ts:111// what it configured, which no document is composing around.
  • packages/cli/src/agent-stack.ts:123// reaches <Session.Launch> under any of them refuses instead of spawning.
  • packages/cli/src/cli.ts:232// so this field is never the source of the document.
  • packages/cli/src/cli.ts:943// decided once, for the whole invocation.
  • packages/cli/src/cli.ts:1264// supplied one would otherwise watch it generate a different one instead.
  • packages/cli/src/cli.ts:1905// --default-agent, one DEFAULT_AGENT_NAME and one permission mode.
  • packages/cli/src/cli.ts:1933// none, and nothing downstream reads a profile to find out which.
  • packages/cli/src/prompt.ts:209// is one validation resolves.
  • packages/cli/tests/support/prompt-harness.ts:90// deno-lint-ignore require-yield
  • packages/cli/tests/support/prompt-harness.ts:109// deno-lint-ignore require-yield
  • packages/cli/tests/support/prompt-harness.ts:131// deno-lint-ignore require-yield
  • packages/cli/tests/support/prompt-harness.ts:138// filesystem must reach the caller's, or fail.

Oxlint slop signals:

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

Static Analysis

Oxlint: 28 diagnostics across 5 files (12 rules)
Density: 0.008 violations/added-line

no-unnecessary-type-arguments (10): packages/cli/src/props.ts
no-useless-spread (3): packages/cli/tests/support/fake-acp.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/tests/support/fake-acp.ts, packages/cli/tests/support/prompt-harness.ts
no-unused-vars (2): packages/cli/src/cli.ts
no-array-sort (1): packages/cli/src/props.ts
no-thenable (1): packages/cli/src/prompt.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unnecessary-type-assertion (1): packages/cli/tests/support/fake-acp.ts
no-unsafe-type-assertion (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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add xmd prompt: generate an executable markdown program from a request, approve it, run it

1 participant