@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.
Files changed (102) hide show
  1. package/AGENTS.md +3 -1
  2. package/BASELINE.md +2 -0
  3. package/CHANGELOG.md +307 -0
  4. package/FAQ.md +47 -0
  5. package/README.md +5 -3
  6. package/VERIFY.md +43 -2
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +97 -18
  9. package/docs/setup-clean-host.md +8 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
  21. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
  22. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  23. package/mcp/entwurf-bridge/src/index.ts +41 -1
  24. package/mcp/entwurf-bridge/test.sh +2 -2
  25. package/package.json +15 -13
  26. package/pi-extensions/entwurf-control.ts +56 -0
  27. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  28. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  29. package/pi-extensions/lib/acp/backend.ts +20 -10
  30. package/pi-extensions/lib/acp/context.ts +30 -10
  31. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  32. package/pi-extensions/lib/acp/session-store.ts +2 -2
  33. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  34. package/pi-extensions/lib/callback-env.ts +78 -0
  35. package/pi-extensions/lib/compaction-send-guard.ts +4 -1
  36. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  37. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  38. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  39. package/pi-extensions/lib/fresh-call-composition.ts +56 -22
  40. package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
  41. package/pi-extensions/lib/herdr-placement.ts +13 -4
  42. package/pi-extensions/lib/mux-fresh-call.ts +23 -8
  43. package/pi-extensions/meta-bridge-omp.ts +35 -48
  44. package/run.sh +327 -246
  45. package/scripts/check-acp-backend-preflight.ts +117 -9
  46. package/scripts/check-acp-carrier-augment.ts +6 -4
  47. package/scripts/check-acp-event-mapper.ts +17 -6
  48. package/scripts/check-acp-prompt-builder.ts +17 -9
  49. package/scripts/check-acp-prompt-lifecycle.ts +23 -6
  50. package/scripts/check-acp-session-reuse.ts +64 -51
  51. package/scripts/check-acp-session-store.ts +31 -8
  52. package/scripts/check-acp-stop-reason.ts +20 -2
  53. package/scripts/check-acp-stream-hooks.ts +22 -5
  54. package/scripts/check-acp-usage-accounting.ts +42 -18
  55. package/scripts/check-entwurf-bridge-boot.ts +16 -1
  56. package/scripts/check-fresh-call-dispatch.ts +4 -5
  57. package/scripts/check-gate-qualification.ts +45 -5
  58. package/scripts/check-herdr-activation.ts +73 -0
  59. package/scripts/check-herdr-fresh-call.ts +22 -6
  60. package/scripts/check-herdr-sandbox.ts +14 -2
  61. package/scripts/check-install-container.sh +1 -1
  62. package/scripts/check-pi-launch.ts +361 -0
  63. package/scripts/check-release-gate-outcomes.ts +7 -0
  64. package/scripts/inventory-verification-surface.ts +86 -10
  65. package/scripts/lib/mutation-qualify.ts +1 -1
  66. package/scripts/mutants/acp-backend-preflight.json +20 -0
  67. package/scripts/mutants/bridge-boot-resume.json +26 -0
  68. package/scripts/mutants/herdr-activation.json +16 -0
  69. package/scripts/mutants/herdr-fresh-call.json +11 -0
  70. package/scripts/mutants/herdr-plugin-build.json +1 -1
  71. package/scripts/mutants/mux-fresh-call.json +21 -8
  72. package/scripts/mutants/omp-fresh.json +5 -9
  73. package/scripts/mutants/pack-install.json +3 -3
  74. package/scripts/mutants/pi-launch.json +113 -0
  75. package/scripts/mutants/resume-args.json +6 -6
  76. package/scripts/mutants/v2-surface.json +2 -2
  77. package/scripts/mutants/v2-visible-resume.json +17 -17
  78. package/scripts/probe-bridge-command.ts +1 -0
  79. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  80. package/scripts/smoke-acp-session-reuse-live.ts +23 -5
  81. package/scripts/smoke-agy-install-state.sh +2 -2
  82. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  83. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  84. package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
  85. package/scripts/smoke-herdr-raw-install-live.sh +877 -0
  86. package/scripts/smoke-omp-fresh-live.ts +4 -2
  87. package/scripts/smoke-omp-mcp-state.sh +1 -1
  88. package/scripts/smoke-pi-provider-state.sh +2 -2
  89. package/scripts/tsconfig.json +5 -0
  90. package/scripts/check-entwurf-facts.ts +0 -391
  91. package/scripts/check-entwurf-resume-args.ts +0 -122
  92. package/scripts/check-entwurf-v2-decider.ts +0 -657
  93. package/scripts/check-entwurf-v2-lock.ts +0 -371
  94. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  95. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  96. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  97. package/scripts/check-entwurf-v2-release.ts +0 -151
  98. package/scripts/check-entwurf-v2-runner.ts +0 -448
  99. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  100. package/scripts/check-entwurf-v2-send.ts +0 -458
  101. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  102. package/scripts/check-socket-discovery.ts +0 -401
@@ -21,7 +21,7 @@
21
21
  * THE TWO AXES ARE NOT EQUALLY EXACT, AND THE DIFFERENCE IS NAMED. On the claude axis
22
22
  * the reported value IS the key, byte for byte. On the pi axis it is a session FILE
23
23
  * PATH, and the key is recovered from that filename by a strict conversion measured on
