@junghanacs/entwurf 0.25.1 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +190 -0
  4. package/DELIVERY.md +1 -1
  5. package/README.md +183 -674
  6. package/VERIFY.md +21 -17
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +115 -18
  9. package/docs/external-mcp-host.md +1 -1
  10. package/docs/setup-clean-host.md +346 -10
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +55 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
  22. package/mcp/entwurf-bridge/src/index.ts +30 -3
  23. package/package.json +13 -13
  24. package/pi-extensions/entwurf-control.ts +50 -637
  25. package/pi-extensions/lib/acp/backend-adapter.ts +81 -1
  26. package/pi-extensions/lib/acp/backend.ts +41 -5
  27. package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
  28. package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
  29. package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
  30. package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
  31. package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
  32. package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
  33. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
  34. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
  35. package/pi-extensions/lib/fresh-call-composition.ts +13 -6
  36. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  37. package/pi-extensions/lib/mux-fresh-call.ts +3 -3
  38. package/run.sh +177 -53
  39. package/scripts/check-entwurf-peers-surface.ts +8 -15
  40. package/scripts/check-entwurf-self-address.ts +7 -11
  41. package/scripts/check-entwurf-v2-surface.ts +20 -112
  42. package/scripts/check-fresh-call-dispatch.ts +12 -13
  43. package/scripts/check-gate-qualification.ts +250 -5
  44. package/scripts/check-herdr-plugin-build.ts +159 -57
  45. package/scripts/check-install-surface.ts +3 -2
  46. package/scripts/check-mux-resume-call.ts +24 -35
  47. package/scripts/check-pi-bridge-sender.ts +130 -0
  48. package/scripts/check-pi-mcp-bridge.ts +251 -0
  49. package/scripts/fixtures/herdr-supply.json +8 -8
  50. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  51. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  52. package/scripts/lib/mutation-qualify.ts +269 -44
  53. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  54. package/scripts/meta-bridge-doctor.sh +16 -5
  55. package/scripts/mutants/acp-foreground-only.json +17 -0
  56. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  57. package/scripts/mutants/acp-vendor-rejection.json +62 -0
  58. package/scripts/mutants/codex-native.json +11 -11
  59. package/scripts/mutants/control-send-receipt.json +6 -4
  60. package/scripts/mutants/copilot-birth.json +2 -1
  61. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  62. package/scripts/mutants/gate-qualification.json +59 -0
  63. package/scripts/mutants/herdr-plugin-build.json +11 -0
  64. package/scripts/mutants/herdr-supply.json +3 -3
  65. package/scripts/mutants/mux-fresh-call.json +43 -45
  66. package/scripts/mutants/mux-resume-call.json +11 -35
  67. package/scripts/mutants/pack-install.json +15 -4
  68. package/scripts/mutants/pi-bridge-sender.json +54 -0
  69. package/scripts/mutants/pi-floor.json +6 -2
  70. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  71. package/scripts/mutants/pi-mcp-register.json +51 -0
  72. package/scripts/mutants/v2-surface.json +6 -6
  73. package/scripts/resident-rpc-drive.ts +5 -1
  74. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  75. package/scripts/smoke-acp-cortex-live.ts +3 -1
  76. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  77. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  78. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  79. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  80. package/scripts/smoke-codex-fresh-live.ts +20 -1
  81. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  82. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  83. package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
  84. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  85. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  86. package/scripts/smoke-resident-garden-guard.sh +3 -1
