@junghanacs/entwurf 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- 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 +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- 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 +258 -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 +170 -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 +176 -67
- 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 +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- 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 +328 -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 +221 -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/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- 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 +198 -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 +101 -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 +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- 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/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -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/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -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 +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- 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 -236
- 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 -469
|
@@ -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
|
|
@@ -162,6 +222,45 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
|
|
|
162
222
|
* parameter marker in the long-description slice: if the slice ever swallows the schema
|
|
163
223
|
* again, this fails instead of silently satisfying the schema's own text.
|
|
164
224
|
*/
|
|
225
|
+
/**
|
|
226
|
+
* HOST TRUNCATION CAP. Measured 2026-07-30 on both Claude ACP and stock Claude Code: a tool
|
|
227
|
+
* description longer than this is cut mid-sentence and the tail is replaced with `… [truncated]`.
|
|
228
|
+
* The shipped entwurf_v2 text was 4,022 chars, so everything from the INTENT contract onward —
|
|
229
|
+
* the reject taxonomy, the three-valued probe, the lock scope — never reached the model that had
|
|
230
|
+
* to choose an intent. Every other assertion in this file checks that a sentence is PRESENT in
|
|
231
|
+
* source; none of them can see that the host threw half of it away. This one can.
|
|
232
|
+
*
|
|
233
|
+
* Judged on `modelText` (the rendered string), not the source slice: pi-native wraps inside one
|
|
234
|
+
* template literal and the MCP surface `+`-concatenates, so only the normalized text has the
|
|
235
|
+
* length the host actually measures.
|
|
236
|
+
*/
|
|
237
|
+
const HOST_DESCRIPTION_CAP = 2048;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The string the HOST measures, recovered from either surface's source scaffolding: drop the
|
|
241
|
+
* `description:` key and the outer template-literal / string-literal fence, and unescape `\"`.
|
|
242
|
+
* Counting the raw slice instead would charge the text for punctuation the model never sees —
|
|
243
|
+
* and the two surfaces carry DIFFERENT punctuation, so the cap would mean two different things.
|
|
244
|
+
*/
|
|
245
|
+
function renderedDescription(raw: string): string {
|
|
246
|
+
const flat = modelText(raw);
|
|
247
|
+
// Anchor on the description's own first word rather than on scaffolding: the two surfaces put
|
|
248
|
+
// DIFFERENT preludes in front of it (the MCP slice starts at the tool name), and counting those
|
|
249
|
+
// would report a number the host never measures — a cap gate that cites the wrong length gets
|
|
250
|
+
// quoted as if it were the real one.
|
|
251
|
+
const from = flat.indexOf("CANONICAL");
|
|
252
|
+
return (from >= 0 ? flat.slice(from) : flat).replace(/[`"],?$/, "").replace(/\\"/g, '"');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function assertDescriptionFitsHostCap(tag: string, longDescRaw: string): void {
|
|
256
|
+
const rendered = renderedDescription(longDescRaw);
|
|
257
|
+
ok(
|
|
258
|
+
`${tag} — long description fits the host's ${HOST_DESCRIPTION_CAP}-char cap (${rendered.length}) ` +
|
|
259
|
+
"[QK:V2SURF-DESC-FITS-HOST-CAP]",
|
|
260
|
+
rendered.length <= HOST_DESCRIPTION_CAP,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
165
264
|
function assertLongDescExcludesParams(tag: string, longDesc: string, markers: readonly string[]): void {
|
|
166
265
|
for (const marker of markers) {
|
|
167
266
|
ok(`${tag} — long description slice excludes the \`${marker}\` parameter marker`, !longDesc.includes(marker));
|
|
@@ -188,7 +287,7 @@ async function main(): Promise<void> {
|
|
|
188
287
|
{
|
|
189
288
|
const full: SurfaceEntwurfV2Params = {
|
|
190
289
|
target: GID,
|
|
191
|
-
intent: "
|
|
290
|
+
intent: "fire-and-forget",
|
|
192
291
|
message: "hi",
|
|
193
292
|
mode: "steer",
|
|
194
293
|
wants_reply: true,
|
|
@@ -196,7 +295,7 @@ async function main(): Promise<void> {
|
|
|
196
295
|
const di = toDispatchInput(full);
|
|
197
296
|
ok(
|
|
198
297
|
"1: target/intent/message pass through",
|
|
199
|
-
di.target === GID && di.intent === "
|
|
298
|
+
di.target === GID && di.intent === "fire-and-forget" && di.message === "hi",
|
|
200
299
|
);
|
|
201
300
|
ok("1: wants_reply → wantsReply (snake→camel)", di.wantsReply === true);
|
|
202
301
|
ok("1: mode passes through", di.mode === "steer");
|
|
@@ -262,41 +361,9 @@ async function main(): Promise<void> {
|
|
|
262
361
|
cr.isError && cr.text.includes("dormant-fire-forget-unsupported"),
|
|
263
362
|
);
|
|
264
363
|
|
|
265
|
-
// spawn lock-retained
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
receipt: { ...SUCCESS_RECEIPT, transport: "spawn-bg" },
|
|
269
|
-
transport: "spawn-bg",
|
|
270
|
-
outcome: {
|
|
271
|
-
transport: "spawn-bg",
|
|
272
|
-
result: {
|
|
273
|
-
kind: "lock-retained",
|
|
274
|
-
released: false,
|
|
275
|
-
reason: "observe-failed",
|
|
276
|
-
diagnostic: {
|
|
277
|
-
targetGardenId: GID,
|
|
278
|
-
lockPath: "/locks/x.lock",
|
|
279
|
-
expectedSocketPath: "/ctl/x.sock",
|
|
280
|
-
observeTimeoutMs: 30000,
|
|
281
|
-
killGraceMs: 5000,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
const ret = renderEntwurfV2Result(retained);
|
|
287
|
-
ok(
|
|
288
|
-
"2: spawn lock-retained → isError + diagnostic surfaced",
|
|
289
|
-
ret.isError && ret.text.includes("LOCK RETAINED") && ret.text.includes("/locks/x.lock"),
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// spawn socket-alive → delivered
|
|
293
|
-
const alive: EntwurfV2RunResult = {
|
|
294
|
-
kind: "executed",
|
|
295
|
-
receipt: { ...SUCCESS_RECEIPT, transport: "spawn-bg" },
|
|
296
|
-
transport: "spawn-bg",
|
|
297
|
-
outcome: { transport: "spawn-bg", result: { kind: "socket-alive", released: true, pid: 7 } },
|
|
298
|
-
};
|
|
299
|
-
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.
|
|
300
367
|
|
|
301
368
|
// meta-mailbox → enqueued
|
|
302
369
|
const mailbox: EntwurfV2RunResult = {
|
|
@@ -334,17 +401,6 @@ async function main(): Promise<void> {
|
|
|
334
401
|
!renderEntwurfV2Result(npRetried).isError && renderEntwurfV2Result(npRetried).text.includes("retry"),
|
|
335
402
|
);
|
|
336
403
|
|
|
337
|
-
// native-push owned reject → hint to switch to fire-and-forget
|
|
338
|
-
const npReject: EntwurfV2RunResult = {
|
|
339
|
-
kind: "rejected",
|
|
340
|
-
receipt: { ok: false, reason: "native-push-no-resume-authority", observedLiveness: "alive" },
|
|
341
|
-
};
|
|
342
|
-
const npRej = renderEntwurfV2Result(npReject);
|
|
343
|
-
ok(
|
|
344
|
-
"2: native-push-no-resume-authority → isError + fire-and-forget hint",
|
|
345
|
-
npRej.isError && npRej.text.includes("fire-and-forget"),
|
|
346
|
-
);
|
|
347
|
-
|
|
348
404
|
// N1: execution-failed with finalizedOutcome + releaseFailed → delivered-but-dirty
|
|
349
405
|
const n1: EntwurfV2RunResult = {
|
|
350
406
|
kind: "execution-failed",
|
|
@@ -371,28 +427,65 @@ async function main(): Promise<void> {
|
|
|
371
427
|
};
|
|
372
428
|
ok("2: plain execution-failed → isError", renderEntwurfV2Result(failed).isError);
|
|
373
429
|
|
|
374
|
-
// Detour B (B-a)
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
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 = {
|
|
378
436
|
kind: "rejected",
|
|
379
|
-
receipt: { ok: false, reason: "
|
|
437
|
+
receipt: { ok: false, reason: "dormant-fire-forget-unsupported", observedLiveness: "dead" },
|
|
380
438
|
};
|
|
381
|
-
const
|
|
439
|
+
const dr = renderEntwurfV2Result(dormantReject);
|
|
382
440
|
ok(
|
|
383
|
-
"2:
|
|
384
|
-
|
|
385
|
-
mr.text.includes("backend-liveness-unsupported") &&
|
|
386
|
-
mr.text.includes("fire-and-forget") &&
|
|
387
|
-
mr.text.includes("mailbox"),
|
|
441
|
+
"2: dormant-fire-forget-unsupported reject → isError + reason surfaced",
|
|
442
|
+
dr.isError && dr.text.includes("dormant-fire-forget-unsupported"),
|
|
388
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") ?? "";
|
|
389
448
|
ok(
|
|
390
|
-
"2B:
|
|
391
|
-
(
|
|
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),
|
|
392
451
|
);
|
|
393
452
|
ok(
|
|
394
|
-
"2B:
|
|
395
|
-
|
|
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),
|
|
396
489
|
);
|
|
397
490
|
ok(
|
|
398
491
|
"2B: actionableRejectHint returns undefined for a reject with no next step",
|
|
@@ -411,22 +504,11 @@ async function main(): Promise<void> {
|
|
|
411
504
|
);
|
|
412
505
|
}
|
|
413
506
|
|
|
414
|
-
// ── 6: parseEntwurfPrefixRootsEnv
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
ok("6: empty string → []", parseEntwurfPrefixRootsEnv("").length === 0);
|
|
420
|
-
ok("6: delimiters-only → []", parseEntwurfPrefixRootsEnv(`${D}${D}`).length === 0);
|
|
421
|
-
const two = parseEntwurfPrefixRootsEnv(`/repos/gh${D}/repos/work`);
|
|
422
|
-
ok("6: delimiter-separated → entries", two.length === 2 && two[0] === "/repos/gh" && two[1] === "/repos/work");
|
|
423
|
-
const trimmed = parseEntwurfPrefixRootsEnv(` /a ${D} ${D} /b `);
|
|
424
|
-
ok("6: trims + drops empty segments", trimmed.length === 2 && trimmed[0] === "/a" && trimmed[1] === "/b");
|
|
425
|
-
// A nonexistent/typo path is KEPT verbatim (no throw, no validation) — preflight's
|
|
426
|
-
// normalize handles it, and a typo must never broaden approve nor fail the dispatch.
|
|
427
|
-
const typo = parseEntwurfPrefixRootsEnv("/this/does/not/exist");
|
|
428
|
-
ok("6: nonexistent path kept verbatim (no throw)", typo.length === 1 && typo[0] === "/this/does/not/exist");
|
|
429
|
-
}
|
|
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.
|
|
430
512
|
|
|
431
513
|
// ── 3: surface source guard — ctx-free ────────────────────────────────────
|
|
432
514
|
{
|
|
@@ -471,7 +553,7 @@ async function main(): Promise<void> {
|
|
|
471
553
|
!/import[^;]*from\s*"\.\/lib\/entwurf-self-address\.(js|ts)"/.test(code),
|
|
472
554
|
);
|
|
473
555
|
// Every SHIPPED rail must appear in the model-facing text. Measured 2026-07-27: both
|
|
474
|
-
// surfaces described only control-socket/
|
|
556
|
+
// surfaces described only control-socket/mailbox and told the model that an
|
|
475
557
|
// `unsupported` citizen is reached "→ mailbox" — false for Antigravity, whose native-push
|
|
476
558
|
// rail is intercepted BEFORE the mailbox mini-table and has no mailbox at all. A model
|
|
477
559
|
// reading that would pick mailbox semantics for a citizen that has none.
|
|
@@ -516,27 +598,7 @@ async function main(): Promise<void> {
|
|
|
516
598
|
] as const) {
|
|
517
599
|
ok(`4: pi-native — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
518
600
|
ok(`4: pi-native — ${what} names the native-push rail`, /native-push/.test(text));
|
|
519
|
-
|
|
520
|
-
// probe-measured, so calling its reject `backend-liveness-unsupported` is the exact lie
|
|
521
|
-
// `entwurf-v2-contract.ts:143` warns about in so many words.
|
|
522
|
-
ok(
|
|
523
|
-
`4: pi-native — ${what} separates the self-fetch and native-push owned rejects`,
|
|
524
|
-
/backend-liveness-unsupported/.test(text) && /native-push-no-resume-authority/.test(text),
|
|
525
|
-
);
|
|
526
|
-
// Direct tripwire for the exact sentence that shipped: merging the two backends under
|
|
527
|
-
// one reason. Presence-only pins cannot catch this — the merged claim can sit right
|
|
528
|
-
// beside the correct literals.
|
|
529
|
-
ok(
|
|
530
|
-
`4: pi-native — ${what} never merges the two backends under one reject reason`,
|
|
531
|
-
!/self-fetch and native-push alike/.test(text),
|
|
532
|
-
);
|
|
533
|
-
// Caller-intent steer (live-peer owned-outcome bug): owned-outcome is dormant-only and
|
|
534
|
-
// NEVER auto-converted. Pinned PER STRING — the file-wide version of this check could
|
|
535
|
-
// be satisfied by whichever description still carried it.
|
|
536
|
-
ok(
|
|
537
|
-
`4: pi-native — ${what} says owned-outcome is dormant-only + never auto-converted`,
|
|
538
|
-
/DORMANT socket-domain citizen/.test(text) && /auto-converted/.test(text),
|
|
539
|
-
);
|
|
601
|
+
assertDormantHonesty(`4: pi-native — ${what}`, text);
|
|
540
602
|
}
|
|
541
603
|
ok("4: pi-native — long description denies native-push a mailbox", /NO mailbox/.test(piLongDesc));
|
|
542
604
|
ok(
|
|
@@ -551,6 +613,21 @@ async function main(): Promise<void> {
|
|
|
551
613
|
]);
|
|
552
614
|
const piModeDesc = sliceDescription(piV2Block, "mode: Type.Optional(", "wants_reply:");
|
|
553
615
|
assertRailSemantics("4: pi-native", piLongDesc, piIntentDesc, piModeDesc);
|
|
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);
|
|
554
631
|
}
|
|
555
632
|
|
|
556
633
|
// ── 5: MCP bridge wiring guard ────────────────────────────────────────────
|
|
@@ -612,20 +689,7 @@ async function main(): Promise<void> {
|
|
|
612
689
|
] as const) {
|
|
613
690
|
ok(`5: MCP — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
614
691
|
ok(`5: MCP — ${what} names the native-push rail`, /native-push/.test(text));
|
|
615
|
-
|
|
616
|
-
`5: MCP — ${what} separates the self-fetch and native-push owned rejects [QK:V2SURF-MERGED-REJECT]`,
|
|
617
|
-
/backend-liveness-unsupported/.test(text) && /native-push-no-resume-authority/.test(text),
|
|
618
|
-
);
|
|
619
|
-
ok(
|
|
620
|
-
`5: MCP — ${what} never merges the two backends under one reject reason`,
|
|
621
|
-
!/self-fetch and native-push alike/.test(text),
|
|
622
|
-
);
|
|
623
|
-
// Same per-string caller-intent pin as block 4 (a sibling reaching in over MCP reads
|
|
624
|
-
// THIS description, not the pi-native one).
|
|
625
|
-
ok(
|
|
626
|
-
`5: MCP — ${what} says owned-outcome is dormant-only + never auto-converted`,
|
|
627
|
-
/DORMANT socket-domain citizen/.test(text) && /auto-converted/.test(text),
|
|
628
|
-
);
|
|
692
|
+
assertDormantHonesty(`5: MCP — ${what}`, text);
|
|
629
693
|
}
|
|
630
694
|
ok("5: MCP — long description denies native-push a mailbox", /NO mailbox/.test(mcpLongDesc));
|
|
631
695
|
ok(
|
|
@@ -635,6 +699,7 @@ async function main(): Promise<void> {
|
|
|
635
699
|
assertLongDescExcludesParams("5: MCP", mcpLongDesc, ["target: z", "intent: z", "mode: z", "wants_reply: z"]);
|
|
636
700
|
const mcpModeDesc = sliceDescription(v2Block, "mode: z", "wants_reply: z");
|
|
637
701
|
assertRailSemantics("5: MCP", mcpLongDesc, mcpIntentDesc, mcpModeDesc);
|
|
702
|
+
assertDescriptionFitsHostCap("5: MCP", mcpLongDesc);
|
|
638
703
|
|
|
639
704
|
// entwurf_peers is a FACT surface, but it still told the model what `unsupported` means —
|
|
640
705
|
// and "does NOT mean unreachable" is false for a record whose backend has no adapter on
|
|
@@ -656,6 +721,11 @@ async function main(): Promise<void> {
|
|
|
656
721
|
"5: MCP — entwurf_peers no longer claims `unsupported` does NOT mean unreachable",
|
|
657
722
|
!/does NOT mean unreachable/.test(peersBlock),
|
|
658
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);
|
|
659
729
|
|
|
660
730
|
// F-7: entwurf_inbox_read described itself as draining "your own" inbox while the handler
|
|
661
731
|
// passes the CALLER-SUPPLIED gardenId straight to readMetaInbox with no comparison against
|