24
- * pi 0.85.1 — a VENDOR FLOOR, not a key equality. Calling both "a unique-key join"
24
+ * pi 0.86.0 — a VENDOR FLOOR, not a key equality. Calling both "a unique-key join"
25
25
  * would hide which one can drift when a vendor renames a file. The conversion is
26
26
  * deliberately strict at both ends so that drift lands as a missed join, never a wrong
27
27
  * one, and a listing that declined to read anything says so (see `declinedReports`).
@@ -65,14 +65,23 @@ const OFFICIAL_REPORTS = {
65
65
  "herdr:pi": { agent: "pi", kind: "path", backend: "pi" },
66
66
  };
67
67
  /**
68
- * A pi session filename, measured 2026-09-14 on pi 0.85.1:
68
+ * A pi session filename, measured 2026-09-20 on pi 0.86.0:
69
69
  *
70
- * 2026-09-14T05-17-03-979Z_01a09e58-f06a-70e8-b14a-1f0f0c7f7c7d.jsonl
70
+ * 2026-09-20T00-35-44-197Z_01a0bc3d-87c4-738e-b391-265c8ba5a1b0.jsonl
71
71
  * └────────── start stamp ─────────┘ └──────── nativeSessionId ────────┘
72
72
  *
73
+ * `[측정 2026-09-20]` that is a REAL file a sandboxed pi 0.86.0 wrote, and its own
74
+ * `{"type":"session"}` header carries `id: 01a0bc3d-87c4-738e-b391-265c8ba5a1b0` —
75
+ * the exact string `piNativeSessionIdFromPath` recovers from the name. The layout was
76
+ * re-measured rather than carried: pi 0.86.0 rewrote session-manager.ts (+269 −141
77
+ * over v0.85.1), but every line that BUILDS a name is byte-unchanged
78
+ * (`${fileTimestamp}_${this.sessionId}.jsonl`, session-manager.ts:991/1527/1703); the
79
+ * only diff in that file's `.jsonl` lines is a lambda parameter rename on the READER
80
+ * side. A source read alone would not have settled it, so the real file is the receipt.
81
+ *
73
82
  * WHY THIS IS PINNED HERE AND NOT INFERRED. The uuid is the join key and it lives in
74
83
  * a VENDOR filename, so this rule depends on pi's naming and would break silently if
75
- * pi changed it. **Vendor floor: pi 0.85.1, measured 2026-09-14** — whether that layout
84
+ * pi changed it. **Vendor floor: pi 0.86.0, measured 2026-09-20** — whether that layout
76
85
  * is a vendor contract or a convention is NOT measured, so this is the one place in the
77
86
  * axis that can drift under us. It is therefore strict on both ends — the name must end
78
87
  * in `.jsonl` and the tail after the last `_` must be a well-formed uuid — and a name
@@ -85,11 +85,12 @@
85
85
  * the session the window is actually in. It still reports no `pane_current_path`, and
86
86
  * there is no "session created" field because nothing here creates one.
87
87
  */
88
+ import { callbackEnvAssignments } from "./callback-env.js";
88
89
  import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
89
90
  import { CODEX_CALLER_SEAT_HINT, resolveCodexCallerSeat, } from "./codex-caller-seat.js";
90
91
  import { CODEX_CALLER_PREFLIGHT_HINT, CODEX_LAUNCH_CWD_PREFLIGHT_HINT, CODEX_PREFLIGHT_HINT, codexLaunchCwdFreshPreflight, } from "./codex-fresh-preflight.js";
91
92
  import { COPILOT_PREFLIGHT_HINT, copilotFreshPreflight, } from "./copilot-fresh-preflight.js";
92
- import { buildOmpBootstrapPayload, composeBackendArgs, composeFreshCallPrompt, FRESH_CALL_BACKENDS, FRESH_CALL_CALLBACK_TOOL, isSafeFreshCallModel, MODEL_MAX_CHARS, mintNonce, normalizeFreshCallInputs, OMP_BOOTSTRAP_FLAG, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, } from "./fresh-call-composition.js";
93
+ import { buildOmpBootstrapPayload, composeBackendArgs, composeFreshCallPrompt, FRESH_CALL_BACKENDS, FRESH_CALL_CALLBACK_TOOL, FRESH_CALL_DELIVERY_TOOL, isSafeFreshCallModel, MODEL_MAX_CHARS, mintNonce, normalizeFreshCallInputs, OMP_BOOTSTRAP_FLAG, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, } from "./fresh-call-composition.js";
93
94
  import { assertLaunchTarget, LaunchPreconditionError, resolveRuntimeOnPath, } from "./mux-launch.js";
94
95
  import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWindowFields, requireSameContext, runTmux, } from "./mux-placement.js";
95
96
  import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.js";
@@ -106,7 +107,7 @@ import { classifyTmuxSessionName, resolveTmuxSessionId } from "./resolve-tmux-se
106
107
  * says a sibling was opened in the operator's tmux session, and the Codex socket path. Both are
107
108
  * arguments to the leaf rather than knowledge inside it.
108
109
  */
109
- export { buildOmpBootstrapPayload, FRESH_CALL_BACKENDS, FRESH_CALL_CALLBACK_TOOL, isSafeFreshCallModel, MODEL_MAX_CHARS, mintNonce, OMP_BOOTSTRAP_FLAG, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, };
110
+ export { buildOmpBootstrapPayload, FRESH_CALL_BACKENDS, FRESH_CALL_CALLBACK_TOOL, FRESH_CALL_DELIVERY_TOOL, isSafeFreshCallModel, MODEL_MAX_CHARS, mintNonce, OMP_BOOTSTRAP_FLAG, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, };
110
111
  /** What the tmux rail tells a sibling about where it woke up. The resume verb has no equivalent
111
112
  * because it composes no first turn at all; a herdr rail will pass its own sentence here. */
