@junghanacs/entwurf 0.23.1 → 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.
Files changed (107) hide show
  1. package/BASELINE.md +1 -0
  2. package/CHANGELOG.md +628 -0
  3. package/DELIVERY.md +24 -1
  4. package/FAQ.md +47 -0
  5. package/README.md +5 -3
  6. package/VERIFY.md +20 -2
  7. package/demo/README.md +5 -2
  8. package/docs/acp-backend-rail.md +49 -2
  9. package/docs/setup-clean-host.md +8 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
  23. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
  24. package/mcp/entwurf-bridge/dist/protocol.js +0 -7
  25. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
  26. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
  27. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  28. package/mcp/entwurf-bridge/src/index.ts +46 -5
  29. package/mcp/entwurf-bridge/test.sh +2 -2
  30. package/package.json +11 -9
  31. package/pi-extensions/entwurf-control.ts +83 -154
  32. package/pi-extensions/lib/acp/backend.ts +16 -6
  33. package/pi-extensions/lib/acp/context.ts +30 -10
  34. package/pi-extensions/lib/acp/session-store.ts +2 -2
  35. package/pi-extensions/lib/callback-env.ts +78 -0
  36. package/pi-extensions/lib/compaction-send-guard.ts +6 -1
  37. package/pi-extensions/lib/control-send-receipt.ts +108 -0
  38. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  39. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
  40. package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
  42. package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
  43. package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
  44. package/pi-extensions/lib/fresh-call-composition.ts +56 -22
  45. package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
  46. package/pi-extensions/lib/herdr-placement.ts +24 -4
  47. package/pi-extensions/lib/mux-fresh-call.ts +23 -8
  48. package/pi-extensions/meta-bridge-omp.ts +35 -48
  49. package/protocol.js +0 -8
  50. package/run.sh +306 -53
  51. package/scripts/check-acp-backend-preflight.ts +117 -9
  52. package/scripts/check-acp-carrier-augment.ts +6 -4
  53. package/scripts/check-acp-event-mapper.ts +17 -6
  54. package/scripts/check-acp-prompt-builder.ts +17 -9
  55. package/scripts/check-acp-prompt-lifecycle.ts +23 -6
  56. package/scripts/check-acp-session-reuse.ts +64 -51
  57. package/scripts/check-acp-session-store.ts +31 -8
  58. package/scripts/check-acp-stop-reason.ts +20 -2
  59. package/scripts/check-acp-stream-hooks.ts +22 -5
  60. package/scripts/check-acp-usage-accounting.ts +33 -9
  61. package/scripts/check-entwurf-bridge-boot.ts +16 -1
  62. package/scripts/check-entwurf-v2-surface.ts +289 -10
  63. package/scripts/check-fresh-call-dispatch.ts +4 -5
  64. package/scripts/check-gate-qualification.ts +51 -9
  65. package/scripts/check-herdr-activation.ts +73 -0
  66. package/scripts/check-herdr-fresh-call.ts +30 -6
  67. package/scripts/check-herdr-placement.ts +51 -0
  68. package/scripts/check-herdr-plugin-build.ts +11 -4
  69. package/scripts/check-herdr-plugin.ts +745 -38
  70. package/scripts/check-herdr-sandbox.ts +14 -2
  71. package/scripts/check-install-container.sh +1 -1
  72. package/scripts/check-mux-launcher-fence.ts +37 -0
  73. package/scripts/check-pi-launch.ts +8 -2
  74. package/scripts/check-release-gate-outcomes.ts +165 -0
  75. package/scripts/doctor-pi-provider.ts +44 -4
  76. package/scripts/herdr-rails.mjs +75 -0
  77. package/scripts/lib/mutation-qualify.ts +1 -1
  78. package/scripts/meta-bridge-state.py +18 -1
  79. package/scripts/mutants/acp-backend-preflight.json +20 -0
  80. package/scripts/mutants/bridge-boot-resume.json +26 -0
  81. package/scripts/mutants/bridge-command-boot.json +38 -0
  82. package/scripts/mutants/control-send-receipt.json +235 -0
  83. package/scripts/mutants/herdr-activation.json +16 -0
  84. package/scripts/mutants/herdr-fresh-call.json +15 -4
  85. package/scripts/mutants/herdr-plugin-build.json +7 -11
  86. package/scripts/mutants/herdr-plugin.json +177 -9
  87. package/scripts/mutants/meta-retire.json +22 -0
  88. package/scripts/mutants/mux-fresh-call.json +21 -8
  89. package/scripts/mutants/omp-fresh.json +5 -9
  90. package/scripts/mutants/pack-install.json +4 -4
  91. package/scripts/mutants/pi-floor.json +17 -0
  92. package/scripts/mutants/release-gate.json +48 -0
  93. package/scripts/mutants/retired-sent-surface.json +32 -0
  94. package/scripts/mutants/v2-surface.json +7 -7
  95. package/scripts/probe-bridge-command.ts +1 -0
  96. package/scripts/smoke-acp-session-reuse-live.ts +23 -5
  97. package/scripts/smoke-agy-install-state.sh +2 -2
  98. package/scripts/smoke-codex-fresh-live.ts +94 -13
  99. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  100. package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
  101. package/scripts/smoke-herdr-raw-install-live.sh +90 -5
  102. package/scripts/smoke-meta-install-state.sh +107 -1
  103. package/scripts/smoke-mux-lifecycle-live.ts +9 -2
  104. package/scripts/smoke-omp-fresh-live.ts +4 -2
  105. package/scripts/smoke-omp-mcp-state.sh +1 -1
  106. package/scripts/smoke-pi-provider-state.sh +108 -2
  107. package/scripts/smoke-setup-verdict.sh +18 -2
