Skip to content

🐛 Prepare the plan profile's adapter through the host, not the document - #674

Merged
taras merged 2 commits into
mainfrom
agent/issue-672-plan-preparation
Aug 31, 2026
Merged

🐛 Prepare the plan profile's adapter through the host, not the document#674
taras merged 2 commits into
mainfrom
agent/issue-672-plan-preparation

Conversation

@taras

@taras taras commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Why

v0.10.1 shipped #673's adapter overlay, and xmd plan now fails earlier than it
did before:

$ xmd plan "ask me for my name and then write it out"
xmd plan asked for a command, which the authorship profile grants to nothing

Materializing an embedded adapter runs npm install. The authorship profile
refuses a command to everything inside it, and #673 put the preparation inside
that scope, so the refusal fires at the provider's availability probe — before
any turn, for both providers.

What changes

Before: the first xmd plan of any invocation that resolves Codex or Claude ends
with the profile's command refusal.

After: preparation runs in the scope xmd plan was called in — captured before
the profile installs its refusals — so the host installs the adapter it is about
to launch, and the document still cannot run anything.

How it works

runPlanCommandDocument → useScope()  (the caller's, with this invocation's capabilities)
   → scoped: refuseDocumentCapabilities() → provider → prepareAgent
        → inScope(host, materialize)  ← runs the install out there, waits for it here

inScope registers the halt before the wait, so an ended or cancelled command
takes an unfinished install with it rather than leaving one running in a scope
that outlives the conversation.

The distinction the profile is making is the point: the document decides what to
write and may run nothing; the host installs the adapter it was always going to
launch.

What must stay true

  • The command document still gets no Files, command, service or XMD-mediated
    network capability — unchanged, and only host preparation escapes it.
  • An ended command leaves no install running — enforced by the ensure-registered
    halt in inScope.

How to verify it

  • AE6 drives the real runPlan with adapters that install by running a command,
    and asserts the command was reached and the invocation completed. Reverting the
    inScope wiring makes it fail with the exact refusal above, which is how it was
    checked.
  • Its Process recorder is installed at: "min" deliberately: at the default
    strength it would answer for the refused call too and pass against the defect.

Focused evidence at 1b6ab7cb:

deno task test packages/cli/tests/agent-adapters.test.ts packages/cli/tests/plan.test.ts \
  packages/cli/tests/plan-cli.test.ts packages/cli/tests/plan-command-document.test.ts

ok | 4 passed (41 steps) | 0 failed, plus deno task check and deno task lint.

Separately, both embedded snapshots were materialized for real on this machine
from the merged code — npm install of each vendored tarball, entry file present
and verified — which is the step the refusal was blocking.

Scope

Included

  • The preparation scope, and the spec sentences in acp-client-spec.md and
    plan-command-spec.md that state which act the refusal covers.
  • A patch bump to 0.10.2, in its own commit: v0.10.1 is published and carries the
    broken half.

Intentionally unchanged

  • The overlay itself, the adapter root, and the xmd run path — xmd run grants
    the document a command, so nothing there was refused.

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.

Follow-up to #673, which fixed #672.

https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7

taras added 2 commits August 30, 2026 22:49
Materializing an embedded adapter runs `npm install`, and the authorship profile
refuses a command to everything inside it — so the first fix turned #672's two
backend failures into `xmd plan asked for a command, which the authorship profile
grants to nothing`, before any turn.

Preparation now runs in the scope `xmd plan` was called in, captured before the
profile installs its refusals. The distinction is the point: the document decides
what to write and may run nothing, while the host installs the adapter it was
always going to launch. The wait is registered with a halt, so an ended command
takes an unfinished install with it rather than leaving one running under a
conversation that is over.

AE6 drives the real command through adapters that install by running one, and
reproduces that exact refusal without this change.

Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
A patch bump: v0.10.1 shipped the adapter overlay without the preparation
scope, so `xmd plan` ends with `asked for a command, which the authorship
profile grants to nothing` before any turn. This release carries that
correction.

Stamps every publishable manifest and restamps the matching workspace entries in
bun.lock, which the bump task deliberately leaves alone. Only the ten
@executablemd members move.

Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7
@github-actions

Copy link
Copy Markdown

PR #674: 🐛 Prepare the plan profile's adapter through the host, not the document

25 files, +165 / -37

Scope

🟡 25 files changed. Are all changes related?

🟡 Changes span 13 directories.

🟡 PR mixes config and source changes.

🟡 package.json changed without dependency justification.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 2 diagnostics across 1 file (2 rules)
Density: 0.012 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

No extraneous code patterns detected.

@taras
taras enabled auto-merge (squash) August 31, 2026 02:58
@taras
taras merged commit f134e28 into main Aug 31, 2026
30 checks passed
@taras
taras deleted the agent/issue-672-plan-preparation branch August 31, 2026 03:00
taras added a commit that referenced this pull request Aug 31, 2026
`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 added a commit that referenced this pull request Aug 31, 2026
* 🐛 Type the review schema's conditional branches so the form can compile

`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

* 🐛 Open the plan review form as the host, not as the document

`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

* ♻️ State the host's own acts once, and put the ceiling on the document 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

* ♻️ Keep the host-act helpers beside the profile that uses them

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

---------

Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
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.

🐛 xmd plan resolves agents through ACPX's stock registry instead of the embedded adapters

1 participant