@@ -53,6 +53,20 @@ const COMPILED_ENTRY = path.join("mcp", "entwurf-bridge", "dist", "mcp", "entwur
53
53
  const COMMIT_A = "a".repeat(40);
54
54
  const COMMIT_B = "b".repeat(40);
55
55
 
56
+ // Fixture inputs, not registry assertions. The npm coherence check reads this checkout's
57
+ // package.json, so its version is input; neither an unpublished version nor the committed
58
+ // carrier may choose which source these deterministic cells exercise. No network is used.
59
+ const FIXTURE_VERSION = (JSON.parse(fs.readFileSync(path.join(REPO, "package.json"), "utf8")) as { version: string })
60
+ .version;
61
+ const FIXTURE_NPM_LOCK = Object.freeze({
62
+ source: "npm",
63
+ name: PACKAGE,
64
+ version: FIXTURE_VERSION,
65
+ integrity: `sha512-${createHash("sha512").update("hpb-npm-fixture").digest("base64")}`,
66
+ });
67
+ const FIXTURE_OTHER_INTEGRITY = `sha512-${createHash("sha512").update("hpb-npm-other-fixture").digest("base64")}`;
68
+ const FIXTURE_CHECKOUT_LOCK = Object.freeze({ source: "herdr-checkout", repository: "junghan0611/entwurf" });
69
+
56
70
  interface SpawnResult {
57
71
  status: number | null;
58
72
  stdout: string;
@@ -175,9 +189,10 @@ function tree(root: string): string[] {
175
189
  */
176
190
  function fixtureAcquire(opts: { activate: "absent" | "incapable" | "recorder"; log?: string; fail?: boolean }) {
177
191
  return ({ identity, prefix }: AcquireArgs) => {
192
+ const version = identity.version ?? FIXTURE_VERSION;
178
193
  const root = path.join(prefix, "node_modules", PACKAGE);
179
194
  fs.mkdirSync(path.join(root, path.dirname(COMPILED_ENTRY)), { recursive: true });
180
- fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name: PACKAGE, version: "0.21.0" })}\n`);
195
+ fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name: PACKAGE, version })}\n`);
181
196
  fs.writeFileSync(path.join(root, COMPILED_ENTRY), "// compiled entry fixture\n");
182
197
  const bin = path.join(prefix, "node_modules", ".bin");
183
198
  fs.mkdirSync(bin, { recursive: true });
@@ -208,7 +223,7 @@ function fixtureAcquire(opts: { activate: "absent" | "incapable" | "recorder"; l
208
223
  .update(identity.commit ?? "x")
209
224
  .digest("hex")}`,
210
225
  packageName: PACKAGE,
211
- packageVersion: "0.21.0",
226
+ packageVersion: version,
212
227
  };
213
228
  };
214
229
  }
@@ -249,6 +264,9 @@ function drive(
249
264
  spawn,
250
265
  write: (t: string) => (out += t),
251
266
  progress,
267
+ // Existing gate-only seam. Production still reads the committed lock, and its
268
+ // real source/coherence is independently checked by check-herdr-runtime-bootstrap.
269
+ readRuntimeLock: () => FIXTURE_NPM_LOCK,
252
270
  ...deps,
253
271
  });
254
272
  return { code, refusal: null, out, progress: progressLines };
@@ -408,57 +426,121 @@ function drive(
408
426
 
409
427
  // ── 6. the two-stage journey, as the RUNNER composes it ────────────────────────
410
428
  {
411
- const env = world("two-stage");
429
+ // Declared axes: both closed sources × {initial A, widened A, changed B, repeated B}.
430
+ // npm identity is the unchanged lock, not a commit; checkout identity IS the commit.
431
+ const cells = [FIXTURE_NPM_LOCK, FIXTURE_CHECKOUT_LOCK].map((lock) => {
432
+ const env = world(`two-stage-${lock.source}`);
433
+ const log = path.join(env.HOME as string, "argv.log");
434
+ let acquisitions = 0;
435
+ const counted = (args: AcquireArgs) => {
436
+ acquisitions++;
437
+ return fixtureAcquire({ activate: "recorder", log })(args);
438
+ };
439
+ const stages = [COMMIT_A, COMMIT_A, COMMIT_B, COMMIT_B].map((commit, index) => {
440
+ const rows: Record<string, string> = { pi: "current (v8) (/home/u/.pi)" };
441
+ if (index > 0) rows.claude = "current (v5) (/home/u/.claude)";
442
+ const result = drive(env, listing(rows), {
443
+ acquire: counted,
444
+ readRuntimeLock: () => lock,
445
+ resolveCommit: () => commit,
446
+ });
447
+ return { code: result.code, acquisitions };
448
+ });
449
+ const calls = fs.readFileSync(log, "utf8").trim().split("\n");
450
+ const { artifactIdentity: identity } = JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as {
451
+ artifactIdentity: Record<string, string>;
452
+ };
453
+ const expectedCounts = lock.source === "npm" ? [1, 1, 1, 1] : [1, 1, 2, 2];
454
+ const exactIdentity =
455
+ lock.source === "npm"
456
+ ? identity.name === PACKAGE &&
457
+ identity.version === FIXTURE_VERSION &&
458
+ identity.expectedIntegrity === FIXTURE_NPM_LOCK.integrity
459
+ : identity.repository === FIXTURE_CHECKOUT_LOCK.repository &&
460
+ identity.commit === COMMIT_B &&
461
+ identity.packageName === PACKAGE &&
462
+ identity.packageVersion === FIXTURE_VERSION;
463
+ return {
464
+ source: lock.source,
465
+ stages,
466
+ calls,
467
+ identity,
468
+ passed:
469
+ stages.every((stage) => stage.code === 0) &&
470
+ JSON.stringify(stages.map((stage) => stage.acquisitions)) === JSON.stringify(expectedCounts) &&
471
+ JSON.stringify(calls) === JSON.stringify(["pi", "pi,claude-code", "pi,claude-code", "pi,claude-code"]) &&
472
+ identity.kind === lock.source &&
473
+ exactIdentity,
474
+ };
475
+ });
476
+ ok(
477
+ "[QK:HPB-SEQUENTIAL-TWO-STAGE] both closed sources hand down `{pi}` then the widened `{pi, claude-code}` " +
478
+ "on every reinstall, never OpenCode. An unchanged npm lock acquires once; checkout acquires again ONLY " +
479
+ "when A becomes B, and repeated B acquires nothing. Counts are exact at EACH step, not an aggregate lower " +
480
+ "bound. Installed identity must retain the input kind and its exact version/integrity or commit/completeness. " +
481
+ "The real writer bytes are HAC-TWO-STAGE-ADD-ONLY; this cell owns runner composition. " +
482
+ `Fixture identities do not claim registry acceptance (cells=${JSON.stringify(cells)})`,
483
+ cells.length === 2 && cells.every((cell) => cell.passed),
484
+ );
485
+ }
486
+
487
+ // ── 5b. the production default reader composes with the committed lock ─────────
488
+ {
489
+ // Independent input oracle: read the committed JSON directly, not the runtime reader
490
+ // whose invocation this cell certifies. All acquisition/activation bytes remain fixture.
491
+ const committed = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, "runtime-lock.json"), "utf8")) as
492
+ | { source: "npm"; name: string; version: string; integrity: string }
493
+ | { source: "herdr-checkout"; repository: string };
494
+ const env = world("default-lock-reader");
412
495
  const log = path.join(env.HOME as string, "argv.log");
413
496
  let acquisitions = 0;
414
- const counted = (args: AcquireArgs) => {
415
- acquisitions++;
416
- return fixtureAcquire({ activate: "recorder", log })(args);
417
- };
418
- const stage1 = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
419
- acquire: counted,
420
- resolveCommit: () => COMMIT_A,
421
- });
422
- const stage2 = drive(env, listing({ pi: "current (v8) (/home/u/.pi)", claude: "current (v5) (/home/u/.claude)" }), {
423
- acquire: counted,
497
+ const acquire = fixtureAcquire({ activate: "recorder", log });
498
+ const result = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
499
+ readRuntimeLock: undefined,
424
500
  resolveCommit: () => COMMIT_A,
501
+ acquire: (args: AcquireArgs) => {
502
+ acquisitions++;
503
+ return acquire(args);
504
+ },
425
505
  });
426
- const stage3 = drive(env, listing({ pi: "current (v8) (/home/u/.pi)", claude: "current (v5) (/home/u/.claude)" }), {
427
- acquire: counted,
428
- resolveCommit: () => COMMIT_B,
429
- });
430
- const calls = fs.readFileSync(log, "utf8").trim().split("\n");
431
- const journal = JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as {
432
- artifactIdentity: { commit: string };
433
- };
506
+ const journal = fs.existsSync(journalPathOf(env))
507
+ ? (JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as { artifactIdentity: Record<string, string> })
508
+ : null;
509
+ const identity = journal?.artifactIdentity;
510
+ const exactIdentity =
511
+ identity !== undefined &&
512
+ identity.kind === committed.source &&
513
+ (committed.source === "npm"
514
+ ? identity.name === committed.name &&
515
+ identity.version === committed.version &&
516
+ identity.expectedIntegrity === committed.integrity
517
+ : identity.repository === committed.repository &&
518
+ identity.commit === COMMIT_A &&
519
+ identity.packageName === PACKAGE &&
520
+ identity.packageVersion === FIXTURE_VERSION);
434
521
  ok(
435
- "[QK:HPB-SEQUENTIAL-TWO-STAGE] the sequence a real operator performs, as the runner composes it: `{pi}` first, " +
436
- "then `{pi, claude-code}` after they integrated Claude Code, then a NEW commit of the same package version. " +
437
- "The unchanged commit acquires nothing while still reconciling the widened set; the new commit reinstalls. " +
438
- "OpenCode is present in every listing and appears in NO activation argv — the bytes those activations write " +
439
- "are `check-herdr-activation`'s HAC-TWO-STAGE-ADD-ONLY, which drives the real Pi writers; what this cell owns " +
440
- `is what the runner hands down (codes=${stage1.code}/${stage2.code}/${stage3.code} acquisitions=${acquisitions} calls=${JSON.stringify(calls)} commit=${journal.artifactIdentity.commit.slice(0, 8)})`,
441
- stage1.code === 0 &&
442
- stage2.code === 0 &&
443
- stage3.code === 0 &&
444
- acquisitions === 2 &&
445
- JSON.stringify(calls) === JSON.stringify(["pi", "pi,claude-code", "pi,claude-code"]) &&
446
- !calls.some((c) => c.includes("opencode")) &&
447
- journal.artifactIdentity.commit === COMMIT_B,
522
+ "[QK:HPB-DEFAULT-LOCK-READ] without a reader override, runBuild reads this plugin's committed lock " +
523
+ "from PLUGIN_DIR, not the repository root or a synthetic fixture. The acquired identity retains the " +
524
+ "independently read source and exact artifact fields; installed activation still runs once for pi. " +
525
+ `This is composition proof, not registry acquisition (code=${result.code} refusal=${result.refusal} acquisitions=${acquisitions} identity=${JSON.stringify(identity)})`,
526
+ result.code === 0 &&
527
+ acquisitions === 1 &&
528
+ exactIdentity &&
529
+ fs.existsSync(log) &&
530
+ fs.readFileSync(log, "utf8") === "pi\n",
448
531
  );
449
532
  }
450
533
 
451
534
  // ── 6a. the authority check happens BEFORE any runtime work ───────────────────
452
535
  {
453
- const IDENTITY = (commit: string) => ({
454
- kind: "herdr-checkout",
455
- repository: "junghan0611/entwurf",
456
- commit,
457
- packageName: PACKAGE,
458
- packageVersion: "0.21.0",
459
- observedDigest: `sha256-${createHash("sha256").update(commit).digest("hex")}`,
536
+ const IDENTITY = (seed: string) => ({
537
+ kind: "npm",
538
+ name: PACKAGE,
539
+ version: FIXTURE_VERSION,
540
+ expectedIntegrity: FIXTURE_NPM_LOCK.integrity,
541
+ observedDigest: `sha256-${createHash("sha256").update(seed).digest("hex")}`,
460
542
  });
461
- /** A host that already has a runtime at commit A and a ledger that says so. */
543
+ /** A host with the explicit npm fixture installed and a ledger describing it. */
462
544
  const seeded = (tag: string, backends: string[]): NodeJS.ProcessEnv => {
463
545
  const env = world(tag);
464
546
  const log = path.join(env.HOME as string, "argv.log");
@@ -468,12 +550,17 @@ function drive(
468
550
  });
469
551
  const states: Record<string, string> = {};
470
552
  for (const b of backends) states[b] = "active";
553
+ const artifactIdentity = (
554
+ JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as {
555
+ artifactIdentity: Record<string, unknown>;
556
+ }
557
+ ).artifactIdentity;
471
558
  activation.writeLedger(
472
559
  activation.resolveActivationLayout(env),
473
560
  activation.ledgerBody({
474
561
  phase: "active",
475
562
  runtimeRoot: activeDirOf(env),
476
- artifactIdentity: IDENTITY(COMMIT_A),
563
+ artifactIdentity,
477
564
  roots: activation.resolveComponentRoots(env),
478
565
  states,
479
566
  }),
@@ -518,21 +605,22 @@ function drive(
518
605
  const emptiedDirSeen = treeDigest(oracleRoot) !== oracleBase;
519
606
  const oracleHonest = sameTree && sameLengthEditSeen && restored && modeSeen && emptiedDirSeen;
520
607
 
521
- // (a) a SOURCE switch: the committed lock would now name npm over a checkout-sourced activation.
608
+ // (a) a SOURCE switch: the committed lock would name checkout over the active npm runtime.
522
609
  // (a) is asked of the SHARED authority directly: the runner reads its lock from the committed
523
610
  // file, and this gate does not add a seam that would let any caller choose the source.
524
611
  const src = seeded("plan-source", ["pi"]);
525
612
  const srcBefore = snapshot(src);
526
613
  const srcRefusal = refusalOf(() =>
527
614
  runner.certifyActivationPlan(src, {
528
- lock: { source: "npm", name: PACKAGE, version: "0.21.0", integrity: "sha512-x" },
615
+ lock: { source: "herdr-checkout", repository: "junghan0611/entwurf" },
529
616
  checkoutRoot: fixtureCheckoutRoot(),
530
617
  requested: ["pi"],
618
+ resolveCommit: () => COMMIT_A,
531
619
  }),
532
620
  );
533
621
  const srcUnchanged = unchanged(src, srcBefore);
534
622
 
535
- // (b) a new commit whose request DROPS a backend the ledger holds.
623
+ // (b) a changed npm integrity whose request DROPS a backend the ledger holds.
536
624
  const drop = seeded("plan-drop", ["pi", "claude-code"]);
537
625
  const dropBefore = snapshot(drop);
538
626
  let dropAcquired = 0;
@@ -541,6 +629,7 @@ function drive(
541
629
  dropAcquired++;
542
630
  return fixtureAcquire({ activate: "recorder", log: path.join(drop.HOME as string, "argv.log") })(args);
543
631
  },
632
+ readRuntimeLock: () => ({ ...FIXTURE_NPM_LOCK, integrity: FIXTURE_OTHER_INTEGRITY }),
544
633
  resolveCommit: () => COMMIT_B,
545
634
  });
546
635
 
@@ -549,7 +638,17 @@ function drive(
549
638
  const tornLedger = activation.resolveActivationLayout(torn).ledgerPath;
550
639
  fs.writeFileSync(
551
640
  tornLedger,
552
- `${JSON.stringify({ ...(JSON.parse(fs.readFileSync(tornLedger, "utf8")) as Record<string, unknown>), artifactIdentity: IDENTITY(COMMIT_B) }, null, 2)}\n`,
641
+ `${JSON.stringify(
642
+ {
643
+ ...(JSON.parse(fs.readFileSync(tornLedger, "utf8")) as Record<string, unknown>),
644
+ artifactIdentity: {
645
+ ...IDENTITY(COMMIT_B),
646
+ expectedIntegrity: FIXTURE_OTHER_INTEGRITY,
647
+ },
648
+ },
649
+ null,
650
+ 2,
651
+ )}\n`,
553
652
  );
554
653
  const tornBefore = snapshot(torn);
555
654
  let tornAcquired = 0;
@@ -561,14 +660,13 @@ function drive(
561
660
  resolveCommit: () => COMMIT_A,
562
661
  });
563
662
 
564
- // (d) the LEGAL case still passes: same source, new commit, request covers the ledger. The
565
- // authority is asked on the PRE-install state, which is the only state it is ever asked about.
663
+ // (d) the LEGAL case still passes: same exact npm source and lock, request covers the ledger.
664
+ // The authority is asked on the PRE-install state, which is the only state it is ever asked about.
566
665
  const legal = seeded("plan-legal", ["pi"]);
567
666
  const legalPlan = runner.certifyActivationPlan(legal, {
568
- lock: { source: "herdr-checkout", repository: "junghan0611/entwurf" },
569
- checkoutRoot: fixtureCheckoutRoot(),
667
+ lock: FIXTURE_NPM_LOCK,
668
+ checkoutRoot: REPO,
570
669
  requested: ["pi"],
571
- resolveCommit: () => COMMIT_B,
572
670
  });
573
671
  const legalDrive = drive(legal, listing({ pi: "current (v8) (/home/u/.pi)" }), {
574
672
  acquire: fixtureAcquire({ activate: "recorder", log: path.join(legal.HOME as string, "argv.log") }),
@@ -577,7 +675,7 @@ function drive(
577
675
 
578
676
  ok(
579
677
  "[QK:HPB-PRE-BOOTSTRAP-AUTHORITY] the activation authority is asked BEFORE any runtime work, through the SAME " +
580
- "shipped functions the installed verb runs: a source switch, a new commit whose request drops an activated " +
678
+ "shipped functions the installed verb runs: a source switch, a request that drops an activated " +
581
679
  "backend, and a ledger that already disagrees with the runtime standing there are each refused with the " +
582
680
  "runtime tree, its journal, our cache, the ledger and the activation log ALL byte-identical and the " +
583
681
  "acquisition never called. The first cut bootstrapped first and let the installed verb refuse afterwards, " +
@@ -596,7 +694,7 @@ function drive(
596
694
  tornAcquired === 0 &&
597
695
  unchanged(torn, tornBefore) &&
598
696
  legalDrive.code === 0 &&
599
- legalPlan.disposition === "rebind",
697
+ legalPlan.disposition === "match",
600
698
  );
601
699
  }
602
700
 
@@ -607,15 +705,19 @@ function drive(
607
705
  const commits = [COMMIT_A, COMMIT_B, COMMIT_B];
608
706
  let call = 0;
609
707
  const moving = () => commits[Math.min(call++, commits.length - 1)] as string;
708
+ // Candidate source is a closed, retained branch. This test seam does not expose a caller input:
709
+ // production `runBuild` reads only the committed lock, while the gate drives the branch whose
710
+ // identity can move between D1 and D2.
610
711
  const drifted = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
611
712
  acquire: fixtureAcquire({ activate: "recorder", log }),
713
+ readRuntimeLock: () => FIXTURE_CHECKOUT_LOCK,
612
714
  resolveCommit: moving,
613
715
  });
614
716
  const journal = JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as { artifactIdentity: { commit: string } };
615
717
  const activated = fs.existsSync(log) ? fs.readFileSync(log, "utf8").trim() : "";
616
718
  ok(
617
719
  "[QK:HPB-BOOTSTRAP-IDENTITY-EXACT] the artifact that lands must be EXACTLY the one the authority check admitted: " +
618
- "when the source moves between the judgement and the install, the run refuses by name instead of handing the " +
720
+ "when the candidate source moves between the judgement and the install, the run refuses by name instead of handing the " +
619
721
  "installed activation verb a different artifact than the one this build was cleared to place — the verb would " +
620
722
  "then re-judge, and pass or fail on a question nobody asked. The refusal is honest about what did happen: the " +
621
723
  `runtime was replaced, and NO activation ran (refusal=${drifted.refusal} landed=${journal.artifactIdentity.commit.slice(0, 8)} admitted=${COMMIT_A.slice(0, 8)} activations=${JSON.stringify(activated)})`,
@@ -709,7 +811,7 @@ function drive(
709
811
  ok(
710
812
  "[QK:HPB-PROGRESS-NAMED-SEQUENCE] a build narrates the five steps IN ORDER and names the long one — herdr pipes " +
711
813
  "both of this process's streams into a buffer it DISCARDS on success (`src/cli/plugin.rs:1328-1373` @ c77af189), " +
712
- "so an operator who has just answered the install prompt sees nothing at all through a multi-minute `npm pack` " +
814
+ "so an operator who has just answered the install prompt sees nothing at all through a multi-minute registry fetch " +
713
815
  "and reads it as a hang. The sequence is reported, not logged: each step names the work about to start, the " +
714
816
  "acquisition step says out loud that silence is expected and which source it is reaching for, and a run with " +
715
817
  "nothing to activate takes exactly ONE step and then closes — it must not narrate work it never did. " +
@@ -721,7 +823,7 @@ function drive(
721
823
  full.progress[0].includes("integration status") &&
722
824
  full.progress[1].includes("pi, claude-code") &&
723
825
  longStep.includes("long step") &&
724
- longStep.includes(COMMIT_A.slice(0, 8)) &&
826
+ longStep.includes(`${PACKAGE}@${FIXTURE_VERSION}`) &&
725
827
  full.progress[3].includes("what landed") &&
726
828
  full.progress[4].includes("wiring pi, claude-code") &&
727
829
  full.progress[5].startsWith("done: ") &&
@@ -260,8 +260,9 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
260
260
  .replace(/'(?:[^'\\]|\\.)*'/g, "''");
261
261
  const RUNSH_DRIVE = /(?:\$\{?RUN\}?|run\.sh)\s+(install|setup|remove-user-scope|remove)\b/;
262
262
  // install/setup write the operator's REAL user-scope surfaces under "$HOME"
263
- // (user settings.json registration ignores PI_CODING_AGENT_DIR), so sandboxing
264
- // the agent dir is not isolation for them; only HOME is. (setup:links died with
263
+ // (settings.json registration follows PI_CODING_AGENT_DIR when set, but provider
264
+ // state and the rest stay HOME/XDG-rooted), so sandboxing the agent dir alone is not
265
+ // isolation for them; HOME is required. (setup:links died with
265
266
  // the target registry — #50 C3.)
266
267
  const HOME_ROOTED = new Set(["install", "setup"]);
267
268
 
@@ -212,6 +212,7 @@ function main(): void {
212
212
 
213
213
  const PI_SURFACE = read("pi-extensions/entwurf-control.ts");
214
214
  const MCP_SURFACE = read("mcp/entwurf-bridge/src/index.ts");
215
+ const PI_CODE = PI_SURFACE.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
215
216
  const V2_RESUME = read("pi-extensions/lib/entwurf-v2-visible-resume.ts");
216
217
  // IMPORTS, not mentions: the module header names the mux side deliberately (that is the
217
218
  // architecture being documented), so an assertion on prose would forbid the explanation
@@ -222,55 +223,43 @@ function main(): void {
222
223
  v2ImportSpecifiers.length > 0 && !v2ImportSpecifiers.some((s) => /mux-/.test(s)),
223
224
  );
224
225
  ok(
225
- "both surfaces are the composition root: each imports the mux launcher AND the v2 composition and joins them itself",
226
- /mux-resume-call/.test(PI_SURFACE) &&
227
- /entwurf-v2-visible-resume/.test(PI_SURFACE) &&
228
- /mux-resume-call/.test(MCP_SURFACE) &&
229
- /entwurf-v2-visible-resume/.test(MCP_SURFACE),
226
+ "the bridge is the composition root: it imports the mux launcher AND the v2 composition and joins them itself",
227
+ /mux-resume-call/.test(MCP_SURFACE) && /entwurf-v2-visible-resume/.test(MCP_SURFACE),
230
228
  );
231
-
232
- // ── the NATIVE PI registration, judged as a registration ─────────────────────
233
- // The MCP half is judged on the real runtime tools/list by check-entwurf-bridge-boot.
234
- // Native pi has no equivalent boot oracle here, so its registration is read from source —
235
- // but narrowly, on the block itself, because "the module is imported" would stay true with
236
- // the tool deleted.
237
- const PI_BLOCK = (PI_SURFACE.split('name: "entwurf_resume_call",')[1] ?? "").split("\n}")[0];
229
+ // #125: pi reaches entwurf_resume_call through Pi's built-in MCP — the bridge above IS its
230
+ // surface. The native pi registration, its target-only TypeBox schema and its description are
231
+ // retired with it (registration and target-only are held on the runtime tools/list by
232
+ // BRIDGEBOOT-RESUME-CALL-REGISTERED / -TARGET-ONLY, the description by DESC-CONTRACT-MCP below).
233
+ // What is left to say about the pi file is that it does not come back as a second root.
238
234
  ok(
239
- "[QK:MUXRESUME-PI-SURFACE-REGISTERED] the native pi surface REGISTERS entwurf_resume_call and calls that registration during setup — an imported-but-unregistered verb is invisible to the operator while every import assertion stays green",
240
- PI_SURFACE.includes('name: "entwurf_resume_call"') && /registerResumeCallTool\(pi\);/.test(PI_SURFACE),
235
+ "the pi extension no longer joins the resume halves itself — no second composition root beside the bridge",
236
+ !/mux-resume-call/.test(PI_CODE) && !/entwurf-v2-visible-resume/.test(PI_CODE),
241
237
  );
238
+ // The pass-through half the native claim used to carry, now on the one surface that has it:
239
+ // the handler hands the validated target to the v2 composition UNCHANGED. The schema proves the
240
+ // input is target-only; this proves the value that reaches the record lookup is that input.
241
+ const MCP_RESUME_BLOCK = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("\n);")[0];
242
242
  ok(
243
- "[QK:MUXRESUME-PI-SURFACE-TARGET-ONLY] the native schema is target-only and the handler passes params.target straight through — a model, task or prompt knob would be a second way to decide what a resumed citizen is, when the record already decided",
244
- /Type\.Object\(\{\s*target: Type\.String\(/.test(PI_BLOCK) &&
245
- /visibleResume\(params\.target,/.test(PI_BLOCK) &&
246
- !/model|task|prompt/.test(PI_BLOCK.split("parameters:")[1]?.split("async execute")[0] ?? ""),
243
+ "[QK:MUXRESUME-MCP-TARGET-PASSTHROUGH] the bridge handler passes its target straight to visibleResume — a rewritten, defaulted or substituted id would reopen a DIFFERENT citizen than the one the caller named",
244
+ /async \(\{ target \}\) =>/.test(MCP_RESUME_BLOCK) &&
245
+ /await visibleResume\(target, makeVisibleResumeDeps\(visibleResumeLaunch\)\)/.test(MCP_RESUME_BLOCK),
247
246
  );
248
247
 
249
- // ── the description contract, both surfaces ──────────────────────────────────
248
+ // ── the description contract ────────────────────────────────────────────────
250
249
  // Not prose parity — three claims an operator acts on, plus the host's own cap.
251
- const PI_DESC = (PI_BLOCK.split("description: `")[1] ?? "").split("`,")[0];
252
250
  const MCP_DESC = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("{")[0];
253
251
  const statesTheContract = (text: string): boolean =>
254
252
  /dormant/i.test(text) && /no turn/i.test(text) && /observation/i.test(text) && /launch/i.test(text);
255
253
 
256
- for (const [which, text] of [
257
- ["native pi", PI_DESC],
258
- ["MCP bridge", MCP_DESC],
259
- ] as const) {
260
- ok(
261
- `${which}: the resume description fits the host's 2048-char tool-description cap`,
262
- text.length > 0 && text.length <= 2048,
263
- );
264
- }
265
- // The two claim tokens are written OUT, not interpolated: qualification requires each
266
- // `[QK:…]` to appear literally exactly once in its gate source, so a token assembled at
267
- // runtime names a claim no manifest can bind to.
268
254
  ok(
269
- "[QK:MUXRESUME-DESC-CONTRACT-PI] native pi: the description states the three things a caller acts on — DORMANT targets only, no turn is run, and TWO receipts of which the observation is the one that says the citizen is back",
270
- statesTheContract(PI_DESC),
255
+ "MCP bridge: the resume description fits the host's 2048-char tool-description cap",
256
+ MCP_DESC.length > 0 && MCP_DESC.length <= 2048,
271
257
  );