@@ -170,14 +170,13 @@ async function main(): Promise<void> {
170
170
 
171
171
  // ── one nonce, into whichever rail ───────────────────────────────────────────────────
172
172
  const nonceSpawn = recordingSpawn();
173
- await dispatchFreshCall(REQUEST, HERDR_ENV, nonceSpawn.spawn, "fresh-call-dispatch-test-nonce");
174
- const startCall = nonceSpawn.calls.find((call) => call[1] === "agent" && call[2] === "start");
173
+ const dispatchNonce = "mux-fresh-call-deadbeefdeadbeefdeadbeef";
174
+ await dispatchFreshCall(REQUEST, HERDR_ENV, nonceSpawn.spawn, dispatchNonce);
175
+ const tabCall = nonceSpawn.calls.find((call) => call[1] === "tab" && call[2] === "create");
175
176
  ok(
176
177
  "[QK:FCD-ONE-NONCE] ONE nonce is minted per call and handed to the selected rail — two mints would mean the sibling calls back with a tag the caller never recorded",
177
178
  dispatchCode.split("mintNonce(").length - 1 === 1 &&
178
- (startCall === undefined ||
179
- startCall.some((token) => token.includes("fresh-call-dispatch-test-nonce")) ||
180
- nonceSpawn.calls.length > 0),
179
+ (tabCall === undefined || tabCall.some((token) => token.includes(dispatchNonce)) || nonceSpawn.calls.length > 0),
181
180
  );
182
181
 
183
182
  // ── the composition root holds no synchronous child authority ────────────────────────
@@ -49,6 +49,7 @@ import * as path from "node:path";
49
49
  import { fileURLToPath } from "node:url";
50
50
  import {
51
51
  classifyMutantRun,
52
+ countOccurrences,
52
53
  createRepoSnapshot,
53
54
  ManifestError,
54
55
  type MutantManifest,
@@ -820,6 +821,7 @@ let manifestCount: number;
820
821
  // manifest lost in a merge must name itself here — "0/0 killed" is not a pass.
821
822
  // Extend this inventory and the manifests TOGETHER, never silently.
822
823
  const EXPECTED_LANE_MUTANTS: Record<string, number> = {
824
+ "acp-backend-preflight": 1,
823
825
  "acp-augment": 10,
824
826
  "acp-cortex": 14,
825
827
  "acp-launch-namespace": 2,
@@ -829,37 +831,39 @@ let manifestCount: number;
829
831
  "acp-stream-hooks": 10,
830
832
  "acp-usage-accounting": 12,
831
833
  "agy-permission": 6,
832
- "bridge-boot-resume": 3,
833
- "bridge-command-boot": 9,
834
+ "bridge-boot-resume": 5,
835
+ "bridge-command-boot": 12,
834
836
  "capability-cache": 3,
835
837
  "codex-app-server-launch": 9,
836
838
  "codex-caller-seat": 28,
839
+ "control-send-receipt": 19,
837
840
  "codex-native": 82,
838
841
  "compaction-send-guard": 7,
839
842
  "control-socket-disconnect": 4,
840
843
  "copilot-birth": 19,
841
844
  "copilot-launch": 14,
842
845
  "pi-launch": 9,
846
+ "pi-floor": 1,
843
847
  "copilot-receive": 20,
844
848
  "entwurf-peers": 1,
845
849
  "fresh-call-dispatch": 12,
846
850
  "fresh-cut": 3,
847
851
  "gate-qualification": 2,
848
852
  "herdr-placement": 13,
849
- "herdr-plugin": 11,
853
+ "herdr-plugin": 25,
850
854
  "herdr-plugin-profile": 14,
851
- "herdr-activation": 24,
855
+ "herdr-activation": 25,
852
856
  "herdr-plugin-build": 13,
853
857
  "herdr-runtime-bootstrap": 31,
854
- "herdr-fresh-call": 37,
858
+ "herdr-fresh-call": 38,
855
859
  "herdr-supply": 11,
856
860
  "meta-facts": 4,
857
861
  "peer-facts": 8,
858
862
  "meta-hook-session-switch": 17,
859
863
  "meta-identity": 4,
860
- "meta-retire": 3,
864
+ "meta-retire": 4,
861
865
  "mux-boundary": 16,
862
- "mux-fresh-call": 58,
866
+ "mux-fresh-call": 59,
863
867
  "mux-launcher-fence": 8,
864
868
  "mux-parent-artifact": 3,
865
869
  "pack-install": 2,
@@ -869,7 +873,8 @@ let manifestCount: number;
869
873
  "omp-fresh": 24,
870
874
  "omp-receive": 11,
871
875
  "probe-ordering": 1,
872
- "release-gate": 19,
876
+ "release-gate": 22,
877
+ "retired-sent-surface": 2,
873
878
  "resume-args": 6,
874
879
  "resume-launch-identity": 6,
875
880
  "self-address": 8,
@@ -902,6 +907,42 @@ let manifestCount: number;
902
907
  (err instanceof Error ? err.message : String(err)),
903
908
  );
904
909
  }
910
+ // FIND MATCHES ITS SUBJECT, EXACTLY ONCE — the body's own precondition, hoisted into the head.
911
+ // `validateManifestSet` above proves the subject is TRACKED; it never opens it. So a change
912
+ // that edits a subject line a mutant quotes leaves the whole deterministic floor green and
913
+ // dies an hour later in CI or the release gate as MUTANT-STALE. That is exactly how the
914
+ // 0.23.2 pi bump landed: `pack_install_leaked_pi`'s pin regex moved 0.85.1 → 0.86.0 in
915
+ // run.sh, `scripts/mutants/pack-install.json` kept quoting the old line, and two claims came
916
+ // back NOT KILLED after ~50 minutes of mutant execution. The check costs one read per
917
+ // mutant and uses `countOccurrences` — the SAME function the body applies at
918
+ // mutation-qualify.ts:743 — so head and body cannot disagree about what "matches" means.
919
+ // This is a STALENESS check, not a kill-proof: it says the mutant still has a subject to
920
+ // corrupt, never that corrupting it is caught. That remains the body's verdict.
921
+ const stale: string[] = [];
922
+ for (const man of manifests) {
923
+ for (const m of man.mutants) {
924
+ const subjectAbs = path.join(REPO_DIR, m.subject);
925
+ let source: string;
926
+ try {
927
+ source = fs.readFileSync(subjectAbs, "utf8");
928
+ } catch (err) {
929
+ stale.push(`${man.lane}/${m.claim}: subject ${m.subject} unreadable (${(err as Error).message})`);
930
+ continue;
931
+ }
932
+ const n = countOccurrences(source, m.find.join("\n"));
933
+ if (n !== 1) stale.push(`${man.lane}/${m.claim}: find matched ${n}× in ${m.subject} (expected exactly 1)`);
934
+ }
935
+ }
936
+ assert.deepEqual(
937
+ stale,
938
+ [],
939
+ "[QK:MUTANT-FIND-MATCHES-SUBJECT] every mutant's `find` must match its subject exactly once. A find that " +
940
+ "matches 0× is a mutant whose production line moved out from under it — the body can write nothing, the " +
941
+ "verdict is MUTANT-STALE, and the claim is silently unproven; a find that matches 2+× cannot say WHICH " +
942
+ "occurrence it corrupts. Either way the manifest and the source have to move together. Found:\n" +
943
+ stale.join("\n"),
944
+ );
945
+
905
946
  manifestCount = manifests.length;
906
947
  console.log(`[${SURFACE}] ${selected.length} mutants across ${manifestCount} lanes (no tiers — full set every run)`);
907
948
  }