112
113
  export const TMUX_FRESH_CALL_OPENING_LINE = "You are a fresh visible citizen that entwurf opened in the operator's tmux session.";
@@ -200,9 +201,11 @@ export function selectFreshCallSeat(placement) {
200
201
  * scrub only on the backend whose measurement surfaced it would encode the claim that the other
201
202
  * four are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
202
203
  * authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
203
- * exactly that after this argv has run. This is a fixed two-variable seam and deliberately NOT a
204
- * general env carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping
205
- * power this rail exists to refuse.
204
+ * exactly that after this argv has run. This is a fixed seam and deliberately NOT a general env
205
+ * carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping power this
206
+ * rail exists to refuse. Two more `-e` assignments ride beside the scrub: the callback pair
207
+ * (`callback-env.ts`), launcher-computed, so the sibling's first action does not retype an
208
+ * address out of prose.
206
209
  */
207
210
  const SCRUBBED_INHERITED_ENV = ["PI_SESSION_ID=", "PI_AGENT_ID="];
208
211
  /** Launch argv: the leaf's detached-append shape, the identity scrub, optionally `-c` at the
@@ -215,7 +218,7 @@ const SCRUBBED_INHERITED_ENV = ["PI_SESSION_ID=", "PI_AGENT_ID="];
215
218
  * name never reaches this function — the seat is resolved to a native `$id` before it is
216
219
  * called, and `assertSelector` refuses anything that is not one. `-d` is not optional: without
217
220
  * it a window opened into another session steals that session's focus (measured). */
218
- export function buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd) {
221
+ export function buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd, callback) {
219
222
  assertSelector("session", targetSessionId);
220
223
  assertLaunchTarget(runtimePath);
221
224
  if (cwd !== undefined) {
@@ -228,6 +231,7 @@ export function buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cw
228
231
  "-d",
229
232
  "-a",
230
233
  ...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
234
+ ...callbackEnvAssignments(callback).flatMap((assignment) => ["-e", assignment]),
231
235
  "-t",
232
236
  `${targetSessionId}:{end}`,
233
237
  ...(cwd === undefined ? [] : ["-c", cwd]),
@@ -403,7 +407,10 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
403
407
  console.error(`[fresh-call] ${unanswered}: ${launchCwd}\n` + ` ${CODEX_LAUNCH_CWD_PREFLIGHT_HINT[unanswered]}`);
404
408
  }
405
409
  }
406
- const run = runTmux(buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd), env);
410
+ const run = runTmux(buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd, {
411
+ target: callerGardenId,
412
+ nonce,
413
+ }), env);
407
414
  assertTmuxOk("new-window", run);
408
415
  let fields;
409
416
  try {
@@ -503,9 +510,11 @@ export function renderFreshCall(result) {
503
510
  `\n` +
504
511
  `This is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does ` +
505
512
  `NOT mean the sibling is running, that its first turn ran, or that the task was delivered.\n` +
506
- `The sibling's garden id arrives separately — it calls entwurf_v2 back with the nonce above as its ` +
507
- `first action, and the sender envelope of THAT message is the address. Nothing is polling for it; ` +
508
- `if it never comes, the window is visible and can be read directly.`,
513
+ `The sibling's garden id arrives separately — its first action is the zero-argument callback verb ` +
514
+ `(codex is the one arm that still sends the nonce as arguments), and the sender envelope of THAT ` +
515
+ `message is the address. The nonce above is the correlation tag it carries; the sibling reads it ` +
516
+ `from its own launch env, never from this receipt. Nothing is polling for it; if it never comes, ` +
517
+ `the window is visible and can be read directly.`,
509
518
  isError: false,
510
519
  };
511
520
  }
@@ -77,7 +77,7 @@
77
77
  */
78
78
  import * as fs from "node:fs";
79
79
  import * as path from "node:path";
80
- import { isPlausibleOwnerPid, ompMetaRootBase, ompMetaRoots, requireGardenId, upsertMetaSession, writeMetaSenderMarker, } from "./lib/meta-session.js";
80
+ import { isPlausibleOwnerPid, ompMetaRootBase, ompMetaRoots, upsertMetaSession, writeMetaSenderMarker, } from "./lib/meta-session.js";
81
81
  /**
82
82
  * The §3.5 host discriminator. THE one new predicate this whole lane is allowed to
83
83
  * introduce, and it is the vendor's own top-level mode — not a heuristic over cwd,
@@ -147,7 +147,7 @@ export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
147
147
  /** Payload grammar version. A payload that does not say exactly this is refused rather than
148
148
  * best-guessed: the launcher and this unit ship in the same package, so a mismatch means a
149
149
  * STALE installed unit — the one condition `doctor-omp-bridge` exists to name out loud. */
