@junghanacs/entwurf 0.12.9 → 0.12.10

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 (69) hide show
  1. package/AGENTS.md +134 -248
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +19 -0
  4. package/DELIVERY.md +4 -3
  5. package/README.md +26 -13
  6. package/VERIFY.md +3 -2
  7. package/demo/demo.sh +7 -1
  8. package/docs/setup-clean-host.md +10 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
  23. package/mcp/entwurf-bridge/src/index.ts +190 -82
  24. package/package.json +5 -3
  25. package/pi-extensions/entwurf-control.ts +69 -27
  26. package/pi-extensions/lib/acp/overlay.ts +5 -3
  27. package/pi-extensions/lib/entwurf-core.ts +26 -9
  28. package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
  29. package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
  30. package/pi-extensions/lib/entwurf-facts.ts +13 -13
  31. package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
  32. package/pi-extensions/lib/entwurf-self-address.ts +8 -9
  33. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
  34. package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
  35. package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
  36. package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
  37. package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
  38. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
  39. package/pi-extensions/lib/meta-session.ts +17 -20
  40. package/run.sh +48 -41
  41. package/scripts/agy-bridge-config.py +249 -49
  42. package/scripts/agy-bridge.sh +59 -14
  43. package/scripts/check-acp-carrier-augment.ts +34 -2
  44. package/scripts/check-acp-sdk-surface.ts +22 -11
  45. package/scripts/check-agy-permission-matrix.py +655 -0
  46. package/scripts/check-entwurf-deliverability.ts +9 -6
  47. package/scripts/check-entwurf-fact-provider.ts +7 -7
  48. package/scripts/check-entwurf-facts.ts +12 -12
  49. package/scripts/check-entwurf-resume-args.ts +22 -60
  50. package/scripts/check-entwurf-self-address.ts +187 -4
  51. package/scripts/check-entwurf-v2-contract.ts +2 -2
  52. package/scripts/check-entwurf-v2-production.ts +5 -5
  53. package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
  54. package/scripts/check-entwurf-v2-surface.ts +327 -14
  55. package/scripts/check-gate-qualification.ts +785 -0
  56. package/scripts/check-meta-identity-consumers.ts +1 -1
  57. package/scripts/check-shell-quote.ts +2 -1
  58. package/scripts/lib/mutation-qualify.ts +794 -0
  59. package/scripts/mutants/acp-augment.json +30 -0
  60. package/scripts/mutants/agy-permission.json +144 -0
  61. package/scripts/mutants/meta-identity.json +17 -0
  62. package/scripts/mutants/self-address.json +59 -0
  63. package/scripts/mutants/v2-surface.json +87 -0
  64. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  65. package/scripts/smoke-acp-v2-send-live.ts +33 -18
  66. package/scripts/smoke-agy-install-state.sh +205 -20
  67. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
  68. package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
  69. package/scripts/check-entwurf-mailbox-guard.ts +0 -266
@@ -108,9 +108,10 @@ const SESSION_MESSAGE_TYPE = "entwurf-message";
108
108
  // with the receive-side [entwurf received ⟵] box. The provider-level context
109
109
  // filter in index.ts drops this customType before the LLM sees it — colocated
110
110
  // with the emitter so sessions without --entwurf-control are still protected.
111
- // Layer A (native path) reuses the same Box builder via renderSentMessage() but
112
- // does NOT emit a CustomMessage; the native tool result already lives in the
113
- // toolResult role and never enters LLM context as a user message.
111
+ // There is no second emitter: the v1 native send path ("Layer A") that also drew
112
+ // this box through renderSentMessage() was removed in the 0.12 cutover, and
113
+ // renderSentMessage is now registered for exactly one customType (see
114
+ // registerMessageRenderer below). Do not describe a native sender-side box.
114
115
  const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
115
116
 
116
117
  // ============================================================================
@@ -509,8 +510,8 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
509
510
  };
510
511
 
511
512
  // Sender-side payload — what renderSentMessage needs to draw the [entwurf sent →]
512
- // box. Carried verbatim by both Layer A (native renderResult) and Layer B
513
- // (CustomMessage details for ACP path). All four envelope fields are intentionally
513
+ // box. Carried by the ACP path's CustomMessage `details` the ONLY carrier since
514
+ // the v1 native renderResult path was removed (0.12 cutover). All four envelope fields are intentionally
514
515
  // echoed in the box even though the sender is "this same session" — operators
515
516
  // reading a busy multi-session transcript should be able to verify at a glance
516
517
  // which 담당자 is on the wire (cwd) and which model identity (agentId) actually
@@ -520,9 +521,10 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
520
521
  // re-renders (resize, expand toggle) keep showing the moment the message was
521
522
  // actually delivered rather than drifting forward to "now".
522
523
  //