@@ -909,7 +950,8 @@ let manifestCount: number;
909
950
  if (MANIFESTS_ONLY) {
910
951
  console.log(
911
952
  `[check-gate-manifests] ok — runner self-test green, ${selected.length} committed mutants across ` +
912
- `${manifestCount} lanes validated against the origin index, and the lane inventory matches its declared ` +
953
+ `${manifestCount} lanes validated against the origin index, every one of their \`find\` strings still ` +
954
+ "matching its subject exactly once, and the lane inventory matching its declared " +
913
955
  "contract. ZERO mutants were executed and this repo was never snapshotted: the body " +
914
956
  "(check-gate-qualification) owns that, unchanged.",
915
957
  );
@@ -205,6 +205,21 @@ const {
205
205
  COMPONENT_STATES,
206
206
  } = mod;
207
207
 
208
+ /** The pi pin package.json declares — the SSOT `run.sh pi_supported_range` derives its range from. */
209
+ const PI_PIN = (
210
+ JSON.parse(fs.readFileSync(path.join(REPO, "package.json"), "utf8")) as {
211
+ devDependencies: Record<string, string>;
212
+ }
213
+ ).devDependencies["@earendil-works/pi-coding-agent"];
214
+
215
+ /** A `pi` stand-in that reports `version` and nothing else — enough for a `--version` floor probe. */
216
+ function fakePi(dir: string, name: string, version: string): string {
217
+ fs.mkdirSync(dir, { recursive: true });
218
+ const file = path.join(dir, name);
219
+ fs.writeFileSync(file, `#!/bin/sh\necho ${version}\n`, { mode: 0o755 });
220
+ return file;
221
+ }
222
+
208
223
  function world(tag: string): NodeJS.ProcessEnv {
209
224
  const home = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), `entwurf-hac-${tag}-`)));
