@junghanacs/entwurf 0.40.0 → 0.41.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 (118) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +55 -2
  3. package/CHANGELOG.md +321 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +18 -2
  6. package/FAQ.md +8 -4
  7. package/README.md +26 -8
  8. package/VERIFY.md +13 -10
  9. package/docs/acp-backend-rail.md +5 -1
  10. package/docs/external-mcp-host.md +8 -6
  11. package/docs/fresh-cut-policy.md +9 -1
  12. package/docs/setup-clean-host.md +24 -8
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +81 -19
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +96 -16
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +5 -29
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +28 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +26 -11
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-retire-request.js +76 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +67 -17
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
  31. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
  32. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
  33. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  34. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  35. package/mcp/entwurf-bridge/src/index.ts +93 -19
  36. package/mcp/entwurf-bridge/test.sh +2 -2
  37. package/mcp/entwurf-bridge/tsconfig.build.json +10 -1
  38. package/package.json +4 -4
  39. package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
  40. package/pi-extensions/entwurf-control.ts +55 -20
  41. package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  42. package/pi-extensions/lib/cc-retire-request.ts +553 -0
  43. package/pi-extensions/lib/control-retire.ts +195 -0
  44. package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
  45. package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
  46. package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
  47. package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
  48. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
  49. package/pi-extensions/lib/fresh-call-composition.ts +15 -0
  50. package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
  51. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  52. package/pi-extensions/lib/meta-session.ts +140 -16
  53. package/pi-extensions/lib/mux-fresh-call.ts +7 -27
  54. package/pi-extensions/lib/mux-launch.ts +29 -0
  55. package/pi-extensions/lib/mux-resume-call.ts +33 -12
  56. package/pi-extensions/lib/pi-retire-request.ts +111 -0
  57. package/pi-extensions/lib/resume-launch-identity.ts +104 -18
  58. package/pi-extensions/lib/retirement-event.ts +253 -0
  59. package/pi-extensions/meta-bridge-hook.ts +142 -13
  60. package/pi-extensions/meta-bridge-pi-durable.ts +56 -2
  61. package/run.sh +78 -7
  62. package/scripts/agy-bridge-config.py +4 -1
  63. package/scripts/build-bridge.sh +14 -0
  64. package/scripts/check-capability-bundle-reach.ts +135 -2
  65. package/scripts/check-entwurf-bridge-boot.ts +2 -1
  66. package/scripts/check-entwurf-fact-provider.ts +1 -0
  67. package/scripts/check-entwurf-v2-production.ts +2 -0
  68. package/scripts/check-entwurf-v2-surface.ts +7 -0
  69. package/scripts/check-gate-qualification.ts +14 -8
  70. package/scripts/check-herdr-activation.ts +2 -2
  71. package/scripts/check-herdr-placement.ts +1 -0
  72. package/scripts/check-hook-launch-topology.ts +6 -15
  73. package/scripts/check-install-container.sh +2 -2
  74. package/scripts/check-meta-doctor-oracle.sh +27 -1
  75. package/scripts/check-meta-facts.ts +8 -3
  76. package/scripts/check-meta-hook-session-switch.ts +230 -13
  77. package/scripts/check-meta-identity-consumers.ts +1 -0
  78. package/scripts/check-meta-listing.ts +1 -0
  79. package/scripts/check-meta-manifest-schema.py +1 -1
  80. package/scripts/check-meta-session.ts +31 -0
  81. package/scripts/check-meta-v3-record.ts +55 -1
  82. package/scripts/check-mux-resume-call.ts +35 -9
  83. package/scripts/check-peer-facts.ts +1 -0
  84. package/scripts/check-pi-durable-contact.ts +3 -1
  85. package/scripts/check-pi-durable-send.ts +4 -2
  86. package/scripts/check-pi-mcp-bridge.ts +1 -0
  87. package/scripts/check-resume-launch-identity.ts +62 -23
  88. package/scripts/lib/meta-hook-bundle.ts +72 -0
  89. package/scripts/meta-bridge-claude-floor.sh +13 -9
  90. package/scripts/meta-bridge-doctor.sh +17 -0
  91. package/scripts/meta-bridge-install.sh +6 -0
  92. package/scripts/meta-facts.ts +1 -1
  93. package/scripts/mutants/bridge-boot-resume.json +5 -2
  94. package/scripts/mutants/cc-retire-pidfd.json +129 -0
  95. package/scripts/mutants/cc-retire-request.json +171 -0
  96. package/scripts/mutants/control-retire.json +105 -0
  97. package/scripts/mutants/copilot-receive.json +2 -6
  98. package/scripts/mutants/entwurf-v2-retire.json +476 -0
  99. package/scripts/mutants/fresh-call-dispatch.json +37 -0
  100. package/scripts/mutants/meta-hook-session-switch.json +88 -0
  101. package/scripts/mutants/meta-identity.json +57 -0
  102. package/scripts/mutants/mux-fresh-call.json +1 -1
  103. package/scripts/mutants/mux-resume-call.json +40 -4
  104. package/scripts/mutants/pi-durable.json +79 -0
  105. package/scripts/mutants/pi-retire-request.json +72 -0
  106. package/scripts/mutants/resume-launch-identity.json +35 -8
  107. package/scripts/mutants/retirement-event.json +195 -0
  108. package/scripts/mutants/v2-surface.json +16 -0
  109. package/scripts/mutants/v2-visible-resume.json +283 -13
  110. package/scripts/probe-bridge-command.ts +1 -0
  111. package/scripts/smoke-agy-install-state.sh +2 -2
  112. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  113. package/scripts/smoke-herdr-raw-install-live.sh +5 -4
  114. package/scripts/smoke-meta-install-state.sh +4 -4
  115. package/scripts/smoke-mux-lifecycle-live.ts +10 -10
  116. package/scripts/smoke-omp-mcp-state.sh +1 -1
  117. package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
  118. package/scripts/smoke-pi-provider-state.sh +3 -3