523
- // wants_reply mirrors the receive-side etiquette badge. Native schema does not
524
- // yet expose it (see registerSessionTool's entwurfSendParameters); leave undefined
525
- // from the native call site until the schema grows the field.
524
+ // wants_reply mirrors the receive-side etiquette badge. It stays optional because
525
+ // the box is drawn from a CustomMessage whose `details` may predate the field; the
526
+ // v1 `registerSessionTool` / `entwurfSendParameters` schema this note used to point
527
+ // at is gone (0.12 cutover), so there is no native call site left to grow.
526
528
  interface SentBoxData {
527
529
  to: string; // target sessionId
528
530
  from?: string; // sender agentId, e.g. "entwurf/claude-opus-5"
@@ -1274,9 +1276,18 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1274
1276
  target: Type.String({ description: "Target garden id (use entwurf_peers to discover)" }),
1275
1277
  intent: StringEnum(["fire-and-forget", "owned-outcome"] as const, {
1276
1278
  description:
1277
- "fire-and-forget = send/reply/hand-off to a LIVE or meta-session target (set wants_reply for an answer); " +
1278
- "owned-outcome = wake a DORMANT pi via spawn-bg resume ONLY on a live target it is rejected " +
1279
- "(owned-live-no-autosend) and never auto-converted",
1279
+ "fire-and-forget = send/reply/hand-off to a LIVE socket target (currently backend pi) or to any " +
1280
+ "citizen with no socket liveness the decider picks that citizen's rail, and a rail can also " +
1281
+ "REJECT: self-fetch (e.g. Claude Code) meta-bridge mailbox while that mailbox is DELIVERABLE, " +
1282
+ "else rejected as mailbox-undeliverable; native-push (e.g. Antigravity) → direct injection into " +
1283
+ "its live conversation, which has NO mailbox, and its probe is three-valued — alive: injected, " +
1284
+ "dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate (two rejects, " +
1285
+ "not one). " +
1286
+ "Set wants_reply for an answer. " +
1287
+ "owned-outcome = wake a DORMANT socket-domain citizen via spawn-bg resume ONLY — on a live target " +
1288
+ "rejected as owned-live-no-autosend, on self-fetch as backend-liveness-unsupported, on " +
1289
+ "native-push as native-push-no-resume-authority (both lack resume authority, but the reasons " +
1290
+ "differ because native-push IS probe-measured), and never auto-converted",
1280
1291
  }),
1281
1292
  message: Type.String({
1282
1293
  description:
@@ -1285,7 +1296,9 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1285
1296
  }),
1286
1297
  mode: Type.Optional(
1287
1298
  StringEnum(["steer", "follow_up"] as const, {
1288
- description: "Delivery mode for a live send: steer (immediate) or follow_up (after task)",
1299
+ description:
1300
+ "Injection style for a CONTROL-SOCKET send only: steer (immediate) or follow_up (after task). " +
1301
+ "The mailbox, native-push, and spawn-bg plans carry no mode, so it has no effect on those rails.",
1289
1302
  }),
1290
1303
  ),
1291
1304
  wants_reply: Type.Optional(Type.Boolean({ description: "Human-conversation reply hint (default false)" })),
@@ -1299,6 +1312,11 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1299
1312
  wants_reply?: boolean;
1300
1313
  };
1301
1314
 
1315
+ // TS2589 ("type instantiation is excessively deep") workaround: pi's registerTool
1316
+ // generic infers the handler signature from the TypeBox schema, and this schema is
1317
+ // deep enough to blow the instantiation budget. Casting the FUNCTION (not the
1318
+ // argument) keeps the schema itself typed while stopping the inference walk.
1319
+ // Revisit when pi's registerTool takes an explicit params type parameter.
1302
1320
  const registerTool = pi.registerTool as (def: any) => void;
1303
1321
 