210
225
  for (const d of [".config", ".state", ".data", ".cache", "pi-agent", ".claude"]) {
@@ -220,6 +235,13 @@ function world(tag: string): NodeJS.ProcessEnv {
220
235
  XDG_CACHE_HOME: path.join(home, ".cache"),
221
236
  PI_CODING_AGENT_DIR: path.join(home, "pi-agent"),
222
237
  CLAUDE_CONFIG_DIR: path.join(home, ".claude"),
238
+ // PI_BIN is a sandbox root like the four XDG vars above, not a convenience. Since #119
239
+ // `install-user-scope` probes `pi --version` against the same closed range setup enforces,
240
+ // so leaving this unset would let the OPERATOR'S installed pi decide whether these cells
241
+ // pass — the gate would go red on a developer machine sitting one minor behind the pin and
242
+ // green on another, for reasons none of these cells are about. It is pinned IN RANGE here;
243
+ // the one cell whose subject IS the floor overrides it per run.
244
+ PI_BIN: fakePi(path.join(home, "bin"), "pi", PI_PIN),
223
245
  };
224
246
  }
225
247
 
@@ -330,6 +352,57 @@ function ledgerWith(
330
352
  );
331
353
  }
332
354
 
355
+ // ── 1b. the plugin door enforces the SAME pi floor the setup door does ────────
356
+ // #119: `setup` refuses an out-of-range pi by name before it writes any Pi wiring, but
357
+ // `install-user-scope` — the door herdr plugin activation actually goes through
358
+ // (plugins/herdr/lib/build.mjs → scripts/herdr-plugin-activate.mjs → `run.sh
359
+ // install-user-scope --plugin-runtime …`) — had no version check at all. Two doors onto ONE
360
+ // registration telling different truths is the whole defect, so the oracle here is the
361
+ // out-of-range/in-range PAIR: the refusal alone would also be satisfied by a door that refuses
362
+ // everything. Absent pi is deliberately NOT asserted as a third state — this verb invents no
363
+ // presence verdict, setup owns that one.
364
+ {
365
+ const env = world("pi-floor");
366
+ const bin = path.join(env.HOME as string, "bin");
367
+ // The supported range is DERIVED from the package.json devDep pin (run.sh
368
+ // `pi_supported_range`), so the fixtures are derived from it too — a hardcoded "0.85.1" here
369
+ // would silently stop being out-of-range the day the pin moves past it.
370
+ const [maj, min] = PI_PIN.split(".").map(Number);
371
+ const belowFloor = `${maj}.${min - 1}.0`;
372
+ const settingsPath = path.join(env.PI_CODING_AGENT_DIR as string, "settings.json");
373
+ fs.writeFileSync(settingsPath, '{"theme":"dark"}\n');
374
+ const before = fs.readFileSync(settingsPath, "utf8");
375
+
376
+ const stale = sh({ ...env, PI_BIN: fakePi(bin, "stale-pi", belowFloor) }, [
377
+ "install-user-scope",
378
+ "--plugin-runtime",
379
+ runtimeRootOf(env),
380
+ ]);
381
+ const afterRefusal = fs.readFileSync(settingsPath, "utf8");
382
+ const current = sh({ ...env, PI_BIN: fakePi(bin, "current-pi", PI_PIN) }, [
383
+ "install-user-scope",
384
+ "--plugin-runtime",
385
+ runtimeRootOf(env),
386
+ ]);
387
+ const afterAccept = readJson(settingsPath) as { packages?: string[] };
388
+
389
+ ok(
390
+ "[QK:HAC-PI-FLOOR-BOTH-DOORS] the plugin activation door enforces the SAME closed pi range as the " +
391
+ "setup door: a below-floor pi is a named FAIL that writes zero settings bytes (not a SKIP, not a " +
392
+ "silent write), while an in-range pi still registers — one registration reached through two doors " +
393
+ "may not report two different verdicts, and the range both read is DERIVED from one package.json pin " +
394
+ `(pin=${PI_PIN} stale=${belowFloor}/exit=${stale.status} named=${(stale.stderr || "").includes("outside the supported range")} ` +
395
+ `untouched=${before === afterRefusal} current=exit=${current.status}/packages=${JSON.stringify(afterAccept.packages)})`,
396
+ stale.status !== 0 &&
397
+ (stale.stderr || "").includes("[install-user-scope] pi: FAIL") &&
398
+ (stale.stderr || "").includes("outside the supported range") &&
399
+ before === afterRefusal &&
400
+ current.status === 0 &&
401
+ Array.isArray(afterAccept.packages) &&
402
+ afterAccept.packages.length === 1,
403
+ );
404
+ }
405
+
333
406
  // ── 2. the plugin command is DERIVED from the runtime root, never supplied ─────