@@ -31,16 +31,18 @@
31
31
  * seat, #105; nothing is ever created), optionally at ONE literal requested
32
32
  * cwd (cross-repo fresh, #73); returns a LAUNCH receipt only, and the new
33
33
  * address arrives later as the sender envelope of the sibling's nonce callback.
34
- * - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
35
- * window; target-only, runs no turn, LAUNCH and OBSERVATION receipts stay
36
- * apart.
34
+ * - entwurf_resume_call — reopen ONE DORMANT pi or Claude Code citizen under its OWN garden id
35
+ * in a visible window; target-only, submits no prompt, LAUNCH and OBSERVATION
36
+ * receipts stay apart, the caller is attributed but grants nothing.
37
+ * - entwurf_retire_call — one operator-requested target, one retirement attempt; actor is not
38
+ * permission, and action / append-record / lock-release receipts stay separate.
37
39
  * - entwurf_callback — ZERO-ARGUMENT fresh-sibling callback. Reads ENTWURF_CALLBACK_TARGET +
38
40
  * ENTWURF_CALLBACK_NONCE from this process env (launcher-injected), validates
39
41
  * grammar, and dispatches through the existing v2 runner. Refuses by name
40
42
  * when env is absent/malformed or this process is a Codex-provenance bridge.
41
43
  * No model-supplied target fallback.
42
44
  *
43
- * That list is the WHOLE public surface — eight verbs — and `check-entwurf-bridge-boot`
45
+ * That list is the WHOLE public surface — nine verbs — and `check-entwurf-bridge-boot`
44
46
  * (G1f) holds it as an exact set on the runtime tools/list, so a verb added or dropped
45
47
  * here without a decision is red rather than merely undocumented.
46
48
  *
@@ -78,6 +80,8 @@ import { ENTWURF_PEERS_RENDER_LIMIT, renderEntwurfPeers } from "../../../pi-exte
78
80
  import { computeSelfAddressability, type MetaDeliveryDomain } from "../../../pi-extensions/lib/entwurf-self-address.ts";
79
81
  import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.ts";
80
82
  import { resolveMailboxWakeModeCapability } from "../../../pi-extensions/lib/entwurf-v2-decider.ts";
83
+ import { retireCitizen } from "../../../pi-extensions/lib/entwurf-v2-retire.ts";
84
+ import { makeProductionRetireDeps } from "../../../pi-extensions/lib/entwurf-v2-retire-production.ts";
81
85
  import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.ts";
82
86
  import {
83
87
  makeVisibleResumeDeps,
@@ -818,7 +822,7 @@ server.tool(
818
822
  // biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
819
823
  .regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
820
824
  .describe(
821
- "Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.",
825
+ "Required runtime model: canonical provider/model for pi (a bare alias is refused), provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name. Neither pi nor pi-durable may name the openrouter provider.",
822
826
  ),
823
827
  task: z
824
828
  .string()
@@ -904,7 +908,7 @@ server.tool(
904
908
  // resume` cannot open a window on its own and `mux-resume-call` cannot read a record — that is
905
909
  // the import fence in docs/mux-launch-rail.md §11, expressed as a function argument.
906
910
  const visibleResumeLaunch: VisibleResumeDeps["launch"] = (input) => {
907
- const launched = resumeCall({ cwd: input.cwd, runtimeArgs: input.runtimeArgs });
911
+ const launched = resumeCall({ backend: input.backend, cwd: input.cwd, runtimeArgs: input.runtimeArgs });
908
912
  return launched.ok
909
913
  ? { ok: true, handle: launched.receipt }
910
914
  : { ok: false, reason: launched.reason, hint: RESUME_CALL_REJECT_HINT[launched.reason] };
@@ -912,27 +916,45 @@ const visibleResumeLaunch: VisibleResumeDeps["launch"] = (input) => {
912
916
 
913
917
  server.tool(
914
918
  "entwurf_resume_call",
915
- "Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux " +
916
- "session. The record supplies everything — which transcript, which model, which provider, which cwd — so " +
917
- "the only input is the target id: there is no model override, no task, and no prompt. This runs NO turn: " +
918
- "the window comes back with the conversation and waits, and talking to it is still entwurf_v2 " +
919
- "fire-and-forget on the socket this call stands up. You get TWO receipts and they mean different things: a " +
920
- "LAUNCH receipt (tmux made a window and was asked to start pi) and an OBSERVATION receipt (the control " +
921
- "socket answered under the same id, or resume-unobserved). Unobserved is a real outcome, not an error to " +
922
- "retry — the window is visible, so read it. A citizen that is already LIVE is refused: address it with " +
923
- "entwurf_v2 instead. Only pi citizens have a same-id resume, because only they stand a control socket up. " +
924
- "Requires that this agent itself runs inside tmux.",
919
+ "Reopen ONE DORMANT pi or Claude Code citizen under its OWN garden id, in a visible window in the operator's " +
920
+ "own tmux session. The record supplies everything — which session, which cwd, and for pi which transcript, " +
921
+ "model and provider — so the only input is the target id: there is no model override, no task, and no " +
922
+ "prompt, and a Claude Code session comes back as `claude --resume <its session id>` with none of the model " +
923
+ "or permission flags it was once started with. This verb submits NO prompt; what the runtime itself does " +
924
+ "after it comes back (for Claude Code, its hook re-arms the doorbell and unread mail may then wake a turn) " +
925
+ "is not measured, and talking to it is still entwurf_v2 fire-and-forget. You get TWO receipts and they mean " +
926
+ "different things: a LAUNCH receipt (tmux made a window and was asked to start the runtime) and an " +
927
+ "OBSERVATION receipt (pi: the control socket answered under the same id; Claude Code: a NEW live process in " +
928
+ "the window this call opened re-armed its receiver marker for the same session with a fresh arm binding the " +
929
+ "record also carries; or resume-unobserved; or record-unreadable with its cause). Unobserved is a real " +
930
+ "outcome, not an error to retry — the window is visible, so read it. A citizen that is already LIVE is " +
931
+ "refused: address it with entwurf_v2 instead. A Claude Code citizen is reopened only when the last validated " +
932
+ "owner the Entwurf hook armed for its session is proven gone AND that arm is still the record's last one; " +
933
+ "no marker, an unreadable or mismatched one, an inconclusive owner or an unbound one is refused, and a " +
934
+ "process holding the session without a committed hook attach is invisible to that proof. Other backends " +
935
+ "have no same-id resume. Requires that this agent itself runs inside tmux (inside herdr it is refused).",
925
936
  {
926
937
  target: z
927
938
  .string()
928
939
  .min(1)
929
940
  .regex(/^\d{8}T\d{6}-[0-9a-f]{6}$/)
930
- .describe("Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers)."),
941
+ .describe("Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers)."),
931
942
  },
932
- async ({ target }) => {
943
+ async ({ target }, extra) => {
944
+ // Who asked, from the same authoritative caller identity fresh_call reads. Attribution only:
945
+ // an anonymous caller is reported as unattributed, never refused, and a record-backed one is
946
+ // granted nothing the target's record and live facts do not already decide.
947
+ let actor: string | null = null;
948
+ try {
949
+ actor = (await buildAuthoritativeSelfEnvelope({ requestMeta: extra._meta })).envelope.sessionId;
950
+ } catch (err) {
951
+ if (!(err instanceof EntwurfEnvelopeWiringError)) {
952
+ return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
953
+ }
954
+ }
933
955
  try {
934
956
  const result = await visibleResume(target, makeVisibleResumeDeps(visibleResumeLaunch));
935
- const rendered = renderVisibleResume(result);
957
+ const rendered = renderVisibleResume(result, { actor });
936
958
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
937
959
  } catch (err) {
938
960
  return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
@@ -940,6 +962,58 @@ server.tool(
940
962
  },
941
963
  );
942
964
 
965
+ server.tool(
966
+ "entwurf_retire_call",
967
+ "Only when the human operator explicitly requested this particular target/work: make ONE retirement attempt for that request. " +
968
+ "Development work, a role or a record-backed actor is NOT a grant. Use host approval where provided/configured; " +
969
+ "Pi and pi-durable have no built-in per-call approval prompt by default. An explicit operator request and " +
970
+ "agent operating policy are required; attribution and MCP annotations are hints, not grants or proof of approval. " +
971
+ "Never use for autonomous cleanup or timers. Never automatically retry an unknown, cancelled or lost result. " +
972
+ "Target must be an existing Pi or Claude Code garden id; self retirement refuses. Pi shutdown-requested is " +
973
+ "only an ACK, not process exit, flush or model silence. Claude Code exited means that fd-pinned process only, " +
974
+ "not conversation end, children or flush. Do not manually /resume or /clear the target during the request. " +
975
+ "The JSON receipt preserves outcome, record and release separately; returning a receipt does not mean the " +
976
+ "target exited. Cancellation can suppress the wire response without undoing an action. For plural explicit " +
977
+ "requests, invoke once for each named target; never infer a roster or expand the request.",
978
+ {
979
+ target: z
980
+ .string()
981
+ .min(1)
982
+ .regex(/^\d{8}T\d{6}-[0-9a-f]{6}$/)
983
+ .describe("Exact garden id of the Pi or Claude Code target the human operator explicitly requested to retire."),
984
+ },
985
+ { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
986
+ async ({ target }, extra) => {
987
+ try {
988
+ const receipt = await retireCitizen(
989
+ target,
990
+ makeProductionRetireDeps({
991
+ resolveCaller: async () => (await resolveAuthoritativeSender({ requestMeta: extra._meta }))?.envelope ?? null,
992
+ signal: extra.signal,
993
+ }),
994
+ );
995
+ // isError reports uncertainty/faults, NOT a common Pi-ACK/CC-exit success boundary.
996
+ // Named refusals are completed decisions. Always preserve the full three-axis JSON.
997
+ const outcome = receipt.outcome;
998
+ const uncertain =
999
+ outcome.kind === "unknown" ||
1000
+ outcome.kind === "preflight-error" ||
1001
+ (outcome.kind === "pi" && outcome.result.boundary === "unknown") ||
1002
+ (outcome.kind === "claude-code" && outcome.result.outcome === "unknown");
1003
+ const recordFault =
1004
+ receipt.record.status === "requested-append-error" || receipt.record.status === "outcome-append-error";
1005
+ const releaseFault = receipt.release.status !== "released" && receipt.release.status !== "not-acquired";
1006
+ const json = JSON.stringify(receipt);
1007
+ return uncertain || recordFault || releaseFault ? textErr(json) : textOk(json);
1008
+ } catch (err) {
1009
+ return textErr(
1010
+ `entwurf_retire_call error: ${err instanceof Error ? err.message : String(err)}. ` +
1011
+ "The outcome may be unknown; this does not prove no action. Do not automatically retry.",
1012
+ );
1013
+ }
1014
+ },
1015
+ );
1016
+
943
1017
  server.tool(
944
1018
  "entwurf_callback",
945
1019
  "ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads " +
@@ -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','entwurf_callback'}
58
+ expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read','entwurf_register_native','entwurf_fresh_call','entwurf_resume_call','entwurf_retire_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 eight garden verbs (no v1 verb, no undecided extra, no duplicate)"
69
+ ok "public tool surface is EXACTLY the nine 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"
@@ -147,6 +147,12 @@
147
147
  // remote-pair-send, entwurf-v2-surface, meta-session) are ALREADY emitted for the bridge's own
148
148
  // entwurf_v2 handler, so this adds ONE leaf JS and ZERO new deps. It is never in index.ts's boot
149
149
  // closure.
150
+ // WHY cc-retire-request rides THIS build (#147 G3): the core-side Claude Code retire adapter
151
+ // must run from the compiled bridge as well as from source. It was first supplied as a private
152
+ // entry; the #147 public retirement factory now reaches it too. Keep the named entry for the
153
+ // standalone compiled adapter consumer proof. Its helper is not TS: the
154
+ // build script byte-copies pi-extensions/lib/cc-retire-pidfd.py beside the emitted adapter.
155
+ // Its repo imports (meta-session, entwurf-v2-visible-resume) are ALREADY emitted for the bridge.
150
156
  "include": [
151
157
  "./src/index.ts",
152
158
  "../../protocol.js",
@@ -168,6 +174,9 @@
168
174
  "../../pi-extensions/meta-bridge-hook-codex.ts",
169
175
  "../../pi-extensions/meta-bridge-omp.ts",
170
176
  "../../pi-extensions/meta-bridge-receive-omp.ts",
171
- "../../pi-extensions/meta-bridge-pi-durable.ts"
177
+ "../../pi-extensions/meta-bridge-pi-durable.ts",
178
+ "../../pi-extensions/lib/cc-retire-request.ts",
179
+ // Keep the standalone Pi adapter consumer entry; the public factory also reaches it now.
180
+ "../../pi-extensions/lib/pi-retire-request.ts"
172
181
  ]
173
182
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.40.0",
4
- "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, reopen pi in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
3
+ "version": "0.41.0",
4
+ "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request and reopen pi or Claude Code in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Junghan Kim",
@@ -95,7 +95,7 @@
95
95
  "node": ">=24.0.0"
96
96
  },
97
97
  "entwurf": {
98
- "claudeCodeFloor": ">=2.1.217"
98
+ "claudeCodeFloor": ">=2.1.306"
99
99
  },
100
100
  "dependencies": {
101
101
  "@agentclientprotocol/claude-agent-acp": "0.90.0",
@@ -196,7 +196,7 @@
196
196
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
197
197
  "check:toolchain": "pnpm lint && pnpm typecheck",
198
198
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior && ./run.sh check-remote-pair-installed-audit",
199
- "check:contracts": "./run.sh check-demo-reproducibility && ./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-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./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-win32-preview && ./run.sh check-pack-win32-carrier && ./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-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module",
199
+ "check:contracts": "./run.sh check-demo-reproducibility && ./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-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-control-retire && ./run.sh check-pi-retire-request && ./run.sh check-cc-retire-pidfd && ./run.sh check-cc-retire-request && ./run.sh check-retirement-event && ./run.sh check-entwurf-v2-retire && ./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-win32-preview && ./run.sh check-pack-win32-carrier && ./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-fresh-call-provider && ./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-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module",
200
200
  "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-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
201
201
  "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-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
202
202
  },
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # WHY THIS EXISTS (measured, #51 B/B2, 2026-07-22 — not inferred)
5
5
  #
6
- # Claude Code >= 2.1.217 supports the shell-less EXEC form: `command` is the
6
+ # Claude Code 2.1.217 and later support the shell-less EXEC form: `command` is the
7
7
  # executable and `args` is argv, each element substituted as a plain string, with
8
8
  # no shell anywhere on the path. Measured at 2.1.217: a hook declared that way
9
9
  # receives every `args` element verbatim (a literal `${HOME}` arrives LITERAL —
@@ -47,8 +47,8 @@ if [ "$#" -eq 0 ]; then
47
47
  still reports the hook as `exit_code: 0, outcome: success` (measured on 2.1.138),
48
48
  so nothing upstream will ever tell you.
49
49
 
50
- entwurf supports Claude Code >= 2.1.217 — the version whose runtime behavior was
51
- actually verified end to end. There is no shell-form fallback.
50
+ entwurf supports Claude Code >= 2.1.306 (the exec form was verified end to end at
51
+ 2.1.217; same-id resume needs the newer CLI). There is no shell-form fallback.
52
52
 
53
53
  Fix: update Claude Code, then re-run
54
54
  ./run.sh install-meta-bridge && ./run.sh doctor-meta-bridge
@@ -59,6 +59,7 @@
59
59
  * - { type: "get_info" }
60
60
  * - { type: "clear", summarize?: boolean }
61
61
  * - { type: "abort" }
62
+ * - { type: "retire", gardenId: "...", nativeSessionId: "..." } (internal, idle TUI only)
62
63
  * Responses are JSON objects with { type: "response", command, success, data?, error? }
63
64
  * (No event channel — the turn_end subscribe surface was removed with the
64
65
  * Send-is-throw cleanup; see note above.)
@@ -79,6 +80,16 @@ import {
79
80
  noteCompactionBefore,
80
81
  noteCompactionTerminal,
81
82
  } from "./lib/compaction-send-guard.js";
83
+ import {
84
+ acceptControlConnection,
85
+ type ControlBinding,
86
+ closeControlConnections,
87
+ endRetireResponse,
88
+ invalidateControlBinding,
89
+ requestControlRetire,
90
+ retireResponseEnvelope,
91
+ startOrRebindControlServer,
92
+ } from "./lib/control-retire.js";
82
93
  import { sendReceiptBoundary } from "./lib/control-send-receipt.js";
83
94
  import { CONTROL_SOCKET_SUFFIX, defaultControlSocketDir } from "./lib/control-socket-path.js";
84
95
  import {
@@ -121,6 +132,10 @@ interface SocketState {
121
132
  server: net.Server | null;
122
133
  socketPath: string | null;
123
134
  context: ExtensionContext | null;
135
+ binding: ControlBinding | null;
136
+ closing: boolean;
137
+ retireRequested: boolean;
138
+ connections: Set<net.Socket>;
124
139
  compaction: CompactionGuard;
125
140
  }
126
141
 
@@ -485,12 +500,27 @@ async function handleCommand(
485
500
  state: SocketState,
486
501
  command: RpcCommand,
487
502
  socket: net.Socket,
503
+ serverBinding: ControlBinding | null,
488
504
  ): Promise<void> {
489
505
  const id = "id" in command && typeof command.id === "string" ? command.id : undefined;
490
506
  const respond = (success: boolean, commandName: string, data?: unknown, error?: string) => {
491
507
  writeResponse(socket, { type: "response", command: commandName, success, data, error, id });
492
508
  };
493
509
 
510
+ // The public core checks caller/self, freshness and target admission, not an operator grant.
511
+ // Attribution is not authorization: an explicit operator request and agent operating policy
512
+ // are required; use host approval where provided/configured. No async gap exists between
513
+ // the receiver's final same-session checks and the SDK void call.
514
+ if (command.type === "retire") {
515
+ const result = requestControlRetire(state, serverBinding, residentGardenId, command);
516
+ endRetireResponse(socket, retireResponseEnvelope(result, id));
517
+ return;
518
+ }
519
+
520
+ if (state.closing || state.retireRequested) {
521
+ respond(false, command.type, undefined, "target-closing");
522
+ return;
523
+ }
494
524
  const ctx = state.context;
495
525
  if (!ctx) {
496
526
  respond(false, command.type, undefined, "Session not ready");
@@ -690,6 +720,7 @@ async function handleCommand(
690
720
 
691
721
  async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: string): Promise<net.Server> {
692
722
  const server = net.createServer((socket) => {
723
+ const serverBinding = acceptControlConnection(state, socket);
693
724
  // FIRST — before setEncoding, before the data handler, and before any response can be
694
725
  // written back. The policy itself (and why this process died without it) lives with the
695
726
  // rest of the wire protocol in lib/entwurf-control-rpc.ts.
@@ -729,7 +760,7 @@ async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: st
729
760
  // here would serialize subsequent commands on the same socket and
730
761
  // tangle teardown ordering during socket close.
731
762
  const commandName = parsed.command?.type ?? "unknown";
732
- void handleCommand(pi, state, parsed.command!, socket).catch((error) => {
763
+ void handleCommand(pi, state, parsed.command!, socket, serverBinding).catch((error) => {
733
764
  const message = error instanceof Error ? error.message : String(error);
734
765
  writeResponse(socket, {
735
766
  type: "response",
@@ -762,24 +793,21 @@ async function startControlServer(
762
793
  state: SocketState,
763
794
  ctx: ExtensionContext,
764
795
  socketPath: string,
796
+ gardenId: string,
765
797
  ): Promise<void> {
766
- await ensureControlDir();
767
- await gcStaleSockets();
768
-
769
- if (state.socketPath === socketPath && state.server) {
770
- state.context = ctx;
771
- return;
772
- }
773
-
774
- await stopControlServer(state);
775
- await removeSocket(socketPath);
776
-
777
- state.context = ctx;
778
- state.socketPath = socketPath;
779
- state.server = await createServer(pi, state, socketPath);
798
+ await startOrRebindControlServer(state, ctx, socketPath, gardenId, {
799
+ prepare: async () => {
800
+ await ensureControlDir();
801
+ await gcStaleSockets();
802
+ },
803
+ stop: () => stopControlServer(state),
804
+ remove: removeSocket,
805
+ create: (address) => createServer(pi, state, address),
806
+ });
780
807
  }
781
808
 
782
809
  async function stopControlServer(state: SocketState): Promise<void> {
810
+ invalidateControlBinding(state);
783
811
  if (!state.server) {
784
812
  await removeSocket(state.socketPath);
785
813
  state.socketPath = null;
@@ -788,7 +816,7 @@ async function stopControlServer(state: SocketState): Promise<void> {
788
816
 
789
817
  const socketPath = state.socketPath;
790
818
  state.socketPath = null;
791
- await new Promise<void>((resolve) => state.server?.close(() => resolve()));
819
+ await closeControlConnections(state.server, state.connections);
792
820
  state.server = null;
793
821
  await removeSocket(socketPath);
794
822
  }
@@ -958,6 +986,11 @@ export default function (pi: ExtensionAPI) {
958
986
  server: null,
959
987
  socketPath: null,
960
988
  context: null,
989
+ binding: null,
990
+ // Before session_start, commands refuse target-closing rather than Session not ready.
991
+ closing: true,
992
+ retireRequested: false,
993
+ connections: new Set(),
961
994
  compaction: createCompactionGuard(),
962
995
  };
963
996
 
@@ -1022,7 +1055,7 @@ export default function (pi: ExtensionAPI) {
1022
1055
  // naming the fresh-cut command, control server refused. There is no mixed
1023
1056
  // store to warn about; the store is either clean or this session has no
1024
1057
  // address.
1025
- await startControlServer(pi, state, ctx, birth.socketPath);
1058
+ await startControlServer(pi, state, ctx, birth.socketPath, birth.gardenId);
1026
1059
  // Identity before UI (#125): the carrier env and the MCP registration that carries the
1027
1060
  // born gardenId are both made before any status rendering, so a visual failure can never
1028
1061
  // leave a born citizen without its identity carrier or its verbs.
@@ -1082,10 +1115,12 @@ export default function (pi: ExtensionAPI) {
1082
1115
  // simply rebinds to the new address. There is no id to police at the pre-event.
1083
1116
 
1084
1117
  pi.on("session_shutdown", async () => {
1085
- noteCompactionTerminal(state.compaction);
1086
- updateStatus(state.context, false, null);
1087
- updateSessionEnv(state.context, false, null);
1118
+ const previousContext = state.context;
1119
+ invalidateControlBinding(state);
1088
1120
  residentGardenId = null;
1121
+ noteCompactionTerminal(state.compaction);
1122
+ updateStatus(previousContext, false, null);
1123
+ updateSessionEnv(previousContext, false, null);
1089
1124
  await stopControlServer(state);
1090
1125
  });
1091
1126