258
+ // The claim token is written OUT, not interpolated: qualification requires each `[QK:…]` to
259
+ // appear literally exactly once in its gate source, so a token assembled at runtime names a
260
+ // claim no manifest can bind to.
272
261
  ok(
273
- "[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the same three things — DORMANT targets only, no turn is run, and TWO receipts whose observation is the fact that matters",
262
+ "[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the three things a caller acts on — DORMANT targets only, no turn is run, and TWO receipts whose observation is the fact that matters",
274
263
  statesTheContract(MCP_DESC),
275
264
  );
276
265
  } finally {
@@ -0,0 +1,130 @@
1
+ /**
2
+ * check-pi-bridge-sender — the pi-session carrier is backed by a pi RECORD before the bridge uses it (#125).
3
+ *
4
+ * Since #125 a pi session reaches every Entwurf verb through the compiled entwurf-bridge (Pi's built-in
5
+ * MCP spawns it with the born gardenId as an explicit `PI_SESSION_ID`), so the bridge's pi-session
6
+ * rail is the main road. `resolveAuthoritativeSender` now checks, after the claims reconcile and
7
+ * before returning the pi branch, that the carrier names an existing, readable pi record — through
8
+ * the same per-entry targeted reader the sender/self policy already uses.
9
+ *
10
+ * WHY A SCRIPTS GATE AND NOT A BESIDE TEST: the subject is the bridge PROCESS answering a real
11
+ * `tools/call` over stdio (the same shape as check-codex-bridge-identity beside it); a vitest lane
12
+ * would have to boot that subprocess anyway. The store is a throwaway `ENTWURF_META_SESSIONS_DIR`, the
13
+ * control-socket dir a throwaway HOME: nothing on the operator host is read or written.
14
+ *
15
+ * Cells, in order (node:assert stops at the first failure, so each mutant dies at its own claim):
16
+ * RECORD-BACKED a pi record with NO control socket: identity is kept, replyable:false — the
17
+ * socket is replyability, never admission.
18
+ * MISSING-REFUSED a well-formed carrier with no record refuses loud; no anonymous/other-rail answer.
19
+ * BACKEND-REFUSED a carrier naming a record of ANOTHER backend refuses by name.
20
+ * UNREADABLE-CAUSE a record file that cannot be parsed refuses with ITS cause, not as "absent".
21
+ */
22
+ import assert from "node:assert/strict";
23
+ import { spawn } from "node:child_process";
24
+ import * as fs from "node:fs";
25
+ import * as os from "node:os";
26
+ import * as path from "node:path";
27
+ import { createInterface } from "node:readline";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
31
+
32
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
33
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-pi-bridge-sender-"));
34
+ const home = path.join(tmp, "home");
35
+ const sessionsDir = path.join(tmp, "meta-sessions");
36
+ fs.mkdirSync(home, { recursive: true });
37
+
38
+ function born(backend: "pi" | "claude-code", nativeSessionId: string): string {
39
+ return upsertMetaSession({
40
+ input: { backend, nativeSessionId, cwd: "/gate/pi-bridge-sender", model: "gate/model" },
41
+ dir: sessionsDir,
42
+ }).record.gardenId;
43
+ }
44
+
45
+ async function selfAs(gardenId: string): Promise<{ text: string; isError: boolean }> {
46
+ const child = spawn("bash", [path.join(ROOT, "mcp/entwurf-bridge/start.sh")], {
47
+ cwd: ROOT,
48
+ env: {
49
+ PATH: process.env.PATH,
50
+ HOME: home,
51
+ PI_CODING_AGENT_DIR: path.join(home, ".pi", "agent"),
52
+ ENTWURF_META_SESSIONS_DIR: sessionsDir,
53
+ PI_SESSION_ID: gardenId,
54
+ PI_AGENT_ID: "gate/pi-bridge-sender",
55
+ },
56
+ stdio: ["pipe", "pipe", "pipe"],
57
+ });
58
+ let stderr = "";
59
+ child.stderr.setEncoding("utf8");
60
+ child.stderr.on("data", (chunk) => {
61
+ stderr += chunk;
62
+ });
63
+ const lines = createInterface({ input: child.stdout });
64
+ const timer = setTimeout(() => child.kill("SIGKILL"), 10_000);
65
+ try {
66
+ const reply = await new Promise<unknown>((resolve, reject) => {
67
+ lines.on("line", (line) => {
68
+ const value = JSON.parse(line) as { id?: unknown };
69
+ if (value.id === 2) resolve(value);
70
+ });
71
+ child.once("error", reject);
72
+ child.once("exit", (code) => reject(new Error(`bridge exited ${code} before replying: ${stderr}`)));
73
+ const send = (msg: unknown): void => {
74
+ child.stdin.write(`${JSON.stringify(msg)}\n`);
75
+ };
76
+ send({
77
+ jsonrpc: "2.0",
78
+ id: 1,
79
+ method: "initialize",
80
+ params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "gate", version: "0" } },
81
+ });
82
+ send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
83
+ send({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "entwurf_self", arguments: {} } });
84
+ });
85
+ const result = (reply as { result?: { content?: Array<{ text?: string }>; isError?: boolean } }).result;
86
+ assert.ok(result, `tools/call returned no result object (stderr: ${stderr})`);
87
+ return { text: result.content?.[0]?.text ?? "", isError: result.isError === true };
88
+ } finally {
89
+ clearTimeout(timer);
90
+ lines.close();
91
+ child.kill("SIGTERM");
92
+ }
93
+ }
94
+
95
+ try {
96
+ const piId = born("pi", "01900000-0000-7000-8000-00000000a001");
97
+ const self = await selfAs(piId);
98
+ assert.ok(
99
+ !self.isError && new RegExp(`sessionId:\\s+${piId}`).test(self.text) && /replyable:\s+false/.test(self.text),
100
+ `[QK:PI-BRIDGE-SENDER-RECORD-BACKED] a pi record with no control socket keeps its identity and answers replyable:false — the socket is replyability, never admission (got: ${self.text})`,
101
+ );
102
+
103
+ const absentId = "20991231T235959-a0a0a0";
104
+ const absent = await selfAs(absentId);
105
+ assert.ok(
106
+ absent.isError && /no meta-record for garden id "20991231T235959-a0a0a0"/.test(absent.text),
107
+ `[QK:PI-BRIDGE-SENDER-MISSING-REFUSED] a well-formed PI_SESSION_ID with no record refuses loud, naming the absent record — no anonymous or other-rail answer (got: ${absent.text})`,
108
+ );
109
+
110
+ const claudeId = born("claude-code", "00000000-0000-4000-8000-00000000c001");
111
+ const foreign = await selfAs(claudeId);
112
+ assert.ok(
113
+ foreign.isError && /names a claude-code record, not a pi citizen/.test(foreign.text),
114
+ `[QK:PI-BRIDGE-SENDER-BACKEND-REFUSED] a PI_SESSION_ID naming another backend's record refuses by name (got: ${foreign.text})`,
115
+ );
116
+
117
+ const brokenId = born("pi", "01900000-0000-7000-8000-00000000a002");
118
+ fs.writeFileSync(path.join(sessionsDir, `${brokenId}.meta.json`), "{ not json");
119
+ const broken = await selfAs(brokenId);
120
+ assert.ok(
121
+ broken.isError && !/no meta-record for garden id/.test(broken.text) && /not valid JSON/.test(broken.text),
122
+ `[QK:PI-BRIDGE-SENDER-UNREADABLE-CAUSE] an unreadable pi record refuses with ITS own cause, never flattened into "absent" (got: ${broken.text})`,
123
+ );
124
+
125
+ console.log(
126
+ "check-pi-bridge-sender: a pi-session carrier is served only when it names a readable pi record; missing, foreign-backend and unreadable records each refuse with their own cause, and socket absence stays replyability",
127
+ );
128
+ } finally {
129
+ fs.rmSync(tmp, { recursive: true, force: true });
130
+ }