334
407
  {
335
408
  const env = world("derived");
@@ -270,7 +270,7 @@ const START_ERR = JSON.stringify({
270
270
  });
271
271
 
272
272
  const CALLER = "20260914T174741-2e9ba9";
273
- const NONCE = "herdr-fresh-call-0123456789abcdef01234567";
273
+ const NONCE = "mux-fresh-call-0123456789abcdef01234567";
274
274
  const HERDR_ENV = { HERDR_ENV: "1", HERDR_BIN_PATH: "/home/operator/.local/bin/herdr", HERDR_PANE_ID: "w7:p1" };
275
275
 
276
276
  type ScriptedReply =
@@ -344,9 +344,19 @@ async function main(): Promise<void> {
344
344
  const code = MODULE_SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
345
345
  const specifiers = [...MODULE_SRC.matchAll(/^import\s[^;]*?from\s+"([^"]+)";$/gm)].map((m) => m[1]);
346
346
  ok(
347
- "[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins and the neutral composition leaf — one mux or entwurf import here would re-couple the two rails and make either one undeletable",
347
+ "[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins, the neutral composition leaf, the callback-env pair, and the dependency-free rails DECLARATION leaf — one mux or entwurf import here would re-couple the two rails and make either one undeletable",
348
348
  specifiers.length > 0 &&
349
- specifiers.every((s) => s.startsWith("node:") || s === "./fresh-call-composition.ts") &&
349
+ specifiers.every(
350
+ (s) =>
351
+ s.startsWith("node:") ||
352
+ s === "./fresh-call-composition.ts" ||
353
+ s === "./callback-env.ts" ||
354
+ // #116 A7 — the ONE place the backend tuple and the per-backend model syntax
355
+ // example are written, shared with the status pane and the install narration.
356
+ // It imports nothing itself, so admitting it couples this rail to a constant
357
+ // rather than to another rail; the mux/entwurf ban below is untouched.
358
+ s === "../../scripts/herdr-rails.mjs",
359
+ ) &&
350
360
  !/from\s+"\.\/(mux-|entwurf-)/.test(code),
351
361
  );
352
362
  // tmux is NAMED in this module, and only in one place: the refusal of a tmux seat input.
@@ -453,15 +463,29 @@ async function main(): Promise<void> {
453
463
  // ── argv grammar ─────────────────────────────────────────────────────────────────────
454
464
  // Pure builders are checked before full launch paths so a framing mutant is attributed to
455
465
  // its own contract rather than making an earlier integration success cell fail first.
456
- const tabArgs = buildHerdrTabCreateArgs({ workspaceId: "w7", cwd: "/repo/dir" });
466
+ const tabCallback = {
467
+ target: "20260101T010101-aaaaaa",
468
+ nonce: "mux-fresh-call-deadbeefdeadbeefdeadbeef",
469
+ };
470
+ const tabArgs = buildHerdrTabCreateArgs({ workspaceId: "w7", cwd: "/repo/dir", callback: tabCallback });
471
+ // The SPECIFIC claim runs before the umbrella one. `ok` throws, so the first red assertion is
472
+ // the only one a mutant run ever prints: with the exact-argv cell first, the callback-pair
473
+ // mutant died on the whole-argv claim below and never reached its own — a WRONG-REASON that
474
+ // qualification is exactly there to catch. Order is the fix; both cells keep their subject,
475
+ // and the argv cell keeps its own mutant, which drops `--no-focus` and this cell cannot see.
476
+ ok(
477
+ "[QK:HFC-CALLBACK-ENV] the launcher-computed callback pair rides as two more --env assignments beside the scrub",
478
+ tabArgs.includes("ENTWURF_CALLBACK_TARGET=20260101T010101-aaaaaa") &&
479
+ tabArgs.includes("ENTWURF_CALLBACK_NONCE=mux-fresh-call-deadbeefdeadbeefdeadbeef"),
480
+ );
457
481
  ok(
458
482
  "[QK:HFC-TAB-ARGV] one placement policy — a NEW TAB in the caller's own workspace, named explicitly so herdr cannot default it to the focused one, focus left alone, cwd carried literally, and BOTH identity carriers scrubbed by explicit repeated --env",
459
483
  tabArgs.join(" ") ===
460
- "tab create --workspace w7 --no-focus --cwd /repo/dir --env PI_SESSION_ID= --env PI_AGENT_ID=",
484
+ "tab create --workspace w7 --no-focus --cwd /repo/dir --env PI_SESSION_ID= --env PI_AGENT_ID= --env ENTWURF_CALLBACK_TARGET=20260101T010101-aaaaaa --env ENTWURF_CALLBACK_NONCE=mux-fresh-call-deadbeefdeadbeefdeadbeef",
461
485
  );
462
486
  ok(
463
487
  "a cwd-free tab create omits --cwd entirely rather than sending an empty value, and the rail adds no layout axis of its own — no label, no ratio, no direction",
464
- !buildHerdrTabCreateArgs({ workspaceId: "w7" }).includes("--cwd") &&
488
+ !buildHerdrTabCreateArgs({ workspaceId: "w7", callback: tabCallback }).includes("--cwd") &&
465
489
  !tabArgs.includes("--label") &&
466
490
  !tabArgs.includes("--ratio") &&
467
491
  !tabArgs.includes("--direction"),
@@ -165,6 +165,57 @@ async function main(): Promise<void> {
165
165
  piNativeSessionIdFromPath("/s/2026-09-14T05-17-03-979Z_not-a-uuid.jsonl") === null &&
166
166
  piNativeSessionIdFromPath(`/s/${PI_NATIVE}.jsonl`) === null,
167
167
  );
168
+ {
169
+ // THE VENDOR HALF OF THAT CLAIM, FROM THE INSTALLED VENDOR — not a literal we typed.
170
+ // The key this parser recovers lives inside a filename pi builds, so the rule is only as
171
+ // true as pi's naming, and a source read cannot settle whether that naming is a contract
172
+ // or a convention. `[측정 2026-09-22, @earendil-works/pi-coding-agent 0.87.0]` the installed
173
+ // `SessionManager` writes a REAL session file here, in a temp dir, with zero model, zero
174
+ // network and zero child process: `create()` then one `appendMessage` flushes
175
+ // `<stamp>_<id>.jsonl` whose own `{"type":"session"}` header carries the same id.
176
+ //
177
+ // The 0.86.0 receipt this replaces needed a provider call and was therefore taken by hand
178
+ // once; it stays in the bump ledger as a 0.86.0 fact. This one re-takes itself on every
179
+ // run, so the vendor floor cannot drift under us silently again.
180
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hp-vendor-"));
181
+ try {
182
+ const { SessionManager } = await import("@earendil-works/pi-coding-agent");
183
+ const manager = await SessionManager.create(process.cwd(), dir);
184
+ await manager.appendMessage({
185
+ role: "assistant",
186
+ content: [{ type: "text", text: "placement vendor-floor probe" }],
187
+ api: "test",
188
+ provider: "test",
189
+ model: "test",
190
+ // Zero everything: this message exists to make the manager flush a file, and a
191
+ // fixture that invented usage numbers would be inventing vendor accounting too.
192
+ usage: {
193
+ input: 0,
194
+ output: 0,
195
+ cacheRead: 0,
196
+ cacheWrite: 0,
197
+ totalTokens: 0,
198
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
199
+ },
200
+ stopReason: "stop",
201
+ timestamp: Date.now(),
202
+ });
203
+ const written = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
204
+ const header = JSON.parse(readFileSync(path.join(dir, written[0]), "utf8").split("\n")[0]);
205
+ const recovered = piNativeSessionIdFromPath(path.join(dir, written[0]));
206
+ ok(
207
+ "the vendor floor is re-measured, not remembered: the installed pi writes a real session file whose " +
208
+ "basename this parser reads back to the same id its own header declares",
209
+ written.length === 1 &&
210
+ header.type === "session" &&
211
+ typeof header.id === "string" &&
212
+ recovered === header.id &&
213
+ recovered === manager.getSessionId(),
214
+ );
215
+ } finally {
216
+ fs.rmSync(dir, { recursive: true, force: true });
217
+ }
218
+ }
168
219
  ok(
169
220
  "[QK:HP-JOIN-NEVER-GUESSES] an unmeasured agent_session kind and an empty value both yield NO key — an " +
170
221
  "unknown herdr shape is declined, never coerced into one of the two rules we actually measured",
@@ -33,6 +33,7 @@ import fs from "node:fs";
33
33
  import os from "node:os";
34
34
  import path from "node:path";
35
35
  import { pathToFileURL } from "node:url";
36
+ import * as HERDR_RAILS from "./herdr-rails.mjs";
36
37
  import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
37
38
  import { treeDigest } from "./lib/tree-digest.ts";
38
39
 
@@ -769,17 +770,23 @@ function drive(
769
770
  // contains `claude-sonnet-5`, so a containment test cannot tell the two grammars apart —
770
771
  // which is the whole claim.
771
772
  const idIn = (lines: string[], backend: string): string | null => {
772
- const line = lines.find((l) => l.startsWith(`note: ${backend}: `) && l.includes("model id looks like"));
773
+ const line = lines.find((l) => l.startsWith(`note: ${backend}: `) && l.includes("model string looks like"));
773
774
  return line === undefined ? null : (/`([^`]+)`/.exec(line)?.[1] ?? null);
774
775
  };
775
776
  const piId = idIn(notesOf(full), "pi");
776
777
  const claudeId = idIn(notesOf(full), "claude-code");
777
778
  return (
778
- piId === "openai-codex/gpt-5.6-terra" &&
779
+ // Compared against the SHARED leaf, not a literal copied into this gate: the install
780
+ // narration and the status pane's RAILS block now read one declaration
781
+ // (`scripts/herdr-rails.mjs`, #116 A7), and a gate holding its own third copy would
782
+ // be the very drift that leaf exists to stop. The GRAMMAR assertions stay literal —
783
+ // provider-qualified vs bare vendor id is the fact, and it must not become whatever
784
+ // the leaf happens to say.
785
+ piId === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE.pi &&
779
786
  piId.includes("/") &&
780
- claudeId === "claude-sonnet-5" &&
787
+ claudeId === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE["claude-code"] &&
781
788
  !claudeId.includes("/") &&
782
- idIn(notesOf(piOnly), "pi") === "openai-codex/gpt-5.6-terra" &&
789
+ idIn(notesOf(piOnly), "pi") === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE.pi &&
783
790
  idIn(notesOf(piOnly), "claude-code") === null
784
791
  );
785
792
  })(),