@junghanacs/entwurf 0.23.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +628 -0
- package/DELIVERY.md +24 -1
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +20 -2
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +46 -5
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +83 -154
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +6 -1
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
- package/pi-extensions/lib/herdr-placement.ts +24 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/protocol.js +0 -8
- package/run.sh +306 -53
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +51 -9
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +30 -6
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +8 -2
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +15 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
- package/scripts/smoke-herdr-raw-install-live.sh +90 -5
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +108 -2
- package/scripts/smoke-setup-verdict.sh +18 -2
package/DELIVERY.md
CHANGED
|
@@ -27,7 +27,7 @@ API. The levels compare capabilities, not implementation shapes.
|
|
|
27
27
|
|
|
28
28
|
| State | Meaning |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `queued` | A
|
|
30
|
+
| `queued` | A receiver queue accepted the message; the backend may not have seen it. Durability is a property of the RAIL, not of this word — see the acceptance table below. |
|
|
31
31
|
| `triggered` | A supported event/API accepted the signal. |
|
|
32
32
|
| `woke` | An idle interactive session began a turn without user typing. |
|
|
33
33
|
| `injected` | The message reached model-visible context. |
|
|
@@ -36,6 +36,29 @@ API. The levels compare capabilities, not implementation shapes.
|
|
|
36
36
|
|
|
37
37
|
Avoid bare `delivered`; name the observed boundary.
|
|
38
38
|
|
|
39
|
+
### Control-socket acceptance boundaries (#120)
|
|
40
|
+
|
|
41
|
+
A control-socket send answers with ONE of these, and the sender renders that word rather than the
|
|
42
|
+
internal route it took. They are not four spellings of success: they differ on every axis an
|
|
43
|
+
operator has to reason about.
|
|
44
|
+
|
|
45
|
+
| Boundary | Order | Durability | Visibility | Rejection |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| `sent` | n/a — the receiver was idle and the turn was TRIGGERED directly | none needed; the turn started | the turn is running, but the model has NOT necessarily read the text yet, and nothing here says the turn completed | an in-band refusal is `rejected`, never this |
|
|
48
|
+
| `queued-steer` | drained after each turn; overtakes every `queued-follow-up` already waiting | **none** — volatile receiver process memory | nothing shows it: it is not in the TUI's pending-message count | an abort clears the queue and the message is gone, silently |
|
|
49
|
+
| `queued-follow-up` | drained only when the receiver's inner loop ends; a steer arriving later still goes first | **none** — volatile receiver process memory | same: invisible in the receiver's own queue display | same: an abort drops it |
|
|
50
|
+
| `accepted-unknown-boundary` | unknown | unknown | unknown | the receiver ACCEPTED; this names our inability to classify its answer (version skew), and it must never be retried or rendered as `sent` |
|
|
51
|
+
|
|
52
|
+
`[측정 2026-09-22, pi-agent-core 0.87.0; 재측정 2026-09-23, 0.87.1 — 좌표 동일]` `dist/agent.js:60-71,96-97,137-138`
|
|
53
|
+
(two independent `one-at-a-time` queues, no FIFO between them)
|
|
54
|
+
and `dist/agent-loop.js:85,186,191-197`
|
|
55
|
+
(steering drained every turn, follow-ups only once the inner loop ends).
|
|
56
|
+
|
|
57
|
+
The **meta-mailbox** rail is the contrast, not a fifth boundary: it writes a durable `*.msg` before
|
|
58
|
+
ringing any doorbell, its filenames are ISO stamps so a drain is a global FIFO, an undrained inbox
|
|
59
|
+
is an honest `mailbox-undeliverable` refusal, and its receipt is that FILE. A mailbox enqueue is a
|
|
60
|
+
durable file receipt; a control-socket queue is process memory.
|
|
61
|
+
|
|
39
62
|
## Levels (D0–D8)
|
|
40
63
|
|
|
41
64
|
These are independent of VERIFY's `L0–L5` evidence levels and BASELINE's question
|
package/FAQ.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Entwurf FAQ
|
|
2
|
+
|
|
3
|
+
Short answers for people meeting Entwurf from a different agent-harness tradition.
|
|
4
|
+
|
|
5
|
+
## Is Entwurf an agent factory or a subagent framework?
|
|
6
|
+
|
|
7
|
+
No. Entwurf is closer to a workshop than a factory: it connects independently owned, visible sibling sessions without turning them into workers of a new central runtime. A harness may run its own subagents or teams, but that remains the harness's responsibility.
|
|
8
|
+
|
|
9
|
+
## What does Entwurf do?
|
|
10
|
+
|
|
11
|
+
It gives an existing session a stable **garden id** and routes a message, reply, or visible sibling launch through the rail that session actually supports. It does not reconstruct prompts, hydrate transcripts, manage credentials, or emulate another harness.
|
|
12
|
+
|
|
13
|
+
## What is a garden id?
|
|
14
|
+
|
|
15
|
+
A garden id is the stable address of one visible, record-backed session. Think of it as an invitation to a shared workshop table, not a worker name, a pane id, a transcript id, or a worktree lease.
|
|
16
|
+
|
|
17
|
+
## How are session state and project state separated?
|
|
18
|
+
|
|
19
|
+
Entwurf does not merge or isolate either one. Each harness keeps its own authentication, tools, transcript, and workspace policy. Git branches, worktrees, file locks, task allocation, and any coordination around a shared checkout belong to the operator and the harnesses involved; a garden id is never a file-isolation claim.
|
|
20
|
+
|
|
21
|
+
## What happens to a harness's internal subagents?
|
|
22
|
+
|
|
23
|
+
The visible top-level session is the garden principal. Internal children remain inside that harness and do not receive a second garden id, separate delivery authority, or an Entwurf-managed access-control layer.
|
|
24
|
+
|
|
25
|
+
## Does sending a message start a process?
|
|
26
|
+
|
|
27
|
+
No. [`entwurf_v2`](./README.md#entwurf_v2--canonical-dispatch-verb) only addresses an existing garden citizen and selects its supported delivery rail. `entwurf_fresh_call` opens a new visible sibling; `entwurf_resume_call` reopens a dormant pi citizen under the same id.
|
|
28
|
+
|
|
29
|
+
## Where do visible siblings open?
|
|
30
|
+
|
|
31
|
+
Inside Herdr, Entwurf opens a new unfocused tab in the caller's workspace and currently admits pi and Claude Code. Outside Herdr, it opens one of the fixed pi, Claude Code, Copilot, OMP, or Codex runtimes in a visible tmux window. The runtime keeps its own model, tools, authentication, and transcript.
|
|
32
|
+
|
|
33
|
+
## Does Entwurf replace Herdr or tmux?
|
|
34
|
+
|
|
35
|
+
No. Herdr and tmux provide visible seats. Entwurf provides identity, delivery, and receipts across independent harness sessions. A pane or tmux session is a location for a sibling, never its address or liveness proof.
|
|
36
|
+
|
|
37
|
+
## How do I install it?
|
|
38
|
+
|
|
39
|
+
Use the [direct installation route](./README.md#install) for the broader harness surface. In an existing Herdr workbench, use the [Herdr integration guide](./plugins/herdr/README.md); it activates only Herdr-integrated pi and Claude Code and leaves the rest to the direct route.
|
|
40
|
+
|
|
41
|
+
## How do I start pi as a citizen?
|
|
42
|
+
|
|
43
|
+
After the direct install, run `entwurf pi`; it starts pi with Entwurf's control surface. The Herdr integration does not put `entwurf` on `PATH`, so its documented command remains `pi --entwurf-control`.
|
|
44
|
+
|
|
45
|
+
## Where is the precise contract?
|
|
46
|
+
|
|
47
|
+
[AGENTS.md](./AGENTS.md) owns the project invariants. [DELIVERY.md](./DELIVERY.md), [VERIFY.md](./VERIFY.md), and [docs/adding-a-harness.md](./docs/adding-a-harness.md) own capability, evidence, and admission details.
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
npm package: <https://www.npmjs.com/package/@junghanacs/entwurf>
|
|
10
10
|
|
|
11
|
+
**Short answers for common questions:** [FAQ.md](./FAQ.md).
|
|
12
|
+
|
|
11
13
|
Legacy package: [`@junghanacs/pi-shell-acp`](https://www.npmjs.com/package/@junghanacs/pi-shell-acp). `entwurf` is its 0.12+ successor line: the same work renamed around the garden-citizen dispatch substrate rather than the pi adapter.
|
|
12
14
|
|
|
13
15
|
> **Repository shape.** This repo is **entwurf-core (v2 dispatch) + native-harness bridges + a pi adapter + an ACP plugin**. Pi is one adapter, not the project subject. Claude Code, GitHub Copilot CLI, and OMP (`omp`) are shipped self-fetch citizens; Antigravity (`agy`) is shipped native-push. Codex CLI is a **native-push citizen, supported in 0.21.0 on Linux** over the operator-owned app-server: vendor-trusted user-scope birth, strict request-scoped identity, loaded-thread probe, one-shot queue delivery, and visible fresh beside the CALLER's own pane. Unrestricted attached-TUI seat inference is not supported or claimed; #95 D1 retired the fixed `codex` tmux home on 2026-09-16, so the operator seats the app-server wherever they like and a Codex caller opens its sibling beside its own TUI pane. Codex remains native so it keeps its vendor tools, delegation, and work context; this is not another way to buy or expose GPT through ACP.
|
|
@@ -252,7 +254,7 @@ cd ~/repos/gh/entwurf
|
|
|
252
254
|
|
|
253
255
|
The full source setup requires Node 24, pnpm, and Python 3 on PATH. Harnesses are
|
|
254
256
|
optional-by-presence: `setup` runs the frozen dependency install, then composes what the operator
|
|
255
|
-
already installed — a compatible `pi` (`>=0.
|
|
257
|
+
already installed — a compatible `pi` (`>=0.87.1 <0.88`), Claude Code, agy, and the Copilot CLI
|
|
256
258
|
each get their wiring completed when detected, an absent harness is an explicit zero-state SKIP,
|
|
257
259
|
and a detected harness that cannot be completed (including a below-floor `pi`) is a named FAIL
|
|
258
260
|
that makes setup exit nonzero. `setup` never installs a harness binary or touches a credential
|
|
@@ -286,7 +288,7 @@ This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending
|
|
|
286
288
|
physical host (control sockets, mux, ACP plugin hosting).
|
|
287
289
|
|
|
288
290
|
To use the `entwurf` provider inside pi, install a compatible pi binary
|
|
289
|
-
separately (`@earendil-works/pi-coding-agent >=0.
|
|
291
|
+
separately (`@earendil-works/pi-coding-agent >=0.87.1 <0.88`). Then point pi at
|
|
290
292
|
the npm-installed package or development clone:
|
|
291
293
|
|
|
292
294
|
```bash
|
|
@@ -636,7 +638,7 @@ A two-pane recording covers the pre-0.12 v1 surface end-to-end — sibling resum
|
|
|
636
638
|
|
|
637
639
|
Live peer messaging carries a sender envelope `{ sessionId, agentId, cwd, timestamp, origin?, replyable? }`; `entwurf_self` returns that authoritative envelope for the current pi session or trusted meta-session. Plain external MCP hosts are non-replyable. A garden-native meta-session carries a trusted `meta-session` envelope, but **`replyable` is a fact its own rail decides, not a consequence of being trusted** — a self-fetch citizen needs a live armed receiver, a native-push citizen needs an alive adapter probe, and a pi session needs its control socket. `entwurf_self` also reports which rail a meta-session reply would ride, because a native-push citizen has no mailbox to name. `wants_reply` is an etiquette marker rendered as a `(wants reply)` badge — not a transport contract, no wait, no polling. **v2 never gates on it:** a `wants_reply` from an external/non-replyable caller is passed through and surfaced honestly beside that sender's `replyable: false`, not rejected — the decider routes on target + intent, never on sender replyability. (The retired v1 `entwurf_send` did reject it; that behaviour went with the verb.)
|
|
638
640
|
|
|
639
|
-
In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes
|
|
641
|
+
In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes eight tools: `entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_callback`, `entwurf_self`, `entwurf_inbox_read`, and the explicit/manual `entwurf_register_native` fallback. Native pi exposes the shared capability directly through the extension surface (`entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_callback` tools; the socket-scan `/entwurf-sessions` command is gone — #50 C4). **For garden-id delivery/reply use `entwurf_v2`** — the canonical surface that classifies the target and routes to live-pi / self-fetch meta-mailbox (Claude Code, Copilot, OMP) / native-push (Antigravity, Codex), and refuses a dormant target instead of waking it. **To open a sibling that does not exist yet use `entwurf_fresh_call`** — WHERE it opens is a fact about the calling process, not a parameter: inside herdr (`HERDR_ENV=1`) it opens a NEW TAB in the caller's own workspace, without taking focus, and admits only `pi` and `claude-code`, refusing `placement` by name; everywhere else it launches one of five fixed backends (`pi`, `claude-code`, `copilot`, `omp`, `codex`) as a visible window on the operator's own tmux server. There is no fallback between the two — an incomplete herdr context is refused by its herdr reason rather than quietly opening a tmux window nobody inside herdr can see. Everything that follows in this paragraph describes the tmux rail; the herdr rail's coordinates, refusals and reclaim are owned by [docs/herdr-launch-rail.md](./docs/herdr-launch-rail.md). On tmux it requires an explicit model, accepts one optional literal absolute `cwd` (omit it or pass `""` to use the caller's cwd) and one optional `placement.tmuxSession` naming an EXISTING session on that server to open the sibling in instead of the caller's own (the operator's project seat — nothing is ever created: an absent session is `tmux-session-missing` and a name outside the addressed grammar `[A-Za-z0-9][A-Za-z0-9_-]*` is `tmux-session-name-invalid`, with no window and no session left behind. The two inputs never infer each other, and the positive form of that matters more than the prohibition: **with `cwd` omitted, a seated sibling starts in THIS agent's own working directory**, measured — it does not inherit the target session's path or its active pane, so naming the `org` seat does not put the sibling in the `org` project. Pass `cwd` when you want a different directory. entwurf never resolves that path: it hands the literal string to tmux, so with a SYMLINKED directory the receipt echoes what you asked for while the record, the transcript path, the callback envelope and `pane_current_path` all show the path the OS resolved it to — two spellings of one place, and the resolved one is what `entwurf_resume_call` later reopens), passes the model and selected directory through the runtime's visible launch path, and hands it a first task; a `copilot` launch goes through entwurf's own managed invocation and is refused before any window opens if this host lacks the Copilot birth, MCP, receiver or visible-footer units, and an `omp` launch carries its task in the `--entwurf-bootstrap` payload instead of an argv prompt because that vendor connects its MCP tools after the session has already started. Omitted placement follows the CALLER rather than the backend being opened: a Codex CALLER opens its sibling beside its own TUI pane, located by the `thread-id` that pane's terminal title carries (0 or 2+ matching panes reject with no fallback); every other caller opens in its own seat. (0.21.0 shipped a backend-selected default instead — an omitted-placement `codex` TARGET resolved a fixed operator-owned session named `codex` — and #95 D1 retired it on 2026-09-16.) Any of the five can still be sent to an operator-owned existing seat with an explicit `placement.tmuxSession`, and named `claude`/`pi` seats remain available that way, but they are not omitted-placement defaults. Read tmux here as the VISIBLE LAUNCH SEAT, never as an address: the garden id is the address, and it arrives in the callback envelope. Sibling symmetry means each backend keeps its own transcript, auth, native tools and visibility — not that the transports are made identical. The sibling's first action is a nonce callback whose sender envelope carries its garden id. The launch receipt records the requested model/cwd/seat plus tmux coordinates — including the RESOLVED target session id, which is where the window actually is — and never claims that the runtime accepted them or completed delivery. (The v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.) Garden-native operator commands require `--entwurf-control`. There is no spawn target allowlist — the target registry is gone (#50 C3): `entwurf_v2` and `entwurf_resume_call` address an existing record-backed citizen, while `entwurf_fresh_call` takes its explicit backend/model/task and optional cwd directly rather than resolving a model tuple from a registry file. **To reopen a DORMANT pi citizen use `entwurf_resume_call {target}`** — the record supplies transcript, model, provider and cwd, so it takes no prompt, no task and no model override; it runs no turn, returns a LAUNCH receipt and a separate OBSERVATION receipt (only the second says the citizen is back), refuses a non-pi target as `target-not-pi`, and on an unobserved socket leaves the visible window open and releases its lock rather than retrying.
|
|
640
642
|
|
|
641
643
|
### `entwurf_v2` — canonical dispatch verb
|
|
642
644
|
|
package/VERIFY.md
CHANGED
|
@@ -5,7 +5,7 @@ invariants live in gates; this file defines evidence strength, release acceptanc
|
|
|
5
5
|
and the manual judgements a gate cannot make.
|
|
6
6
|
|
|
7
7
|
> **Current surface.** `entwurf-bridge` exposes `entwurf_v2`, `entwurf_peers`,
|
|
8
|
-
> `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_self`, `entwurf_inbox_read`, and `entwurf_register_native`. The ACP
|
|
8
|
+
> `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_callback`, `entwurf_self`, `entwurf_inbox_read`, and `entwurf_register_native`. The ACP
|
|
9
9
|
> backends are Claude and Snowflake Cortex Code. Antigravity is a separate shipped
|
|
10
10
|
> native-push citizen lane. Codex is a native-push citizen supported in 0.21.0, whose birth hook is
|
|
11
11
|
> operator-owned and gated on one vendor trust receipt, with strict request-scoped identity and
|
|
@@ -132,6 +132,24 @@ that moved out of `scripts/` and into a file beside its subject would otherwise
|
|
|
132
132
|
> `LIVE=1 ENTWURF_CODEX_APP_SERVER_PID=<existing-app-server-pid>
|
|
133
133
|
> ENTWURF_CODEX_FRESH_MODEL=<codex-model>
|
|
134
134
|
> ENTWURF_CODEX_FRESH_PI_MODEL=<pi-model> ./run.sh smoke-codex-fresh-live`.
|
|
135
|
+
>
|
|
136
|
+
> Those three values are the step's operator-supplied prerequisite, and
|
|
137
|
+
> **`release-gate --cut` inherits that triple** — it is exported onto the aggregate invocation (the
|
|
138
|
+
> release skill's P5 carries it), never inferred by the gate, so the no-inference rule above holds
|
|
139
|
+
> unchanged inside the aggregate. **An ABSENT value is a protocol SKIP naming what was missing; a PRESENT but wrong one
|
|
140
|
+
> stays a FAIL.** That split is the honest reading of the step-outcome protocol — a step that never
|
|
141
|
+
> ran and a step that ran and broke are different facts — and it is what makes this MUST reproducible
|
|
142
|
+
> at every cut instead of inviting a standing exception: before 2026-09-23 all three were assertions, so their
|
|
143
|
+
> absence threw, and because the aggregate supplied none of them this step went red inside every
|
|
144
|
+
> cut on every host (0.25.0's candidate receipt, `MUST PASS=23 FAIL=1`, is that shape). A SKIP is
|
|
145
|
+
> **not** a softening: `--cut` refuses it as `cut: BLOCKED (MUST SKIP)`, so a cut without this
|
|
146
|
+
> receipt is still impossible — the operator supplies the triple and re-runs. A host with no Codex
|
|
147
|
+
> app-server therefore cannot take a cut, which is the intended cost of #87's release stop
|
|
148
|
+
> (「A visible-fresh receipt outside the MUST tier is no longer the general rule」above) rather than
|
|
149
|
+
> an accident. The installed-config preflights are deliberately NOT in that split and remain FAIL
|
|
150
|
+
> on absence — a missing Codex birth, user MCP/status/terminal-title config, runtime, bridge, tmux
|
|
151
|
+
> seat, or an unanswered launch directory is host state `setup` and the Codex doctors own, not a
|
|
152
|
+
> per-invocation input.
|
|
135
153
|
> **Both of THIS cell's live-spend defaults are `gpt-5.6-luna` from 2026-09-17**
|
|
136
154
|
> (`openai-codex/gpt-5.6-luna` on the pi side) — the Codex leg came down off `sol`. It is a tier
|
|
137
155
|
> choice and not a gate change; the receipt this cell is accepted on must be the one that actually
|
|
@@ -269,7 +287,7 @@ The goal is not merely "invoke Claude Code." We want:
|
|
|
269
287
|
**One install command to remember: `./run.sh setup <project>`.** It is idempotent — re-run the exact same command whenever anything looks wrong. There is no second install surface to juggle: from a clone `setup` runs the whole floor in order.
|
|
270
288
|
|
|
271
289
|
1. `pnpm install` — installs the pinned development dependencies and builds the bridge (source-checkout bootstrap only; an installed package never runs npm/pnpm inside `node_modules`)
|
|
272
|
-
2. pi wiring → `<project>/.pi/settings.json` + user-scope registration — only when a `pi` inside the supported range (`>=0.
|
|
290
|
+
2. pi wiring → `<project>/.pi/settings.json` + user-scope registration — only when a `pi` inside the supported range (`>=0.87.1 <0.88`) is on PATH; absent pi is an explicit zero-state SKIP, a below-floor pi is a detected FAIL. The user-scope entry is owner-recorded (#86 C2): another root's live-or-missing ownership makes this step a zero-write refusal (setup: pi FAIL) that names `takeover-user-scope`; the install-states bind the exact managed settings path (a drifted/symlinked/corrupt target is a zero-write refusal) and the inverse removes only the recorded owner's exact entry; `doctor-pi-package` reports the verdict
|
|
273
291
|
3. Claude meta-bridge global plugin — only when `claude` is on PATH; otherwise skipped cleanly
|
|
274
292
|
4. source stable-bin exposure — including certified `entwurf` → this checkout's `run.sh`, the managed runtime Copilot fresh resolves; helper units are attempted independently and a foreign helper is a named FAIL
|
|
275
293
|
5. agy bridge + exact permission + statusline + `PreInvocation` hook — only when `agy` is on PATH; each adapter is idempotent and independently doctorable
|
package/demo/README.md
CHANGED
|
@@ -46,7 +46,10 @@ a background driver types the scene prompts into the sender pane.
|
|
|
46
46
|
1. **Spawn** a sonnet sibling (memory write — "tempered indigo").
|
|
47
47
|
2. **Resume** that same sibling (memory recall — identity preservation,
|
|
48
48
|
the #9 regression guard).
|
|
49
|
-
3. **Cross-session greeting** via `entwurf_send
|
|
49
|
+
3. **Cross-session greeting** via `entwurf_send`. *(Historical: `entwurf_send` was retired in the
|
|
50
|
+
0.12 cutover, and the "sent box" half of that pairing was never actually reachable — the
|
|
51
|
+
sender-side renderer had zero producers and was removed in #120 P3. What the recording shows on
|
|
52
|
+
the receive side is real; the send side is a box the operator could not have seen.)*
|
|
50
53
|
|
|
51
54
|
Run:
|
|
52
55
|
|
|
@@ -139,7 +142,7 @@ SCENE_DELAY=30 FINAL_PAUSE=10 bash demo.sh
|
|
|
139
142
|
|
|
140
143
|
## Prerequisites
|
|
141
144
|
|
|
142
|
-
- `pi` on PATH (current floor 0.
|
|
145
|
+
- `pi` on PATH (current floor 0.87.1)
|
|
143
146
|
- `entwurf` provider configured + auth ready for the selected sender/peer models
|
|
144
147
|
- `asciinema` installed
|
|
145
148
|
- `agg` installed (optional — only for GIF conversion)
|
package/docs/acp-backend-rail.md
CHANGED
|
@@ -92,8 +92,8 @@ undifferentiated "supported" column is what let a Claude PASS read as if it also
|
|
|
92
92
|
|
|
93
93
|
| Surface | Declaration | Class | What a green actually says |
|
|
94
94
|
|---|---|---|---|
|
|
95
|
-
| Entwurf package | `0.
|
|
96
|
-
| pi runtime | devDep exact `0.
|
|
95
|
+
| Entwurf package | `0.25.0` | shipped baseline | the package contract these rows belong to |
|
|
96
|
+
| pi runtime | devDep exact `0.87.1`, peer `>=0.87.1 <0.88` | **exact** oracle + **closed range** | built and certified against 0.87.1; hosts inside the range are accepted, and the ceiling moves only on measurement |
|
|
97
97
|
| ACP wire SDK | `@agentclientprotocol/sdk 1.4.0` | **exact** | the shared wire oracle both adapters speak |
|
|
98
98
|
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.79.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
|
|
99
99
|
| Claude Agent SDK | `0.3.274` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
|
|
@@ -476,6 +476,53 @@ already emitted a `usage_update` at `compact_boundary` and 0.75.1 still does
|
|
|
476
476
|
|
|
477
477
|
Receipt, limits and the `completed`-branch gap: `scripts/raw-acp-compaction-measure/README.md`.
|
|
478
478
|
|
|
479
|
+
## 11-9. A steer reaches an ACP receiver one child turn late (#120 P5, measurement only)
|
|
480
|
+
|
|
481
|
+
`entwurf_v2 mode:"steer"` injects into the receiver's pi steering queue; it is not an ACP concept.
|
|
482
|
+
On a NATIVE pi receiver that queue is drained seconds later, and on an ACP receiver it is drained
|
|
483
|
+
one whole child turn later. The difference is not a different drain point — it is the same one.
|
|
484
|
+
|
|
485
|
+
`[측정 2026-09-22, pi-agent-core 0.87.0; 재측정 2026-09-23, 0.87.1 — 좌표 동일]` `dist/agent-loop.js:141` awaits `streamAssistantResponse`
|
|
486
|
+
once per turn, and the steering drain is `:186`, after the `turn_end` emit at `:185`. For this rail
|
|
487
|
+
that one await is `pi-extensions/lib/acp/backend.ts:815` —
|
|
488
|
+
`await Promise.race([session.connection.prompt(promptArgs), lifecycle])` — so it spans the entire
|
|
489
|
+
`session/prompt` round trip. The child's own tool calls and subagent turns are invisible to pi, so
|
|
490
|
+
pi cannot reach `:186` until the child turn ends. Same code path as native, one long await.
|
|
491
|
+
|
|
492
|
+
That is a property, not a silent failure: the sender's receipt already says `queued-steer` and
|
|
493
|
+
`DELIVERY.md` already states that both queues are volatile and unordered against each other, so
|
|
494
|
+
nothing here is promised and then quietly not delivered. `[QK:PI-QUEUE-NO-MIDFLIGHT-MERGE]`
|
|
495
|
+
(`test/pi-queue.oracle.test.ts`) proves the mechanism on the installed vendor: a message queued
|
|
496
|
+
while a turn is in flight never joins that turn and arrives as a later one. The delay's SIZE is
|
|
497
|
+
that turn's duration, which the same cell fixes by construction.
|
|
498
|
+
|
|
499
|
+
**Forwarding is not adopted here.** `[측정 2026-09-22]` standard ACP has no steer method at all —
|
|
500
|
+
enumerating the installed `@agentclientprotocol/sdk`'s `AGENT_METHODS` and `CLIENT_METHODS` yields
|
|
501
|
+
zero entries containing `steer`. What exists is a PRIVATE vendor extension,
|
|
502
|
+
`_session/steering` in `@agentclientprotocol/claude-agent-acp@0.79.0`
|
|
503
|
+
(`dist/acp-agent.js:163`, handler at `:7792`), client→agent, advertised through
|
|
504
|
+
`InitializeResponse._meta.steering.supported`. Our client sends `initialize`, `session/new`,
|
|
505
|
+
`session/prompt`, `session/set_config_option` and `session/cancel` (`acp-client.ts:138-149`) and
|
|
506
|
+
has no steering path at all.
|
|
507
|
+
|
|
508
|
+
Adopting it would mean owning five things this driver does not own today, each a new contract
|
|
509
|
+
rather than a wiring change:
|
|
510
|
+
|
|
511
|
+
1. reading the `_meta.steering.supported` capability, and deciding what a backend without it is;
|
|
512
|
+
2. a typed schema and version fence for a request that is outside the SDK's method set, so a
|
|
513
|
+
private extension changing shape is a named refusal rather than a silent no-op;
|
|
514
|
+
3. a backend-specific concurrent-request seam from the control extension to a live ACP connection —
|
|
515
|
+
today a turn is one in-flight `prompt`, and this adds a second request during it;
|
|
516
|
+
4. an exactly-once / ordering policy that picks the outer pi queue or the vendor injection but never
|
|
517
|
+
both, since those are two independent orderings that currently do not meet;
|
|
518
|
+
5. a receipt mapping for the extension's outcomes — `injected`, `promptRequired`, `startedNewTurn` —
|
|
519
|
+
plus the idle race, cancel and release meanings behind them. (`idleBehavior:"promptRequired"`
|
|
520
|
+
lets a host refuse the detached `startedNewTurn` path, but that opt-in is itself a new contract.)
|
|
521
|
+
|
|
522
|
+
The adapter keeps its own turn-settlement bookkeeping internally, and our event mapper ignores
|
|
523
|
+
update kinds it does not handle (`event-mapper.ts:377-378`), so nothing above is a claim that
|
|
524
|
+
Entwurf must replicate vendor internals. The reason to stay put is the five contracts, not a ledger.
|
|
525
|
+
|
|
479
526
|
## Open work
|
|
480
527
|
|
|
481
528
|
- causal MCP-readiness diagnosis and, only with proof, a backend-invariant fence;
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -22,7 +22,7 @@ evidence split, and its three states must not collapse into "macOS is supported"
|
|
|
22
22
|
| npm/pnpm | npm is bundled with Node; pnpm is required for source setup | package or source installation |
|
|
23
23
|
| Python 3 | required by `setup`/`install` (project-path normalization + settings writers); `--help`/`check-bridge` stay Python-free | pi/Claude/agy/Copilot/Codex wiring writers |
|
|
24
24
|
| entwurf | global/project-local `@junghanacs/entwurf`, or a source checkout | operator command and garden capability |
|
|
25
|
-
| pi | optional-by-presence, `>=0.
|
|
25
|
+
| pi | optional-by-presence, `>=0.87.1 <0.88` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
|
|
26
26
|
| Claude Code | optional, **`>=2.1.217`** — the exec-form hook floor | Claude ACP auth/runtime and mailbox-backed native citizen |
|
|
27
27
|
| GitHub Copilot CLI | optional-by-presence, operator-installed and authenticated — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/receiver/footer) | self-fetch citizen and visible fresh |
|
|
28
28
|
| OMP (`omp`) | optional-by-presence, operator-installed — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/`tools.xdev` setting/receiver) | self-fetch citizen and visible fresh (accepted on one host — see §4b) |
|
|
@@ -156,7 +156,7 @@ on Darwin; that is wiring, not a rail receipt. If `pi` is on PATH,
|
|
|
156
156
|
Install the exact release floor, then wire the project:
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
|
-
npm install -g @earendil-works/pi-coding-agent@0.
|
|
159
|
+
npm install -g @earendil-works/pi-coding-agent@0.87.1
|
|
160
160
|
pi --version
|
|
161
161
|
|
|
162
162
|
cd ~/entwurf-smoke
|
|
@@ -164,7 +164,7 @@ entwurf install .
|
|
|
164
164
|
pi -e "$(npm root -g)/@junghanacs/entwurf" --list-models entwurf
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
The supported range is `>=0.
|
|
167
|
+
The supported range is `>=0.87.1 <0.88`. It is a hard minimum: installing this
|
|
168
168
|
release onto a 0.83.x pi host upgrades the runtime rather than keeping the older
|
|
169
169
|
minor. A host using only the external MCP bridge can skip pi until it needs a
|
|
170
170
|
control socket; no delivery rail launches a pi process.
|
|
@@ -264,9 +264,10 @@ process. Open omp yourself and it is a two-way citizen either way — it sends u
|
|
|
264
264
|
garden id and a reply lands on it.
|
|
265
265
|
|
|
266
266
|
The first turn of a fresh omp sibling is a TWO-STAGE BOOTSTRAP, not a positional prompt: the
|
|
267
|
-
launcher carries `{v,
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
launcher carries `{v,task}` on the fixed registered flag `--entwurf-bootstrap` while the
|
|
268
|
+
caller's address rides the sibling's own process env, and the installed birth extension waits
|
|
269
|
+
until the zero-argument callback tool is actually callable, sends a callback-only prompt, and
|
|
270
|
+
delivers the operator's task only after that exact call succeeds.
|
|
270
271
|
That is a measured correction — the interactive host defers MCP discovery, so a positional
|
|
271
272
|
first turn began before the tool it named existed.
|
|
272
273
|
|
|
@@ -306,7 +307,7 @@ as a note rather than a fault.
|
|
|
306
307
|
|
|
307
308
|
**entwurf sets no OMP version floor in code, and will not grow one on schedule.** Detection is
|
|
308
309
|
presence-only (`command -v "${OMP_BIN:-omp}"`); there is no `entwurf.ompFloor`, no coherence
|
|
309
|
-
gate, and no exact pin — unlike Node (`engines.node`), pi (`>=0.
|
|
310
|
+
gate, and no exact pin — unlike Node (`engines.node`), pi (`>=0.87.1 <0.88`) and Claude Code
|
|
310
311
|
(`entwurf.claudeCodeFloor`), each of which has an enforcement point. That asymmetry is a
|
|
311
312
|
decision, not an omission. A floor is the answer to a vendor that fails SILENTLY — Claude Code
|
|
312
313
|
earned one because an older binary validates the exec manifest, drops `args` at runtime, and
|
|
@@ -34,8 +34,13 @@
|
|
|
34
34
|
* - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
|
|
35
35
|
* window; target-only, runs no turn, LAUNCH and OBSERVATION receipts stay
|
|
36
36
|
* apart.
|
|
37
|
+
* - entwurf_callback — ZERO-ARGUMENT fresh-sibling callback. Reads ENTWURF_CALLBACK_TARGET +
|
|
38
|
+
* ENTWURF_CALLBACK_NONCE from this process env (launcher-injected), validates
|
|
39
|
+
* grammar, and dispatches through the existing v2 runner. Refuses by name
|
|
40
|
+
* when env is absent/malformed or this process is a Codex-provenance bridge.
|
|
41
|
+
* No model-supplied target fallback.
|
|
37
42
|
*
|
|
38
|
-
* That list is the WHOLE public surface —
|
|
43
|
+
* That list is the WHOLE public surface — eight verbs — and `check-entwurf-bridge-boot`
|
|
39
44
|
* (G1f) holds it as an exact set on the runtime tools/list, so a verb added or dropped
|
|
40
45
|
* here without a decision is red rather than merely undocumented.
|
|
41
46
|
*
|
|
@@ -64,6 +69,7 @@ import * as process from "node:process";
|
|
|
64
69
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
65
70
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
66
71
|
import { z } from "zod";
|
|
72
|
+
import { readCallbackEnv } from "../../../pi-extensions/lib/callback-env.js";
|
|
67
73
|
import { controlSocketPathIn, defaultControlSocketDir } from "../../../pi-extensions/lib/control-socket-path.js";
|
|
68
74
|
import { resolveMailboxReceiverFacts } from "../../../pi-extensions/lib/entwurf-deliverability.js";
|
|
69
75
|
import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.js";
|
|
@@ -348,7 +354,9 @@ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids: message, r
|
|
|
348
354
|
"alone does not say which rail that citizen answers on. Give target + intent; the decider picks transport " +
|
|
349
355
|
"from liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge " +
|
|
350
356
|
"mailbox; probe-alive native-push citizen → direct injection into its conversation) and reports ONE " +
|
|
351
|
-
"outcome
|
|
357
|
+
"outcome, named per rail: a control receiver's acceptance boundary (`sent` / `queued-steer` / " +
|
|
358
|
+
"`queued-follow-up` / `accepted-unknown-boundary` — those four only), a mailbox ENQUEUE receipt, a " +
|
|
359
|
+
"native-push INJECTION, a rejection, or lock-dirty. EXISTING targets only; discover with " +
|
|
352
360
|
"entwurf_peers. A peer entwurf_peers shows as liveness=alive → fire-and-forget. A " +
|
|
353
361
|
"citizen with NO socket liveness (liveness=unsupported) is ALSO fire-and-forget — unsupported means only " +
|
|
354
362
|
'"no control-socket probe." The decider resolves the actual rail at dispatch time: a self-fetch ' +
|
|
@@ -383,9 +391,8 @@ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids: message, r
|
|
|
383
391
|
mode: z
|
|
384
392
|
.enum(["steer", "follow_up"])
|
|
385
393
|
.optional()
|
|
386
|
-
.describe("Injection style for a CONTROL-SOCKET send only: steer (
|
|
387
|
-
"
|
|
388
|
-
"effect on those rails."),
|
|
394
|
+
.describe("Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. " +
|
|
395
|
+
"The mailbox and native-push plans carry no mode, so it has no effect on those rails."),
|
|
389
396
|
wants_reply: z.boolean().optional().describe("Human-conversation reply hint (default false)"),
|
|
390
397
|
}, async ({ target, intent, message, mode, wants_reply }, extra) => {
|
|
391
398
|
try {
|
|
@@ -740,6 +747,32 @@ server.tool("entwurf_resume_call", "Reopen ONE DORMANT pi citizen under its OWN
|
|
|
740
747
|
return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
|
|
741
748
|
}
|
|
742
749
|
});
|
|
750
|
+
server.tool("entwurf_callback", "ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads " +
|
|
751
|
+
"ENTWURF_CALLBACK_TARGET and ENTWURF_CALLBACK_NONCE from this process environment " +
|
|
752
|
+
"(injected by the launcher next to the identity scrub), validates garden-id and nonce " +
|
|
753
|
+
"grammar, and dispatches through the existing v2 runner with intent fire-and-forget, " +
|
|
754
|
+
"message=nonce, wants_reply=false. The target is re-resolved by record/decider — env " +
|
|
755
|
+
"names the id, it does not skip dispatch. REFUSES BY NAME when the pair is absent, " +
|
|
756
|
+
"malformed, or this process is a Codex-provenance bridge (window env never reaches that " +
|
|
757
|
+
"MCP child). No arguments, no fallback to a model-supplied target.", {}, async (_args, extra) => {
|
|
758
|
+
const read = readCallbackEnv(process.env);
|
|
759
|
+
if (!read.ok) {
|
|
760
|
+
return textErr(`entwurf_callback: ${read.reason}`);
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const sender = await buildSendSenderEnvelope({ requestMeta: extra._meta });
|
|
764
|
+
const rendered = await runAndRenderEntwurfV2FromSurface({
|
|
765
|
+
target: read.target,
|
|
766
|
+
intent: "fire-and-forget",
|
|
767
|
+
message: read.nonce,
|
|
768
|
+
wants_reply: false,
|
|
769
|
+
}, { senderProvider: () => sender });
|
|
770
|
+
return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
return textErr(`entwurf_callback error: ${err instanceof Error ? err.message : String(err)}`);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
743
776
|
// ============================================================================
|
|
744
777
|
// Main
|
|
745
778
|
// ============================================================================
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
//
|
|
9
9
|
// S2c/S2d boundary (GPT S2c Q2): this is CONVERSATION TRANSCRIPT PASSTHROUGH, not
|
|
10
10
|
// rich-carrier identity injection. Deliberately EXCLUDED here (all S2d):
|
|
11
|
-
// -
|
|
11
|
+
// - the system prompt — never read into the prompt or `_meta.systemPrompt`
|
|
12
12
|
// (the billing carrier stays absent — NEXT §S2-scout 핀1);
|
|
13
13
|
// - `~/AGENTS.md` / cwd AGENTS / bridge identity narrative;
|
|
14
14
|
// - first-user-message augment + project-context de-dup;
|
|
15
|
-
// -
|
|
15
|
+
// - the declared tool surface — the ACP child tool surface is the S2b
|
|
16
16
|
// `_meta.claudeCode.options` SSOT, never re-sent here.
|
|
17
|
+
// Since pi 0.86 both of those ride a leading `role:"system"` message inside the
|
|
18
|
+
// `TranscriptContext` (pi-ai `normalizeContext`) rather than their own `Context`
|
|
19
|
+
// fields, so the exclusion is enforced by `renderMessage`'s explicit
|
|
20
|
+
// `case "system"` below, not by simply not reading two fields.
|
|
17
21
|
// Structured tool replay is also excluded: tool calls/results render as plain
|
|
18
22
|
// transcript text, never as ACP tool invocations (the child runs its own tools).
|
|
19
23
|
// MUST equal event-mapper.ts `LIFECYCLE_NOTICE_SIGNATURE` (the SSOT/producer).
|
|
@@ -69,13 +73,23 @@ function renderMessage(message) {
|
|
|
69
73
|
const tag = message.isError ? "Tool error" : "Tool result";
|
|
70
74
|
return text ? `${tag} (${message.toolName}): ${text}` : undefined;
|
|
71
75
|
}
|
|
76
|
+
case "system":
|
|
77
|
+
// The never-forward-systemPrompt invariant, stated rather than inherited.
|
|
78
|
+
// Since pi 0.86 the system prompt and the declared tool surface arrive AS
|
|
79
|
+
// transcript messages (`normalizeContext`), so what used to be "we simply
|
|
80
|
+
// do not read `context.systemPrompt`" is now an active decision on every
|
|
81
|
+
// turn. `default` would already skip it; the explicit case exists so
|
|
82
|
+
// deleting it is a visible change and a new pi role cannot quietly start
|
|
83
|
+
// leaking the prompt into the ACP child's transcript.
|
|
84
|
+
return undefined;
|
|
72
85
|
default:
|
|
73
86
|
return undefined;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
/**
|
|
77
|
-
* Flatten a pi
|
|
78
|
-
*
|
|
90
|
+
* Flatten a pi transcript into a single transcript string. Excludes the system
|
|
91
|
+
* prompt and the declared tool surface by construction — since 0.86 both ride
|
|
92
|
+
* the leading `role:"system"` message, which `renderMessage` drops explicitly.
|
|
79
93
|
*/
|
|
80
94
|
export function contextTranscript(context) {
|
|
81
95
|
const lines = [];
|
|
@@ -87,7 +101,7 @@ export function contextTranscript(context) {
|
|
|
87
101
|
return lines.join("\n\n");
|
|
88
102
|
}
|
|
89
103
|
/**
|
|
90
|
-
* Convert a pi
|
|
104
|
+
* Convert a pi transcript into the ACP `prompt` array (a single text block holding
|
|
91
105
|
* the flattened transcript). Empty history yields an empty array — the caller
|
|
92
106
|
* decides whether that is a hard error.
|
|
93
107
|
*/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback-env — the launch-computed pair that lets a fresh sibling call home
|
|
3
|
+
* without copying a garden id or nonce out of prose.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. Until this seam, `composeFreshCallFraming` put
|
|
6
|
+
* `target=<caller gid>` and `message=<nonce>` into the first-turn prompt, and
|
|
7
|
+
* the sibling had to retype them into `entwurf_v2`. That is the one place in
|
|
8
|
+
* the garden Hard Rule 2 (record is the sole address axis) was violated: the
|
|
9
|
+
* transcript became the address carrier, and a model that dropped a `$` or a
|
|
10
|
+
* hex nibble produced a refused dispatch that looked like a flaky backend.
|
|
11
|
+
*
|
|
12
|
+
* WHAT TRAVELS. Two process-env names, injected by the launcher next to the
|
|
13
|
+
* identity scrub (`PI_SESSION_ID=` / `PI_AGENT_ID=`), never as a general env
|
|
14
|
+
* carrier. The no-arg `entwurf_callback` verb reads BOTH from its own process
|
|
15
|
+
* and refuses by name when they are absent, malformed, or this process is a
|
|
16
|
+
* Codex-provenance bridge (window env never reaches that MCP child — measured).
|
|
17
|
+
*
|
|
18
|
+
* This module does not dispatch. It parses, validates, and formats. Launchers
|
|
19
|
+
* and the verb share it so a drifted regex cannot inject what the verb rejects.
|
|
20
|
+
*/
|
|
21
|
+
import { SESSION_ID_RE } from "./session-id.js";
|
|
22
|
+
export const CALLBACK_TARGET_ENV = "ENTWURF_CALLBACK_TARGET";
|
|
23
|
+
export const CALLBACK_NONCE_ENV = "ENTWURF_CALLBACK_NONCE";
|
|
24
|
+
/** Exactly what `mintNonce` emits (`fresh-call-composition.ts`): `mux-fresh-call-` + 12 random
|
|
25
|
+
* bytes as hex. ONE minter, ONE grammar — both rails call the same `mintNonce`, so a second
|
|
26
|
+
* accepted prefix would be a spelling no launcher produces and this leaf exists to refuse
|
|
27
|
+
* exactly that. A gate fixture is an oracle and never a reason to widen the subject. */
|
|
28
|
+
export const CALLBACK_NONCE_RE = /^mux-fresh-call-[0-9a-f]{24}$/;
|
|
29
|
+
function parseCallbackPair(target, nonce) {
|
|
30
|
+
if (typeof target !== "string" || typeof nonce !== "string") {
|
|
31
|
+
return { ok: false, reason: "callback-env-malformed" };
|
|
32
|
+
}
|
|
33
|
+
if (!SESSION_ID_RE.test(target) || !CALLBACK_NONCE_RE.test(nonce)) {
|
|
34
|
+
return { ok: false, reason: "callback-env-malformed" };
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, target, nonce };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format the two `KEY=value` assignments a launcher injects. Throws rather than
|
|
40
|
+
* emitting a pair the verb would refuse — a launch that cannot name its caller
|
|
41
|
+
* must not open a window whose first action is a guaranteed reject.
|
|
42
|
+
*/
|
|
43
|
+
export function callbackEnvAssignments(params) {
|
|
44
|
+
const parsed = parseCallbackPair(params.target, params.nonce);
|
|
45
|
+
if (!parsed.ok) {
|
|
46
|
+
throw new Error(`callback-env: refusing to inject ${parsed.reason}`);
|
|
47
|
+
}
|
|
48
|
+
return [`${CALLBACK_TARGET_ENV}=${parsed.target}`, `${CALLBACK_NONCE_ENV}=${parsed.nonce}`];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read the pair from a process environment. Codex provenance is a named refuse
|
|
52
|
+
* even when the pair is well-formed: that bridge is an app-server child, and a
|
|
53
|
+
* pane-injected value would be the wrong citizen's (or last-launch-wins).
|
|
54
|
+
*
|
|
55
|
+
* Both names absent → `callback-env-absent`. One present, or either failing
|
|
56
|
+
* grammar → `callback-env-malformed`. No fallback to a model-supplied target.
|
|
57
|
+
*/
|
|
58
|
+
export function readCallbackEnv(env = process.env) {
|
|
59
|
+
if (env.ENTWURF_BRIDGE_NATIVE_HOST?.trim() === "codex") {
|
|
60
|
+
return { ok: false, reason: "codex-callback-env-unsupported" };
|
|
61
|
+
}
|
|
62
|
+
const target = env[CALLBACK_TARGET_ENV];
|
|
63
|
+
const nonce = env[CALLBACK_NONCE_ENV];
|
|
64
|
+
if (target === undefined && nonce === undefined) {
|
|
65
|
+
return { ok: false, reason: "callback-env-absent" };
|
|
66
|
+
}
|
|
67
|
+
return parseCallbackPair(target, nonce);
|
|
68
|
+
}
|