Skip to content

🐛 Serve the plan review: type its schema, and open it as the host - #675

Merged
taras merged 4 commits into
mainfrom
agent/plan-review-schema
Aug 31, 2026
Merged

🐛 Serve the plan review: type its schema, and open it as the host#675
taras merged 4 commits into
mainfrom
agent/plan-review-schema

Conversation

@taras

@taras taras commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Why

Two defects stood between xmd plan and a working review, both found by running
the command end to end after #674.

1. The review schema could not be compiled for the browser.

$ xmd plan "ask me for my name and then write it out"
<WebForm> schema could not be compiled for the browser: strict mode: missing type
"object" for keyword "required" at "609c3087#" (strictTypes)

Compiling for the browser takes the schema apart — RJSF's parser extracts if,
then and the rest and compiles each as a schema of its own. The review schema's
two branches reached required through the parent object's type, so by then
they were typeless schemas carrying required, which strict mode refuses. The
server, compiling the whole schema in one piece, accepts them: the one shape the
two validators read differently.

2. The browser could not be opened.

http://127.0.0.1:58432/f/yd1cUByBFQ.../
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. The authorship profile refuses
a 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 plan ends at the review with a schema error; with that fixed, it
serves the form but leaves you to open it.

After: the review compiles, and the host opens it.

How it works

<Elicit schema> → installWebElicitation → parseDeclaration → compileForm
                                             → server: one piece, accepts
                                             → browser: parsed apart, each branch a root
                                       → announceForm → FormOpener.open
                                             → run in the scope xmd plan was called in

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 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 leans
    on 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 and
    registers more than one validator, which is the decomposition itself.
  • packages/cli/tests/plan-command-document.test.tsC9 runs the packaged
    document and asserts both branches of the schema it emits declare object.
  • packages/cli/tests/plan-host-acts.test.tsPH1 drives the real command with
    a 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 the
    profile's refusal still outranks it; at full strength it would answer for a
    refused call too and pass against the defect.
  • Each was checked by reverting its fix: C9 fails on the document, PH1 fails
    with the exact refusal above.
  • specs/web-form-spec.md, specs/acp-client-spec.md and
    specs/plan-command-spec.md state the branch-type rule and which acts the
    profile's refusal does not cover.
deno task test packages/cli/tests/plan-command-document.test.ts \
  packages/web/tests/compile.test.ts packages/cli/tests/plan-cli.test.ts \
  packages/cli/tests/plan.test.ts packages/cli/tests/plan-host-acts.test.ts

All passed, plus deno task check and deno task lint.

Scope

Included

  • The two branch types in packages/cli/src/documents/plan-command.md.
  • The FormOpener hoist in the authorship profile, and that Api's export from
    @executablemd/web so a host can compose around it.
  • Three tests and the three spec sentences.

Intentionally unchanged

  • No version bump: v0.10.2 is still a draft release and the manifests on main
    already say 0.10.2, so both fixes belong in it.
  • The compile boundary itself. That the server accepts a schema the browser
    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 run and xmd workflow, which grant their documents a command and refused
    neither act.

Scope confirmation

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

Found by running xmd plan end to end after #674. Observability of that run is
tracked separately in #676.

https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7

`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
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

PR #675: 🐛 Serve the plan review: type its schema, and open it as the host

9 files, +299 / -45

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

  • packages/cli/src/agent-stack.ts:126 (removed)
  • packages/cli/src/authorship-profile.ts:152// gave this invocation.
  • packages/cli/src/authorship-profile.ts:195// and run in the host's scope (src/host-acts.ts).
  • packages/cli/src/authorship-profile.ts:210// cannot answer for , or anything else.

Static Analysis

Oxlint: 2 diagnostics across 1 file (2 rules)
Density: 0.007 violations/added-line

consistent-function-scoping (1): packages/cli/src/authorship-profile.ts
no-base-to-string (1): packages/cli/src/authorship-profile.ts

Correctness

FILE: packages/cli/src/authorship-profile.ts
PATTERN: wrapper indirection
CONCERN: openFormsThroughHost wraps FormOpener.around with a thin scope delegation, adding no additional logic beyond forwarding.
QUESTION: Is this wrapper necessary, or could it be inlined to reduce indirection?

FILE: packages/cli/src/authorship-profile.ts
PATTERN: signal cluster
CONCERN: Multiple Oxlint violations (consistent-function-scoping, no-base-to-string) in the same file.
QUESTION: Are these violations indicative of unreviewed generated code or style issues that should be addressed?

`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
@taras taras changed the title 🐛 Type the review schema's conditional branches so the form can compile 🐛 Serve the plan review: type its schema, and open it as the host Aug 31, 2026
…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

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

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

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

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

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

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

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

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

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
// cannot answer for `<CheckDraft>`, `<Prompt>` or anything else.

@taras
taras enabled auto-merge (squash) August 31, 2026 03:38
@taras
taras merged commit d115501 into main Aug 31, 2026
30 checks passed
@taras
taras deleted the agent/plan-review-schema branch August 31, 2026 03:45
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.

1 participant