150
- export const OMP_BOOTSTRAP_VERSION = 1;
150
+ export const OMP_BOOTSTRAP_VERSION = 2;
151
151
  /**
152
152
  * The model-facing callback tool, spelled for omp. `[측정]` the vendor's minter sanitises to
153
153
  * `[a-z_]` and EATS the digit in `entwurf_v2` (`mcp/tool-bridge.ts:384-396`;
@@ -161,7 +161,7 @@ export const OMP_BOOTSTRAP_VERSION = 1;
161
161
  * spellings are held equal by `test/omp-fresh-bootstrap.contract.test.ts`, the same shape
162
162
  * `check-omp-fresh-preflight` already uses for the preflight's reproduced oracles.
163
163
  */
164
- export const OMP_BOOTSTRAP_CALLBACK_TOOL = "mcp__entwurf_bridge_entwurf_v";
164
+ export const OMP_BOOTSTRAP_CALLBACK_TOOL = "mcp__entwurf_bridge_entwurf_callback";
165
165
  /** Task ceiling, held equal to the launcher's `TASK_MAX_CHARS` by the same contract test. */
166
166
  export const OMP_BOOTSTRAP_TASK_MAX_CHARS = 16000;
167
167
  /** How long readiness may take before this bootstrap FAILS and the task is never sent.
@@ -175,12 +175,10 @@ export const OMP_BOOTSTRAP_READY_TIMEOUT_MS = 60_000;
175
175
  * MCP ones (`runner.ts:907-978`). Polling the two public snapshots is the smallest honest
176
176
  * primitive; a FIXED DELAY is forbidden because the gap is a race, not a constant. */
177
177
  export const OMP_BOOTSTRAP_POLL_MS = 100;
178
- /** The nonce the launcher mints (`mintNonce`: `mux-fresh-call-` + 12 random bytes as hex). */
179
- const BOOTSTRAP_NONCE_RE = /^mux-fresh-call-[0-9a-f]{24}$/;
180
178
  /** The wire keys, closed. An unknown key is a REFUSAL rather than an ignored extra: this
181
179
  * flag is the one thing a caller can put arbitrary bytes into, so the decoder's job is to
182
180
  * make "what the launcher meant" and "what this unit will act on" the same set. */
183
- const BOOTSTRAP_KEYS = new Set(["v", "target", "nonce", "task"]);
181
+ const BOOTSTRAP_KEYS = new Set(["v", "task"]);
184
182
  /**
185
183
  * Read the flag value into a payload, or name why not.
186
184
  *
@@ -212,22 +210,12 @@ export function decodeOmpBootstrapPayload(raw) {
212
210
  if (!BOOTSTRAP_KEYS.has(key))
213
211
  return { ok: false, reason: "payload-unknown-key" };
214
212
  }
215
- let target;
216
- try {
217
- target = requireGardenId(obj.target);
218
- }
219
- catch {
220
- return { ok: false, reason: "target-invalid" };
221
- }
222
- const nonce = obj.nonce;
223
- if (typeof nonce !== "string" || !BOOTSTRAP_NONCE_RE.test(nonce))
224
- return { ok: false, reason: "nonce-invalid" };
225
213
  const task = obj.task;
226
214
  if (typeof task !== "string" || task.trim().length === 0)
227
215
  return { ok: false, reason: "task-empty" };
228
216
  if (task.length > OMP_BOOTSTRAP_TASK_MAX_CHARS)
229
217
  return { ok: false, reason: "task-too-long" };
230
- return { ok: true, value: { target, nonce, task } };
218
+ return { ok: true, value: { task } };
231
219
  }
232
220
  /**
233
221
  * Stage one's whole message: call back, and nothing else.
@@ -238,18 +226,13 @@ export function decodeOmpBootstrapPayload(raw) {
238
226
  * no competing goal at all. Stage two exists so this message never has to compete with the
239
227
  * work; adding "then do X" back into it would rebuild exactly the prompt that failed.
240
228
  */
