@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.
- package/AGENTS.md +134 -248
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +19 -0
- package/DELIVERY.md +4 -3
- package/README.md +26 -13
- package/VERIFY.md +3 -2
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +10 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
- package/mcp/entwurf-bridge/src/index.ts +190 -82
- package/package.json +5 -3
- package/pi-extensions/entwurf-control.ts +69 -27
- package/pi-extensions/lib/acp/overlay.ts +5 -3
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
- package/pi-extensions/lib/meta-session.ts +17 -20
- package/run.sh +48 -41
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +7 -7
- package/scripts/check-entwurf-facts.ts +12 -12
- package/scripts/check-entwurf-resume-args.ts +22 -60
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +5 -5
- package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-gate-qualification.ts +785 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-shell-quote.ts +2 -1
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- 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
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
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
|
|
513
|
-
//
|
|
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.
|
|
524
|
-
//
|
|
525
|
-
//
|
|
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
|
|
1278
|
-
"
|
|
1279
|
-
"(
|
|
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:
|
|
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
|
|
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
|
|
1314
|
-
target's liveness + your intent, runs it under the v2 lock policy
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
|
1322
|
-
-
|
|
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
|
|
1326
|
-
— it routes to the live control-socket; set wants_reply:true if you need an answer
|
|
1327
|
-
owned-outcome).
|
|
1328
|
-
fire-and-forget
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
|
1437
|
-
//
|
|
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.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
-
*
|
|
5
|
-
* - pi-extensions/entwurf.ts (pi
|
|
6
|
-
* -
|
|
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.
|
|
10
|
-
* requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs
|
|
11
|
-
*
|
|
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
|
|
71
|
-
//
|
|
72
|
-
// scripts/check-shell-quote.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.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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"
|
|
93
|
-
*
|
|
94
|
-
* `
|
|
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 →
|
|
9
|
-
* → pre-quarantine
|
|
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
|
-
*
|
|
24
|
-
* socket): gardenId is the universal address and a send path
|
|
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,
|
|
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
|
|
134
|
-
const scan = await scanSocketProbes(
|
|
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.
|
|
162
|
-
// predicate is SHARED with
|
|
163
|
-
// listing and dispatch cannot drift
|
|
164
|
-
//
|
|
165
|
-
//
|
|
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 (
|
|
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
|
-
`
|
|
178
|
-
"
|
|
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
|
-
*
|
|
79
|
-
* one OR a symlinked/forged one. The gardenId is the universal
|
|
80
|
-
* so a
|
|
81
|
-
* socket
|
|
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
|
|
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
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
|
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
|
|
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 (
|
|
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).
|
|
4
|
-
*
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
29
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
|
71
|
-
* - `launchArgs` is included
|
|
72
|
-
* - provider/model/prompt identity is
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
//
|
|
81
|
+
// Suffix — the 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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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).
|
|
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:
|