@junghanacs/entwurf 0.24.0 → 0.25.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/CHANGELOG.md +455 -0
- package/DELIVERY.md +24 -1
- package/README.md +2 -2
- package/VERIFY.md +19 -1
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +4 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +13 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +11 -0
- package/mcp/entwurf-bridge/dist/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/src/index.ts +5 -4
- package/package.json +10 -9
- package/pi-extensions/entwurf-control.ts +27 -154
- package/pi-extensions/lib/compaction-send-guard.ts +5 -3
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +13 -3
- package/pi-extensions/lib/herdr-placement.ts +11 -0
- package/protocol.js +0 -8
- package/run.sh +197 -29
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-gate-qualification.ts +7 -4
- package/scripts/check-herdr-fresh-call.ts +10 -2
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +7 -4
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-fresh-call.json +4 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-herdr-fresh-call-live.ts +15 -8
- package/scripts/smoke-herdr-raw-install-live.sh +59 -2
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- package/scripts/smoke-pi-provider-state.sh +106 -0
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -227,6 +227,54 @@
|
|
|
227
227
|
"timeoutSeconds": 180,
|
|
228
228
|
"signature": "[QK:CODEX-FIRST-ADMISSION-IS-RELEASE-MUST]",
|
|
229
229
|
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"claim": "CODEX-OPERATOR-PREREQ-ABSENT-SKIPS",
|
|
233
|
+
"title": "a constant-false guard leaves the decline arm as dead code, so every absent operator input falls through to the assertions and exits FAIL again — the shape that made this MUST step red inside every cut and would require a per-cut exception",
|
|
234
|
+
"subject": "scripts/smoke-codex-fresh-live.ts",
|
|
235
|
+
"find": [
|
|
236
|
+
"\tconst absentInputs = OPERATOR_INPUTS.filter(([name]) => (process.env[name]?.trim() ?? \"\") === \"\");",
|
|
237
|
+
"\tif (absentInputs.length > 0) {"
|
|
238
|
+
],
|
|
239
|
+
"replace": [
|
|
240
|
+
"\tconst absentInputs = OPERATOR_INPUTS.filter(([name]) => (process.env[name]?.trim() ?? \"\") === \"\");",
|
|
241
|
+
"\tif (absentInputs.length > 0 && process.env.ENTWURF_NEVER_SET === \"1\") {"
|
|
242
|
+
],
|
|
243
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
244
|
+
"timeoutSeconds": 300,
|
|
245
|
+
"signature": "[QK:CODEX-OPERATOR-PREREQ-ABSENT-SKIPS]",
|
|
246
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"claim": "CODEX-OPERATOR-PREREQ-WRONG-FAILS",
|
|
250
|
+
"title": "turning the pid grammar into a decline means a malformed, dead or non-Codex app-server pid the operator DID supply reports as a missing prerequisite, so a broken answer buys silence where a cut reads silence as an unmet precondition rather than a defect",
|
|
251
|
+
"subject": "scripts/smoke-codex-fresh-live.ts",
|
|
252
|
+
"find": [
|
|
253
|
+
"\tok(",
|
|
254
|
+
"\t\t\"ENTWURF_CODEX_APP_SERVER_PID explicitly names the existing operator-owned app-server\",",
|
|
255
|
+
"\t\t/^[1-9]\\d*$/.test(pidText),"
|
|
256
|
+
],
|
|
257
|
+
"replace": [
|
|
258
|
+
"\tif (!/^[1-9]\\d*$/.test(pidText)) skipLive(LABEL, \"no usable app-server pid was supplied\");",
|
|
259
|
+
"\tok(",
|
|
260
|
+
"\t\t\"an app-server pid was supplied\",",
|
|
261
|
+
"\t\ttrue,"
|
|
262
|
+
],
|
|
263
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
264
|
+
"timeoutSeconds": 300,
|
|
265
|
+
"signature": "[QK:CODEX-OPERATOR-PREREQ-WRONG-FAILS]",
|
|
266
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"claim": "CODEX-OPERATOR-PREREQ-STATED-NOT-SEARCHED",
|
|
270
|
+
"title": "deriving the app-server pid by process search in the release instructions teaches the exact inference the MUST step refuses, and on a host running several app-servers it hands the gate an arbitrary process to verify as the operator's answer",
|
|
271
|
+
"subject": ".claude/skills/entwurf-release/SKILL.md",
|
|
272
|
+
"find": [": \"${CODEX_APP_SERVER_PID:?state the app-server pid you started; never search for it}\""],
|
|
273
|
+
"replace": ["CODEX_APP_SERVER_PID=$(pgrep -f 'codex.*app-server' | head -1)"],
|
|
274
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
275
|
+
"timeoutSeconds": 300,
|
|
276
|
+
"signature": "[QK:CODEX-OPERATOR-PREREQ-STATED-NOT-SEARCHED]",
|
|
277
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
230
278
|
}
|
|
231
279
|
]
|
|
232
280
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "retired-sent-surface",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "NO-RENDERER-WITHOUT-PRODUCER",
|
|
7
|
+
"title": "a renderer is registered for a customType nothing in this extension emits — a producer-less orphan box no operator can ever reach, the same shape that shipped for two releases behind comments naming producers that did not exist",
|
|
8
|
+
"subject": "pi-extensions/entwurf-control.ts",
|
|
9
|
+
"find": ["\tpi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);"],
|
|
10
|
+
"replace": [
|
|
11
|
+
"\tpi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);\n\tpi.registerMessageRenderer(\"entwurf-orphan\", renderSessionMessage);"
|
|
12
|
+
],
|
|
13
|
+
"gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
|
|
14
|
+
"timeoutSeconds": 300,
|
|
15
|
+
"signature": "[QK:NO-RENDERER-WITHOUT-PRODUCER]",
|
|
16
|
+
"signatureSource": "scripts/check-entwurf-v2-surface.ts"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"claim": "RETIRED-SENT-SURFACE-ABSENT",
|
|
20
|
+
"title": "the retired sender-side echo constant comes back as a public export of a SHIPPED file — a name an npm consumer can import and nothing in the repo can ever produce, which is the promise #120 P3 removed rather than renamed",
|
|
21
|
+
"subject": "protocol.js",
|
|
22
|
+
"find": ["export const ENTWURF_PROJECT_CONTEXT_OPEN_TAG = \"<project-context\";"],
|
|
23
|
+
"replace": [
|
|
24
|
+
"export const ENTWURF_PROJECT_CONTEXT_OPEN_TAG = \"<project-context\";\nexport const ENTWURF_SENT_MESSAGE_TYPE = \"entwurf-sent\";"
|
|
25
|
+
],
|
|
26
|
+
"gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
|
|
27
|
+
"timeoutSeconds": 300,
|
|
28
|
+
"signature": "[QK:RETIRED-SENT-SURFACE-ABSENT]",
|
|
29
|
+
"signatureSource": "scripts/check-entwurf-v2-surface.ts"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"claim": "V2PROD-INBAND-ERROR-WIRED",
|
|
32
|
-
"title": "the production sendOverSocket closure stops copying response.error onto the RpcSendResult, so a receiver's named in-band refusal is stranded one hop before driveSend and the caller sees a reasonless rejected",
|
|
32
|
+
"title": "the production sendOverSocket closure stops copying response.error onto the RpcSendResult, so a receiver's named in-band refusal is stranded one hop before driveSend and the caller sees a reasonless rejected (#120 P2: the wire seam now also classifies the acceptance boundary, so the same line carries one more field — the mutation still drops the receiver's own error)",
|
|
33
33
|
"subject": "pi-extensions/lib/entwurf-v2-production.ts",
|
|
34
|
-
"find": ["\t\t\treturn { success: response.success, error: response.error };"],
|
|
35
|
-
"replace": ["\t\t\treturn { success: response.success };"],
|
|
34
|
+
"find": ["\t\t\treturn { success: response.success, error: response.error, boundary };"],
|
|
35
|
+
"replace": ["\t\t\treturn { success: response.success, boundary };"],
|
|
36
36
|
"gate": ["bash", "run.sh", "check-entwurf-v2-production"],
|
|
37
37
|
"timeoutSeconds": 60,
|
|
38
38
|
"signature": "[QK:V2PROD-INBAND-ERROR-WIRED]",
|
|
@@ -40,15 +40,15 @@
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"claim": "V2SURF-PI-MODE-SCOPE",
|
|
43
|
-
"title": "pi-native mode param reverts to \"Delivery mode for a live send\" (false for native-push)",
|
|
43
|
+
"title": "pi-native mode param reverts to \"Delivery mode for a live send\" (false for native-push) (#120 P2: the description was rewritten to stop claiming an interrupt, so the mutation now strips the rail scope from the corrected sentence)",
|
|
44
44
|
"subject": "pi-extensions/entwurf-control.ts",
|
|
45
45
|
"find": [
|
|
46
|
-
"\t\t\t\t\t\"Injection style for a CONTROL-SOCKET send only: steer (
|
|
46
|
+
"\t\t\t\t\t\"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. \" +",
|
|
47
47
|
"\t\t\t\t\t\"The mailbox and native-push plans carry no mode, so it has no effect on those rails.\","
|
|
48
48
|
],
|
|
49
49
|
"replace": [
|
|
50
|
-
"\t\t\t\t\t\"Delivery mode for a live send: steer (
|
|
51
|
-
"\t\t\t\t\t\"
|
|
50
|
+
"\t\t\t\t\t\"Delivery mode for a live send: steer (ask the steering queue) or follow_up (ask the follow-up queue). \" +",
|
|
51
|
+
"\t\t\t\t\t\"Both queues are volatile process memory.\","
|
|
52
52
|
],
|
|
53
53
|
"gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
|
|
54
54
|
"timeoutSeconds": 60,
|
|
@@ -669,12 +669,75 @@ async function run(): Promise<void> {
|
|
|
669
669
|
"set LIVE=1 plus ENTWURF_CODEX_APP_SERVER_PID, ENTWURF_CODEX_FRESH_MODEL, and ENTWURF_CODEX_FRESH_PI_MODEL to run; this opens real Codex and Pi windows and spends model turns.",
|
|
670
670
|
);
|
|
671
671
|
}
|
|
672
|
+
|
|
673
|
+
// ── THE OPERATOR-SUPPLIED PREREQUISITE TRIPLE, AND WHY ITS ABSENCE IS A SKIP ──
|
|
674
|
+
//
|
|
675
|
+
// Entwurf does not own the Codex app-server (VERIFY: "The A cell takes no inferred
|
|
676
|
+
// server or models"), so these three values can only come from the operator. That
|
|
677
|
+
// stays true. What was wrong is the CLASSIFICATION of their absence.
|
|
678
|
+
//
|
|
679
|
+
// `[측정 2026-09-23]` the skip line directly above promised four prerequisites in one
|
|
680
|
+
// breath — LIVE=1 *plus* the three env names — but only the first could ever produce a
|
|
681
|
+
// SKIP. The other three were `ok()` assertions, and `ok()` throws, so a missing
|
|
682
|
+
// operator input exited as FAIL. `release_gate()` supplies none of them, so this MUST
|
|
683
|
+
// step declined as a FAILURE inside every aggregate on every host: 0.25.0's own
|
|
684
|
+
// candidate receipt reads `MUST PASS=23 FAIL=1`, and the release record had to carry a
|
|
685
|
+
// prose classification explaining that the one red was "the shape of that step", and the
|
|
686
|
+
// cut stayed BLOCKED. A contract that manufactures a red every cut and then needs a human
|
|
687
|
+
// to excuse it is not a floor.
|
|
688
|
+
//
|
|
689
|
+
// `scripts/lib/step-outcome.sh` already wrote the rule this violated: "a step that RAN
|
|
690
|
+
// AND BROKE and a step that NEVER RAN are different facts, and a release record that
|
|
691
|
+
// blurs them is worthless: the first is a defect to fix, the second is a prerequisite to
|
|
692
|
+
// supply." An absent operator input is the second.
|
|
693
|
+
//
|
|
694
|
+
// So the axis is split, and the split is the whole repair:
|
|
695
|
+
// ABSENT (unset/empty) -> skipLive: nothing ran, the missing names are printed, and
|
|
696
|
+
// `--cut` still refuses it as BLOCKED (MUST SKIP). Nothing is
|
|
697
|
+
// excused — a cut without this receipt remains impossible.
|
|
698
|
+
// PRESENT BUT WRONG -> FAIL, unchanged: a malformed or dead PID, a process whose
|
|
699
|
+
// argv is not a Codex app-server, a foreign uid, A == S. The
|
|
700
|
+
// operator answered, and the answer is a defect.
|
|
701
|
+
// Nothing is inferred either way, and no default is invented for a value the operator
|
|
702
|
+
// did not give. The reproducible green is the invocation VERIFY and the release skill's
|
|
703
|
+
// P5 both now spell out, and `release_gate` needs no plumbing for it: measured on this
|
|
704
|
+
// source, that function exports no scrub and unsets nothing, so an exported triple
|
|
705
|
+
// already reaches this step.
|
|
706
|
+
//
|
|
707
|
+
// DELIBERATELY NOT MOVED: the installed-config preflights below (`codexFreshPreflight`,
|
|
708
|
+
// `codexCallerFreshPreflight`, `codexLaunchCwdFreshPreflight`). Those are host state
|
|
709
|
+
// `setup` establishes and the Codex doctors cover, and the file header's contract
|
|
710
|
+
// already decided them — "missing system birth, user MCP/status/terminal-title config,
|
|
711
|
+
// app-server, models, runtime, bridge, or tmux seat is a FAIL". A one-time answered
|
|
712
|
+
// launch directory is host setup, not a per-invocation input, and reclassifying it would
|
|
713
|
+
// be a second contract change riding this one.
|
|
714
|
+
const OPERATOR_INPUTS = [
|
|
715
|
+
["ENTWURF_CODEX_APP_SERVER_PID", "the pid of the app-server YOU started (never inferred)"],
|
|
716
|
+
["ENTWURF_CODEX_FRESH_MODEL", "the Codex-side model this cell may spend a turn on"],
|
|
717
|
+
["ENTWURF_CODEX_FRESH_PI_MODEL", "the pi-side model of the callback sibling"],
|
|
718
|
+
] as const;
|
|
719
|
+
const absentInputs = OPERATOR_INPUTS.filter(([name]) => (process.env[name]?.trim() ?? "") === "");
|
|
720
|
+
if (absentInputs.length > 0) {
|
|
721
|
+
skipLive(
|
|
722
|
+
LABEL,
|
|
723
|
+
`this cell takes no inferred server or models, and ${absentInputs.length} operator-supplied ` +
|
|
724
|
+
`prerequisite(s) are absent: ${absentInputs.map(([name, why]) => `${name} (${why})`).join("; ")}. ` +
|
|
725
|
+
"Supply them on the invocation — LIVE=1 ENTWURF_CODEX_APP_SERVER_PID=<pid> " +
|
|
726
|
+
"ENTWURF_CODEX_FRESH_MODEL=<codex-model> ENTWURF_CODEX_FRESH_PI_MODEL=<pi-model> — either for the " +
|
|
727
|
+
"standalone run or for `./run.sh release-gate <scratch> --cut`, which inherits them. " +
|
|
728
|
+
"A cut is still blocked without this receipt: --cut reads a MUST SKIP as red.",
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
672
732
|
ok("the measured/certified host axis is Linux", process.platform === "linux", `host=${process.platform}`);
|
|
673
733
|
|
|
734
|
+
// Present, so a malformed value is the operator's answer being wrong — a FAIL, not a
|
|
735
|
+
// declined prerequisite. The absence arm above already left.
|
|
674
736
|
const pidText = process.env.ENTWURF_CODEX_APP_SERVER_PID?.trim() ?? "";
|
|
675
737
|
ok(
|
|
676
738
|
"ENTWURF_CODEX_APP_SERVER_PID explicitly names the existing operator-owned app-server",
|
|
677
739
|
/^[1-9]\d*$/.test(pidText),
|
|
740
|
+
`ENTWURF_CODEX_APP_SERVER_PID=${JSON.stringify(pidText)} is not a positive decimal pid`,
|
|
678
741
|
);
|
|
679
742
|
const appServerPid = Number(pidText);
|
|
680
743
|
const procDir = `/proc/${appServerPid}`;
|
|
@@ -749,10 +812,13 @@ async function run(): Promise<void> {
|
|
|
749
812
|
`fixture-session=${fixtureCoordinate.sessionId}\nfixture-name=${fixtureSessionName}\n` +
|
|
750
813
|
`fixture-window=${fixtureCoordinate.windowId}`;
|
|
751
814
|
|
|
815
|
+
// Both are non-empty: the prerequisite arm at the top of `run` left otherwise. Asserting
|
|
816
|
+
// `.length > 0` again here would be a tautology dressed as a check, so the pair is
|
|
817
|
+
// RECORDED instead — the tier a live receipt is read on is a fact the evidence needs, and
|
|
818
|
+
// 0.25.0 already had to explain in prose that its pi leg ran on terra rather than luna.
|
|
752
819
|
const codexModel = process.env.ENTWURF_CODEX_FRESH_MODEL?.trim() ?? "";
|
|
753
820
|
const piModel = process.env.ENTWURF_CODEX_FRESH_PI_MODEL?.trim() ?? "";
|
|
754
|
-
|
|
755
|
-
ok("ENTWURF_CODEX_FRESH_PI_MODEL explicitly names the Pi callback sibling model", piModel.length > 0);
|
|
821
|
+
receipts["1b-operator-supplied-models"] = `codex=${codexModel}\npi=${piModel}`;
|
|
756
822
|
const codexRuntime = resolveExecutable("codex", appServerEnv);
|
|
757
823
|
ok("the Codex runtime resolves on the app-server PATH", codexRuntime !== null);
|
|
758
824
|
const bridgeRuntime = resolveExecutable("entwurf-bridge", appServerEnv);
|
|
@@ -1016,11 +1082,19 @@ async function run(): Promise<void> {
|
|
|
1016
1082
|
message: initialPiInstruction,
|
|
1017
1083
|
});
|
|
1018
1084
|
receipts["5-fixture-to-initial-pi-v2"] = instructInitialPi.text;
|
|
1085
|
+
// #120 P2: the control-socket rail answers with the ACCEPTANCE BOUNDARY the receiver observed,
|
|
1086
|
+
// and every member of that closed set is a success. The initial Pi is mid-turn while the fixture
|
|
1087
|
+
// addresses it, so `queued-follow-up` is an ORDINARY answer here — measured 2026-09-23, this
|
|
1088
|
+
// exact step. The old `sent|delivered` pin passed only when the caller happened to be idle, and
|
|
1089
|
+
// before P2 it passed on the busy path too because the rail said `sent` either way; `delivered`
|
|
1090
|
+
// was never a member of the set at all. The sibling LIVE smokes took this widening in 6d90d73
|
|
1091
|
+
// (`smoke-herdr-fresh-call-live.ts`, `smoke-mux-lifecycle-live.ts`); this step was missed.
|
|
1019
1092
|
ok(
|
|
1020
1093
|
"the fixture addressed the real initial Pi caller through an actual control-socket entwurf_v2 receipt",
|
|
1021
1094
|
!instructInitialPi.isError &&
|
|
1022
|
-
/control-socket
|
|
1023
|
-
|
|
1095
|
+
/entwurf_v2 control-socket → (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/.test(
|
|
1096
|
+
instructInitialPi.text,
|
|
1097
|
+
),
|
|
1024
1098
|
instructInitialPi.text,
|
|
1025
1099
|
);
|
|
1026
1100
|
|
|
@@ -1325,15 +1399,22 @@ async function run(): Promise<void> {
|
|
|
1325
1399
|
assertExactSourceToolCalls(
|
|
1326
1400
|
piSourceToolReceipts(readInitialPiEntries()),
|
|
1327
1401
|
[
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1402
|
+
// 0.24.0 moved a pi sibling's first action to the ZERO-ARGUMENT eighth verb, and this
|
|
1403
|
+
// smoke's own launch receipt says so in the same run: "its first action is the
|
|
1404
|
+
// zero-argument callback verb (codex is the one arm that still sends the nonce as
|
|
1405
|
+
// arguments)". The Codex audit below therefore keeps its `entwurf_v2` shape, and only
|
|
1406
|
+
// this one moves. `{}` is matched by deep-strict equality, so it pins the verb's actual
|
|
1407
|
+
// contract — a target or message typed here would be a second address axis and the verb
|
|
1408
|
+
// refuses it by name.
|
|
1409
|
+
//
|
|
1410
|
+
// Nothing is weakened by dropping the nonce from THIS expectation: the correlation is
|
|
1411
|
+
// proven earlier and from the delivered body, which is where a nonce can actually be
|
|
1412
|
+
// corrupted — `the initial Pi callback carries the exact launch nonce` (:970-973) reads
|
|
1413
|
+
// the fixture's own mailbox, requires exactly one exact match and refuses duplicates,
|
|
1414
|
+
// and the two cells beside it bind that callback's sender envelope to a real pi V3
|
|
1415
|
+
// citizen. This audit's job is the different one its name says: exactly these calls,
|
|
1416
|
+
// once each, and nothing else.
|
|
1417
|
+
{ toolName: "entwurf_callback", arguments: {} },
|
|
1337
1418
|
{
|
|
1338
1419
|
toolName: "entwurf_fresh_call",
|
|
1339
1420
|
arguments: {
|
|
@@ -83,6 +83,16 @@ import { joinKeyOf, parseHerdrPaneList } from "../pi-extensions/lib/herdr-placem
|
|
|
83
83
|
import { assessLauncherCleanup, snapshotClaudeLauncher, verifyClaudeLauncher } from "./lib/claude-launcher-fence.ts";
|
|
84
84
|
import { skipLive } from "./lib/live-skip.ts";
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* #120 P2: the control-socket rail answers with the ACCEPTANCE BOUNDARY the receiver observed, and
|
|
88
|
+
* every member of that set is a success. The caller here is mid-turn while its child calls back,
|
|
89
|
+
* so `queued-steer` is the ORDINARY answer — the old literal `→ sent` pin would have gone red on
|
|
90
|
+
* the common case and passed only when the caller happened to be idle. What must still be absent
|
|
91
|
+
* is a REFUSAL, which the negative beside it now names directly (`rejected` / `failed`) instead of
|
|
92
|
+
* as "anything that is not the word sent".
|
|
93
|
+
*/
|
|
94
|
+
const CONTROL_ACCEPTED = /entwurf_v2 control-socket → (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/;
|
|
95
|
+
|
|
86
96
|
const LABEL = "smoke-herdr-fresh-call-live";
|
|
87
97
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
88
98
|
const REAL_HOME = process.env.HOME ?? os.homedir();
|
|
@@ -849,10 +859,7 @@ async function main(): Promise<void> {
|
|
|
849
859
|
callId === null
|
|
850
860
|
? -1
|
|
851
861
|
: childRecords.findIndex(
|
|
852
|
-
(record, i) =>
|
|
853
|
-
i > callAt &&
|
|
854
|
-
record.includes("entwurf_v2 control-socket → sent") &&
|
|
855
|
-
toolResultIdOf(record) === callId,
|
|
862
|
+
(record, i) => i > callAt && CONTROL_ACCEPTED.test(record) && toolResultIdOf(record) === callId,
|
|
856
863
|
);
|
|
857
864
|
// The LAST mention of the task token, so the birth prompt — which carries it, first —
|
|
858
865
|
// cannot be what satisfies "the work came after".
|
|
@@ -864,14 +871,14 @@ async function main(): Promise<void> {
|
|
|
864
871
|
}
|
|
865
872
|
}
|
|
866
873
|
ok(
|
|
867
|
-
`${cell.label}: the child decoded the one-line birth argv and ran the task only AFTER its callback — the
|
|
874
|
+
`${cell.label}: the child decoded the one-line birth argv and ran the task only AFTER its callback — the ACCEPTED outcome belongs, by toolCallId, to the very call that carried the delivered nonce, and the task token appears in a record after it`,
|
|
868
875
|
nonce.length > 0 && callAt >= 0 && sentAt > callAt && workedAt > sentAt,
|
|
869
876
|
);
|
|
870
877
|
ok(
|
|
871
|
-
`${cell.label}: the child's OWN tool result says the callback was
|
|
872
|
-
|
|
878
|
+
`${cell.label}: the child's OWN tool result says the callback was ACCEPTED on the rail its caller answers on — one of \`sent|queued-steer|queued-follow-up|accepted-unknown-boundary\` — not a timeout, not a reject, not a dirty lock`,
|
|
879
|
+
CONTROL_ACCEPTED.test(childText) &&
|
|
873
880
|
!/entwurf_v2 [a-z-]+ (?:execution failed:|DELIVERED \()/.test(childText) &&
|
|
874
|
-
!/entwurf_v2 control-socket → (
|
|
881
|
+
!/entwurf_v2 control-socket → (?:rejected|failed)/.test(childText),
|
|
875
882
|
);
|
|
876
883
|
} else {
|
|
877
884
|
const childActivity = mcpActivityBySession(root).get(childNative) ?? [];
|
|
@@ -490,7 +490,7 @@ for (const name of ["entwurf", "entwurf-bridge", "entwurf-statusline"]) {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// The REAL subcommand, from the installed bin, under node_modules — which is the branch of
|
|
493
|
-
// start.sh that runs the prebuilt dist. Its own oracle is an EXACT set (run.sh
|
|
493
|
+
// start.sh that runs the prebuilt dist. Its own oracle is an EXACT set (`EXPECT_TOOLS` in run.sh check-bridge), so a
|
|
494
494
|
// zero exit here is the artifact listing all seven garden verbs and no eighth.
|
|
495
495
|
const run = spawnSync(path.join(binDir, "entwurf"), ["check-bridge"], { encoding: "utf8" });
|
|
496
496
|
const said = `${run.stdout ?? ""}${run.stderr ?? ""}`;
|
|
@@ -507,6 +507,7 @@ const verbs = (said.match(/entwurf_[a-z0-9_]+/g) ?? []).filter((v, i, a) => a.in
|
|
|
507
507
|
// one; the two disagreeing is the signal, and a verb genuinely added upstream reddens this
|
|
508
508
|
// smoke until somebody says so here too.
|
|
509
509
|
const EXPECTED_VERBS = [
|
|
510
|
+
"entwurf_callback",
|
|
510
511
|
"entwurf_fresh_call",
|
|
511
512
|
"entwurf_inbox_read",
|
|
512
513
|
"entwurf_peers",
|
|
@@ -519,7 +520,7 @@ console.log(` VERBS ${verbs.join(",") || "<none>"}`);
|
|
|
519
520
|
if (run.status === 0) ok(`entwurf check-bridge exit 0 from the installed bin (${verbs.length} verbs listed)`);
|
|
520
521
|
else no(`entwurf check-bridge exit ${run.status}: ${said.trim().slice(0, 400)}`);
|
|
521
522
|
JSON.stringify(verbs) === JSON.stringify(EXPECTED_VERBS)
|
|
522
|
-
? ok(`the receipt names EXACTLY the
|
|
523
|
+
? ok(`the receipt names EXACTLY the eight garden verbs (${EXPECTED_VERBS.length}), parsed from what the installed bin answered`)
|
|
523
524
|
: no(`verb set mismatch — want ${JSON.stringify(EXPECTED_VERBS)} got ${JSON.stringify(verbs)}`);
|
|
524
525
|
process.exit(bad);
|
|
525
526
|
' "$ACTIVE" "$REGISTRY" "$PLUGIN_ID" || fail=1
|
|
@@ -740,6 +741,62 @@ if printf '%s' "$fan_with_server" | grep -qx '1 citizen(s) not shown: nobody cou
|
|
|
740
741
|
else
|
|
741
742
|
bad "the fan drew no '1 citizen(s) not shown ... (unobserved)' accounting for the citizen born in [7]"
|
|
742
743
|
fi
|
|
744
|
+
|
|
745
|
+
# ── 8b. the SAME fan with the ENTWURF_BIN crutch REMOVED (#116 A-D3=C) ──────
|
|
746
|
+
# The two runs above hand the pane `ENTWURF_BIN`, which is exactly the operator crutch D3=C exists
|
|
747
|
+
# to delete: with it, this cell stays green whether or not the ledger fallback exists, so it can
|
|
748
|
+
# certify nothing about the first screen a real user sees. `plugins/herdr/README.md` says the plugin
|
|
749
|
+
# puts NOTHING on PATH, so pathless is the state a correct install actually leaves behind.
|
|
750
|
+
# The herdr server from [8] is still up — the fan's second read needs it, and [9] stops it below.
|
|
751
|
+
PATHLESS="$(printf '%s' "$PATH" | tr ':' '\n' | while IFS= read -r d; do [ -n "$d" ] && [ -x "$d/entwurf" ] || printf '%s\n' "$d"; done | paste -sd:)"
|
|
752
|
+
run_fan_pathless() {
|
|
753
|
+
env -u ENTWURF_BIN PATH="$PATHLESS" HERDR_BIN_PATH="$(command -v herdr)" node "$FAN" </dev/null 2>&1
|
|
754
|
+
}
|
|
755
|
+
if command -v entwurf >/dev/null 2>&1 && PATH="$PATHLESS" command -v entwurf >/dev/null 2>&1; then
|
|
756
|
+
bad "the pathless PATH still resolves an entwurf — this cell would prove nothing"
|
|
757
|
+
else
|
|
758
|
+
ok "the pathless PATH resolves no entwurf (the crutch is really gone, not merely unset)"
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
fan_pathless="$(run_fan_pathless)"; fan_pathless_rc=$?
|
|
762
|
+
echo "$fan_pathless" | sed 's/^/ /'
|
|
763
|
+
echo " → exit $fan_pathless_rc"
|
|
764
|
+
# The IDENTICAL literal the crutched run asserts at the cell above, so the two runs are comparable
|
|
765
|
+
# rather than two different claims.
|
|
766
|
+
if [ "$fan_pathless_rc" -eq 0 ] && printf '%s' "$fan_pathless" | grep -qx '1 citizen(s) not shown: nobody could observe placement for them (unobserved)\.'; then
|
|
767
|
+
ok "[QK:HRIL-FAN-LEDGER-FALLBACK] with NO ENTWURF_BIN and no entwurf on PATH, the certified activation ledger supplies the runtime and the citizen read completes — the same accounting sentence, from a binary nobody put on PATH"
|
|
768
|
+
else
|
|
769
|
+
bad "the ledger-fallback claim failed: pathless fan rc=$fan_pathless_rc did not reproduce the crutched run's citizen accounting"
|
|
770
|
+
fi
|
|
771
|
+
if printf '%s' "$fan_pathless" | grep -q 'via activation-ledger'; then
|
|
772
|
+
ok "the pathless run says WHICH source supplied the binary, so the fallback cannot be mistaken for a PATH hit"
|
|
773
|
+
else
|
|
774
|
+
bad "the pathless run did not name activation-ledger as the binary's source"
|
|
775
|
+
fi
|
|
776
|
+
|
|
777
|
+
# ── 8c. pathless AND no ledger: the CITIZEN axis alone skips, and it is not red ──
|
|
778
|
+
# The other half of the same contract. A host that never activated must not read as broken, and the
|
|
779
|
+
# three blocks that need no entwurf binary must still answer.
|
|
780
|
+
# XDG_STATE_HOME is redirected TOO, and that is the whole isolation: the ledger lives under XDG
|
|
781
|
+
# **state** ($LEDGER above) while the runtime lives under XDG **data**. Moving only the second would
|
|
782
|
+
# leave this cell reading the real host's ledger, and "no ledger here" would be unfalsifiable.
|
|
783
|
+
HOMELESS="$(mktemp -d)"
|
|
784
|
+
fan_no_ledger="$(env -u ENTWURF_BIN PATH="$PATHLESS" HOME="$HOMELESS" XDG_DATA_HOME="$HOMELESS/.local/share" \
|
|
785
|
+
XDG_STATE_HOME="$HOMELESS/.local/state" \
|
|
786
|
+
HERDR_BIN_PATH="$(command -v herdr)" node "$FAN" </dev/null 2>&1)"; fan_no_ledger_rc=$?
|
|
787
|
+
echo "$fan_no_ledger" | sed 's/^/ /'
|
|
788
|
+
echo " → exit $fan_no_ledger_rc"
|
|
789
|
+
if [ "$fan_no_ledger_rc" -eq 0 ] &&
|
|
790
|
+
printf '%s' "$fan_no_ledger" | grep -q 'entwurf-not-found' &&
|
|
791
|
+
printf '%s' "$fan_no_ledger" | grep -q '\[1\] HERDR INTEGRATION' &&
|
|
792
|
+
printf '%s' "$fan_no_ledger" | grep -q '\[2\] ENTWURF ACTIVATION' &&
|
|
793
|
+
printf '%s' "$fan_no_ledger" | grep -q '\[3\] RAILS'; then
|
|
794
|
+
ok "[QK:HRIL-FAN-NO-LEDGER-SKIPS-CITIZENS-ONLY] with no ledger and no runtime the CITIZEN block alone skips by name at exit 0, and blocks 1-3 still render — never activating is a state, not a defect, and this is what stops D3=C drifting into 'install it from the pane'"
|
|
795
|
+
else
|
|
796
|
+
bad "the no-ledger citizen-skip claim failed: fan rc=$fan_no_ledger_rc did not render blocks 1-3 with a citizen-only skip"
|
|
797
|
+
fi
|
|
798
|
+
rm -rf "$HOMELESS"
|
|
799
|
+
|
|
743
800
|
herdr server stop >/dev/null 2>&1 || true
|
|
744
801
|
|
|
745
802
|
# ── 8a. the SHIPPED LAUNCHER, from the bin the user actually received ───────
|
|
@@ -380,6 +380,74 @@ cell(
|
|
|
380
380
|
"#94 compaction keys relinquish across nested/string axes: restore, delete, operator-changed, fresh, type-strict",
|
|
381
381
|
compaction_cells,
|
|
382
382
|
)
|
|
383
|
+
|
|
384
|
+
TURN_DURATION = "showTurnDuration"
|
|
385
|
+
|
|
386
|
+
def turn_duration_cells():
|
|
387
|
+
# GLG's ruling: showTurnDuration reports how long a turn took. That is
|
|
388
|
+
# operator INFORMATION, not a single-driver policy surface, so entwurf must
|
|
389
|
+
# neither own it nor force it to false.
|
|
390
|
+
#
|
|
391
|
+
# The claim is measured against the two constants and the real leaf, never
|
|
392
|
+
# quoted from a comment: a drift that puts the key back under management has
|
|
393
|
+
# to redden HERE and not only at the drive level, because the drive level
|
|
394
|
+
# runs one install shape while the constant governs every host.
|
|
395
|
+
managed = m.managed_keys()["settings"]["scalar"]
|
|
396
|
+
assert TURN_DURATION not in managed, managed
|
|
397
|
+
assert TURN_DURATION not in m._managed_scalar_names
|
|
398
|
+
assert TURN_DURATION in m._retired_scalar_names
|
|
399
|
+
retired = {name: last for name, _path, last in m.RETIRED_SETTINGS_SCALARS}
|
|
400
|
+
# The last managed value is the comparison relinquishment turns on. A wrong
|
|
401
|
+
# one here would either skip a value we really wrote or overwrite one we
|
|
402
|
+
# never did, so it is pinned by type as well as equality.
|
|
403
|
+
assert retired[TURN_DURATION] is False, retired[TURN_DURATION]
|
|
404
|
+
|
|
405
|
+
path = [TURN_DURATION]
|
|
406
|
+
|
|
407
|
+
def call(current=MISSING, old=MISSING):
|
|
408
|
+
settings = {} if current is MISSING else {TURN_DURATION: current}
|
|
409
|
+
keys = {} if old is MISSING else {TURN_DURATION: copy.deepcopy(old)}
|
|
410
|
+
state = {"files": {"settings": {"keys": keys}}}
|
|
411
|
+
m.relinquish_retired_scalar(state, settings, TURN_DURATION, path, False)
|
|
412
|
+
return state, settings
|
|
413
|
+
|
|
414
|
+
def led(existed, value):
|
|
415
|
+
return {"kind": "scalar", "path": path, "original": {"existed": existed, "value": value}}
|
|
416
|
+
|
|
417
|
+
# THE MEASURED HOST SHAPE (2026-09-23): ledger says we wrote false over a
|
|
418
|
+
# pre-install false, and the operator has since set true. Their true must
|
|
419
|
+
# survive, and the entry must be consumed so uninstall cannot reach back.
|
|
420
|
+
state, settings = call(True, led(True, False))
|
|
421
|
+
assert settings[TURN_DURATION] is True, settings
|
|
422
|
+
assert TURN_DURATION not in state["files"]["settings"]["keys"]
|
|
423
|
+
# Consumption is the inverse: restore_entry is the only writer uninstall has,
|
|
424
|
+
# and with the row gone there is no entry left to hand it.
|
|
425
|
+
before = copy.deepcopy((state, settings))
|
|
426
|
+
m.relinquish_retired_scalar(state, settings, TURN_DURATION, path, False)
|
|
427
|
+
assert (state, settings) == before
|
|
428
|
+
|
|
429
|
+
# Still carrying OUR false, with provenance -> undo our own write honestly.
|
|
430
|
+
state, settings = call(False, led(True, False))
|
|
431
|
+
assert settings[TURN_DURATION] is False, settings
|
|
432
|
+
state, settings = call(False, led(False, None))
|
|
433
|
+
assert TURN_DURATION not in settings, settings
|
|
434
|
+
|
|
435
|
+
# No provenance at all -> the value is the operator's whatever it says.
|
|
436
|
+
for current in [True, False, MISSING]:
|
|
437
|
+
state, settings = call(current)
|
|
438
|
+
assert settings == ({} if current is MISSING else {TURN_DURATION: current})
|
|
439
|
+
|
|
440
|
+
# Type strictness on the bool axis: `0 == False` while `type(0) is not bool`,
|
|
441
|
+
# so an untyped comparison would rewrite a value that was never ours.
|
|
442
|
+
state, settings = call(0, led(True, False))
|
|
443
|
+
assert settings[TURN_DURATION] == 0 and type(settings[TURN_DURATION]) is int
|
|
444
|
+
|
|
445
|
+
cell(
|
|
446
|
+
"[QK:META-RETIRE-TURN-DURATION-OPERATOR-OWNED]",
|
|
447
|
+
"showTurnDuration is operator-owned: unmanaged in the SSOT, retired at false, "
|
|
448
|
+
"and relinquishment preserves the measured live true while consuming the ledger entry",
|
|
449
|
+
turn_duration_cells,
|
|
450
|
+
)
|
|
383
451
|
sys.exit(failed)
|
|
384
452
|
PY
|
|
385
453
|
then ok "#71 retirement leaf: provenance, bool strictness, malformed refusal, one-shot convergence"
|
|
@@ -409,6 +477,10 @@ assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['va
|
|
|
409
477
|
assert 'autoCompactEnabled' not in s['files']['settings']['keys']
|
|
410
478
|
assert 'env.DISABLE_AUTOCOMPACT' not in s['files']['settings']['keys']
|
|
411
479
|
assert 'skipDangerousModePermissionPrompt' not in s['files']['settings']['keys']
|
|
480
|
+
# Same for showTurnDuration: a fresh install takes no ownership evidence for it,
|
|
481
|
+
# so on a host entwurf meets for the first time there is nothing to relinquish
|
|
482
|
+
# and nothing uninstall could ever restore.
|
|
483
|
+
assert 'showTurnDuration' not in s['files']['settings']['keys']
|
|
412
484
|
assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['value']['command'] == 'old'
|
|
413
485
|
PY
|
|
414
486
|
then ok "state captures original scalar/map values, excludes the retired scalar, and is mode 0600"; else bad "state did not capture original values / retirement / mode 0600"; fi
|
|
@@ -440,6 +512,18 @@ state['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']={
|
|
|
440
512
|
'kind':'scalar', 'path':['env','DISABLE_AUTOCOMPACT'],
|
|
441
513
|
'original':{'existed':True, 'value':'0'}
|
|
442
514
|
}
|
|
515
|
+
# showTurnDuration: the upgrade shape MEASURED on the source host 2026-09-23,
|
|
516
|
+
# planted verbatim so apply() is judged against the state a real prior version
|
|
517
|
+
# left behind. The ledger proves entwurf wrote `false` over a pre-install
|
|
518
|
+
# `false`; the live value is the `true` the operator set afterwards. This is the
|
|
519
|
+
# only branch where relinquishment could plausibly damage an operator choice —
|
|
520
|
+
# restoring the snapshot here would hand `false` back to somebody who had since
|
|
521
|
+
# turned the display on — so it is the branch the drive level must carry.
|
|
522
|
+
settings['showTurnDuration']=True
|
|
523
|
+
state['files']['settings']['keys']['showTurnDuration']={
|
|
524
|
+
'kind':'scalar', 'path':['showTurnDuration'],
|
|
525
|
+
'original':{'existed':True, 'value':False}
|
|
526
|
+
}
|
|
443
527
|
json.dump(settings, open(sp,'w'), indent=2); open(sp,'a').write('\n')
|
|
444
528
|
json.dump(state, open(stp,'w'), indent=2); open(stp,'a').write('\n')
|
|
445
529
|
PY
|
|
@@ -459,9 +543,16 @@ assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
|
|
|
459
543
|
assert 'autoCompactEnabled' not in settings
|
|
460
544
|
assert settings['env']['KEEP_ME'] == 'yes'
|
|
461
545
|
assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
|
|
462
|
-
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','
|
|
546
|
+
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
|
|
463
547
|
assert settings[key] is False, key
|
|
464
548
|
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
549
|
+
# showTurnDuration retirement, drive level. Two facts that must not be read off
|
|
550
|
+
# one assertion: the operator's PRE-install `true` (fixture) is still `true`
|
|
551
|
+
# AFTER apply, and it is still `true` because apply never wrote the key rather
|
|
552
|
+
# than because it happened to write the same value. The upgrade shape planted
|
|
553
|
+
# above supplies the second half — a ledger entry proving entwurf wrote `false`
|
|
554
|
+
# here, over a live `true` the operator set afterwards.
|
|
555
|
+
assert settings['showTurnDuration'] is True
|
|
465
556
|
# #94 drive-level relinquishment, both branches:
|
|
466
557
|
# nested path -> the operator's pre-entwurf '0' is back, and the env map that
|
|
467
558
|
# carries it survives with the neighbour key untouched.
|
|
@@ -473,6 +564,9 @@ state=json.load(open(cfg + '/entwurf.install-state.json'))
|
|
|
473
564
|
assert 'skipDangerousModePermissionPrompt' not in state['files']['settings']['keys']
|
|
474
565
|
assert 'autoCompactEnabled' not in state['files']['settings']['keys']
|
|
475
566
|
assert 'env.DISABLE_AUTOCOMPACT' not in state['files']['settings']['keys']
|
|
567
|
+
# The consumed entry is the whole inverse: with no ledger row left, uninstall()
|
|
568
|
+
# has no authority over this key and can never write our `false` back.
|
|
569
|
+
assert 'showTurnDuration' not in state['files']['settings']['keys']
|
|
476
570
|
for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
|
|
477
571
|
assert item in settings['permissions']['allow'], item
|
|
478
572
|
assert settings['permissions']['allow'].count('Read') == 1
|
|
@@ -493,6 +587,13 @@ then ok "apply installs managed keyset without clobbering unrelated keys"; else
|
|
|
493
587
|
if py check >/dev/null 2>&1; then ok "survival check passes after relinquishing the retired scalar"; else bad "survival check failed right after retirement apply"; fi
|
|
494
588
|
MANAGED_JSON="$(py managed-keys)"
|
|
495
589
|
if printf '%s' "$MANAGED_JSON" | grep -q 'skipDangerousModePermissionPrompt'; then bad "managed-keys still claims the retired operator key"; else ok "managed-keys returns skipDangerousModePermissionPrompt to the operator"; fi
|
|
590
|
+
# managed-keys is the CROSS-REPO contract: check-keyset-overlap and the
|
|
591
|
+
# agent-config fragment read it to stay disjoint. Measured 2026-09-23 —
|
|
592
|
+
# `check-keyset-overlap agent-config/claude/settings.fragment.json` failed on
|
|
593
|
+
# exactly one key, showTurnDuration, because both sides claimed it. Dropping our
|
|
594
|
+
# claim is what resolves that collision, so the SSOT output is asserted here and
|
|
595
|
+
# not only in the Python constants.
|
|
596
|
+
if printf '%s' "$MANAGED_JSON" | grep -q 'showTurnDuration'; then bad "managed-keys still claims showTurnDuration — a consumer fragment setting the operator's own value would keep colliding"; else ok "managed-keys returns showTurnDuration to the operator (cross-repo collision resolved at the SSOT)"; fi
|
|
496
597
|
# A provenance-less true cannot be changed safely, but doctor/check must make it visible.
|
|
497
598
|
python3 - <<'PY'
|
|
498
599
|
import json, os
|
|
@@ -577,6 +678,11 @@ assert settings['env']['KEEP_ME'] == 'yes'
|
|
|
577
678
|
assert settings['statusLine']['command'] == '/old/user/statusline.sh'
|
|
578
679
|
assert settings['promptSuggestionEnabled'] is True
|
|
579
680
|
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
681
|
+
# showTurnDuration survives uninstall at the operator's value. The install above
|
|
682
|
+
# ran against a ledger row saying entwurf had written `false` here; relinquish
|
|
683
|
+
# consumed that row, so uninstall reaches this key with no authority at all and
|
|
684
|
+
# the `true` the operator set is still `true`. The key must also be ABSENT from
|
|
685
|
+
# the removal list below — a retired key is neither restored nor deleted.
|
|
580
686
|
assert settings['showTurnDuration'] is True
|
|
581
687
|
# One list, one meaning: these are the keys UNINSTALL removed. `autoCompactEnabled`
|
|
582
688
|
# is not among them post-#94 — that entwurf never introduced it is a different
|
|
@@ -786,9 +786,16 @@ async function main(): Promise<void> {
|
|
|
786
786
|
mode: "follow_up",
|
|
787
787
|
message: `Scene 2 (${liveNeedle}): you were told a color. Reply with that color only, then stop.`,
|
|
788
788
|
});
|
|
789
|
+
// #120 P2: the rail answers with the ACCEPTANCE BOUNDARY, and this send names `follow_up`
|
|
790
|
+
// against a citizen that may well be mid-turn — `queued-follow-up` is a correct, normal
|
|
791
|
+
// answer here. Pinning the word `sent` would have demanded an idle receiver the scene
|
|
792
|
+
// never promises. What is asserted is the rail plus an accepted boundary; a refusal
|
|
793
|
+
// still fails through `isError`.
|
|
789
794
|
ok(
|
|
790
|
-
`${cell}: tools/call entwurf_v2 fire-and-forget
|
|
791
|
-
!send.isError &&
|
|
795
|
+
`${cell}: tools/call entwurf_v2 fire-and-forget was ACCEPTED on the CONTROL-SOCKET rail by the fresh citizen`,
|
|
796
|
+
!send.isError &&
|
|
797
|
+
send.text.includes("control-socket") &&
|
|
798
|
+
/→ (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/.test(send.text),
|
|
792
799
|
`--- response ---\n${send.text}\n--- bridge stderr ---\n${bridge.stderrTail()}`,
|
|
793
800
|
);
|
|
794
801
|
receipts[`${cell}/3-live-send`] = send.text;
|