🐛 Serve the plan review: type its schema, and open it as the host - #675
Conversation
`xmd plan` completed its turn and then ended at the review with
<WebForm> schema could not be compiled for the browser: strict mode:
missing type "object" for keyword "required" (strictTypes)
Compiling a schema for the browser takes it apart: RJSF's parser extracts `if`
and `then` and compiles each as a schema of its own. The review schema's two
branches reached `required` through the parent's `type`, so by the time they were
compiled they were typeless schemas, which strict mode refuses. The server,
compiling the whole schema in one piece, accepts them — the one shape the two
sides read differently.
Both branches now declare `type: "object"`. The web suite pins the rule in both
directions, the packaged-document suite pins that this document follows it, and
the spec says so where the preflight boundary is described.
Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
PR #675: 🐛 Serve the plan review: type its schema, and open it as the host9 files, +299 / -45 Scope✅ PR scope looks good. Structural✅ No structural bloat detected. Slop
Static AnalysisOxlint: 2 diagnostics across 1 file (2 rules) consistent-function-scoping (1): packages/cli/src/authorship-profile.ts CorrectnessFILE: packages/cli/src/authorship-profile.ts FILE: packages/cli/src/authorship-profile.ts |
`xmd plan` printed its review URL and then said it could not open it:
could not open a browser automatically (xmd plan asked for a command,
which the authorship profile grants to nothing). Open the URL above to
continue.
Opening a form runs `open`, `xdg-open` or `start`, and the profile refuses a
command to everything inside it — the same shape as the adapter install fixed in
#674. Showing a person the review is the host's act: the host's provider asking
the host's question about a URL the host is serving, decided by no document,
agent or authored element.
`FormOpener` is where that act already had a seam, so the profile composes around
it and runs the open in the scope the command was called in. Nothing else moves:
a file, a command, the network and a service stay refused, and a failed open is
still a warning printed beside a URL that stands on its own.
Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
…t alone
The two fixes for `xmd plan`'s capability refusals were written at their call
sites, one per act. This says the rule once instead.
`src/host-acts.ts` owns it: a profile refuses its document ambiently, a call
carries no mark saying who made it, and a provider's work happens inside the
document execution that reached it — core constructs the root provider from
`Execution.around({ document })` — so the scope has to come from the command,
which is the one party that exists before the ceiling does. A host takes its
scope and states which acts are its own; everything else stays refused.
The adapter install is now stated where a host states its dependencies,
`hostAcpDependencies(stack, host)`, so `xmd run` and `xmd plan` say the same
thing and the plan profile stops special-casing it. `xmd run` has no ceiling, so
there it changes nothing.
`refuseDocumentCapabilities()` moves onto a scope holding the document execution
and nothing else. No behavior depends on it — the acts that needed hoisting are
inside that execution either way — but the ceiling now covers what it claims to.
AE6 and PH1 are unchanged and still pass: they assert the outcome, so they hold
the refactor to the behavior the two fixes established.
Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
| // host's scope. Under a profile that refuses its document a command, that | ||
| // is the difference between installing the adapter and being refused as | ||
| // though the document had asked (src/host-acts.ts); under a host with no | ||
| // ceiling it changes nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // ceiling it changes nothing. |
| // on disk, and opening the review form — and both run a command, which the | ||
| // profile refuses to everything inside it. The refusals are installed on the | ||
| // scope below, so this one still answers with the capabilities the entrypoint | ||
| // gave this invocation. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // gave this invocation. |
| // document and Prompt semantics need a durable stream; nothing | ||
| // about writing a Plan needs a durable one, and `--journal` | ||
| // belongs to the Plan you approved rather than to the | ||
| // conversation that wrote it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // conversation that wrote it. |
| stream: new InMemoryStream(), | ||
| // No repository component search. What the document may name is | ||
| // what this profile declares, so a file in the caller's tree | ||
| // cannot answer for `<CheckDraft>`, `<Prompt>` or anything else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // cannot answer for `<CheckDraft>`, `<Prompt>` or anything else. |
The previous commit mostly moved code. `inScope` and the form-opener middleware changed file; the adapter binding moved into `hostAcpDependencies`, which then needed a `Scope` every caller but one passes for nothing; and `hostScope()` was a function whose body was `useScope()`. A module for two helpers with a single caller is premature, and a shared signature is the wrong place for a concern only the ceiling has. Both helpers go back beside their caller, carrying the prose that split was written to hold: why an ambient refusal cannot tell the parties apart, and why the wait is bound to a halt rather than decoupled the way `scope-eval` decouples. What that commit actually changed stays: the ceiling is installed on a scope holding the document execution and nothing else, and the spec says what the refusal can and cannot distinguish. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
| // on disk, and opening the review form — and both run a command, which the | ||
| // profile refuses to everything inside it. The refusals are installed on the | ||
| // scope below, so this one still answers with the capabilities the entrypoint | ||
| // gave this invocation. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // gave this invocation. |
| // the host's own acts from the document's on its own — a provider's work | ||
| // happens inside this execution, and `API.Process.exec` looks the same | ||
| // whichever party reached it — which is why those acts are stated above | ||
| // and run in the host's scope (src/host-acts.ts). |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and run in the host's scope (src/host-acts.ts). |
| stream: new InMemoryStream(), | ||
| // No repository component search. What the document may name is | ||
| // what this profile declares, so a file in the caller's tree | ||
| // cannot answer for `<CheckDraft>`, `<Prompt>` or anything else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // cannot answer for `<CheckDraft>`, `<Prompt>` or anything else. |
Why
Two defects stood between
xmd planand a working review, both found by runningthe command end to end after #674.
1. The review schema could not be compiled for the browser.
Compiling for the browser takes the schema apart — RJSF's parser extracts
if,thenand the rest and compiles each as a schema of its own. The review schema'stwo branches reached
requiredthrough the parent object'stype, so by thenthey were typeless schemas carrying
required, which strict mode refuses. Theserver, compiling the whole schema in one piece, accepts them: the one shape the
two validators read differently.
2. The browser could not be opened.
Opening a form runs
open,xdg-openorstart. The authorship profile refusesa command to everything inside it — the same shape as the adapter install fixed in
#674 — so a person had to copy the URL out of the terminal by hand.
What changes
Before:
xmd planends at the review with a schema error; with that fixed, itserves the form but leaves you to open it.
After: the review compiles, and the host opens it.
How it works
Showing a person the review is the host's act — the host's provider asking the
host's question about a URL the host is serving, decided by no document, agent or
authored element.
FormOpeneris where that act already had a seam, so theprofile composes around it rather than widening anything. A file, a command, the
network and a service stay refused to the document, and a failed open is still a
warning beside a URL that stands on its own.
How to verify it
packages/web/tests/compile.test.ts— "refuses a conditional branch that leanson its parent for a type" pins the rule in both directions: the untyped shape
fails naming the browser compile and
strictTypes, the typed one compiles andregisters more than one validator, which is the decomposition itself.
packages/cli/tests/plan-command-document.test.ts—C9runs the packageddocument and asserts both branches of the schema it emits declare
object.packages/cli/tests/plan-host-acts.test.ts—PH1drives the real command witha review that opens a form first, and asserts the platform's browser command was
reached with the form's URL. Its Process recorder is installed
at: "min"so theprofile's refusal still outranks it; at full strength it would answer for a
refused call too and pass against the defect.
C9fails on the document,PH1failswith the exact refusal above.
specs/web-form-spec.md,specs/acp-client-spec.mdandspecs/plan-command-spec.mdstate the branch-type rule and which acts theprofile's refusal does not cover.
All passed, plus
deno task checkanddeno task lint.Scope
Included
packages/cli/src/documents/plan-command.md.FormOpenerhoist in the authorship profile, and that Api's export from@executablemd/webso a host can compose around it.Intentionally unchanged
v0.10.2is still a draft release and the manifests onmainalready say
0.10.2, so both fixes belong in it.generator refuses is now documented rather than removed; making the two agree
is a change to
<WebForm>'s contract and belongs in its own issue.xmd runandxmd workflow, which grant their documents a command and refusedneither act.
Scope confirmation
Found by running
xmd planend to end after #674. Observability of that run istracked separately in #676.
https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7