@junghanacs/entwurf 0.23.0 → 0.24.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/AGENTS.md +3 -1
- package/BASELINE.md +2 -0
- package/CHANGELOG.md +307 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +43 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +97 -18
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- 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 +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -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/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +15 -13
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +20 -10
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +327 -246
- 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 +42 -18
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +45 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +361 -0
- package/scripts/check-release-gate-outcomes.ts +7 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/herdr-plugin-build.json +1 -1
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +877 -0
- 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 +2 -2
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -391
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
package/docs/acp-backend-rail.md
CHANGED
|
@@ -92,11 +92,11 @@ 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.24.0` | shipped baseline | the package contract these rows belong to |
|
|
96
|
+
| pi runtime | devDep exact `0.86.0`, peer `>=0.86.0 <0.87` | **exact** oracle + **closed range** | built and certified against 0.86.0; 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
|
-
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.
|
|
99
|
-
| Claude Agent SDK | `0.3.
|
|
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
|
+
| Claude Agent SDK | `0.3.274` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
|
|
100
100
|
| Anthropic SDK | `0.100.1` | **exact**, peer-resolution only | satisfies the Agent SDK peer floor (0.93.0+); never an API client here (gate L4) |
|
|
101
101
|
| Claude Code runtime | `>=2.1.217` (`entwurf.claudeCodeFloor`) | **floor** | below it, hook args are silently dropped; entwurf enforces this itself |
|
|
102
102
|
| Node | `>=24` (`engines.node`) | **floor** | single axis, derived everywhere else |
|
|
@@ -168,6 +168,83 @@ different reasons, and collapsing them would hide a real risk**:
|
|
|
168
168
|
`resolvedProvider` and up onto the unconditional `session/new` path. entwurf passes `settings`
|
|
169
169
|
as an OBJECT (`tool-surface.ts:153`), so the branch is never taken. That makes "entwurf never
|
|
170
170
|
hands `settings` as a path" a contract rather than an accident; it is stated at that call site.
|
|
171
|
+
- **0.77.0's BREAKING change does not reach us, twice over.** `claudeCode.options.agent` is no
|
|
172
|
+
longer forwarded — the adapter now shallow-copies the options object and `delete`s `agent` on
|
|
173
|
+
the unconditional `session/new` path (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js:5934-5941 직독]`).
|
|
174
|
+
entwurf's `buildClaudeSessionMeta` never sets that key (`tool-surface.ts:145-199` 직독: the
|
|
175
|
+
literal carries `model`/`tools`/`settingSources`/`settings` plus optional `plugins`/
|
|
176
|
+
`disallowedTools`/`extraArgs`, and nothing else), and the removed agent-picker exports
|
|
177
|
+
(`DEFAULT_AGENT_ID`, `AGENT_CONFIG_ID`, `BUILTIN_AGENT_NAMES`, `discoverCustomAgents`) are
|
|
178
|
+
imported nowhere: `[측정 2026-09-18]` `git grep -c` over `pi-extensions/`, `test/`, `scripts/`,
|
|
179
|
+
`mcp/` is **0**. We never imported the adapter as a library at all — we spawn its binary.
|
|
180
|
+
- **0.77.0's `allowDangerouslySkipPermissions` opt-out is a new lever we deliberately do not
|
|
181
|
+
pull, and our effective permission mode is unchanged.** 0.76.0 sent
|
|
182
|
+
`allowDangerouslySkipPermissions: ALLOW_BYPASS` unconditionally and computed
|
|
183
|
+
`initialPermissionMode = creationOpts.permissionMode ?? resolvePermissionMode(settings…)`.
|
|
184
|
+
0.79.0 computes `allowBypass = ALLOW_BYPASS && sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false`
|
|
185
|
+
and routes the whole thing through `resolvePermissionMode(…, logger, allowBypass)`
|
|
186
|
+
(`[측정 2026-09-18, 0.79.0 dist/acp-agent.js:5932-5933 직독]`). entwurf sets that option
|
|
187
|
+
nowhere, so `undefined !== false` holds and `allowBypass === ALLOW_BYPASS`;
|
|
188
|
+
`ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX` is **byte-identical** between the two
|
|
189
|
+
versions (`dist/permissions/modes.js` 직독). Our overlay pins
|
|
190
|
+
`permissions.defaultMode: "bypassPermissions"` (`overlay.ts:122`), which resolves the same
|
|
191
|
+
under both. The lever now EXISTS for a host that wants a non-bypass sibling; declaring it is a
|
|
192
|
+
separate axis, not a one-line flip, and nothing in this bump takes it.
|
|
193
|
+
- **0.77.0's system-reminder strip never touches our first-user-message augment.**
|
|
194
|
+
`INJECTED_CONTEXT_MARKERS = ["system-reminder"]` joins the local-command markers in
|
|
195
|
+
`stripMarkerTags`, and `stripLocalCommandMetadata` has exactly two call sites
|
|
196
|
+
(`[측정 2026-09-18, 0.79.0 dist/acp-agent.js grep -n]`): `:4129`, gated on the message content
|
|
197
|
+
containing `<local-command-stdout>`, and `:5061`, in the `session/load` transcript replay. Both
|
|
198
|
+
run agent→client on text coming BACK from the transcript; our augment rides client→agent on the
|
|
199
|
+
first `session/prompt` and is never re-emitted, because entwurf calls `session/load` nowhere
|
|
200
|
+
(`[측정 2026-09-18]` `git grep loadSession` in `pi-extensions/lib/acp/` hits only the
|
|
201
|
+
`session-store.ts` capability TYPE, never a wire call). The augment also emits no
|
|
202
|
+
`<system-reminder>` tag of its own (`augment.ts` 직독), so no prose of ours is strippable.
|
|
203
|
+
- **0.78.0's compaction update is a NEW `sessionUpdate` kind and is inert for us, twice over.**
|
|
204
|
+
`compaction_update` and `compaction_summary_chunk` are new in `dist/context-compaction.js`
|
|
205
|
+
(`[측정 2026-09-18]` `sessionUpdate: "…"` literal sweep across both dists: 0.76.0 has 15 distinct kinds,
|
|
206
|
+
0.79.0 has 17, and the two new ones are exactly these). Gate one: the lifecycle's
|
|
207
|
+
`presentation` is `clientSupportsCompactionUpdates(this.clientCapabilities) ? "compaction_update"
|
|
208
|
+
: "tool_call"` (`dist/acp-agent.js:1875-1879`), and that predicate reads
|
|
209
|
+
`capabilities?.session?.compaction` — entwurf sends `clientCapabilities: {}` (`backend.ts:1758`),
|
|
210
|
+
so we keep the 0.75.0 `tool_call` presentation §11-8 already measured. The replay path
|
|
211
|
+
(`:5082`) is guarded by the same predicate at `:4915`. Gate two, independent: our mapper's
|
|
212
|
+
update switch has a `default: break` — "unknown update kinds are ignored (forward-compatible)"
|
|
213
|
+
(`event-mapper.ts:311-313`, `:377-378`). The "map every terminal reason, unknown is an error" rule
|
|
214
|
+
is about ACP **stopReason**, a different axis; `stopReason` literals are unchanged across the
|
|
215
|
+
two dists (`[측정 2026-09-18]` sweep: `"cancelled"` only, both versions). No code needed.
|
|
216
|
+
- **0.78.0's checkpoint file-change report and AIR diff counts are both behind the AIR gate.**
|
|
217
|
+
`supportsAgentFileChangeReport` is `clientSupportsAirCapability(capabilities, "agentFileChangeReport")`
|
|
218
|
+
(`dist/file-change-audit.js:27-29` 직독), and `air-extension.js`'s only delta is one added
|
|
219
|
+
constant `AIR_DIFF_STATS_KEY` (full-file `diff`, one line). Same `clientCapabilities: {}`
|
|
220
|
+
argument as the 0.76.0 `recommendedValue` entry — re-measured, not inherited.
|
|
221
|
+
- **0.79.0's shell-command permission prompts reach our permission handler's INPUT and change no
|
|
222
|
+
decision.** The change reorders options: when the CLI hints `defaultToNo` (new in 0.79.0 —
|
|
223
|
+
`[측정 2026-09-18]` `grep -rn defaultToNo` over the 0.76.0 dist is **0 hits**), the option array
|
|
224
|
+
now sorts reject-first (`dist/permissions/options.js:5-10`). entwurf's approve-all policy
|
|
225
|
+
selects `options.find((o) => o.kind === "allow_once" || o.kind === "allow_always")` and only
|
|
226
|
+
falls back to `options[0]` when that find fails (`backend.ts:824-831`) — a find by KIND, so
|
|
227
|
+
order cannot flip it. The fallback is unreachable besides: every builder in
|
|
228
|
+
`dist/permissions/options/` routes through `withOptionalUpdate`/`withGeneratedUpdate`, both of
|
|
229
|
+
which lead with `allowOnce()`, and the hand-rolled `tools.js` sets each carry an `allow_once` or
|
|
230
|
+
`allow_always` (직독 of `shared.js` + `tools.js`). The title change (Bash/PowerShell titles now
|
|
231
|
+
bypass `humanText` compaction) lands on a field we never read.
|
|
232
|
+
- **0.77.0–0.79.0's remaining fixes are unreachable under our capability posture.** The
|
|
233
|
+
AskUserQuestion multi-select/custom-text fixes (#1031, #1131) require form elicitation:
|
|
234
|
+
the adapter computes `disallowedTools = elicitationSupport.form ? [] : ["AskUserQuestion"]`
|
|
235
|
+
from `clientCapabilities.elicitation.form`, which our `{}` leaves false, so AskUserQuestion is
|
|
236
|
+
disabled on every session we open. The TaskList regex fix (#1006) is internal parsing. #1128's
|
|
237
|
+
tool names land on `presentation.toolCall._meta.claudeCode` on the permission-request path
|
|
238
|
+
(`dist/acp-agent.js:5317-5325`), which our handler ignores; our `titleForTool` already read
|
|
239
|
+
`_meta.claudeCode.toolName` as a fallback behind `update.title`, so it is additive at worst.
|
|
240
|
+
- **Our model-forcing and accounting wire calls are byte-identical across 0.76.0 → 0.79.0.**
|
|
241
|
+
`[측정 2026-09-18, brace-matched extraction from both dists, md5]`: `setSessionConfigOption`
|
|
242
|
+
(4,858 B, identical), `sessionUsage` (373 B, identical), `turnQuotaMeta` and `quotaTokenCount`
|
|
243
|
+
(identical md5), and `resolveModelPreference` lives in `dist/session-model.js`, whose whole file
|
|
244
|
+
is byte-identical (`md5 cfd031d0…` both versions). The `settings`-as-STRING-PATH branch our
|
|
245
|
+
call-site contract names is still on the unconditional `session/new` path
|
|
246
|
+
(`dist/acp-agent.js:6010-6012`).
|
|
247
|
+
|
|
171
248
|
- **The one 0.73.0 → 0.75.1 change that DOES reach us:** context compaction is now surfaced as a
|
|
172
249
|
synthetic ACP tool lifecycle (0.75.0, #991) — a `tool_call` with `kind: "think"`, title
|
|
173
250
|
`Compact conversation`, and `_meta.contextCompaction` schema v1 — where it used to arrive as
|
|
@@ -292,23 +369,25 @@ caller-session `_meta`, and cross-machine certification.
|
|
|
292
369
|
|
|
293
370
|
A backend can return `newSession` before its declared MCP server is callable. This was
|
|
294
371
|
observed intermittently on the Claude rail and directly on Cortex's private `mcp.json`
|
|
295
|
-
path. Neither `claude-agent-acp` 0.
|
|
372
|
+
path. Neither `claude-agent-acp` 0.79.0 nor the Cortex landing adds a client-side
|
|
296
373
|
readiness fence over a session's declared MCP servers, and entwurf's common loop
|
|
297
374
|
calls `mcpServerStatus()` nowhere.
|
|
298
|
-
(Re-measured at the 0.
|
|
299
|
-
not reused, the way the 0.
|
|
300
|
-
`mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.
|
|
301
|
-
`[측정 2026-09-
|
|
375
|
+
(Re-measured at the 0.76.0 → 0.79.0 bump, not inherited — the previous bump's argument is
|
|
376
|
+
not reused, the way the 0.75.1 → 0.76.0 entry did not reuse 0.73.0 → 0.75.1's.
|
|
377
|
+
`mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.76.0 and 2 at v0.79.0**
|
|
378
|
+
`[측정 2026-09-18, upstream v0.79.0/src/acp-agent.ts read directly, grep -n]`; they first
|
|
302
379
|
appeared in 0.71.0 via `0cbbaf3` (MCP OAuth, LLM-25012), so the ADAPTER calls it where it
|
|
303
|
-
once did not. Both were re-read at `v0.
|
|
304
|
-
(v0.75.1: `:1736` / `:1829`; v0.73.0: `:1618` / `:1711`): the
|
|
305
|
-
`authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every
|
|
306
|
-
`needs-auth`; the second polls a SINGLE named server to `connected` under
|
|
307
|
-
Neither waits on every declared server before `newSession` returns. That
|
|
308
|
-
handshake, not a readiness fence, so the boundary below is unchanged. The
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
380
|
+
once did not. Both were re-read at `v0.79.0 src/acp-agent.ts:1773` and `:1866`
|
|
381
|
+
(v0.76.0: `:1762` / `:1855`; v0.75.1: `:1736` / `:1829`; v0.73.0: `:1618` / `:1711`): the
|
|
382
|
+
first sits inside `authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every
|
|
383
|
+
status that is not `needs-auth`; the second polls a SINGLE named server to `connected` under
|
|
384
|
+
an OAuth deadline. Neither waits on every declared server before `newSession` returns. That
|
|
385
|
+
is an auth handshake, not a readiness fence, so the boundary below is unchanged. The
|
|
386
|
+
surrounding 200-line window is byte-identical (`diff v0.76.0:1662-1862 v0.79.0:1673-1873`,
|
|
387
|
+
empty) and the region moved +11 while the file shrank 10,405 → 10,329 lines. The 0.77.0
|
|
388
|
+
agent-picker removal, the 0.78.0 compaction/checkpoint/AIR work and the 0.79.0 permission
|
|
389
|
+
presentation touch no part of this path, so the other reachable-surface findings stand as
|
|
390
|
+
re-measured in the capability-posture section above.
|
|
312
391
|
This bump changes no readiness behavior and closes no part of #72.)
|
|
313
392
|
|
|
314
393
|
### 11-7-a/b. Instrument and first measurement
|
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.86.0 <0.87` — 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.86.0
|
|
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.86.0 <0.87`. 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.86.0 <0.87`) 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";
|
|
@@ -740,6 +746,32 @@ server.tool("entwurf_resume_call", "Reopen ONE DORMANT pi citizen under its OWN
|
|
|
740
746
|
return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
|
|
741
747
|
}
|
|
742
748
|
});
|
|
749
|
+
server.tool("entwurf_callback", "ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads " +
|
|
750
|
+
"ENTWURF_CALLBACK_TARGET and ENTWURF_CALLBACK_NONCE from this process environment " +
|
|
751
|
+
"(injected by the launcher next to the identity scrub), validates garden-id and nonce " +
|
|
752
|
+
"grammar, and dispatches through the existing v2 runner with intent fire-and-forget, " +
|
|
753
|
+
"message=nonce, wants_reply=false. The target is re-resolved by record/decider — env " +
|
|
754
|
+
"names the id, it does not skip dispatch. REFUSES BY NAME when the pair is absent, " +
|
|
755
|
+
"malformed, or this process is a Codex-provenance bridge (window env never reaches that " +
|
|
756
|
+
"MCP child). No arguments, no fallback to a model-supplied target.", {}, async (_args, extra) => {
|
|
757
|
+
const read = readCallbackEnv(process.env);
|
|
758
|
+
if (!read.ok) {
|
|
759
|
+
return textErr(`entwurf_callback: ${read.reason}`);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const sender = await buildSendSenderEnvelope({ requestMeta: extra._meta });
|
|
763
|
+
const rendered = await runAndRenderEntwurfV2FromSurface({
|
|
764
|
+
target: read.target,
|
|
765
|
+
intent: "fire-and-forget",
|
|
766
|
+
message: read.nonce,
|
|
767
|
+
wants_reply: false,
|
|
768
|
+
}, { senderProvider: () => sender });
|
|
769
|
+
return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
return textErr(`entwurf_callback error: ${err instanceof Error ? err.message : String(err)}`);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
743
775
|
// ============================================================================
|
|
744
776
|
// Main
|
|
745
777
|
// ============================================================================
|
|
@@ -123,8 +123,8 @@ export const claudeAdapter = {
|
|
|
123
123
|
},
|
|
124
124
|
// MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
|
|
125
125
|
// session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
|
|
126
|
-
// and a context-occupancy scalar (`used`; both read at 0.
|
|
127
|
-
// `dist/acp-agent.js:
|
|
126
|
+
// and a context-occupancy scalar (`used`; both read at 0.79.0
|
|
127
|
+
// `dist/acp-agent.js:3504-3519`). Both are measured, so backend.ts seals them.
|
|
128
128
|
// The token partition is NOT declared here: ACP's only token carrier is a
|
|
129
129
|
// per-turn round-trip aggregate, which is not what pi's four fields mean.
|
|
130
130
|
sealsTurnAccounting: true,
|
|
@@ -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
|
*/
|
|
@@ -124,7 +124,8 @@ export function buildClaudeSessionMeta(params, normalizedSystemPrompt) {
|
|
|
124
124
|
// claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
|
|
125
125
|
// JSON.parse for that form moved out of `resolvedProvider` up onto the
|
|
126
126
|
// unconditional `session/new` path — a shape that WOULD reach us, and does not,
|
|
127
|
-
// only because we never hand it a path.
|
|
127
|
+
// only because we never hand it a path. Still on that path at 0.79.0
|
|
128
|
+
// (`dist/acp-agent.js:6010-6012`). Keep it inline: a path would put file
|
|
128
129
|
// IO, and its failure modes, inside every session creation.
|
|
129
130
|
settings: {
|
|
130
131
|
permissions: {
|
|
@@ -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
|
+
}
|
|
@@ -102,7 +102,7 @@ export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"];
|
|
|
102
102
|
// unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
|
|
103
103
|
// THIS set is a deliberate future decision, gated by a real compatible
|
|
104
104
|
// control-socket predicate — never by silently mapping sessions to dead/indeterminate
|
|
105
|
-
// (R1 핵심).
|
|
105
|
+
// (R1 핵심). pi-extensions/lib/entwurf-facts.test.ts pins this == ["pi"] and asserts the native-push
|
|
106
106
|
// domain is disjoint from it.
|
|
107
107
|
export const LIVENESS_DOMAIN_BACKENDS = ["pi"];
|
|
108
108
|
export function isLivenessSupported(backend) {
|
|
@@ -113,7 +113,7 @@ export function isLivenessSupported(backend) {
|
|
|
113
113
|
// live vendor app-server target probe), NOT a control socket. This domain is DISJOINT
|
|
114
114
|
// from LIVENESS_DOMAIN_BACKENDS: an Antigravity or Codex session is `unsupported` on
|
|
115
115
|
// the socket FACT axis (entwurf_peers) yet independently measured on the native-push
|
|
116
|
-
// axis. The two are separate rails on purpose —
|
|
116
|
+
// axis. The two are separate rails on purpose — pi-extensions/lib/entwurf-facts.test.ts pins both sets
|
|
117
117
|
// and asserts their intersection is ∅ (a backend can never be in both domains).
|
|
118
118
|
export const NATIVE_PUSH_BACKENDS = ["antigravity", "codex"];
|
|
119
119
|
export function nativePushSupported(backend) {
|
|
@@ -79,7 +79,7 @@ export const ENTWURF_V2_LOCK_DIR = path.join(os.homedir(), ".pi", "entwurf-v2-lo
|
|
|
79
79
|
export const LOCK_SUFFIX = ".lock";
|
|
80
80
|
export const LOCK_OWNER = "entwurf_v2";
|
|
81
81
|
/** The reject reason a lock conflict maps to. Kept as a literal here (the lock
|
|
82
|
-
* primitive stays decoupled from the full contract); `
|
|
82
|
+
* primitive stays decoupled from the full contract); `pi-extensions/lib/entwurf-v2-lock.test.ts`
|
|
83
83
|
* cross-checks it against the contract's ENTWURF_V2_REJECT_REASONS so the two
|
|
84
84
|
* cannot drift. */
|
|
85
85
|
export const LOCK_CONFLICT_REASON = "target-locked";
|
|
@@ -50,6 +50,16 @@ export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "code
|
|
|
50
50
|
* permission dialect: omp's approval layer consults the same minted string (`source-audit.md`).
|
|
51
51
|
*/
|
|
52
52
|
export const FRESH_CALL_CALLBACK_TOOL = {
|
|
53
|
+
pi: "entwurf_callback",
|
|
54
|
+
"claude-code": "mcp__entwurf-bridge__entwurf_callback",
|
|
55
|
+
copilot: "entwurf-bridge-entwurf_callback",
|
|
56
|
+
omp: "mcp__entwurf_bridge_entwurf_callback",
|
|
57
|
+
// Codex's tool process is the operator app-server, not the pane: the no-arg
|
|
58
|
+
// verb refuses there by name. First action stays the delivery verb with args.
|
|
59
|
+
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
60
|
+
};
|
|
61
|
+
/** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
|
|
62
|
+
export const FRESH_CALL_DELIVERY_TOOL = {
|
|
53
63
|
pi: "entwurf_v2",
|
|
54
64
|
"claude-code": "mcp__entwurf-bridge__entwurf_v2",
|
|
55
65
|
copilot: "entwurf-bridge-entwurf_v2",
|
|
@@ -95,8 +105,11 @@ export const FRESH_CALL_PEERS_TOOL = {
|
|
|
95
105
|
* `[측정, n=3, same model]` adding the sentence below produced 3/3 callbacks. It is a FACT about the
|
|
96
106
|
* runtime, in the register the rest of this framing uses since the prohibitions came out: it grants
|
|
97
107
|
* nothing, forbids nothing, and names the one call that turns a listed name into a callable tool.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
108
|
+
* ALL THREE tools ride ONE `ToolSearch` select — the birth callback, the DELIVERY verb the last
|
|
109
|
+
* line of the framing names, and the peers listing the corroboration sentence offers. The delivery
|
|
110
|
+
* verb has to be in the same select or the deferral simply moves from the first action to the last:
|
|
111
|
+
* a sibling that finishes its task and cannot call `entwurf_v2` is the same production silence,
|
|
112
|
+
* spent instead of saved.
|
|
100
113
|
*
|
|
101
114
|
* Every other backend gets an EMPTY array, deliberately: pi has no deferred-tool surface, and the
|
|
102
115
|
* other three were never measured to need it. A hint invented for a runtime nobody probed would be
|
|
@@ -106,7 +119,7 @@ export const FRESH_CALL_TOOL_LOAD_HINT = {
|
|
|
106
119
|
pi: [],
|
|
107
120
|
"claude-code": [
|
|
108
121
|
"If ${callbackTool} is not callable yet, the entwurf-bridge server is still connecting or its",
|
|
109
|
-
'tools are deferred: load them first with ToolSearch("select:${callbackTool},${peersTool}") —',
|
|
122
|
+
'tools are deferred: load them first with ToolSearch("select:${callbackTool},${deliveryTool},${peersTool}") —',
|
|
110
123
|
"it waits for the server — then call ${callbackTool}.",
|
|
111
124
|
],
|
|
112
125
|
copilot: [],
|
|
@@ -125,21 +138,21 @@ export const FRESH_CALL_TOOL_LOAD_HINT = {
|
|
|
125
138
|
export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
126
139
|
/** Payload grammar version, matched exactly by the decoder. A bump means a stale installed
|
|
127
140
|
* unit, which is the one thing `doctor-omp-bridge` exists to say out loud. */
|
|
128
|
-
export const OMP_BOOTSTRAP_VERSION =
|
|
141
|
+
export const OMP_BOOTSTRAP_VERSION = 2;
|
|
129
142
|
/**
|
|
130
143
|
* The whole of what a fresh omp sibling is launched with.
|
|
131
144
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
145
|
+
* TWO FIELDS, CLOSED. Address (target + nonce) rides process env, not this payload — a second
|
|
146
|
+
* copy here would be a second address axis. The decoder refuses an unknown key, so this object
|
|
147
|
+
* is the entire remaining contract: the task released after the no-arg callback succeeds.
|
|
148
|
+
* `callerGardenId` / `nonce` stay on the function so launcher call sites do not grow a second
|
|
149
|
+
* shape; they are not serialised.
|
|
137
150
|
*/
|
|
138
151
|
export function buildOmpBootstrapPayload(params) {
|
|
152
|
+
void params.callerGardenId;
|
|
153
|
+
void params.nonce;
|
|
139
154
|
return JSON.stringify({
|
|
140
155
|
v: OMP_BOOTSTRAP_VERSION,
|
|
141
|
-
target: params.callerGardenId,
|
|
142
|
-
nonce: params.nonce,
|
|
143
156
|
task: params.task,
|
|
144
157
|
});
|
|
145
158
|
}
|
|
@@ -271,7 +284,11 @@ export function composeBackendArgs(backend, composition, model, resolveCodexSock
|
|
|
271
284
|
case "pi":
|
|
272
285
|
return [composition.prompt, "--entwurf-control", "--model", model];
|
|
273
286
|
case "claude-code":
|
|
274
|
-
return [
|
|
287
|
+
return [
|
|
288
|
+
composition.prompt,
|
|
289
|
+
`--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]},${FRESH_CALL_DELIVERY_TOOL["claude-code"]}`,
|
|
290
|
+
`--model=${model}`,
|
|
291
|
+
];
|
|
275
292
|
case "copilot":
|
|
276
293
|
return ["copilot", "--interactive", composition.prompt, "--model", model, "--yolo"];
|
|
277
294
|
case "omp":
|
|
@@ -319,26 +336,41 @@ export function composeBackendArgs(backend, composition, model, resolveCodexSock
|
|
|
319
336
|
* corroborate the caller instead of an instruction not to look.
|
|
320
337
|
*/
|
|
321
338
|
export function composeFreshCallFraming(params) {
|
|
322
|
-
const
|
|
339
|
+
const birthTool = FRESH_CALL_CALLBACK_TOOL[params.backend];
|
|
340
|
+
const deliveryTool = FRESH_CALL_DELIVERY_TOOL[params.backend];
|
|
323
341
|
const peersTool = FRESH_CALL_PEERS_TOOL[params.backend];
|
|
324
342
|
// The hint is a per-backend TEMPLATE so the two tool dialects stay the single source of their
|
|
325
343
|
// own spelling here too — a hint that hard-coded Claude's name would go stale the day a dialect
|
|
326
344
|
// moves, and it would go stale silently, in the one sentence a stuck child depends on.
|
|
327
|
-
const loadHint = FRESH_CALL_TOOL_LOAD_HINT[params.backend].map((line) => line
|
|
345
|
+
const loadHint = FRESH_CALL_TOOL_LOAD_HINT[params.backend].map((line) => line
|
|
346
|
+
.replaceAll("${callbackTool}", birthTool)
|
|
347
|
+
.replaceAll("${deliveryTool}", deliveryTool)
|
|
348
|
+
.replaceAll("${peersTool}", peersTool));
|
|
328
349
|
const hintBlock = loadHint.length === 0 ? [] : ["", ...loadHint];
|
|
329
350
|
if (params.openingLine.length === 0) {
|
|
330
351
|
throw new Error("fresh-call composition: openingLine is empty — the rail must state where it placed the sibling");
|
|
331
352
|
}
|
|
353
|
+
const firstAction = params.backend === "codex"
|
|
354
|
+
? [
|
|
355
|
+
`FIRST ACTION, before reading files or anything else: call ${birthTool} with ` +
|
|
356
|
+
`target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
357
|
+
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
358
|
+
`${params.nonce} is a correlation tag this caller minted for this one launch: it names no`,
|
|
359
|
+
"secret and grants no access. Sending it is how the caller learns the garden id you were born",
|
|
360
|
+
"with, which it has no other way to know. Your own record does not carry the caller's address,",
|
|
361
|
+
"so reporting your identity here does not reach it — that tool call does.",
|
|
362
|
+
"entwurf_callback is refused on this rail: the tool process is the operator app-server, not this pane.",
|
|
363
|
+
]
|
|
364
|
+
: [
|
|
365
|
+
`FIRST ACTION, before reading files or anything else: call ${birthTool} with no arguments.`,
|
|
366
|
+
"That call is how the caller learns the garden id you were born with. There are no",
|
|
367
|
+
"parameters: a target or message you type would be a second address axis.",
|
|
368
|
+
"Your own record does not carry the caller's address, so reporting your identity here does not reach it — that tool call does.",
|
|
369
|
+
];
|
|
332
370
|
return [
|
|
333
371
|
params.openingLine,
|
|
334
372
|
"",
|
|
335
|
-
|
|
336
|
-
`target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
337
|
-
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
338
|
-
`${params.nonce} is a correlation tag this caller minted for this one launch: it names no`,
|
|
339
|
-
"secret and grants no access. Sending it is how the caller learns the garden id you were born",
|
|
340
|
-
"with, which it has no other way to know. Your own record does not carry the caller's address,",
|
|
341
|
-
"so reporting your identity here does not reach it — that tool call does.",
|
|
373
|
+
...firstAction,
|
|
342
374
|
"",
|
|
343
375
|
`You can corroborate the caller first if you want to: ${peersTool} is a read-only listing of`,
|
|
344
376
|
`this garden's citizens and ${params.callerGardenId} is one of them. That listing is capped, so`,
|
|
@@ -356,7 +388,7 @@ export function composeFreshCallFraming(params) {
|
|
|
356
388
|
// its window is not the delivery. Nothing here watches for completion or sends on the
|
|
357
389
|
// sibling's behalf — this rail launches and lets go (Hard Rule 16, and the reason there is no
|
|
358
390
|
// supervisor anywhere in it).
|
|
359
|
-
`When the task reaches its requested final result, send that result to the same target with ${
|
|
391
|
+
`When the task reaches its requested final result, send that result to the same target with ${deliveryTool}.`,
|
|
360
392
|
"Output in this sibling window is not delivered to the caller.",
|
|
361
393
|
];
|
|
362
394
|
}
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
import { createHash } from "node:crypto";
|
|
44
44
|
import { statSync } from "node:fs";
|
|
45
45
|
import path from "node:path";
|
|
46
|
+
import { callbackEnvAssignments } from "./callback-env.js";
|
|
46
47
|
import { buildOmpBootstrapPayload, composeBackendArgs, composeFreshCallFraming, normalizeFreshCallInputs, TASK_MAX_CHARS, } from "./fresh-call-composition.js";
|
|
47
48
|
/** The pilot set, closed. `[#116 decision]` herdr's own `--kind` enum is much larger, and that is
|
|
48
49
|
* NOT evidence of support: every other backend is a pre-mutation named reject here, and there is
|
|
@@ -235,6 +236,7 @@ export function buildHerdrTabCreateArgs(params) {
|
|
|
235
236
|
"PI_SESSION_ID=",
|
|
236
237
|
"--env",
|
|
237
238
|
"PI_AGENT_ID=",
|
|
239
|
+
...callbackEnvAssignments(params.callback).flatMap((assignment) => ["--env", assignment]),
|
|
238
240
|
];
|
|
239
241
|
}
|
|
240
242
|
/** `agent start <name> --kind <kind> --pane <id> -- <backend argv>`. Everything after `--` is the
|
|
@@ -565,6 +567,7 @@ export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL
|
|
|
565
567
|
const tabRun = await run(buildHerdrTabCreateArgs({
|
|
566
568
|
workspaceId: callerPane.workspaceId,
|
|
567
569
|
...(cwd === undefined ? {} : { cwd }),
|
|
570
|
+
callback: { target: callerGardenId, nonce },
|
|
568
571
|
}));
|
|
569
572
|
if (tabRun.status !== 0) {
|
|
570
573
|
// WHETHER ANYTHING EXISTS depends on WHO failed, and the stderr says which. herdr's own
|
|
@@ -881,9 +884,10 @@ export function renderHerdrFreshCall(result) {
|
|
|
881
884
|
`mean the sibling is running, that its first turn ran, or that the task was delivered. The tab and pane ` +
|
|
882
885
|
`coordinates are a view and can change under the sibling — they are not an address and nothing may be ` +
|
|
883
886
|
`dispatched to them.\n` +
|
|
884
|
-
`The sibling's garden id arrives separately —
|
|
885
|
-
`
|
|
886
|
-
`
|
|
887
|
+
`The sibling's garden id arrives separately — its first action is the zero-argument callback verb, and ` +
|
|
888
|
+
`the sender envelope of THAT message is the address. The nonce above is the correlation tag it carries; ` +
|
|
889
|
+
`the sibling reads it from its own launch env, never from this receipt. Nothing is polling for it; if it ` +
|
|
890
|
+
`never comes, the pane is visible and can be read directly.`,
|
|
887
891
|
isError: false,
|
|
888
892
|
};
|
|
889
893
|
}
|