1304
1322
  registerTool({
@@ -1306,28 +1324,51 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1306
1324
  label: "Dispatch (v2)",
1307
1325
  description: `CANONICAL delivery surface for a garden id. When you have a garden id and want to
1308
1326
  reach whoever it names — message / reply / hand-off — use THIS verb. A garden id alone does not
1309
- reveal whether the target is a live pi session, a dormant pi session, or a Claude Code meta-session,
1327
+ reveal which rail that citizen answers on — a live socket session, a dormant one, a mailbox-backed
1328
+ self-fetch session, or a native-push session —
1310
1329
  and entwurf_v2 is the one surface that reads that and routes correctly (so "when unsure which
1311
1330
  transport, use entwurf_v2"). It dispatches to EXISTING targets; brand-new sibling creation is deferred
1312
1331
  to a later v2 lane. Dispatch to a garden citizen through the unified entwurf_v2 verb: the 5b decider
1313
- picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue) from the
1314
- target's liveness + your intent, runs it under the v2 lock policy (pi paths take a per-target lock;
1315
- the mailbox path is lock-free, guarded by active-receiver deliverability), and reports one outcome
1316
- (delivered / rejected / lock-retained / delivered-but-lock-dirty).
1332
+ picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue / native-push
1333
+ direct injection) from the target's liveness + your intent, runs it under the v2 lock policy, and reports
1334
+ one outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
1335
+
1336
+ LOCK POLICY (do not over-generalize it): the per-target lock is taken for a control-socket-DOMAIN
1337
+ dispatch, which is BOTH the live send AND the dormant cell's spawn-bg resume — spawn-bg is a separate
1338
+ relaunch transport yet it still runs under that domain's lock. The mailbox and native-push rails are
1339
+ lock-free: the mailbox is guarded instead by active-receiver deliverability, and native-push by its
1340
+ adapter probe.
1317
1341
 
1318
1342
  - target: the garden id of the citizen to reach (required).
1319
1343
  - intent: fire-and-forget (a send with no owned result) or owned-outcome (you own the result).
1320
1344
  - message: the message/prompt to dispatch (required).
1321
- - mode: steer or follow_up for a live send (optional).
1322
- - wants_reply: reply hint for a live send (optional, default false).
1345
+ - mode: steer or follow_up for a CONTROL-SOCKET send (optional). The mailbox, native-push, and
1346
+ spawn-bg plans carry no mode, so it has no effect on those rails — a native-push send IS live and
1347
+ still ignores it.
1348
+ - wants_reply: reply hint; it rides every rail (optional, default false).
1323
1349
 
1324
1350
  CHOOSING INTENT (picking wrong is rejected, never auto-fixed): to message / reply / hand off a peer
1325
- that entwurf_peers shows as liveness=alive (a live pi citizen), use intent: fire-and-forget
1326
- — it routes to the live control-socket; set wants_reply:true if you need an answer (wants_reply is NOT
1327
- owned-outcome). For a meta-session (liveness=unsupported, e.g. Claude Code), replies are ALSO
1328
- fire-and-forget (→ mailbox). owned-outcome is ONLY for waking a DORMANT pi citizen (spawn-bg resume);
1329
- on a live target it is rejected as owned-live-no-autosend, on an unsupported backend as
1330
- backend-liveness-unsupported, and is NEVER auto-converted so pick the right intent up front.
1351
+ that entwurf_peers shows as liveness=alive (a live socket citizen, currently backend pi), use intent:
1352
+ fire-and-forget — it routes to the live control-socket; set wants_reply:true if you need an answer
1353
+ (wants_reply is NOT owned-outcome). Replies to a citizen with NO socket liveness
1354
+ (liveness=unsupported) are ALSO fire-and-forget, and the decider picks that citizen's own rail: a
1355
+ self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, while a native-push backend
1356
+ (e.g. Antigravity) gets direct injection into its live conversation and has NO mailbox at all — do
1357
+ not assume mailbox semantics for every unsupported citizen. A native-push target IS measured by its
1358
+ own adapter probe, and that probe is THREE-valued, so the send is never silently queued: alive →
1359
+ injected; dead → rejected as native-push-target-dead; indeterminate → rejected as
1360
+ native-push-probe-indeterminate. Those last two stay separate on purpose — "we could not establish
1361
+ it" is not "it is gone". THERE IS A THIRD RESULT, so do not read
1362
+ liveness=unsupported as "reachable by some rail": the mailbox path delivers only to a DELIVERABLE
1363
+ citizen (a self-fetch backend whose receiver is live and armed), so a terminated Claude Code session —
1364
+ and any record whose backend has no adapter on this lane, e.g. codex — is rejected as
1365
+ mailbox-undeliverable rather than queued into an inbox nobody drains. unsupported means only "this
1366
+ backend has no control-socket probe".
1367
+ owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently backend
1368
+ pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor native-push
1369
+ has resume authority, but they reject under DIFFERENT reasons — self-fetch as
1370
+ backend-liveness-unsupported, native-push as native-push-no-resume-authority.
1371
+ It is NEVER auto-converted — so pick the right intent up front.
1331
1372
 
1332
1373
  The decider — not this surface — chooses the transport.`,
1333
1374
  parameters: entwurfV2Parameters,
@@ -1433,8 +1474,9 @@ async function renderEntwurfPeersForSurface(): Promise<{ text: string; payload:
1433
1474
  }
1434
1475
 
1435
1476
  function registerListSessionsTool(pi: ExtensionAPI): void {
1436
- // Same TS2589 workaround as registerSessionTool — see the comment block
1437
- // in that function for the revisit conditions.
1477
+ // Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there
1478
+ // for the revisit conditions. (It used to point at registerSessionTool, which was
1479
+ // removed in the 0.12 cutover, so the pointer dangled.)
1438
1480
  const registerTool = pi.registerTool as (def: any) => void;
1439
1481
  registerTool({
1440
1482
  name: "entwurf_peers",
@@ -9,9 +9,11 @@
9
9
  // operator entries a backend needs (credentials, caches, built-in skills)
10
10
  // reachable through a TIGHT symlink whitelist — nothing else.
11
11
  //
12
- // Scope (NEXT §스코프 / §S2-scout 핀3): Claude-only on this lane. Codex/Gemini
13
- // overlays (CODEX_HOME / admin.toml) are 0.11.0 behavior-oracle territory and
14
- // out of scope the bridge backends are not in v2 yet.
12
+ // Scope (NEXT §스코프 / §S2-scout 핀3): Claude-only on this lane. Claude is the one
13
+ // shipped ACP backend, so this materializer is written for its config surface alone.
14
+ // Codex/Gemini overlays (CODEX_HOME / admin.toml) are behavior-oracle territory and
15
+ // out of scope here; a SECOND ACP backend enters only once the entry conditions in
16
+ // docs/acp-backend-rail.md are met, and it brings its own overlay.
15
17
  //
16
18
  // Two deliberate divergences from the literal 0.11.0 illustrative comment block
17
19
  // (the 0.11.0 CODE already does both — only its top doc-comment drew projects/
@@ -1,14 +1,29 @@
1
1
  /**
2
2
  * entwurf-core — sync entwurf execution, host-agnostic.
3
3
  *
4
- * Single implementation shared by:
5
- * - pi-extensions/entwurf.ts (pi native tool surface)
6
- * - mcp/entwurf-bridge/src/index.ts (MCP tool surface for ACP hosts)
4
+ * DIRECT importers, MEASURED from the import graph (2026-07-27) — not from memory:
5
+ * - pi-extensions/entwurf-control.ts (the pi adapter)
6
+ * - pi-extensions/lib/entwurf-v2-spawn-production.ts (v2 spawn-bg launch identity)
7
+ * - pi-extensions/lib/project-trust-handler.ts · lib/acp/augment.ts ·
8
+ * lib/native-push/adapter.ts
9
+ * - plus gates/smokes (check-shell-quote, check-package-source-routing, …)
10
+ *
11
+ * The MCP bridge is a TRANSITIVE consumer, not a direct one, and the distinction is the
12
+ * whole point: `mcp/entwurf-bridge/src/index.ts → entwurf-v2-surface.ts →
13
+ * entwurf-v2-production.ts → entwurf-v2-spawn-production.ts → entwurf-core.ts`. So the old
14
+ * header's claim — that the bridge imports this shared implementation the way a v1 tool
15
+ * surface did — was false, but so is "the bridge does not reach it": it reaches it at
16
+ * runtime, only for spawn-bg production.
17
+ *
18
+ * NOT consumers on any path: `pi-extensions/entwurf.ts` (the v1 pi native tool surface) and
19
+ * `pi-extensions/lib/entwurf-async.ts` were REMOVED in the 0.12 cutover. Do not reintroduce
20
+ * either as a live consumer.
7
21
  *
8
22
  * This module MUST NOT import anything from @earendil-works/pi-coding-agent or any
9
- * other pi runtime API. It is pure Node + @sinclair/typebox-free. Anything that
10
- * requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs
11
- * in the async entwurf path, which stays in pi-extensions/entwurf.ts for now.
23
+ * other pi runtime API. It is pure Node + @sinclair/typebox-free. Anything that
24
+ * requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs to a
25
+ * pi-adapter surface, never here — the v1 async entwurf path that used to hold that
26
+ * work went away with `pi-extensions/entwurf.ts`.
12
27
  *
13
28
  * Scope:
14
29
  * - sync execution (spawn pi, collect message_end events, return summary)
@@ -67,9 +82,11 @@ const PI_SETTINGS_PATH = process.env.PI_SETTINGS_PATH
67
82
  : path.join(AGENT_DIR, "settings.json");
68
83
  export const ENTWURF_CODEX_ACP_ENV = "ENTWURF_ACP_FOR_CODEX";
69
84
 
70
- // Currently unused: remote/SSH entwurf is fail-fast in 0.9.0 (garden-native
71
- // identity is local-FS only). Retained for #11 remote revival; parity-gated by
72
- // scripts/check-shell-quote.ts across entwurf.ts / entwurf-core.ts / entwurf-async.ts.
85
+ // Currently unused: remote/SSH entwurf is fail-fast (garden-native identity is
86
+ // local-FS only). Retained for #11 remote revival and pinned by
87
+ // scripts/check-shell-quote.ts, whose SOURCE_SITES is now THIS FILE ALONE — the
88
+ // entwurf.ts / entwurf-async.ts halves of that former parity check were removed
89
+ // with those files, so there is no cross-file parity left to assert.
73
90
  // biome-ignore lint/correctness/noUnusedVariables: retained for #11 remote revival; parity-gated.
74
91
  function shellQuote(value: string): string {
75
92
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -18,9 +18,10 @@
18
18
  * drains its own inbox on wake. A DIRECT-INJECT backend (pi / codex / antigravity)
19
19
  * has no mailbox drain at all — enqueuing for it is the SE-1 false success
20
20
  * ("✓ delivered" into a void). So deliverable = wakeMode === "self-fetch" AND the
21
- * receiver is active. This is the guard that the v1 fallback, MCP v1, pi-native v1,
22
- * and the v2 decider/send-fallback enqueue sites must all pass before writing a
23
- * .msg (slice 2d).
21
+ * receiver is active. Every enqueue site must pass this guard before writing a .msg
22
+ * (slice 2d). The shipped sites are the v2 decider and its send-fallback re-resolve;
23
+ * the v1 fallback / MCP v1 / pi-native v1 sites this once also listed were removed in
24
+ * the 0.12 cutover — do not read them as live.
24
25
  *
25
26
  * - nativePushDeliverable(facts): the SEPARATE deliverability predicate for a
26
27
  * NATIVE-PUSH backend (antigravity). A native-push citizen has no mailbox and no
@@ -89,9 +90,12 @@ export interface ReceiverIdentityFacts {
89
90
  * Does this presence marker actually belong to the target identity? A marker that is
90
91
  * absent, or whose garden id / backend / native session id has drifted from the record,
91
92
  * is NOT this receiver — fail-closed (a stale/foreign marker must never raise a dead
92
- * target to "active"). The single source of truth for "marker ↔ identity match" shared
93
- * by the v1 mailbox guard (gatherMailboxDeliverabilityFacts) and the v2 production
94
- * `mailboxDeliverabilityFor` seam, so the two paths cannot drift to different meanings.
93
+ * target to "active"). The single source of truth for "marker ↔ identity match". Its
94
+ * PRODUCTION consumers are the v2 `mailboxDeliverabilityFor` seam and the MCP bridge's
95
+ * `entwurf_self`. There is no second implementation: `entwurf-mailbox-guard.ts` used to
96
+ * wrap this atom with its own enqueue orchestration, had ZERO production importers (import
97
+ * graph measured 2026-07-27), and was DELETED rather than left as a green gate proving only
98
+ * retired behaviour. A new enqueue site consults this predicate through that seam.
95
99
  */
96
100
  export function receiverMarkerMatchesIdentity(
97
101
  marker: ReceiverIdentityFacts | null | undefined,
@@ -5,8 +5,8 @@
5
5
  * (one-way: provider → facts / socket-discovery / meta-session) — no import cycle
6
6
  * with `entwurf-facts.ts` (which owns `SocketProbe`/`resolveFactList`).
7
7
  *
8
- * listAllMetaIdentities → pi gid 추출 → scanSocketProbes(piGids)
9
- * → pre-quarantine non-pi/socket conflicts → resolveFactList(clean)
8
+ * listAllMetaIdentities → socket-domain gid extraction → scanSocketProbes
9
+ * → pre-quarantine out-of-domain/socket conflicts → resolveFactList(clean)
10
10
  * → { facts, diagnostics }
11
11
  *
12
12
  * Two throw-vs-diagnostics policies, kept distinct (GPT힣 C-원칙):
@@ -20,14 +20,14 @@
20
20
  * — that is not a re-implementation of the collision rule, it is input
21
21
  * sanitation that leaves the pure-core invariant intact.
22
22
  *
23
- * The non-pi+socket collision quarantines BOTH sides (the PeerFact AND the
24
- * socket): gardenId is the universal address and a send path reads the socket
23
+ * An out-of-socket-domain record/socket collision quarantines BOTH sides (the
24
+ * PeerFact AND the socket): gardenId is the universal address and a send path
25
25
  * first, so surfacing the record alone (as a clean `unsupported` PeerFact) while
26
26
  * a same-gid socket exists would be half a lie. Both leave the normal output;
27
27
  * one diagnostic carries the fact. (pi + same-gid socket = the normal merge.)
28
28
  */
29
29
 
30
- import { type FactList, isNonPiGardenIdSocketConflict, resolveFactList } from "./entwurf-facts.ts";
30
+ import { type FactList, isOutOfSocketDomainGardenIdConflict, resolveFactList } from "./entwurf-facts.ts";
31
31
  import { isLivenessSupported } from "./entwurf-v2-contract.ts";
32
32
  import {
33
33
  type ActiveStoreEntry,
@@ -130,8 +130,8 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
130
130
  // 2. socket axis — probe (dir sockets) ∪ (in-domain citizen canonical paths).
131
131
  // Its three hazards (symlink forgery / malformed name / dir-read error) are
132
132
  // folded into diagnostics here so the listing survives but never lies.
133
- const piGids = identities.filter((i) => isLivenessSupported(i.backend)).map((i) => i.gardenId);
134
- const scan = await scanSocketProbes(piGids, deps.socket ?? {});
133
+ const socketDomainGids = identities.filter((i) => isLivenessSupported(i.backend)).map((i) => i.gardenId);
134
+ const scan = await scanSocketProbes(socketDomainGids, deps.socket ?? {});
135
135
  const probes = scan.probes;
136
136
  const socketGids = new Set(probes.map((p) => p.gardenId));
137
137
  const symlinkedGids = new Set(scan.symlinkedGardenIds);
@@ -158,24 +158,22 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
158
158
  });
159
159
  }
160
160
 
161
- // 3. pre-quarantine non-pi citizens that collide with a control socket. The
162
- // predicate is SHARED with the v2 decider (isNonPiGardenIdSocketConflict) so
163
- // listing and dispatch cannot drift, and it unions socketGids with the
164
- // symlinkedGids: a symlinked socket is never probed (absent from socketGids),
165
- // so the old socketGids-only check let a non-pi citizen with a forged
166
- // (symlinked) socket survive as a clean PeerFact while the legacy send path
167
- // still followed the symlink — the gap this closes.
161
+ // 3. Pre-quarantine citizens outside the control-socket capability domain that
162
+ // collide with a control socket. The predicate is SHARED with v2 dispatch so
163
+ // listing and dispatch cannot drift. It unions real and symlinked socket gids:
164
+ // the earlier real-socket-only check let an out-of-domain record survive as a
165
+ // clean PeerFact beside a forged symlink receiver.
168
166
  const conflictGids = new Set<string>();
169
167
  for (const id of identities) {
170
- if (isNonPiGardenIdSocketConflict(id.backend, id.gardenId, socketGids, symlinkedGids)) {
168
+ if (isOutOfSocketDomainGardenIdConflict(id.backend, id.gardenId, socketGids, symlinkedGids)) {
171
169
  conflictGids.add(id.gardenId);
172
170
  diagnostics.push({
173
171
  kind: "garden-id-socket-conflict",
174
172
  gardenId: id.gardenId,
175
173
  backend: id.backend,
176
174
  message:
177
- `non-pi citizen (${id.backend}) shares its gardenId with a control socket (real or symlinked) — address ` +
178
- "ambiguity; both the citizen and the socket are quarantined from the listing.",
175
+ `out-of-socket-domain citizen (${id.backend}) shares its gardenId with a control socket ` +
176
+ "(real or symlinked) address ambiguity; both sides are quarantined from the listing.",
179
177
  });
180
178
  }
181
179
  }
@@ -75,27 +75,27 @@ export function resolvePeerFact(identity: MetaIdentity, socket: SocketLiveness |
75
75
  }
76
76
 
77
77
  /**
78
- * A non-pi RECORD whose gardenId collides with a control socket — a real (probed)
79
- * one OR a symlinked/forged one. The gardenId is the universal address (동결결정3),
80
- * so a non-pi citizen sharing it with a socket means a send-path that reaches the
81
- * socket first hits a DIFFERENT receiver than the record names — an address split.
78
+ * An OUT-OF-SOCKET-DOMAIN record whose gardenId collides with a control socket —
79
+ * a real (probed) one OR a symlinked/forged one. The gardenId is the universal
80
+ * address, so a citizen without socket capability sharing it with a socket means
81
+ * the socket reaches a DIFFERENT receiver than the record names — an address split.
82
82
  * Both the citizen and the socket are quarantined from the facts listing.
83
83
  *
84
84
  * The union `socketGids ∪ symlinkedGardenIds` is load-bearing: `socketGids` are
85
85
  * gids with a real probed `*.sock`, but `symlinkedGardenIds` are NEVER probed (P1)
86
86
  * and so are absent from `socketGids`. Looking at `socketGids` alone (the
87
- * fact-provider:125 gap this closes) let a non-pi citizen with a *symlinked* socket
87
+ * fact-provider:125 gap this closes) let an out-of-domain citizen with a *symlinked* socket
88
88
  * survive as a clean PeerFact while the legacy send path still followed the symlink
89
89
  * to a forged receiver. Both axes claim the gid → both must quarantine it.
90
90
  *
91
- * SCOPE: this is the RECORD-side, non-pi conflict only — shared by the fact-provider
92
- * (listing) and the v2 decider (dispatch) so the two cannot drift (4c "재유도 금지"
93
- * 동형; only the observation-bit source is parameterized). A pi citizen whose own
94
- * canonical socket is a symlink is NOT this predicate's concern — that is a
91
+ * SCOPE: this is the record-side, out-of-socket-domain conflict only — shared by
92
+ * listing and dispatch so they cannot drift; only the observation-bit source is
93
+ * parameterized. A citizen IN the socket domain whose own canonical socket is a
94
+ * symlink is NOT this predicate's concern — that is a
95
95
  * target-specific lstat conflict the decider's `inspectTargetControlSocket` raises
96
96
  * as `address-conflict`, kept deliberately separate (GPT 1차 검수 C).
97
97
  */
98
- export function isNonPiGardenIdSocketConflict(
98
+ export function isOutOfSocketDomainGardenIdConflict(
99
99
  backend: string,
100
100
  gardenId: string,
101
101
  socketGids: ReadonlySet<string>,
@@ -163,8 +163,8 @@ export interface FactList {
163
163
  * `indeterminate` and strand a dormant citizen as un-resumable); a dormant
164
164
  * citizen's absent socket file is probed to `dead` (ENOENT) by the wiring and
165
165
  * arrives here AS `dead` → dormant → resumable.
166
- * - out-of-domain citizen WITH a control socket at its gardenId → fail-loud
167
- * (address ambiguity; a non-pi citizen must not own a pi control socket).
166
+ * - out-of-socket-domain citizen WITH a control socket at its gardenId → fail-loud
167
+ * (address ambiguity; a backend without socket capability cannot own that rail).
168
168
  * - out-of-domain citizen without a socket → `unsupported` (via resolvePeerFact).
169
169
  * - a probed gardenId with NO citizen → `RecordLessSocketFact` (#50 C4: a
170
170
  * diagnostic subject the provider folds into a `record-less-socket`
@@ -202,7 +202,7 @@ export function resolveFactList(identities: MetaIdentity[], socketProbes: Socket
202
202
  if (probeMap.has(gid)) {
203
203
  throw new Error(
204
204
  `resolveFactList: out-of-domain citizen ${gid} (${identity.backend}) has a control socket — ` +
205
- "address ambiguity (a non-pi citizen must not own a pi control socket)",
205
+ "address ambiguity (an out-of-socket-domain citizen cannot own a control socket)",
206
206
  );
207
207
  }
208
208
  socket = null;
@@ -1,40 +1,40 @@
1
1
  /**
2
2
  * entwurf-resume-args — the SINGLE source of truth for the `pi` argv a resume spawn is
3
- * launched with (0.11 Stage 0 step 5c-3b). Two variants share this builder so the legacy
4
- * async-entwurf worker and the v2 spawn-bg resident citizen can NEVER drift in their launch
5
- * shape — the one place the `--no-extensions` / `--entwurf-control` decision lives.
3
+ * launched with (0.11 Stage 0 step 5c-3b). One place owns the
4
+ * `--no-extensions` / `--entwurf-control` decision so the launch shape cannot drift.
6
5
  *
7
- * The load-bearing difference (A1):
8
- * - legacy a one-shot async worker. `pi -p <prompt>` runs the resume turn and EXITS;
9
- * `proc.on('close')` then delivers the followUp. It is launched `--no-extensions` AND
10
- * WITHOUT `--entwurf-control` precisely BECAUSE a control-socket server would keep
11
- * `pi -p` from exiting (entwurf.ts:22 that keep-alive was a bug for a one-shot worker).
12
- * - v2-control — a RESIDENT, addressable garden citizen. The resume turn STILL runs via
13
- * `-p <prompt>` (the prompt-as-turn authority is unchanged), but the child is launched
14
- * WITH `--entwurf-control` and WITHOUT `--no-extensions`, so the very keep-alive legacy
15
- * avoided is now the GOAL: the resumed session stands its control socket up and stays
16
- * live. The 5c-3a watcher's `socket-alive` is exactly that "resumed citizen is up and
17
- * addressable" observation (→ release the per-gid lock, child lives on); `child-exited`
18
- * is the early-exit/failure observation. `plan.launchArgs` (`--approve` or empty, from
19
- * the decider's preflight) rides along as pi flags before the prompt.
6
+ * The shipped posture (A1): a RESIDENT, addressable garden citizen. The resume turn runs via
7
+ * `-p <prompt>` (the prompt-as-turn authority), and the child is launched WITH
8
+ * `--entwurf-control` and WITHOUT `--no-extensions` the keep-alive is the GOAL here: the
9
+ * resumed session stands its control socket up and stays addressable. The 5c-3a watcher's
10
+ * `socket-alive` is exactly that "resumed citizen is up" observation (→ release the per-gid
11
+ * lock, child lives on); `child-exited` is the early-exit/failure observation.
12
+ * `plan.launchArgs` (`--approve` or empty, from the decider's preflight) rides along as pi
13
+ * flags before the prompt.
14
+ *
15
+ * A second `legacy` one-shot posture (`--no-extensions`, no control socket, so `pi -p` could
16
+ * exit) lived here until 2026-07-27 and was removed with its launcher and the v1 verbs.
20
17
  *
21
18
  * Provider/model identity is the caller's existing authority (readSessionIdentity /
22
19
  * getEntwurfExplicitExtensions) — this builder only LAYS OUT argv, it never resolves
23
- * identity. `explicitExtensionArgs` is preserved verbatim in BOTH variants: a recorded
24
- * `provider=entwurf` resume needs the bridge re-injected to resolve the provider, and
25
- * dropping it when `--no-extensions` is removed would re-introduce the "Unknown provider"
26
- * footgun (#29). (A future slice may dedup against settings-loaded extensions; not here.)
20
+ * identity. `explicitExtensionArgs` is preserved verbatim: a recorded `provider=entwurf`
21
+ * resume needs the bridge re-injected to resolve the provider, and dropping it would
22
+ * re-introduce the "Unknown provider" footgun (#29). (A future slice may dedup against
23
+ * settings-loaded extensions; not here.)
27
24
  *
28
- * This module is import-free on purpose: the legacy launcher (entwurf-async.ts) is
29
- * root-typechecked and imports it as `./entwurf-resume-args.js`, while the v2 adapter and
30
- * the gate import the same source — a self-contained string builder keeps both configs happy.
25
+ * This module is import-free on purpose: the v2 adapter and the gate import the same source,
26
+ * and a self-contained string builder keeps both tsconfigs happy.
31
27
  */
32
28
 
33
- export type ResumeArgsVariant = "legacy" | "v2-control";
29
+ /** The one shipped launch posture: a resident citizen (`--entwurf-control`, extensions
30
+ * loaded). The `legacy` one-shot variant was removed with its launcher (2026-07-27) — an
31
+ * exported branch no product path took was what let this module's prose claim a second
32
+ * live consumer for months. Reviving a one-shot posture means adding it back deliberately,
33
+ * with a consumer, not un-deleting a dead enum member. */
34
+ export type ResumeArgsVariant = "v2-control";
34
35
 
35
36
  export interface ResumePiArgsInput {
36
- /** legacy = one-shot worker (`--no-extensions`, no control socket); v2-control =
37
- * resident citizen (`--entwurf-control`, extensions loaded). */
37
+ /** v2-control = resident citizen (`--entwurf-control`, extensions loaded). */
38
38
  variant: ResumeArgsVariant;
39
39
  /** ABSOLUTE path of the session JSONL to resume — pi's `--session <path>` (#50 C2).
40
40
  * It replaced `--session-id <gardenId>`, which did two jobs that are no longer the
@@ -46,7 +46,7 @@ export interface ResumePiArgsInput {
46
46
  * instead of resuming one. A path cannot do that. */
47
47
  sessionFile: string;
48
48
  /** The explicit `--extension …` re-injection (ACP bridge / provider resolution).
49
- * Preserved verbatim in BOTH variants — load-bearing for a entwurf resume. */
49
+ * Preserved verbatim — load-bearing for a entwurf resume. */
50
50
  explicitExtensionArgs: readonly string[];
51
51
  /** Recorded provider (may be null/undefined — then no `--provider` flag is emitted). */
52
52
  provider: string | null | undefined;
@@ -54,37 +54,31 @@ export interface ResumePiArgsInput {
54
54
  model: string;
55
55
  /** The resume prompt — the final positional, run as the model turn under `-p`. */
56
56
  prompt: string;
57
- /** v2-control ONLY: the decider's `plan.launchArgs` (`["--approve"]` or `[]`). Ignored
58
- * for legacy (the legacy path computes its own preflight elsewhere). */
57
+ /** The decider's `plan.launchArgs` (`["--approve"]` or `[]`). */
59
58
  launchArgs?: readonly string[];
60
59
  }
61
60
 
62
61
  /**
63
- * Build the `pi` argv for a resume spawn. The SHARED prefix is `--mode json -p` (headless
64
- * JSON child, prompt-as-turn); the variant then chooses the extension/socket posture; the
65
- * SHARED suffix is `[…ext args] --session <file> [--provider <p>] --model <m> <prompt>`.
62
+ * Build the `pi` argv for a resume spawn. The prefix is `--mode json -p` (headless JSON
63
+ * child, prompt-as-turn); then the resident posture; then
64
+ * `[…ext args] --session <file> [--provider <p>] --model <m> <prompt>`.
66
65
  *
67
66
  * Invariants the gate pins:
68
- * - legacy carries `--no-extensions` and NO `--entwurf-control`.
69
67
  * - v2-control carries `--entwurf-control` and NO `--no-extensions`, plus `-p` + prompt.
70
- * - `explicitExtensionArgs` appears exactly once in both.
71
- * - `launchArgs` is included for v2-control (before the suffix) and ignored for legacy.
72
- * - provider/model/prompt identity is laid out identically in both.
68
+ * - `explicitExtensionArgs` appears exactly once.
69
+ * - `launchArgs` is included before the suffix.
70
+ * - provider/model/prompt identity layout is fixed.
73
71
  */
74
72
  export function buildResumePiArgs(input: ResumePiArgsInput): string[] {
75
73
  const args: string[] = ["--mode", "json", "-p"];
76
74
 
77
- if (input.variant === "legacy") {
78
- // One-shot worker: no extensions, no control socket (so `pi -p` can exit).
79
- args.push("--no-extensions");
80
- } else {
81
- // Resident citizen: stand the control socket up (A1) and keep extensions loaded.
82
- // The keep-alive legacy avoided is the goal here. `--approve`/launchArgs ride along.
83
- args.push("--entwurf-control");
84
- args.push(...(input.launchArgs ?? []));
85
- }
75
+ // Resident citizen: stand the control socket up (A1) and keep extensions loaded. The
76
+ // keep-alive the removed one-shot launcher had to avoid is precisely the goal here
77
+ // the resumed session must stay addressable. `--approve`/launchArgs ride along.
78
+ args.push("--entwurf-control");
79
+ args.push(...(input.launchArgs ?? []));
86
80
 
87
- // Shared suffix identical identity layout in both variants.
81
+ // Suffixthe fixed identity layout.
88
82
  args.push(...input.explicitExtensionArgs);
89
83
  args.push("--session", input.sessionFile);
90
84
  if (input.provider) args.push("--provider", input.provider);
@@ -3,14 +3,13 @@
3
3
  *
4
4
  * "Can a reply to THIS session actually land where its model will see it?"
5
5
  *
6
- * Today both the MCP bridge (buildStrictPiSenderEnvelope /
7
- * buildTrustedMetaSenderEnvelope / entwurf_self) and pi-native answer that from env
8
- * presence alone and hardcode `replyable: true`: a pi session with no
9
- * --entwurf-control socket, or a meta citizen whose owner has exited / whose
10
- * idle-watch was never armed, all claim replyable while delivery silently fails
11
- * (SE-1: "all layers say yes, only delivery says no" = a Crash-Don't-Warn
12
- * violation). This module is the single truth-table both surfaces compute from,
13
- * with every fact INJECTED (no IO) so the gate can pin each row.
6
+ * This module closed the former split where the MCP and native pi surfaces inferred
7
+ * `replyable: true` from identity-carrier presence alone. Identity and reachability
8
+ * are separate: pi's env carries the garden id established by record birth, while
9
+ * replyability requires its control socket; a native marker must be record-backed,
10
+ * while replyability additionally requires its own rail's live facts. This is the
11
+ * single truth-table both surfaces compute from, with every fact INJECTED (no IO) so
12
+ * the gate can pin each row.
14
13
  *
15
14
  * Axes by origin:
16
15
  * - pi-session: replyable ⟺ a live control socket exists at the canonical path.
@@ -33,7 +32,7 @@
33
32
  * — which never arms a watch — would report replyable:false forever.
34
33
  * - external-mcp: never replyable — no authoritative reply address.
35
34
  *
36
- * `origin` stays identity PROVENANCE (where the sender identity came from), never a rail.
35
+ * `origin` stays sender-carrier PROVENANCE, never the citizen identity authority and never a rail.
37
36
  * Which rail a meta citizen's reply rides is a SECOND axis — `metaDeliveryDomain`, derived
38
37
  * by the caller from `nativePushSupported(backend)`, not from `wakeMode` (direct-inject also
39
38
  * covers codex/pi, which have no native-push adapter). Fail-closed: an unsupplied domain is
@@ -9,7 +9,12 @@
9
9
  * from the bridge boot closure would re-couple the harness-neutral meta-bridge to
10
10
  * pi (the `check-entwurf-bridge-pi-free` gate fails if it does). The bridge needs
11
11
  * the contract CONSTANTS + `resolveDispatch` (pi-free core), never these schemas.
12
- * Consumers: the pi MCP-tool param surface and `check-entwurf-v2-contract`.
12
+ * Consumer: `check-entwurf-v2-contract` ONLY. This is a gate-side REPRESENTATION of the
13
+ * frozen contract, not a shipped tool schema — measured 2026-07-27, nothing else in the
14
+ * import graph reaches it. The two surfaces a model actually reads build their own:
15
+ * `pi-extensions/entwurf-control.ts` (TypeBox `entwurfV2Parameters` + tool description) and
16
+ * `mcp/entwurf-bridge/src/index.ts` (Zod). Change a model-facing description THERE; changing
17
+ * it here reaches no model, and `check-entwurf-v2-surface` is what pins those two.
13
18
  *
14
19
  * StringEnum (typebox 1.x) inside Type.Object (typebox 0.34) — the same mix the
15
20
  * existing entwurf tools use. The logic types in the core are hand-written unions,
@@ -37,7 +42,7 @@ export const EntwurfV2InputSchema = Type.Object(
37
42
  target: Type.String({
38
43
  pattern: SESSION_ID_RE.source,
39
44
  description:
40
- "garden-id of an EXISTING citizen (pattern-enforced). spawn-new is out of v2 scope (legacy entwurf keeps it); a malformed/typo gid is bad-target.",
45
+ "garden-id of an EXISTING citizen (pattern-enforced). Fresh sibling creation is out of scope on every current surface there is no fallback verb that still spawns. A malformed/typo gid is bad-target, never a new spawn.",
41
46
  }),
42
47
  intent: StringEnum(ENTWURF_INTENTS, {
43
48
  description: