@junghanacs/entwurf 0.13.1 → 0.14.1
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 +59 -15
- package/BASELINE.md +3 -3
- package/CHANGELOG.md +74 -0
- package/CONTRIBUTING.md +13 -9
- package/DELIVERY.md +7 -6
- package/README.md +37 -28
- package/VERIFY.md +22 -14
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +9 -4
- package/docs/external-mcp-host.md +4 -5
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +155 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +300 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +141 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +173 -28
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +22 -11
- package/pi-extensions/entwurf-control.ts +227 -40
- package/pi-extensions/lib/acp/backend.ts +71 -12
- package/pi-extensions/lib/acp/overlay.ts +8 -6
- package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +381 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +189 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/run.sh +471 -201
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +157 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +99 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +283 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +84 -76
- package/scripts/check-release-gate-outcomes.ts +127 -7
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-bridge-state.py +75 -1
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/meta-retire.json +47 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +229 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +0 -1037
- package/scripts/mutants/release-gate.json +35 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +58 -18
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-native-push-live.ts +6 -17
- package/scripts/smoke-entwurf-chain-live.ts +11 -3
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +169 -3
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -275
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -467
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* pass through, absent `mode`/`wants_reply` stay undefined (decider defaults, no double).
|
|
8
8
|
* 2. renderEntwurfV2Result — each result kind → the right `{ text, isError }`, surfacing the
|
|
9
9
|
* carry-overs: reject reason + target-locked diagnostic / control N3 rejectReason /
|
|
10
|
-
*
|
|
10
|
+
* N1 delivered-but-lock-dirty — plus the two reject HINTS an operator meets after the
|
|
11
|
+
* visible-first cut (dormant-fire-forget-unsupported, indeterminate-no-spawn).
|
|
11
12
|
* 3. surface source guard — `entwurf-v2-surface.ts` is ctx-free (no ExtensionContext/API).
|
|
12
13
|
* 4. control wiring guard — `entwurf-control.ts` registers `entwurf_v2`, reaches the fence
|
|
13
14
|
* ONLY via a NON-LITERAL dynamic import (a string-const specifier), NEVER a static import
|
|
@@ -23,8 +24,6 @@ import { fileURLToPath } from "node:url";
|
|
|
23
24
|
import type { EntwurfV2RunResult } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
24
25
|
import {
|
|
25
26
|
actionableRejectHint,
|
|
26
|
-
ENTWURF_PREFIX_ROOTS_ENV,
|
|
27
|
-
parseEntwurfPrefixRootsEnv,
|
|
28
27
|
renderEntwurfV2Result,
|
|
29
28
|
type SurfaceEntwurfV2Params,
|
|
30
29
|
toDispatchInput,
|
|
@@ -64,13 +63,12 @@ function modelText(slice: string): string {
|
|
|
64
63
|
* unsupported-citizen outcome set in ways the code does not support. Measured against
|
|
65
64
|
* source, not prose:
|
|
66
65
|
* - LOCK: `entwurf-v2-decider.ts` returns a held claim ONLY on the control-socket-domain
|
|
67
|
-
* branch
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* exactly backwards.
|
|
66
|
+
* branch. The mailbox and native-push branches both carry `lock: null`. "socket paths
|
|
67
|
+
* take a per-target lock" read as "the socket transport locks", which got native-push
|
|
68
|
+
* (a live send that does NOT lock) exactly backwards. The DOMAIN wording survived the
|
|
69
|
+
* visible-first cut; the second in-domain transport it also covered did not.
|
|
72
70
|
* - MODE: `mode` exists on exactly ONE ExecutionPlan variant, `control-socket`. The
|
|
73
|
-
* meta-mailbox and native-push plans have no such field
|
|
71
|
+
* meta-mailbox and native-push plans have no such field.
|
|
74
72
|
* "mode applies to a live send" is false for native-push, which IS a live send.
|
|
75
73
|
* - THIRD RESULT: `resolveDispatch` downgrades the unsupported fire-and-forget cell to
|
|
76
74
|
* `mailbox-undeliverable` when the separate deliverability fact is false, so an
|
|
@@ -90,10 +88,9 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
|
|
|
90
88
|
`${tag} — long description scopes the per-target lock to the control-socket DOMAIN [QK:V2SURF-MCP-LOCK-DOMAIN]`,
|
|
91
89
|
/control-socket-DOMAIN\s+dispatch/.test(longDesc) && /lock-free/.test(longDesc),
|
|
92
90
|
);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
91
|
+
// The spawn-bg half of this claim went with the transport itself (visible-first cut): there is
|
|
92
|
+
// no second in-domain transport left to run under the lock, so asserting one would be a green
|
|
93
|
+
// gate proving retired behavior. What survives is the DOMAIN scope, pinned just above.
|
|
97
94
|
ok(
|
|
98
95
|
`${tag} — long description says the mailbox AND native-push rails are lock-free`,
|
|
99
96
|
/mailbox and native-push rails are lock-free/.test(longDesc),
|
|
@@ -154,6 +151,69 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
|
|
|
154
151
|
);
|
|
155
152
|
}
|
|
156
153
|
|
|
154
|
+
/**
|
|
155
|
+
* DORMANT HONESTY — the model-facing replacement for the retired `V2SURF-MERGED-REJECT` claim,
|
|
156
|
+
* whose subject (two distinct OWNED rejects) left with the `owned-outcome` intent.
|
|
157
|
+
*
|
|
158
|
+
* After the visible-first cut a dormant socket-domain citizen is unreachable by every verb, and
|
|
159
|
+
* these four strings are the only place a caller learns that BEFORE spending a dispatch. So each
|
|
160
|
+
* one must, on its own:
|
|
161
|
+
* - name the reason it will actually get back (`dormant-fire-forget-unsupported`);
|
|
162
|
+
* - name the intent that used to answer there (`owned-outcome`) — silently deleting it leaves
|
|
163
|
+
* an operator who read last week's docs with no way to find out what happened;
|
|
164
|
+
* - say it was `withdrawn`, ADJACENTLY to that name. Presence alone is not enough: naming a
|
|
165
|
+
* retired intent without the retirement verb beside it reads as an offer, which is exactly
|
|
166
|
+
* the "advertised as selectable" failure this claim exists to forbid;
|
|
167
|
+
* - carry NEITHER retired advertisement form (`owned-outcome = …` enum styling, or the old
|
|
168
|
+
* "DORMANT socket-domain citizen … never auto-converted" sentence that described picking it).
|
|
169
|
+
*
|
|
170
|
+
* ORDER IS LOAD-BEARING, and not for readability. `ok` throws on the FIRST failure, and the kill
|
|
171
|
+
* signature is read off the failing line — so the `[QK:…]` label must sit on the assertion this
|
|
172
|
+
* claim's mutant reaches FIRST. That mutant drops the reason AND the withdrawal in one edit; with
|
|
173
|
+
* the reason check leading, an UNLABELLED line would throw first and the kill would read as
|
|
174
|
+
* WRONG-REASON. The adjacency check therefore goes first and carries the only token; the other two
|
|
175
|
+
* run unlabelled behind it — still real assertions, just not this claim's signature.
|
|
176
|
+
*/
|
|
177
|
+
function assertDormantHonesty(tag: string, rawText: string): void {
|
|
178
|
+
const text = modelText(rawText);
|
|
179
|
+
const at = text.indexOf("owned-outcome");
|
|
180
|
+
ok(
|
|
181
|
+
`${tag} — names the withdrawn intent owned-outcome and says "withdrawn" beside it (not as an offer) [QK:V2SURF-DORMANT-HONESTY]`,
|
|
182
|
+
at >= 0 && /withdrawn/.test(text.slice(at, at + 200)),
|
|
183
|
+
);
|
|
184
|
+
ok(`${tag} — names the reason a dormant target actually returns`, /dormant-fire-forget-unsupported/.test(text));
|
|
185
|
+
ok(
|
|
186
|
+
`${tag} — never advertises owned-outcome as selectable`,
|
|
187
|
+
!/owned-outcome\s*=/.test(text) && !/DORMANT socket-domain citizen/.test(text) && !/auto-converted/.test(text),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* PEERS DEAD-ROW HONESTY. `entwurf_peers` is a FACT surface and stays one: this claim must never
|
|
193
|
+
* be satisfiable by adding a per-row action/routing field, which is why it is asserted on the
|
|
194
|
+
* DESCRIPTION and paired with a negative that forbids the row-level fix. What the description owes
|
|
195
|
+
* a caller is that `dead` is a reported fact carrying no invitation — the citizen is dormant and,
|
|
196
|
+
* since the visible-first cut, reachable by no verb at all.
|
|
197
|
+
*/
|
|
198
|
+
function assertPeersDeadRowHonesty(tag: string, rawBlock: string): void {
|
|
199
|
+
const text = modelText(rawBlock);
|
|
200
|
+
ok(
|
|
201
|
+
`${tag} — entwurf_peers says a dead/dormant row is a REPORTED FACT that grants no action [QK:V2SURF-PEERS-DEAD-ROW]`,
|
|
202
|
+
/REPORTED FACT/.test(text) && /grants no action/.test(text) && /not an invitation to dispatch/.test(text),
|
|
203
|
+
);
|
|
204
|
+
ok(
|
|
205
|
+
`${tag} — entwurf_peers calls a dead row currently unreachable by any verb`,
|
|
206
|
+
/dormant/.test(text) && /unreachable by any verb/.test(text),
|
|
207
|
+
);
|
|
208
|
+
// The forbidden repair: teaching the row itself to carry a verb. The facts-only rule is what
|
|
209
|
+
// keeps this surface from becoming a second dispatch table, so a description that promised a
|
|
210
|
+
// per-row action would be a worse fix than the confusion it removes.
|
|
211
|
+
ok(
|
|
212
|
+
`${tag} — entwurf_peers still denies per-row routing verbs (facts-only)`,
|
|
213
|
+
/never per-row routing verbs|no per-row routing field/.test(text),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
157
217
|
/**
|
|
158
218
|
* Containment-disjointness is NOT enough to prove a description slice is tight. Round 5
|
|
159
219
|
* (2026-07-27) shipped a "long description" slice of 4,468 chars that SPANNED the parameter
|
|
@@ -227,7 +287,7 @@ async function main(): Promise<void> {
|
|
|
227
287
|
{
|
|
228
288
|
const full: SurfaceEntwurfV2Params = {
|
|
229
289
|
target: GID,
|
|
230
|
-
intent: "
|
|
290
|
+
intent: "fire-and-forget",
|
|
231
291
|
message: "hi",
|
|
232
292
|
mode: "steer",
|
|
233
293
|
wants_reply: true,
|
|
@@ -235,7 +295,7 @@ async function main(): Promise<void> {
|
|
|
235
295
|
const di = toDispatchInput(full);
|
|
236
296
|
ok(
|
|
237
297
|
"1: target/intent/message pass through",
|
|
238
|
-
di.target === GID && di.intent === "
|
|
298
|
+
di.target === GID && di.intent === "fire-and-forget" && di.message === "hi",
|
|
239
299
|
);
|
|
240
300
|
ok("1: wants_reply → wantsReply (snake→camel)", di.wantsReply === true);
|
|
241
301
|
ok("1: mode passes through", di.mode === "steer");
|
|
@@ -301,41 +361,9 @@ async function main(): Promise<void> {
|
|
|
301
361
|
cr.isError && cr.text.includes("dormant-fire-forget-unsupported"),
|
|
302
362
|
);
|
|
303
363
|
|
|
304
|
-
// spawn lock-retained
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
receipt: { ...SUCCESS_RECEIPT, transport: "spawn-bg" },
|
|
308
|
-
transport: "spawn-bg",
|
|
309
|
-
outcome: {
|
|
310
|
-
transport: "spawn-bg",
|
|
311
|
-
result: {
|
|
312
|
-
kind: "lock-retained",
|
|
313
|
-
released: false,
|
|
314
|
-
reason: "observe-failed",
|
|
315
|
-
diagnostic: {
|
|
316
|
-
targetGardenId: GID,
|
|
317
|
-
lockPath: "/locks/x.lock",
|
|
318
|
-
expectedSocketPath: "/ctl/x.sock",
|
|
319
|
-
observeTimeoutMs: 30000,
|
|
320
|
-
killGraceMs: 5000,
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
const ret = renderEntwurfV2Result(retained);
|
|
326
|
-
ok(
|
|
327
|
-
"2: spawn lock-retained → isError + diagnostic surfaced",
|
|
328
|
-
ret.isError && ret.text.includes("LOCK RETAINED") && ret.text.includes("/locks/x.lock"),
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
// spawn socket-alive → delivered
|
|
332
|
-
const alive: EntwurfV2RunResult = {
|
|
333
|
-
kind: "executed",
|
|
334
|
-
receipt: { ...SUCCESS_RECEIPT, transport: "spawn-bg" },
|
|
335
|
-
transport: "spawn-bg",
|
|
336
|
-
outcome: { transport: "spawn-bg", result: { kind: "socket-alive", released: true, pid: 7 } },
|
|
337
|
-
};
|
|
338
|
-
ok("2: spawn socket-alive → not error", !renderEntwurfV2Result(alive).isError);
|
|
364
|
+
// The two spawn-bg render cells (lock-retained / socket-alive) were deleted with the
|
|
365
|
+
// transport itself. Nothing renders them anymore, and `EntwurfV2Transport` no longer
|
|
366
|
+
// admits the literal — a cell kept here would only prove the type is still wrong.
|
|
339
367
|
|
|
340
368
|
// meta-mailbox → enqueued
|
|
341
369
|
const mailbox: EntwurfV2RunResult = {
|
|
@@ -373,17 +401,6 @@ async function main(): Promise<void> {
|
|
|
373
401
|
!renderEntwurfV2Result(npRetried).isError && renderEntwurfV2Result(npRetried).text.includes("retry"),
|
|
374
402
|
);
|
|
375
403
|
|
|
376
|
-
// native-push owned reject → hint to switch to fire-and-forget
|
|
377
|
-
const npReject: EntwurfV2RunResult = {
|
|
378
|
-
kind: "rejected",
|
|
379
|
-
receipt: { ok: false, reason: "native-push-no-resume-authority", observedLiveness: "alive" },
|
|
380
|
-
};
|
|
381
|
-
const npRej = renderEntwurfV2Result(npReject);
|
|
382
|
-
ok(
|
|
383
|
-
"2: native-push-no-resume-authority → isError + fire-and-forget hint",
|
|
384
|
-
npRej.isError && npRej.text.includes("fire-and-forget"),
|
|
385
|
-
);
|
|
386
|
-
|
|
387
404
|
// N1: execution-failed with finalizedOutcome + releaseFailed → delivered-but-dirty
|
|
388
405
|
const n1: EntwurfV2RunResult = {
|
|
389
406
|
kind: "execution-failed",
|
|
@@ -410,28 +427,65 @@ async function main(): Promise<void> {
|
|
|
410
427
|
};
|
|
411
428
|
ok("2: plain execution-failed → isError", renderEntwurfV2Result(failed).isError);
|
|
412
429
|
|
|
413
|
-
// Detour B (B-a)
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
430
|
+
// Detour B (B-a) after the visible-first cut: the two OWNED rejects this block used to
|
|
431
|
+
// exercise (`backend-liveness-unsupported`, `owned-live-no-autosend`) left the reason
|
|
432
|
+
// union with the intent that produced them. The cell that now carries the whole cost of
|
|
433
|
+
// the cut is `dormant-fire-forget-unsupported`, and it is the one an operator actually
|
|
434
|
+
// meets — so it, not a retired reason, is what this block must prove.
|
|
435
|
+
const dormantReject: EntwurfV2RunResult = {
|
|
417
436
|
kind: "rejected",
|
|
418
|
-
receipt: { ok: false, reason: "
|
|
437
|
+
receipt: { ok: false, reason: "dormant-fire-forget-unsupported", observedLiveness: "dead" },
|
|
419
438
|
};
|
|
420
|
-
const
|
|
439
|
+
const dr = renderEntwurfV2Result(dormantReject);
|
|
421
440
|
ok(
|
|
422
|
-
"2:
|
|
423
|
-
|
|
424
|
-
mr.text.includes("backend-liveness-unsupported") &&
|
|
425
|
-
mr.text.includes("fire-and-forget") &&
|
|
426
|
-
mr.text.includes("mailbox"),
|
|
441
|
+
"2: dormant-fire-forget-unsupported reject → isError + reason surfaced",
|
|
442
|
+
dr.isError && dr.text.includes("dormant-fire-forget-unsupported"),
|
|
427
443
|
);
|
|
444
|
+
// The dormant hint is the ONLY place a caller learns why an id that entwurf_peers listed
|
|
445
|
+
// is unreachable. A reader who takes "reject" for "wrong id" goes looking in the wrong
|
|
446
|
+
// place, so the hint must distinguish the two: record intact, session not running.
|
|
447
|
+
const dormantHint = actionableRejectHint("dormant-fire-forget-unsupported") ?? "";
|
|
428
448
|
ok(
|
|
429
|
-
"2B:
|
|
430
|
-
(
|
|
449
|
+
"2B: dormant hint separates 'record intact' from 'session not running' (not a bad id)",
|
|
450
|
+
/record is intact/.test(dormantHint) && /session is not running/.test(dormantHint),
|
|
431
451
|
);
|
|
432
452
|
ok(
|
|
433
|
-
"2B:
|
|
434
|
-
|
|
453
|
+
"2B: dormant hint names the withdrawn intent and the visible-first rule, and hands the operator the resume verb",
|
|
454
|
+
/owned-outcome|resume that used to answer here/.test(dormantHint) &&
|
|
455
|
+
/withdrawn/.test(dormantHint) &&
|
|
456
|
+
/visible-first/.test(dormantHint) &&
|
|
457
|
+
/entwurf_resume_call/.test(dormantHint),
|
|
458
|
+
);
|
|
459
|
+
// The frozen wire id still spells "-no-spawn" although nothing spawns anymore. The id is
|
|
460
|
+
// deliberately NOT renamed (renaming a public reject is its own contract cut), so the
|
|
461
|
+
// hint carries the whole correction: probe inconclusive, and nothing ran.
|
|
462
|
+
const indetReject: EntwurfV2RunResult = {
|
|
463
|
+
kind: "rejected",
|
|
464
|
+
receipt: { ok: false, reason: "indeterminate-no-spawn", observedLiveness: "indeterminate" },
|
|
465
|
+
};
|
|
466
|
+
const ir = renderEntwurfV2Result(indetReject);
|
|
467
|
+
ok(
|
|
468
|
+
"2: indeterminate-no-spawn reject → isError + reason surfaced",
|
|
469
|
+
ir.isError && ir.text.includes("indeterminate-no-spawn"),
|
|
470
|
+
);
|
|
471
|
+
const indetHint = actionableRejectHint("indeterminate-no-spawn") ?? "";
|
|
472
|
+
// ORDER IS LOAD-BEARING (same rule as assertDormantHonesty): `ok` throws on the FIRST
|
|
473
|
+
// failure and the kill signature is read off the failing line, so the labelled assertion
|
|
474
|
+
// must be the one this claim's mutant reaches first. Its terse replacement text fails the
|
|
475
|
+
// length floor too — leading with that UNLABELLED check would throw first and the kill
|
|
476
|
+
// would read as WRONG-REASON. The no-start facts are also the claim's actual subject, so
|
|
477
|
+
// they lead and carry the only token; existence/UNKNOWN run unlabelled behind them.
|
|
478
|
+
ok(
|
|
479
|
+
"2B: indeterminate hint states NOTHING was delivered and NO process was started " +
|
|
480
|
+
"[QK:V2SURF-INDETERMINATE-NO-START]",
|
|
481
|
+
/NOTHING was delivered/.test(indetHint) && /NO process was started/.test(indetHint),
|
|
482
|
+
);
|
|
483
|
+
ok("2B: indeterminate hint exists at all (the frozen '-no-spawn' id cannot explain itself)", indetHint.length > 80);
|
|
484
|
+
// "the PROBE was inconclusive", never "the socket answered": `indeterminate` also covers a
|
|
485
|
+
// probe that got no answer, was refused by permissions, or timed out.
|
|
486
|
+
ok(
|
|
487
|
+
"2B: indeterminate hint says the PROBE was inconclusive and liveness is UNKNOWN, not a measured death",
|
|
488
|
+
/probe was inconclusive/.test(indetHint) && /UNKNOWN/.test(indetHint) && /not a measured death/.test(indetHint),
|
|
435
489
|
);
|
|
436
490
|
ok(
|
|
437
491
|
"2B: actionableRejectHint returns undefined for a reject with no next step",
|
|
@@ -450,22 +504,11 @@ async function main(): Promise<void> {
|
|
|
450
504
|
);
|
|
451
505
|
}
|
|
452
506
|
|
|
453
|
-
// ── 6: parseEntwurfPrefixRootsEnv
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
ok("6: empty string → []", parseEntwurfPrefixRootsEnv("").length === 0);
|
|
459
|
-
ok("6: delimiters-only → []", parseEntwurfPrefixRootsEnv(`${D}${D}`).length === 0);
|
|
460
|
-
const two = parseEntwurfPrefixRootsEnv(`/repos/gh${D}/repos/work`);
|
|
461
|
-
ok("6: delimiter-separated → entries", two.length === 2 && two[0] === "/repos/gh" && two[1] === "/repos/work");
|
|
462
|
-
const trimmed = parseEntwurfPrefixRootsEnv(` /a ${D} ${D} /b `);
|
|
463
|
-
ok("6: trims + drops empty segments", trimmed.length === 2 && trimmed[0] === "/a" && trimmed[1] === "/b");
|
|
464
|
-
// A nonexistent/typo path is KEPT verbatim (no throw, no validation) — preflight's
|
|
465
|
-
// normalize handles it, and a typo must never broaden approve nor fail the dispatch.
|
|
466
|
-
const typo = parseEntwurfPrefixRootsEnv("/this/does/not/exist");
|
|
467
|
-
ok("6: nonexistent path kept verbatim (no throw)", typo.length === 1 && typo[0] === "/this/does/not/exist");
|
|
468
|
-
}
|
|
507
|
+
// ── 6: RETIRED — parseEntwurfPrefixRootsEnv / ENTWURF_PREFIX_ROOTS ────────
|
|
508
|
+
// The prefix-roots env SSOT existed to feed the trust preflight, and the preflight existed
|
|
509
|
+
// to guard the resume verdict. Both left with the intent. The surface no longer exports
|
|
510
|
+
// either symbol, so there is nothing here to certify — and a gate that kept parsing a
|
|
511
|
+
// deleted export would be testing itself.
|
|
469
512
|
|
|
470
513
|
// ── 3: surface source guard — ctx-free ────────────────────────────────────
|
|
471
514
|
{
|
|
@@ -510,7 +553,7 @@ async function main(): Promise<void> {
|
|
|
510
553
|
!/import[^;]*from\s*"\.\/lib\/entwurf-self-address\.(js|ts)"/.test(code),
|
|
511
554
|
);
|
|
512
555
|
// Every SHIPPED rail must appear in the model-facing text. Measured 2026-07-27: both
|
|
513
|
-
// surfaces described only control-socket/
|
|
556
|
+
// surfaces described only control-socket/mailbox and told the model that an
|
|
514
557
|
// `unsupported` citizen is reached "→ mailbox" — false for Antigravity, whose native-push
|
|
515
558
|
// rail is intercepted BEFORE the mailbox mini-table and has no mailbox at all. A model
|
|
516
559
|
// reading that would pick mailbox semantics for a citizen that has none.
|
|
@@ -555,27 +598,7 @@ async function main(): Promise<void> {
|
|
|
555
598
|
] as const) {
|
|
556
599
|
ok(`4: pi-native — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
557
600
|
ok(`4: pi-native — ${what} names the native-push rail`, /native-push/.test(text));
|
|
558
|
-
|
|
559
|
-
// probe-measured, so calling its reject `backend-liveness-unsupported` is the exact lie
|
|
560
|
-
// `entwurf-v2-contract.ts:143` warns about in so many words.
|
|
561
|
-
ok(
|
|
562
|
-
`4: pi-native — ${what} separates the self-fetch and native-push owned rejects`,
|
|
563
|
-
/backend-liveness-unsupported/.test(text) && /native-push-no-resume-authority/.test(text),
|
|
564
|
-
);
|
|
565
|
-
// Direct tripwire for the exact sentence that shipped: merging the two backends under
|
|
566
|
-
// one reason. Presence-only pins cannot catch this — the merged claim can sit right
|
|
567
|
-
// beside the correct literals.
|
|
568
|
-
ok(
|
|
569
|
-
`4: pi-native — ${what} never merges the two backends under one reject reason`,
|
|
570
|
-
!/self-fetch and native-push alike/.test(text),
|
|
571
|
-
);
|
|
572
|
-
// Caller-intent steer (live-peer owned-outcome bug): owned-outcome is dormant-only and
|
|
573
|
-
// NEVER auto-converted. Pinned PER STRING — the file-wide version of this check could
|
|
574
|
-
// be satisfied by whichever description still carried it.
|
|
575
|
-
ok(
|
|
576
|
-
`4: pi-native — ${what} says owned-outcome is dormant-only + never auto-converted`,
|
|
577
|
-
/DORMANT socket-domain citizen/.test(text) && /auto-converted/.test(text),
|
|
578
|
-
);
|
|
601
|
+
assertDormantHonesty(`4: pi-native — ${what}`, text);
|
|
579
602
|
}
|
|
580
603
|
ok("4: pi-native — long description denies native-push a mailbox", /NO mailbox/.test(piLongDesc));
|
|
581
604
|
ok(
|
|
@@ -591,6 +614,20 @@ async function main(): Promise<void> {
|
|
|
591
614
|
const piModeDesc = sliceDescription(piV2Block, "mode: Type.Optional(", "wants_reply:");
|
|
592
615
|
assertRailSemantics("4: pi-native", piLongDesc, piIntentDesc, piModeDesc);
|
|
593
616
|
assertDescriptionFitsHostCap("4: pi-native", piLongDesc);
|
|
617
|
+
|
|
618
|
+
// 4b: pi-native entwurf_peers. Asserted on THIS surface too, not only the MCP one: the two
|
|
619
|
+
// descriptions are read by different callers (a resident pi model vs a sibling reaching in
|
|
620
|
+
// over MCP), and a correction that lands on one of them is a correction half the garden
|
|
621
|
+
// never sees. Same real-boundary rule as every other slice here.
|
|
622
|
+
const piPeersStart = src.indexOf('name: "entwurf_peers"');
|
|
623
|
+
const piPeersEnd = src.indexOf("parameters:", piPeersStart + 1);
|
|
624
|
+
ok(
|
|
625
|
+
"4b: pi-native — entwurf_peers description block has a REAL end boundary",
|
|
626
|
+
piPeersStart !== -1 && piPeersEnd > piPeersStart,
|
|
627
|
+
);
|
|
628
|
+
const piPeersBlock = src.slice(piPeersStart, piPeersEnd);
|
|
629
|
+
ok("4b: pi-native — entwurf_peers description is isolated (non-vacuous)", piPeersBlock.length > 200);
|
|
630
|
+
assertPeersDeadRowHonesty("4b: pi-native", piPeersBlock);
|
|
594
631
|
}
|
|
595
632
|
|
|
596
633
|
// ── 5: MCP bridge wiring guard ────────────────────────────────────────────
|
|
@@ -652,20 +689,7 @@ async function main(): Promise<void> {
|
|
|
652
689
|
] as const) {
|
|
653
690
|
ok(`5: MCP — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
654
691
|
ok(`5: MCP — ${what} names the native-push rail`, /native-push/.test(text));
|
|
655
|
-
|
|
656
|
-
`5: MCP — ${what} separates the self-fetch and native-push owned rejects [QK:V2SURF-MERGED-REJECT]`,
|
|
657
|
-
/backend-liveness-unsupported/.test(text) && /native-push-no-resume-authority/.test(text),
|
|
658
|
-
);
|
|
659
|
-
ok(
|
|
660
|
-
`5: MCP — ${what} never merges the two backends under one reject reason`,
|
|
661
|
-
!/self-fetch and native-push alike/.test(text),
|
|
662
|
-
);
|
|
663
|
-
// Same per-string caller-intent pin as block 4 (a sibling reaching in over MCP reads
|
|
664
|
-
// THIS description, not the pi-native one).
|
|
665
|
-
ok(
|
|
666
|
-
`5: MCP — ${what} says owned-outcome is dormant-only + never auto-converted`,
|
|
667
|
-
/DORMANT socket-domain citizen/.test(text) && /auto-converted/.test(text),
|
|
668
|
-
);
|
|
692
|
+
assertDormantHonesty(`5: MCP — ${what}`, text);
|
|
669
693
|
}
|
|
670
694
|
ok("5: MCP — long description denies native-push a mailbox", /NO mailbox/.test(mcpLongDesc));
|
|
671
695
|
ok(
|
|
@@ -697,6 +721,11 @@ async function main(): Promise<void> {
|
|
|
697
721
|
"5: MCP — entwurf_peers no longer claims `unsupported` does NOT mean unreachable",
|
|
698
722
|
!/does NOT mean unreachable/.test(peersBlock),
|
|
699
723
|
);
|
|
724
|
+
// Visible-first cut, operator-surface half: a dormant citizen still gets a row here, and a
|
|
725
|
+
// caller who reads "it is in the list" as "I can dispatch to it" spends a dispatch to find
|
|
726
|
+
// out otherwise. The FACT stays a fact — no per-row action field, no routing verb, which is
|
|
727
|
+
// the rule this surface exists under — so the correction has to live in the description.
|
|
728
|
+
assertPeersDeadRowHonesty("5: MCP", peersBlock);
|
|
700
729
|
|
|
701
730
|
// F-7: entwurf_inbox_read described itself as draining "your own" inbox while the handler
|
|
702
731
|
// passes the CALLER-SUPPLIED gardenId straight to readMetaInbox with no comparison against
|