241
- export function buildOmpCallbackOnlyPrompt(params) {
229
+ export function buildOmpCallbackOnlyPrompt() {
242
230
  return [
243
231
  "You are a fresh visible citizen that entwurf opened in the operator's tmux session.",
244
232
  "",
245
- `FIRST AND ONLY ACTION RIGHT NOW: call ${OMP_BOOTSTRAP_CALLBACK_TOOL} with ` +
246
- `target=${params.target}, intent=fire-and-forget, wants_reply=false, and ` +
247
- `message set to exactly ${params.nonce} — that string alone, nothing added.`,
233
+ `FIRST AND ONLY ACTION RIGHT NOW: call ${OMP_BOOTSTRAP_CALLBACK_TOOL} with no arguments.`,
248
234
  "That call is how the agent that opened you learns your address. Make the call before",
249
- "reading files, before planning, and before answering in prose. Do not reword the message.",
250
- "",
251
- "Do not inspect environment variables, do not call entwurf_self, and do not start an MCP",
252
- "server yourself. Your own report of your identity is not the address anyone needs.",
235
+ "reading files, before planning, and before answering in prose. There are no parameters.",
253
236
  "",
254
237
  "Your actual task arrives as the NEXT user message, immediately after that call succeeds.",
255
238
  "Do not ask for it and do not guess at it.",
@@ -521,22 +504,32 @@ export function ompCallbackToolReady(pi) {
521
504
  return registered && active.includes(OMP_BOOTSTRAP_CALLBACK_TOOL);
522
505
  }
523
506
  /**
524
- * Does this tool event name OUR callback, with OUR target and OUR nonce?
507
+ * Does this tool event name OUR callback?
525
508
  *
526
509
  * The event carries no server name, no session id and no original MCP tool name
527
- * (`extensions/types.ts:916-1017`), so the match is built from what IS there: the canonical
528
- * minted tool name plus the two argument values this rail already owns as contract. Exact
529
- * string equality on both — a prefix or `includes` here would let a sibling's nonce release
530
- * this session's task.
510
+ * (`extensions/types.ts:916-1017`), so the match is built from what IS there: exact string
511
+ * equality on the canonical minted tool name a prefix or `includes` here would let a
512
+ * neighbouring tool release this session's task.
513
+ *
514
+ * The target/nonce half of this predicate is GONE, and that is the point of the out-of-band
515
+ * callback: the address rides the sibling's process env, so the verb takes no arguments and
516
+ * there are no argument values left to compare. What replaces the comparison is the ABSENCE
517
+ * of arguments — a call carrying a target or a message is a model supplying an address, which
518
+ * is the second address axis Hard Rule 2 refuses, and it must not release the task. Correlation
519
+ * to THIS bootstrap is not weakened by the removal: it is carried by the stored `toolCallId`
520
+ * (one session, one live tool call) plus the fact that only this session's env named the caller.
531
521
  */
532
- function matchesCallback(event, payload) {
522
+ function matchesCallback(event, _payload) {
533
523
  if (event?.toolName !== OMP_BOOTSTRAP_CALLBACK_TOOL)
534
524
  return false;
535
525
  const input = event?.input;
536
- if (typeof input !== "object" || input === null || Array.isArray(input))
526
+ if (input === undefined || input === null)
527
+ return true;
528
+ if (typeof input !== "object" || Array.isArray(input))
537
529
  return false;
538
530
  const args = input;
539
- return args.target === payload.target && args.message === payload.nonce;
531
+ // No-arg verb: a supplied target/message is a second address axis — not our callback.
532
+ return args.target === undefined && args.message === undefined;
540
533
  }
541
534
  /**
542
535
  * Create one bootstrap for one minted session.
@@ -580,7 +573,7 @@ export function createOmpBootstrap(opts) {
580
573
  if (isFinal())
581
574
  return;
582
575
  phase = "failed";
583
- log("WARN", `bootstrap-failed nonce=${payload.nonce}: ${why}; the task was NOT sent`);
576
+ log("WARN", `bootstrap-failed: ${why}; the task was NOT sent`);
584
577
  }
585
578
  /**
586
579
  * Both stages send the SAME way: `pi.sendUserMessage(content)` with NO delivery option.
@@ -616,8 +609,8 @@ export function createOmpBootstrap(opts) {
616
609
  // two callback prompts for one bootstrap.
617
610
  phase = "callback-sent";
618
611
  stopTimer();
619
- log("INFO", `bootstrap-ready nonce=${payload.nonce}: callback tool live, sending callback-only prompt`);
620
- send(buildOmpCallbackOnlyPrompt({ target: payload.target, nonce: payload.nonce }));
612
+ log("INFO", "bootstrap-ready: callback tool live, sending callback-only prompt");
613
+ send(buildOmpCallbackOnlyPrompt());
621
614
  return;
622
615
  }
623
616
  if (timers.now() >= deadline) {
@@ -654,7 +647,7 @@ export function createOmpBootstrap(opts) {
654
647
  if (typeof id !== "string" || id.length === 0)
655
648
  return;
656
649
  callId = id;
657
- log("INFO", `bootstrap-callback-observed nonce=${payload.nonce} toolCallId=${id}`);
650
+ log("INFO", `bootstrap-callback-observed toolCallId=${id}`);
658
651
  },
659
652
  onToolResult(event) {
660
653
  if (phase !== "callback-sent" || callId === null)
@@ -664,7 +657,7 @@ export function createOmpBootstrap(opts) {
664
657
  if (!matchesCallback(event, payload))
665
658
  return;
666
659
  if (event?.isError !== false) {
667
- log("WARN", `bootstrap-callback-errored nonce=${payload.nonce} toolCallId=${callId}; the task was NOT sent`);
660
+ log("WARN", `bootstrap-callback-errored toolCallId=${callId}; the task was NOT sent`);
668
661
  return;
669
662
  }
670
663
  // RELEASED, AND THE TASK IS NOT SENT HERE. This handler runs inside the callback
@@ -673,7 +666,7 @@ export function createOmpBootstrap(opts) {
673
666
  // record that the callback provably succeeded; `onTurnEnd` owns the send.
674
667
  phase = "released";
675
668
  stopTimer();
676
- log("INFO", `bootstrap-released nonce=${payload.nonce} toolCallId=${callId}: task armed for the next turn_end`);
669
+ log("INFO", `bootstrap-released toolCallId=${callId}: task armed for the next turn_end`);
677
670
  },
678
671
  /**
679
672
  * The stage-two boundary: the callback turn is over, so the session is at the edge
@@ -695,7 +688,7 @@ export function createOmpBootstrap(opts) {
695
688
  if (phase !== "released")
696
689
  return;
697
690
  phase = "task-sent";
698
- log("INFO", `bootstrap-task-sent nonce=${payload.nonce}: delivering the task at the turn_end boundary`);
691
+ log("INFO", "bootstrap-task-sent: delivering the task at the turn_end boundary");
699
692
  send(payload.task);
700
693
  },
701
694
  invalidate(why) {
@@ -706,7 +699,7 @@ export function createOmpBootstrap(opts) {
706
699
  if (isFinal())
707
700
  return;
708
701
  phase = "failed";
709
- log("INFO", `bootstrap-invalidated nonce=${payload.nonce}: ${why}; the task was NOT sent`);
702
+ log("INFO", `bootstrap-invalidated: ${why}; the task was NOT sent`);
710
703
  },
711
704
  };
712
705
  }
@@ -869,7 +862,7 @@ export function startOmpBootstrap(opts) {
869
862
  const handle = createOmpBootstrap({ payload: decoded.value, pi, timers, log });
870
863
  activeBootstrap = { sessionId: envelope.nativeSessionId, handle };
871
864
  bootstrapConsumed = true;
872
- log("INFO", `bootstrap-armed nonce=${decoded.value.nonce} target=${decoded.value.target}`);
865
+ log("INFO", "bootstrap-armed: payload admitted, waiting for callback tool");
873
866
  handle.start();
874
867
  }
875
868
  /**
@@ -42,6 +42,7 @@ export const EXPECTED_TOOLS = [
42
42
  "entwurf_register_native",
43
43
  "entwurf_fresh_call",
44
44
  "entwurf_resume_call",
45
+ "entwurf_callback",
45
46
  ];
46
47
  const DEFAULT_TIMEOUT_MS = 10_000;
47
48
  /** How long a probed child gets to die politely before it is killed outright. */
@@ -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 — seven verbs — and `check-entwurf-bridge-boot`
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
  *
@@ -65,6 +70,7 @@ import * as process from "node:process";
65
70
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
66
71
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
67
72
  import { z } from "zod";
73
+ import { readCallbackEnv } from "../../../pi-extensions/lib/callback-env.ts";
68
74
  import { controlSocketPathIn, defaultControlSocketDir } from "../../../pi-extensions/lib/control-socket-path.js";
69
75
  import { resolveMailboxReceiverFacts } from "../../../pi-extensions/lib/entwurf-deliverability.ts";
70
76
  import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.ts";
@@ -877,6 +883,40 @@ server.tool(
877
883
  },
878
884
  );
879
885
 
886
+ server.tool(
887
+ "entwurf_callback",
888
+ "ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads " +
889
+ "ENTWURF_CALLBACK_TARGET and ENTWURF_CALLBACK_NONCE from this process environment " +
890
+ "(injected by the launcher next to the identity scrub), validates garden-id and nonce " +
891
+ "grammar, and dispatches through the existing v2 runner with intent fire-and-forget, " +
892
+ "message=nonce, wants_reply=false. The target is re-resolved by record/decider — env " +
893
+ "names the id, it does not skip dispatch. REFUSES BY NAME when the pair is absent, " +
894
+ "malformed, or this process is a Codex-provenance bridge (window env never reaches that " +
895
+ "MCP child). No arguments, no fallback to a model-supplied target.",
896
+ {},
897
+ async (_args, extra) => {
898
+ const read = readCallbackEnv(process.env);
899
+ if (!read.ok) {
900
+ return textErr(`entwurf_callback: ${read.reason}`);
901
+ }
902
+ try {
903
+ const sender = await buildSendSenderEnvelope({ requestMeta: extra._meta });
904
+ const rendered = await runAndRenderEntwurfV2FromSurface(
905
+ {
906
+ target: read.target,
907
+ intent: "fire-and-forget",
908
+ message: read.nonce,
909
+ wants_reply: false,
910
+ },
911
+ { senderProvider: () => sender },
912
+ );
913
+ return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
914
+ } catch (err) {
915
+ return textErr(`entwurf_callback error: ${err instanceof Error ? err.message : String(err)}`);
916
+ }
917
+ },
918
+ );
919
+
880
920
  // ============================================================================
881
921
  // Main
882
922
  // ============================================================================
@@ -55,7 +55,7 @@ names=set(raw)
55
55
  # was an artifact-surface hole: a bundle missing entwurf_fresh_call / entwurf_resume_call
56
56
  # answered tools/list and passed. Equality also refuses an undecided extra verb; the
57
57
  # duplicate arm catches a name registered twice, which set membership cannot see.
58
- expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read','entwurf_register_native','entwurf_fresh_call','entwurf_resume_call'}
58
+ expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read','entwurf_register_native','entwurf_fresh_call','entwurf_resume_call','entwurf_callback'}
59
59
  legacy={'entwurf','entwurf_resume','entwurf_send'} & names
60
60
  if len(raw)!=len(names):
61
61
  raise SystemExit(f"duplicate tool registrations: {sorted(raw)}")
@@ -66,7 +66,7 @@ if legacy:
66
66
  if names!=expected:
67
67
  raise SystemExit(f"tool set MISMATCH — missing {sorted(expected-names)}, unexpected {sorted(names-expected)}")
68
68
  PY
69
- ok "public tool surface is EXACTLY the seven garden verbs (no v1 verb, no undecided extra, no duplicate)"
69
+ ok "public tool surface is EXACTLY the eight garden verbs (no v1 verb, no undecided extra, no duplicate)"
70
70
 
71
71
  SELF_JSON="$(printf '%s\n' "$OUT" | grep '"id":3' | tail -1)"
72
72
  [[ "$SELF_JSON" == *"isError"* ]] || fail "entwurf_self without identity should be an error: $SELF_JSON"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -67,13 +67,15 @@
67
67
  "pi/skill-plugin-example/",
68
68
  "run.sh",
69
69
  "AGENTS.md",
70
+ "FAQ.md",
70
71
  "BASELINE.md",
71
72
  "VERIFY.md",
72
73
  "DELIVERY.md",
73
74
  "CONTRIBUTING.md",
74
75
  "CHANGELOG.md",
75
76
  "!**/__pycache__",
76
- "!**/*.pyc"
77
+ "!**/*.pyc",
78
+ "!pi-extensions/**/*.test.ts"
77
79
  ],
78
80
  "engines": {
79
81
  "node": ">=24.0.0"
@@ -82,7 +84,7 @@
82
84
  "claudeCodeFloor": ">=2.1.217"
83
85
  },
84
86
  "dependencies": {
85
- "@agentclientprotocol/claude-agent-acp": "0.76.0",
87
+ "@agentclientprotocol/claude-agent-acp": "0.79.0",
86
88
  "@agentclientprotocol/sdk": "1.4.0",
87
89
  "@anthropic-ai/sdk": "0.100.1",
88
90
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -90,16 +92,16 @@
90
92
  "zod": "^3.25.0 || ^4.0.0"
91
93
  },
92
94
  "peerDependencies": {
93
- "@earendil-works/pi-ai": ">=0.85.1 <0.86",
94
- "@earendil-works/pi-coding-agent": ">=0.85.1 <0.86",
95
- "@earendil-works/pi-tui": ">=0.85.1 <0.86",
95
+ "@earendil-works/pi-ai": ">=0.86.0 <0.87",
96
+ "@earendil-works/pi-coding-agent": ">=0.86.0 <0.87",
97
+ "@earendil-works/pi-tui": ">=0.86.0 <0.87",
96
98
  "typebox": "*"
97
99
  },
98
100
  "devDependencies": {
99
101
  "@biomejs/biome": "^2.4.13",
100
- "@earendil-works/pi-ai": "0.85.1",
101
- "@earendil-works/pi-coding-agent": "0.85.1",
102
- "@earendil-works/pi-tui": "0.85.1",
102
+ "@earendil-works/pi-ai": "0.86.0",
103
+ "@earendil-works/pi-coding-agent": "0.86.0",
104
+ "@earendil-works/pi-tui": "0.86.0",
103
105
  "@types/node": "^24.3.0",
104
106
  "husky": "^9.1.7",
105
107
  "rregex": "1.13.1",
@@ -127,7 +129,6 @@
127
129
  "check-meta-capability-source": "./run.sh check-meta-capability-source",
128
130
  "check-socket-probe": "./run.sh check-socket-probe",
129
131
  "check-project-trust-handler": "./run.sh check-project-trust-handler",
130
- "check-entwurf-v2-lock": "./run.sh check-entwurf-v2-lock",
131
132
  "check-model-lock": "./run.sh check-model-lock",
132
133
  "check-dep-versions": "./run.sh check-dep-versions",
133
134
  "check-node-floor-coherence": "./run.sh check-node-floor-coherence",
@@ -135,6 +136,7 @@
135
136
  "check-copilot-birth-hook": "./run.sh check-copilot-birth-hook",
136
137
  "check-copilot-statusline": "./run.sh check-copilot-statusline",
137
138
  "check-copilot-launch": "./run.sh check-copilot-launch",
139
+ "check-pi-launch": "./run.sh check-pi-launch",
138
140
  "smoke-copilot-statusline-state": "./run.sh smoke-copilot-statusline-state",
139
141
  "smoke-copilot-mcp-state": "./run.sh smoke-copilot-mcp-state",
140
142
  "smoke-omp-bridge-state": "./run.sh smoke-omp-bridge-state",
@@ -173,9 +175,9 @@
173
175
  "check:full": "bash scripts/check-elapsed.sh check:full check:core check:hermetic check:package",
174
176
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
175
177
  "check:toolchain": "pnpm lint && pnpm typecheck",
176
- "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
177
- "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-resume-args && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
178
- "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
178
+ "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-tests-beside-behavior",
179
+ "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
180
+ "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
179
181
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
180
182
  },
181
183
  "pi": {
@@ -81,6 +81,7 @@ import type {
81
81
  import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
82
82
  import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
83
83
  import { ENTWURF_SENT_MESSAGE_TYPE } from "../protocol.js";
84
+ import { readCallbackEnv } from "./lib/callback-env.js";
84
85
  import {
85
86
  type CompactionGuard,
86
87
  compactionSendReject,
@@ -1118,6 +1119,7 @@ export default function (pi: ExtensionAPI) {
1118
1119
  if (shouldRegisterControlTools(pi)) {
1119
1120
  registerListSessionsTool(pi);
1120
1121
  registerEntwurfV2Tool(pi);
1122
+ registerCallbackTool(pi);
1121
1123
  registerFreshCallTool(pi);
1122
1124
  registerResumeCallTool(pi);
1123
1125
  }
@@ -1740,6 +1742,60 @@ interface VisibleResumeModule {
1740
1742
  renderVisibleResume(result: { ok: boolean }): { text: string; isError: boolean };
1741
1743
  }
1742
1744
 
1745
+ function registerCallbackTool(pi: ExtensionAPI): void {
1746
+ const registerTool = pi.registerTool as (def: any) => void;
1747
+ registerTool({
1748
+ name: "entwurf_callback",
1749
+ label: "Fresh Callback",
1750
+ description: `ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads ENTWURF_CALLBACK_TARGET and ENTWURF_CALLBACK_NONCE from this process environment (injected by the launcher next to the identity scrub), validates garden-id and nonce grammar, and dispatches through the existing v2 runner with intent fire-and-forget, message=nonce, wants_reply=false. The target is re-resolved by record/decider. REFUSES BY NAME when the pair is absent, malformed, or this process is a Codex-provenance bridge. No arguments, no fallback to a model-supplied target.`,
1751
+ parameters: Type.Object({}),
1752
+ async execute(
1753
+ _toolCallId: string,
1754
+ _params: Record<string, never>,
1755
+ _signal: AbortSignal | undefined,
1756
+ _onUpdate: unknown,
1757
+ ctx: ExtensionContext,
1758
+ ) {
1759
+ const read = readCallbackEnv(process.env);
1760
+ if (!read.ok) {
1761
+ return {
1762
+ content: [{ type: "text", text: `entwurf_callback: ${read.reason}` }],
1763
+ isError: true,
1764
+ };
1765
+ }
1766
+ try {
1767
+ const selfMod = (await import(ENTWURF_SELF_ADDRESS_MODULE)) as unknown as EntwurfSelfAddressModule;
1768
+ const senderProvider = (): SenderEnvelope | undefined => {
1769
+ const s = buildLocalSenderEnvelope(ctx);
1770
+ return s ? decoratePiSenderAddressability(s, selfMod.computeSelfAddressability) : undefined;
1771
+ };
1772
+ const mod = (await import(ENTWURF_V2_SURFACE_MODULE)) as unknown as EntwurfV2SurfaceModule;
1773
+ const rendered = await mod.runAndRenderEntwurfV2FromSurface(
1774
+ {
1775
+ target: read.target,
1776
+ intent: "fire-and-forget",
1777
+ message: read.nonce,
1778
+ wants_reply: false,
1779
+ },
1780
+ { senderProvider },
1781
+ );
1782
+ return {
1783
+ content: [{ type: "text", text: rendered.text }],
1784
+ isError: rendered.isError,
1785
+ details: { isError: rendered.isError },
1786
+ };
1787
+ } catch (err) {
1788
+ const msg = err instanceof Error ? err.message : String(err);
1789
+ return {
1790
+ content: [{ type: "text", text: `entwurf_callback error: ${msg}` }],
1791
+ isError: true,
1792
+ details: { error: msg },
1793
+ };
1794
+ }
1795
+ },
1796
+ });
1797
+ }
1798
+
1743
1799
  /**
1744
1800
  * Unlike `entwurf_fresh_call`, this tool needs NO caller identity: a resume names an existing
1745
1801
  * citizen, so the address is the parameter rather than something the sibling has to report back.
@@ -46,8 +46,8 @@ import type { AcpTextBlock } from "./context.js";
46
46
  * shape) and never onto those four. A field rename upstream fails the typecheck
47
47
  * here rather than silently zeroing the operator's cache-effect badge.
48
48
  */
49
- /** One `_meta.quota.token_count` row (claude-agent-acp 0.76.0
50
- * `dist/acp-agent.js:6566-6575`). `cachedInputTokens` is cache READS — the name
49
+ /** One `_meta.quota.token_count` row (claude-agent-acp 0.79.0
50
+ * `dist/acp-agent.js:6579-6588`). `cachedInputTokens` is cache READS — the name
51
51
  * differs from `usage.cachedReadTokens` because the shape is shared with
52
52
  * codex-acp; `cachedWriteTokens` is Claude's extra sibling. */
53
53
  export type AcpQuotaTokenCount = {
@@ -64,7 +64,7 @@ export type AcpPromptResponse = {
64
64
  /** Vendor-private, version-pinned. `_meta` is a standard ACP extension slot
65
65
  * whose values a client may not assume, and `quota` is NOT in
66
66
  * claude-agent-acp's exported types — it is produced by the private
67
- * `turnQuotaMeta()` (read at 0.76.0 `dist/acp-agent.js:6549-6558`). Read
67
+ * `turnQuotaMeta()` (read at 0.79.0 `dist/acp-agent.js:6562-6571`). Read
68
68
  * defensively, never structurally required, and re-measure on a pin move. */
69
69
  _meta?: {
70
70
  quota?: {
@@ -150,7 +150,7 @@ export interface AcpAdapterSettingsParams {
150
150
  *
151
151
  * During stream handling the vendor keeps a per-message snapshot in
152
152
  * `lastAssistantUsage`, but its `usage_update` publishes only the scalar `used`
153
- * value (read at claude-agent-acp 0.76.0 `dist/acp-agent.js:3857-3882`). Until
153
+ * value (read at claude-agent-acp 0.79.0 `dist/acp-agent.js:3920-3945`). Until
154
154
  * that partition is carried on the wire, NOTHING honest can go in pi's four
155
155
  * fields, so entwurf writes none of them.
156
156
  */
@@ -342,8 +342,8 @@ export const claudeAdapter: AcpBackendAdapter = {
342
342
 
343
343
  // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
344
344
  // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
345
- // and a context-occupancy scalar (`used`; both read at 0.76.0
346
- // `dist/acp-agent.js:3471-3486`). Both are measured, so backend.ts seals them.
345
+ // and a context-occupancy scalar (`used`; both read at 0.79.0
346
+ // `dist/acp-agent.js:3504-3519`). Both are measured, so backend.ts seals them.
347
347
  // The token partition is NOT declared here: ACP's only token carrier is a
348
348
  // per-turn round-trip aggregate, which is not what pi's four fields mean.
349
349
  sealsTurnAccounting: true,