@junghanacs/entwurf 0.50.0 → 0.52.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 (39) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +140 -0
  3. package/ENTWURF.json +10 -4
  4. package/README.md +16 -8
  5. package/VERIFY.md +16 -9
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/external-mcp-host.md +2 -2
  9. package/docs/setup-clean-host.md +92 -35
  10. package/mcp/entwurf-bridge/dist/ENTWURF.json +10 -4
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +4 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +4 -4
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  14. package/package.json +11 -18
  15. package/pi/pi-durable/carrier/harness-setup.js +1 -1
  16. package/pi/pi-durable/carrier/runtime.js +221 -50
  17. package/pi/pi-durable/carrier/subagent.js +6 -3
  18. package/pi/pi-durable/carrier/tui.js +60 -22
  19. package/pi/pi-durable/overlay/upstream-pin.json +19 -18
  20. package/pi-extensions/lib/entwurf-control-rpc.ts +5 -1
  21. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +73 -69
  22. package/pi-extensions/lib/entwurf-v2-contract.ts +4 -4
  23. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  24. package/pi-extensions/meta-bridge-pi-durable.ts +1 -1
  25. package/run.sh +148 -46
  26. package/scripts/check-entwurf-v2-contract.ts +4 -4
  27. package/scripts/check-entwurf-v2-surface.ts +7 -21
  28. package/scripts/check-gate-qualification.ts +3 -3
  29. package/scripts/check-pi-durable-send.ts +4 -1
  30. package/scripts/lib/codex-fresh-source-receipts.ts +12 -6
  31. package/scripts/mutants/agent-guidance.json +2 -2
  32. package/scripts/mutants/codex-native.json +2 -2
  33. package/scripts/mutants/control-send-receipt.json +47 -7
  34. package/scripts/mutants/pack-install.json +4 -4
  35. package/scripts/mutants/pi-durable.json +189 -3
  36. package/scripts/mutants/pi-floor.json +20 -5
  37. package/scripts/mutants/v2-surface.json +4 -6
  38. package/scripts/smoke-acp-cortex-live.ts +1 -1
  39. package/scripts/smoke-acp-v2-send-live.ts +1 -1
package/run.sh CHANGED
@@ -1120,7 +1120,7 @@ check_entwurf_v2_contract() {
1120
1120
  # visible-first cut, single verdict per cell, 1 allow / 2 reject. N1
1121
1121
  # indeterminate never dispatches, and nothing on any cell starts a process.
1122
1122
  # R5 taxonomy covers table reasons + pre-claims bad-target/target-locked. Plus
1123
- # a schema↔types drift guard on the TypeBox input/receipt. Pure, no API.
1123
+ # a schema↔types drift guard on the pi-schema input/receipt. Pure, no API.
1124
1124
  run_ts scripts/check-entwurf-v2-contract.ts
1125
1125
  }
1126
1126
 
@@ -1156,8 +1156,10 @@ check_control_send_receipt() {
1156
1156
  # test titles `run_vitest` emits. Narrow file filter, same reason as check-entwurf-v2-send.
1157
1157
  # It stays inside check:contracts even though the discovery door also runs this file — see
1158
1158
  # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1159
- section "control-socket acceptance boundary (mutant execution coordinate)"
1160
- run_vitest pi-extensions/lib/control-send-receipt.test.ts
1159
+ # #159 adds the three delivery-input seams here as a shared mutant coordinate; discovery remains
1160
+ # the beside glob. This reuses the existing gate instead of adding a second help/catalog surface.
1161
+ section "control-socket acceptance and received guidance (mutant execution coordinate)"
1162
+ run_vitest pi-extensions/lib/control-send-receipt.test.ts pi-extensions/entwurf-control.test.ts pi-extensions/lib/entwurf-v2-mailbox.test.ts pi-extensions/lib/entwurf-v2-native-push.test.ts
1161
1163
  }
1162
1164
 
1163
1165
  check_control_retire() {
@@ -2340,21 +2342,64 @@ function stripBumpLedger(file, text) {
2340
2342
  return kept.join('\n');
2341
2343
  }
2342
2344
 
2343
- let rangeDecls = 0, exactDecls = 0;
2345
+ // PUBLISHED-SCOPE REGIONS (#155). Between an SDK bump in this checkout and the release that ships it,
2346
+ // an operator doc must keep telling a reader how to install the package that IS on npm — whose Pi
2347
+ // range is the previous floor — while the checkout's own sentences already name the new one. Those
2348
+ // are two different subjects in one file, so the doc marks the published one explicitly:
2349
+ // <!-- entwurf:published @junghanacs/entwurf@<version> pi <sdk> --> … <!-- /entwurf:published -->
2350
+ // Inside a region every scan below binds to <sdk>, outside it to the pinned floor. The region is not
2351
+ // a skip: it is scanned, it must hold at least one declaration, and after the four-surface assertion
2352
+ // below the published-scope claim holds its pair to PUBLISHED_ARTIFACT — a published package's own
2353
+ // manifest, frozen here. It is deliberately NOT derived from this checkout (package.json's version,
2354
+ // the workspace closure): a published package's contract stays true when the checkout moves on, so a
2355
+ // release preparation must not falsify it. `[측정 2026-11-03]` the source-only qualification snapshot
2356
+ // has no .git, so a release tag cannot be that oracle either.
2357
+ const PUBLISHED_OPEN = /<!-- entwurf:published @junghanacs\/entwurf@(\d+\.\d+\.\d+) pi (\d+\.\d+\.\d+) -->/g;
2358
+ const PUBLISHED_CLOSE = '<!-- /entwurf:published -->';
2359
+ function publishedRegions(text) {
2360
+ const regions = [], problems = [];
2361
+ let from = 0;
2362
+ for (const open of text.matchAll(PUBLISHED_OPEN)) {
2363
+ if (open.index < from) { problems.push(`a region opens inside the previous one at offset ${open.index}`); continue; }
2364
+ const end = text.indexOf(PUBLISHED_CLOSE, open.index);
2365
+ const next = text.slice(open.index + open[0].length).search(/<!-- entwurf:published /);
2366
+ if (end < 0 || (next >= 0 && open.index + open[0].length + next < end)) { problems.push(`the region opened by "${open[0]}" is not closed before the next one`); continue; }
2367
+ regions.push({ start: open.index, end, version: open[1], sdk: open[2], body: text.slice(open.index, end) });
2368
+ from = end + PUBLISHED_CLOSE.length;
2369
+ }
2370
+ const opens = [...text.matchAll(/<!-- entwurf:published /g)].length;
2371
+ const closes = text.split(PUBLISHED_CLOSE).length - 1;
2372
+ if (opens !== regions.length + problems.length) problems.push(`${opens - regions.length} published marker(s) do not have the exact opening shape`);
2373
+ if (closes !== regions.length) problems.push(`${closes} closing marker(s) for ${regions.length} region(s)`);
2374
+ return { regions, problems };
2375
+ }
2376
+ const floorAt = (regions, index) => regions.find((r) => index > r.start && index < r.end)?.sdk ?? piAi;
2377
+ const nextMinor = (v) => { const [a, b] = v.split('.').map(Number); return `${a}.${b + 1}`; };
2378
+
2379
+ let rangeDecls = 0, exactDecls = 0, sourceRangeDecls = 0;
2344
2380
  for (const file of BASELINE_DOCS) {
2345
2381
  const text = stripBumpLedger(file, readFileSync(file, 'utf8'));
2382
+ const { regions } = publishedRegions(text);
2346
2383
  // Closed-range declarations: `>=<floor> <<major>.<ceiling>` (spaces optional).
2347
- for (const [decl, floor, ceilMajor, ceilMinor] of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?(\d+)\.(\d+)/g)) {
2384
+ for (const m of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?(\d+)\.(\d+)/g)) {
2385
+ const [decl, floor, ceilMajor, ceilMinor] = m;
2386
+ const want = floorAt(regions, m.index);
2348
2387
  rangeDecls++;
2349
- assert.equal(floor, piAi,
2350
- `${file}: declared pi floor in "${decl}" is ${floor}, but the pinned SDK is ${piAi} — a baseline doc may not advertise a version no gate drives`);
2351
- assert.equal(`${ceilMajor}.${ceilMinor}`, `${piMaj}.${piMin + 1}`,
2352
- `${file}: declared pi ceiling in "${decl}" must be the next minor (${piMaj}.${piMin + 1})`);
2388
+ if (want === piAi) sourceRangeDecls++;
2389
+ assert.equal(floor, want, want === piAi
2390
+ ? `${file}: declared pi floor in "${decl}" is ${floor}, but the pinned SDK is ${piAi} — a baseline doc may not advertise a version no gate drives`
2391
+ : `${file}: declared pi floor in "${decl}" is ${floor}, but its published-scope region names pi ${want}`);
2392
+ assert.equal(`${ceilMajor}.${ceilMinor}`, nextMinor(want),
2393
+ `${file}: declared pi ceiling in "${decl}" must be the next minor (${nextMinor(want)})`);
2353
2394
  }
2354
2395
  // Exact install pins: `@earendil-works/pi-<pkg>@<version>`.
2355
- for (const [decl, ver] of text.matchAll(/@earendil-works\/pi-(?:ai|coding-agent|tui)@(\d+\.\d+\.\d+)/g)) {
2396
+ for (const m of text.matchAll(/@earendil-works\/pi-(?:ai|coding-agent|tui)@(\d+\.\d+\.\d+)/g)) {
2397
+ const [decl, ver] = m;
2398
+ const want = floorAt(regions, m.index);
2356
2399
  exactDecls++;
2357
- assert.equal(ver, piAi, `${file}: install example "${decl}" pins ${ver}, but the pinned SDK is ${piAi}`);
2400
+ assert.equal(ver, want, want === piAi
2401
+ ? `${file}: install example "${decl}" pins ${ver}, but the pinned SDK is ${piAi}`
2402
+ : `${file}: install example "${decl}" pins ${ver}, but its published-scope region names pi ${want}`);
2358
2403
  }
2359
2404
  }
2360
2405
  // Prose declarations carry the pin in sentences the two patterns above cannot
@@ -2366,9 +2411,11 @@ const PROSE_DECLS = [
2366
2411
  ['ROADMAP.md', /floor = \*\*(\d+\.\d+\.\d+)\*\*/, 'floor = **<version>**'],
2367
2412
  ];
2368
2413
  for (const [file, re, shape] of PROSE_DECLS) {
2369
- const m = readFileSync(file, 'utf8').match(re);
2414
+ const text = readFileSync(file, 'utf8');
2415
+ const m = text.match(re);
2370
2416
  assert.ok(m, `${file}: the baseline sentence "${shape}" is gone — restore it or update check-dep-versions; a doc reword must not silently drop the pin from the gate`);
2371
- assert.equal(m[1], piAi, `${file}: "${shape}" declares ${m[1]}, but the pinned SDK is ${piAi}`);
2417
+ const want = floorAt(publishedRegions(text).regions, m.index);
2418
+ assert.equal(m[1], want, `${file}: "${shape}" declares ${m[1]}, but the ${want === piAi ? 'pinned SDK' : 'published-scope region'} is ${want}`);
2372
2419
  }
2373
2420
  // ── #120 P4: ONE FLOOR, FOUR MECHANICAL SURFACES ──────────────────────────
2374
2421
  // The bump is mostly a number substitution, and that is exactly how a bump goes
@@ -2390,6 +2437,8 @@ const PI_CONSTELLATION = [
2390
2437
  '@earendil-works/pi-codemode', '@earendil-works/pi-mcp',
2391
2438
  // #130: the durable library the pi-durable carrier imports — a production dependency since 0.32.
2392
2439
  '@earendil-works/pi-durable',
2440
+ // `[측정 2026-11-03]` 1.5.0 (#155): pi-schema joins — declared by chord, pi-agent-core, pi-ai and pi-durable.
2441
+ '@earendil-works/pi-schema',
2393
2442
  ];
2394
2443
  const sameSet = (got, want, where) => {
2395
2444
  const missing = want.filter((n) => !got.includes(n));
@@ -2404,7 +2453,7 @@ for (const [, name, ver] of runSh.matchAll(/"(@earendil-works\/[a-z-]+)@(\d+\.\d
2404
2453
  if (ver !== piAi) floorGaps.push(`run.sh install constellation: ${name}@${ver} is not the floor ${piAi}`);
2405
2454
  }
2406
2455
  sameSet(installed, PI_CONSTELLATION, 'run.sh install constellation');
2407
- // pnpm-workspace.yaml minimumReleaseAgeExclude: the same eight, each ADMITTING the
2456
+ // pnpm-workspace.yaml minimumReleaseAgeExclude: the same constellation, each ADMITTING the
2408
2457
  // current floor. History stays (`0.85.0 || 0.85.1 || …`) — only the newest term is
2409
2458
  // the contract, because that is what an install resolves against.
2410
2459
  const closure = [...readFileSync('pnpm-workspace.yaml', 'utf8').matchAll(/^\s*-\s*'(@earendil-works\/[a-z-]+)@([^']+)'/gm)];
@@ -2437,22 +2486,65 @@ const CURRENT_CONTRACT_DECLS = [
2437
2486
  ['docs/acp-backend-rail.md', /SDK exact `(\d+\.\d+\.\d+)`, host `(>=[\d.]+ <\d+\.\d+)`/, "the ACP support row's exact+range pair"],
2438
2487
  ];
2439
2488
  for (const [file, re, shape] of CURRENT_CONTRACT_DECLS) {
2440
- const m = readFileSync(file, 'utf8').match(re);
2489
+ const text = readFileSync(file, 'utf8');
2490
+ const m = text.match(re);
2441
2491
  if (!m) { floorGaps.push(`${file}: ${shape} is gone — a reworded current-contract sentence must fail loud, not drop out silently`); continue; }
2492
+ // A sentence inside a published-scope region states the published package's contract; it is
2493
+ // held to that region's pi here and the region itself to its oracles below.
2494
+ const floor = floorAt(publishedRegions(text).regions, m.index);
2495
+ const range = `>=${floor} <${nextMinor(floor)}`;
2442
2496
  for (const captured of m.slice(1)) {
2443
- const expected = captured.startsWith('>=') ? nextFloorRange : piAi;
2444
- if (captured !== expected) floorGaps.push(`${file}: ${shape} declares "${captured}", but the floor is ${piAi} (${nextFloorRange})`);
2497
+ const expected = captured.startsWith('>=') ? range : floor;
2498
+ if (captured !== expected) floorGaps.push(`${file}: ${shape} declares "${captured}", but the ${floor === piAi ? 'floor' : 'published-scope region'} is ${floor} (${range})`);
2445
2499
  }
2446
2500
  }
2447
2501
  assert.deepEqual(floorGaps, [],
2448
2502
  `[QK:PI-FLOOR-SINGLE-SOURCE] the pi floor is declared on four mechanical surfaces — the durable pin's SDK set with package.json's exact production dependencies, run.sh's install constellation, the pnpm workspace closure, and the current-contract sentences in the docs — and every one of them must name the SAME floor. A bump that moves three of the four is the ordinary way this breaks, and the surface left behind is the one nothing else reads`);
2449
2503
 
2504
+ // The published-scope regions' own oracle (see PUBLISHED-SCOPE REGIONS above). Checked after the
2505
+ // four surfaces so a stranded closure row is still attributed to the claim above.
2506
+ // PUBLISHED_ARTIFACT is the one published pair a region may name, copied from the artifact, not from
2507
+ // this checkout. Read 2026-11-11 from package/package.json in the preserved registry-accepted
2508
+ // 0.51.0 release TGZ (TGZ SHA256 04fcce26d0a6dec8b8bb9a7f14a843eee0defb313aedafa34080600853d36e4d;
2509
+ // raw manifest SHA256 b5043ba8edde6f2b9d13052a98c3cd808a3b30f1023da41b22ee022470a96120).
2510
+ // Its ten @earendil-works production dependencies are exact 1.5.0, independent of this checkout.
2511
+ const PUBLISHED_ARTIFACT = {
2512
+ version: '0.51.0',
2513
+ dependencies: {
2514
+ '@earendil-works/chord': '1.5.0', '@earendil-works/pi-agent-core': '1.5.0', '@earendil-works/pi-ai': '1.5.0',
2515
+ '@earendil-works/pi-codemode': '1.5.0', '@earendil-works/pi-coding-agent': '1.5.0', '@earendil-works/pi-durable': '1.5.0',
2516
+ '@earendil-works/pi-mcp': '1.5.0', '@earendil-works/pi-schema': '1.5.0', '@earendil-works/pi-telemetry': '1.5.0', '@earendil-works/pi-tui': '1.5.0',
2517
+ },
2518
+ };
2519
+ const publishedSdk = [...new Set(Object.values(PUBLISHED_ARTIFACT.dependencies))];
2520
+ const publishedGaps = [];
2521
+ if (publishedSdk.length !== 1) publishedGaps.push(`PUBLISHED_ARTIFACT ${PUBLISHED_ARTIFACT.version} carries no single SDK generation (${publishedSdk.join(', ')})`);
2522
+ let publishedRegionCount = 0;
2523
+ const PUBLISHED_REGION_MAX_LINES = 40;
2524
+ for (const file of [...new Set([...BASELINE_DOCS, ...CURRENT_CONTRACT_DECLS.map(([f]) => f)])]) {
2525
+ const { regions, problems } = publishedRegions(readFileSync(file, 'utf8'));
2526
+ for (const p of problems) publishedGaps.push(`${file}: ${p}`);
2527
+ for (const r of regions) {
2528
+ publishedRegionCount++;
2529
+ const where = `${file}: region "@junghanacs/entwurf@${r.version} pi ${r.sdk}"`;
2530
+ if (r.version !== PUBLISHED_ARTIFACT.version) publishedGaps.push(`${where} names a package no frozen published manifest records (only @junghanacs/entwurf@${PUBLISHED_ARTIFACT.version})`);
2531
+ else if (publishedSdk.length !== 1 || r.sdk !== publishedSdk[0]) publishedGaps.push(`${where} names pi ${r.sdk}, but the published ${r.version} manifest ships pi ${publishedSdk.join(' | ')}`);
2532
+ const decls = [...r.body.matchAll(/>=\s?\d+\.\d+\.\d+\s?<\s?\d+\.\d+|@earendil-works\/pi-(?:ai|coding-agent|tui)@\d+\.\d+\.\d+/g)].length;
2533
+ if (decls === 0) publishedGaps.push(`${where} holds no pi declaration — an empty region only hides prose from the scan`);
2534
+ const lines = r.body.split('\n').length;
2535
+ if (lines > PUBLISHED_REGION_MAX_LINES) publishedGaps.push(`${where} spans ${lines} lines (max ${PUBLISHED_REGION_MAX_LINES}) — mark the table, example or sentence, not the section around it`);
2536
+ }
2537
+ }
2538
+ if (publishedRegionCount > 0 && sourceRangeDecls < 2) publishedGaps.push(`only ${sourceRangeDecls} range declaration(s) remain outside published-scope regions — the checkout's own floor must still be declared where this gate reads it`);
2539
+ assert.deepEqual(publishedGaps, [],
2540
+ `[QK:PI-FLOOR-PUBLISHED-SCOPE] a published-scope region in a baseline doc names a published package and the SDK its frozen manifest ships (PUBLISHED_ARTIFACT, independent of this checkout's version and closure), holds at least one declaration, stays narrow, and leaves the checkout's floor declared outside it`);
2541
+
2450
2542
  // Guard the guard: if the patterns ever stop matching, the loops above pass
2451
2543
  // vacuously and the docs fall back OUT of the gate without a word.
2452
2544
  assert.ok(rangeDecls >= 5, `expected at least 5 pi range declarations across the baseline docs, found ${rangeDecls} — the doc scan matched (almost) nothing and would pass vacuously`);
2453
2545
  assert.ok(exactDecls >= 1, `expected at least 1 exact pi install pin in the baseline docs, found ${exactDecls}`);
2454
2546
 
2455
- console.log(`[check-dep-versions] ok — pi ${piAi} is coherent across the durable pin's SDK set, package.json (exact production dependencies, no dev/peer duplicate), run.sh (install pins), and ${BASELINE_DOCS.length} baseline docs (${rangeDecls} range + ${exactDecls} exact + ${PROSE_DECLS.length} prose declarations)`);
2547
+ console.log(`[check-dep-versions] ok — pi ${piAi} is coherent across the durable pin's SDK set, package.json (exact production dependencies, no dev/peer duplicate), run.sh (install pins), and ${BASELINE_DOCS.length} baseline docs (${rangeDecls} range + ${exactDecls} exact + ${PROSE_DECLS.length} prose declarations; ${publishedRegionCount} published-scope region(s))`);
2456
2548
  EOF
2457
2549
  )
2458
2550
  }
@@ -4040,7 +4132,7 @@ check_pack_win32_carrier() {
4040
4132
  # which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
4041
4133
  # below so a future suffix change cannot pass vacuously.)
4042
4134
  pack_install_leaked_pi() {
4043
- grep '^@earendil-works+' | grep -Ev '@1\.4\.0(_|$)' || true
4135
+ grep '^@earendil-works+' | grep -Ev '@1\.5\.2(_|$)' || true
4044
4136
  }
4045
4137
 
4046
4138
  # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
@@ -4057,18 +4149,18 @@ check_pack_pin_matcher() {
4057
4149
  # Cell 1 — the version boundary. Every measured peer-hash shape is a fixture: the
4058
4150
  # 0.84.4-era `_@modelcontextprotocol+sdk@…`, the `_ws@8.21.3` observed on a real
4059
4151
  # 0.85.1 tree, and `[측정 2026-09-20]` the combined suffix a 0.86.0 install carries.
4060
- # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`1.4.0-beta.1`,
4152
+ # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`1.5.2-beta.1`,
4061
4153
  # the prerelease shape that an unanchored match would bless) and an off-pin version.
4062
4154
  matcher_probe=$(printf '%s\n' \
4063
- '@earendil-works+pi-ai@1.4.0' \
4064
- '@earendil-works+pi-ai@1.4.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
4065
- '@earendil-works+pi-ai@1.4.0_ws@8.21.3' \
4066
- '@earendil-works+pi-ai@1.4.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
4067
- '@earendil-works+pi-ai@1.4.0-beta.1' \
4155
+ '@earendil-works+pi-ai@1.5.2' \
4156
+ '@earendil-works+pi-ai@1.5.2_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
4157
+ '@earendil-works+pi-ai@1.5.2_ws@8.21.3' \
4158
+ '@earendil-works+pi-ai@1.5.2_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
4159
+ '@earendil-works+pi-ai@1.5.2-beta.1' \
4068
4160
  '@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
4069
- if [ "$matcher_probe" != '@earendil-works+pi-ai@1.4.0-beta.1
4161
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@1.5.2-beta.1
4070
4162
  @earendil-works+pi-agent-core@0.87.1' ]; then
4071
- fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.4.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.4.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
4163
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.5.2-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.5.2 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
4072
4164
  return 1
4073
4165
  fi
4074
4166
 
@@ -4076,9 +4168,9 @@ check_pack_pin_matcher() {
4076
4168
  # (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
4077
4169
  # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
4078
4170
  matcher_probe=$(printf '%s\n' \
4079
- '@earendil-works+chord@1.4.0' \
4171
+ '@earendil-works+chord@1.5.2' \
4080
4172
  '@earendil-works+chord@0.87.1' \
4081
- '@earendil-works+pi-ai@1.4.0' | pack_install_leaked_pi)
4173
+ '@earendil-works+pi-ai@1.5.2' | pack_install_leaked_pi)
4082
4174
  if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
4083
4175
  fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it was a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0, `[측정 2026-09-23]` at 0.87.1 and `[측정 2026-09-30]` at 0.99.1, where pi-coding-agent adds pi-codemode and pi-mcp beside them; `[측정 2026-10-13]` at 1.2.0 pi-durable alone declares it) — got: ${matcher_probe:-<nothing leaked>}"
4084
4176
  return 1
@@ -4291,7 +4383,9 @@ ORACLE
4291
4383
  # SDK 1.4 pi-durable imports pi-ai's new `utils/identity`, absent from the older pi-ai control versions
4292
4384
  # checked here (1.0.3 and 1.3.0 both fail to load); pi-telemetry 1.0.3/1.3.0 and chord 1.3.0 lack exports
4293
4385
  # it imports. pi-codemode 1.3.0 loads, and depends on no SDK member, so this tree carries exactly one
4294
- # depth-2 mismatch (L4 keeps depth 3).
4386
+ # depth-2 mismatch (L4 keeps depth 3). `[측정 2026-11-03]` SDK 1.5.0 pi-durable still loads beside
4387
+ # pi-codemode 1.3.0 (registry tarball, integrity-matched) and 1.4.0 (#155 import probe; its codemode
4388
+ # imports are the root, /portable, /source and /declarations entries), so the control version holds.
4295
4389
  printf '%s\n' "{\"name\":\"pd-mismatch\",\"version\":\"1.0.0\",\"private\":true,\"overrides\":{\"@earendil-works/pi-durable\":{\"@earendil-works/pi-codemode\":\"1.3.0\"}}}" > "$box/pd-mismatch/package.json"
4296
4390
  out=$(cd "$box/pd-mismatch" && npm install "$tgz" --ignore-scripts --no-audit --no-fund 2>&1) || {
4297
4391
  fail "[check-pack-install] pi-durable L3 (mismatch control) install failed:"; echo "$out" | tail -10 | sed 's/^/ /' >&2; return 1; }
@@ -4421,7 +4515,13 @@ ORACLE
4421
4515
  local pin_minor="${pin_ver#*.}"; pin_minor="${pin_minor%%.*}"
4422
4516
  local pin_ceiling="${pin_ver%%.*}.$((pin_minor + 1))"
4423
4517
  mkdir -p "$host_old"
4424
- for spec in "${host_specs[@]}"; do host_old_specs+=("${spec%@*}@$host_old_ver"); done
4518
+ # The old-host fixture installs the members its own generation had. `[측정 2026-11-03]` pi-schema's
4519
+ # registry versions are 0.0.0-stage, 0.0.0 and 1.5.0 (#155: born with the 1.5 SDK), so it has no
4520
+ # release at the fixed old version — the only member skipped here; the pinned/aligned cells keep all ten.
4521
+ for spec in "${host_specs[@]}"; do
4522
+ [ "${spec%@*}" = '@earendil-works/pi-schema' ] && continue
4523
+ host_old_specs+=("${spec%@*}@$host_old_ver")
4524
+ done
4425
4525
  out=$(cd "$host_old" && npm install "${host_old_specs[@]}" --ignore-scripts --no-audit --no-fund 2>&1) || {
4426
4526
  fail "[check-pack-install] could not install the previous-host pi fixture ($host_old_ver):"; echo "$out" | tail -10 | sed 's/^/ /' >&2; return 1; }
4427
4527
  for cell in old aligned; do
@@ -4986,20 +5086,22 @@ _check_pack_install_impl() {
4986
5086
  # oracle walked 10 member edges with 0 mismatches (.tmp-verify/145-stage1/08-pack-install.log).
4987
5087
  # `[측정 2026-10-22]` at 1.4.0 the nine members are again exact 1.4.0 edges (registry manifests; #148 S0),
4988
5088
  # and pi-tui 1.4.0 adds one third-party dependency (marked-cjk-friendly 0.1.2) outside the closure.
4989
- echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.4.x SDK + chord + codemode + mcp + durable + typebox)"
5089
+ # `[측정 2026-11-03]` at 1.5.0 the set grows to ten: pi-schema 1.5.0 is new and declared by chord,
5090
+ # pi-agent-core, pi-ai and pi-durable; every member edge is exact 1.5.0 (registry manifests; #155).
5091
+ echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.5.x SDK + chord + codemode + mcp + durable + schema)"
4990
5092
  local install_log
4991
5093
  install_log=$(cd "$tmp" && pnpm add \
4992
5094
  "$tgz_path" \
4993
- "@earendil-works/pi-ai@1.4.0" \
4994
- "@earendil-works/pi-coding-agent@1.4.0" \
4995
- "@earendil-works/pi-tui@1.4.0" \
4996
- "@earendil-works/pi-agent-core@1.4.0" \
4997
- "@earendil-works/pi-telemetry@1.4.0" \
4998
- "@earendil-works/chord@1.4.0" \
4999
- "@earendil-works/pi-codemode@1.4.0" \
5000
- "@earendil-works/pi-mcp@1.4.0" \
5001
- "@earendil-works/pi-durable@1.4.0" \
5002
- "typebox@latest" \
5095
+ "@earendil-works/pi-ai@1.5.2" \
5096
+ "@earendil-works/pi-coding-agent@1.5.2" \
5097
+ "@earendil-works/pi-tui@1.5.2" \
5098
+ "@earendil-works/pi-agent-core@1.5.2" \
5099
+ "@earendil-works/pi-telemetry@1.5.2" \
5100
+ "@earendil-works/chord@1.5.2" \
5101
+ "@earendil-works/pi-codemode@1.5.2" \
5102
+ "@earendil-works/pi-mcp@1.5.2" \
5103
+ "@earendil-works/pi-durable@1.5.2" \
5104
+ "@earendil-works/pi-schema@1.5.2" \
5003
5105
  --ignore-workspace --ignore-scripts 2>&1) || {
5004
5106
  fail "[check-pack-install] pnpm add failed:"
5005
5107
  echo "$install_log" | tail -10 | sed 's/^/ /' >&2
@@ -5008,17 +5110,17 @@ _check_pack_install_impl() {
5008
5110
 
5009
5111
  # A pin is a wish until the resolved tree is read back. Assert it: EVERY
5010
5112
  # @earendil-works package present — direct or transitive, top level or nested,
5011
- # chord included — must be the pinned 0.86.0. Anything else means an unpinned caret
5113
+ # chord included — must be the pinned 1.5.2. Anything else means an unpinned caret
5012
5114
  # floated and the rest of this gate would be exercising a runtime nobody verified,
5013
- # while still printing "pinned pi 0.86.0". Fail loud instead of proving the wrong floor.
5115
+ # while still printing a pinned runtime. Fail loud instead of proving the wrong floor.
5014
5116
  local leaked_pi
5015
5117
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
5016
5118
  if [ -n "$leaked_pi" ]; then
5017
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.4.0):"
5119
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.5.2):"
5018
5120
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
5019
5121
  return 1
5020
5122
  fi
5021
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.4.0 (chord and pi-durable included)"
5123
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.5.2 (chord, pi-durable and pi-schema included)"
5022
5124
 
5023
5125
  # Resolve the installed package.json and confirm pi.extensions
5024
5126
  # arrived intact. If pi.extensions is empty or missing, the
@@ -25,7 +25,7 @@
25
25
  * - R5: taxonomy covers the table reasons + pre-claims bad-target /
26
26
  * target-locked (so bucket B F2 won't reopen it), and
27
27
  * is PRE-DISPATCH only (no send-fail-fallback member).
28
- * - schema ↔ types drift guard: the TypeBox input/receipt property keysets and
28
+ * - schema ↔ types drift guard: the pi-schema input/receipt property keysets and
29
29
  * the StringEnum members match the hand-written contract constants.
30
30
  */
31
31
 
@@ -82,7 +82,7 @@ function eq(label: string, actual: unknown, expected: unknown): void {
82
82
  }
83
83
 
84
84
  // Pull the literal members out of a StringEnum / Optional(StringEnum) schema,
85
- // whichever shape typebox emits — so the schema↔const drift guard is real.
85
+ // whichever shape the schema builder emits — so the schema↔const drift guard is real.
86
86
  function enumValues(schema: unknown): string[] {
87
87
  const s = schema as { enum?: unknown[]; anyOf?: { const?: unknown }[] };
88
88
  if (Array.isArray(s?.enum)) return s.enum.map(String);
@@ -626,8 +626,8 @@ ok(
626
626
  !("action" in rej) && !("transport" in rej) && !("ownership" in rej),
627
627
  );
628
628
  eq("receipt.reject.reason enum === ENTWURF_V2_REJECT_REASONS", enumValues(rej.reason), [...ENTWURF_V2_REJECT_REASONS]);
629
- // ?6: reject.observedLiveness is a required-nullable union — Type.Union([StringEnum
630
- // (FACT_LIVENESSES), Type.Null()]) — NOT Optional and NOT a bare StringEnum. The
629
+ // ?6: reject.observedLiveness is a required-nullable union — union([StringEnum
630
+ // (FACT_LIVENESSES), nul()]) — NOT Optional and NOT a bare StringEnum. The
631
631
  // key is always present (required), the value may be a 4-value liveness OR null.
632
632
  const rejLiveness = rej.observedLiveness as { anyOf?: { type?: string }[] };
633
633
  eq("?6: receipt.reject.observedLiveness is a 2-branch union (liveness | null)", rejLiveness.anyOf?.length, 2);
@@ -92,7 +92,7 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
92
92
  );
93
93
  ok(
94
94
  `${tag} — long description names the THIRD result for an unsupported citizen`,
95
- /mailbox-undeliverable/.test(longDesc) && /THIRD RESULT/.test(longDesc),
95
+ /mailbox-undeliverable/.test(longDesc) && /target without a live socket, the THIRD RESULT/.test(longDesc),
96
96
  );
97
97
  ok(
98
98
  `${tag} — intent param names a rail's reject, not only its allow cell`,
@@ -139,30 +139,16 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
139
139
  * whose subject (two distinct OWNED rejects) left with the `owned-outcome` intent.
140
140
  *
141
141
  * After the visible-first cut a dormant socket-domain citizen rejects delivery by v2, and
142
- * these four strings are the only place a caller learns that BEFORE spending a dispatch. So each
143
- * one must, on its own:
144
- * - name the reason it will actually get back (`dormant-fire-forget-unsupported`);
145
- * - name the intent that used to answer there (`owned-outcome`) — silently deleting it leaves
146
- * an operator who read last week's docs with no way to find out what happened;
147
- * - say it was `withdrawn`, ADJACENTLY to that name. Presence alone is not enough: naming a
148
- * retired intent without the retirement verb beside it reads as an offer, which is exactly
149
- * the "advertised as selectable" failure this claim exists to forbid;
150
- * - carry NEITHER retired advertisement form (`owned-outcome = …` enum styling, or the old
151
- * "DORMANT socket-domain citizen … never auto-converted" sentence that described picking it).
152
- *
153
- * ORDER IS LOAD-BEARING, and not for readability. `ok` throws on the FIRST failure, and the kill
154
- * signature is read off the failing line — so the `[QK:…]` label must sit on the assertion this
155
- * claim's mutant reaches FIRST. That mutant drops the reason AND the withdrawal in one edit; with
156
- * the reason check leading, an UNLABELLED line would throw first and the kill would read as
157
- * WRONG-REASON. The adjacency check therefore goes first and carries the only token; the other two
158
- * run unlabelled behind it — still real assertions, just not this claim's signature.
142
+ * both caller-facing strings must independently name the actual dormant rejection and say that
143
+ * delivery never launches or resumes a process. Neither may advertise the retired offer. History
144
+ * belongs in the issue and literal amendment, not in the current caller contract. The labelled
145
+ * assertion comes first because qualification reads the first failing line as the kill signature.
159
146
  */
160
147
  function assertDormantHonesty(tag: string, rawText: string): void {
161
148
  const text = rawText;
162
- const at = text.indexOf("owned-outcome");
163
149
  ok(
164
- `${tag} — names the withdrawn intent owned-outcome and says "withdrawn" beside it (not as an offer) [QK:V2SURF-DORMANT-HONESTY]`,
165
- at >= 0 && /withdrawn/.test(text.slice(at, at + 200)),
150
+ `${tag} — delivery never launches or resumes a process and names the dormant rejection [QK:V2SURF-DORMANT-HONESTY]`,
151
+ /This delivery verb never launches or resumes a process/.test(text) && /dormant-fire-forget-unsupported/.test(text),
166
152
  );
167
153
  ok(`${tag} — names the reason a dormant target actually returns`, /dormant-fire-forget-unsupported/.test(text));
168
154
  ok(
@@ -3460,7 +3460,7 @@ let manifestCount: number;
3460
3460
  "pi-retire-request": 6,
3461
3461
  "retirement-event": 16,
3462
3462
  "control-retire": 9,
3463
- "control-send-receipt": 20,
3463
+ "control-send-receipt": 23,
3464
3464
  "codex-native": 95,
3465
3465
  "native-push-paused": 1,
3466
3466
  "chain-terminus-publication": 1,
@@ -3473,11 +3473,11 @@ let manifestCount: number;
3473
3473
  "copilot-birth": 19,
3474
3474
  "copilot-launch": 14,
3475
3475
  "pi-launch": 9,
3476
- "pi-floor": 3,
3476
+ "pi-floor": 4,
3477
3477
  "pi-bridge-sender": 4,
3478
3478
  "pi-mcp-bridge": 2,
3479
3479
  "pi-mcp-register": 3,
3480
- "pi-durable": 171,
3480
+ "pi-durable": 186,
3481
3481
  "copilot-receive": 20,
3482
3482
  "entwurf-peers": 1,
3483
3483
  "fresh-call-dispatch": 18,
@@ -900,7 +900,10 @@ async function cell(): Promise<void> {
900
900
  hostReceiver.ownerKind === "pi-durable-host";
901
901
  ok(
902
902
  `[QK:${QK}-SENDER-REPLYABILITY] the host's receiver marker is armed and owned by the spawned host, and the landed body says what that predicts: replyable (armed ${hostArmed})`,
903
- hostArmed && body.includes(`session: ${hostGid} (meta-session, replyable — reply via entwurf_v2`),
903
+ hostArmed &&
904
+ body.includes(
905
+ `session: ${hostGid} (meta-session, replyable — when a reply is requested, send the result via entwurf_v2`,
906
+ ),
904
907
  );
905
908
  const hostRecords = fs
906
909
  .readdirSync(dirs.sessions)
@@ -452,14 +452,20 @@ export function codexSourceToolReceipts(thread: unknown): SourceToolReceipt[] {
452
452
  }
453
453
 
454
454
  function exactSenderInfoSuffix(text: string, nonce: string): string | null {
455
- const prefix = `${nonce}\n\n<sender_info>`;
455
+ // Independent expected frame: never import the producer's guidance as the receipt oracle.
456
+ const guidance =
457
+ "\n\n[entwurf: When a reply is requested, send the result via entwurf_v2 to the requested recipient (default: sender_info.sessionId); a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment.]";
458
+ const open = "<sender_info>";
456
459
  const suffix = "</sender_info>";
457
- if (!text.startsWith(prefix) || !text.endsWith(suffix)) return null;
458
- const json = text.slice(prefix.length, -suffix.length);
459
- if (json.includes("\n") || json.includes("<sender_info>")) return null;
460
+ const at = text.lastIndexOf(open);
461
+ if (at < 0 || !text.endsWith(suffix)) return null;
462
+ const json = text.slice(at + open.length, -suffix.length);
463
+ if (json.includes("\n") || json.includes(open)) return null;
460
464
  try {
461
- const sender = JSON.parse(json) as { sessionId?: unknown };
462
- return typeof sender.sessionId === "string" ? sender.sessionId : null;
465
+ const sender = JSON.parse(json) as { sessionId?: unknown; replyable?: unknown };
466
+ const prefix = `${nonce}${sender.replyable === true ? guidance : ""}\n\n${open}`;
467
+ if (!text.startsWith(prefix) || !text.endsWith(suffix)) return null;
468
+ return at === prefix.length - open.length && typeof sender.sessionId === "string" ? sender.sessionId : null;
463
469
  } catch {
464
470
  return null;
465
471
  }
@@ -6,8 +6,8 @@
6
6
  "claim": "GUIDANCE-REVIEWED-REVISION",
7
7
  "title": "a valid Root revision advances beyond the independently reviewed payload revision while every history entry and decoded tool string remains valid",
8
8
  "subject": "ENTWURF.json",
9
- "find": ["\t\"revision\": 19,", "\t\"history\": ["],
10
- "replace": ["\t\"revision\": 20,", "\t\"history\": ["],
9
+ "find": ["\t\"revision\": 20,", "\t\"history\": ["],
10
+ "replace": ["\t\"revision\": 21,", "\t\"history\": ["],
11
11
  "gate": ["bash", "run.sh", "check-agent-guidance-beside"],
12
12
  "timeoutSeconds": 120,
13
13
  "signature": "[QK:GUIDANCE-REVIEWED-REVISION]",
@@ -715,9 +715,9 @@
715
715
  "claim": "CODEX-LIVE-RAW-CALLBACK-REQUIRED",
716
716
  "title": "a Pi callback with arbitrary bytes after its sender envelope is accepted as exact",
717
717
  "subject": "scripts/lib/codex-fresh-source-receipts.ts",
718
- "find": ["\tif (!text.startsWith(prefix) || !text.endsWith(suffix)) return null;"],
718
+ "find": ["\tconst suffix = \"</sender_info>\";"],
719
719
  "replace": [
720
- "\tif (!text.startsWith(prefix)) return null;\n\tconst suffixAt = text.indexOf(suffix, prefix.length);\n\tif (suffixAt < 0) return null;\n\ttext = text.slice(0, suffixAt + suffix.length); // mutant: discard callback suffix"
720
+ "\tconst suffix = \"</sender_info>\";\n\tconst suffixAt = text.indexOf(suffix);\n\tif (suffixAt >= 0) text = text.slice(0, suffixAt + suffix.length); // mutant: discard callback suffix"
721
721
  ],
722
722
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
723
723
  "timeoutSeconds": 180,
@@ -11,7 +11,8 @@
11
11
  "gate": ["bash", "run.sh", "check-control-send-receipt"],
12
12
  "timeoutSeconds": 600,
13
13
  "signature": "[QK:SEND-RECEIPT-IDLE-IS-SENT]",
14
- "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
14
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts",
15
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
15
16
  },
16
17
  {
17
18
  "claim": "SEND-RECEIPT-BUSY-STEER-IS-QUEUED",
@@ -22,7 +23,8 @@
22
23
  "gate": ["bash", "run.sh", "check-control-send-receipt"],
23
24
  "timeoutSeconds": 600,
24
25
  "signature": "[QK:SEND-RECEIPT-BUSY-STEER-IS-QUEUED]",
25
- "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
26
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts",
27
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
26
28
  },
27
29
  {
28
30
  "claim": "SEND-RECEIPT-BUSY-FOLLOWUP-IS-QUEUED",
@@ -33,7 +35,8 @@
33
35
  "gate": ["bash", "run.sh", "check-control-send-receipt"],
34
36
  "timeoutSeconds": 600,
35
37
  "signature": "[QK:SEND-RECEIPT-BUSY-FOLLOWUP-IS-QUEUED]",
36
- "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
38
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts",
39
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
37
40
  },
38
41
  {
39
42
  "claim": "SEND-RECEIPT-UNKNOWN-IS-ACCEPTED-NOT-SENT",
@@ -44,7 +47,8 @@
44
47
  "gate": ["bash", "run.sh", "check-control-send-receipt"],
45
48
  "timeoutSeconds": 600,
46
49
  "signature": "[QK:SEND-RECEIPT-UNKNOWN-IS-ACCEPTED-NOT-SENT]",
47
- "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
50
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts",
51
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
48
52
  },
49
53
  {
50
54
  "claim": "SEND-BOUNDARY-CARRIED",
@@ -194,7 +198,7 @@
194
198
  "claim": "PI-CORE-PIN-TRACKS-FLOOR",
195
199
  "title": "the queue oracle's pi-agent-core pin drifts off the floor the rest of the pi runtime is pinned to, so the oracle keeps answering honestly — about a pi nobody in this repo runs",
196
200
  "subject": "package.json",
197
- "find": ["\"@earendil-works/pi-agent-core\": \"1.4.0\","],
201
+ "find": ["\"@earendil-works/pi-agent-core\": \"1.5.2\","],
198
202
  "replace": ["\"@earendil-works/pi-agent-core\": \"0.87.1\","],
199
203
  "gate": ["bash", "run.sh", "check-dep-versions"],
200
204
  "timeoutSeconds": 300,
@@ -240,15 +244,51 @@
240
244
  "title": "the caller-facing outcome list drops the native-push rail, so a caller reading the one blurb it reads before dispatching is told the control rail's vocabulary covers a rail that answers with a direct injection instead",
241
245
  "subject": "ENTWURF.json",
242
246
  "find": [
243
- "native-push INJECTION, a rejection, or lock-dirty. This receipt is not the colleague's reply or evidence of reading. EXISTING targets only; discover with "
247
+ "native-push INJECTION, a rejection, or lock-dirty. A receipt is not a reply or proof of reading. A local final is not delivery:"
244
248
  ],
245
249
  "replace": [
246
- "a rejection, or lock-dirty. This receipt is not the colleague's reply or evidence of reading. EXISTING targets only; discover with "
250
+ "a rejection, or lock-dirty. A receipt is not a reply or proof of reading. A local final is not delivery:"
247
251
  ],
248
252
  "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
249
253
  "timeoutSeconds": 300,
250
254
  "signature": "[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL]",
251
255
  "signatureSource": "scripts/check-entwurf-v2-surface.ts"
256
+ },
257
+ {
258
+ "claim": "PI-RECEIVED-REPLY-GUIDANCE",
259
+ "title": "the Pi receiver omits guidance for a replyable sender, so a local final can be mistaken for a reply",
260
+ "subject": "pi-extensions/lib/entwurf-control-rpc.ts",
261
+ "find": ["sender.replyable === true"],
262
+ "replace": ["sender.replyable === false"],
263
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
264
+ "timeoutSeconds": 300,
265
+ "signature": "[QK:PI-RECEIVED-REPLY-GUIDANCE]",
266
+ "signatureSource": "pi-extensions/entwurf-control.test.ts",
267
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
268
+ },
269
+ {
270
+ "claim": "MAILBOX-RECEIVED-REPLY-GUIDANCE",
271
+ "title": "the mailbox receiver's guidance drops the local-final boundary",
272
+ "subject": "pi-extensions/lib/meta-mailbox-body.ts",
273
+ "find": ["a local final is not delivery."],
274
+ "replace": ["a local final is delivery."],
275
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
276
+ "timeoutSeconds": 300,
277
+ "signature": "[QK:MAILBOX-RECEIVED-REPLY-GUIDANCE]",
278
+ "signatureSource": "pi-extensions/lib/entwurf-v2-mailbox.test.ts",
279
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
280
+ },
281
+ {
282
+ "claim": "NATIVE-PUSH-RECEIVED-REPLY-GUIDANCE",
283
+ "title": "the native-push receiver's guidance drops the no-send boundary",
284
+ "subject": "pi-extensions/lib/meta-mailbox-body.ts",
285
+ "find": ["Honor routing and no-send instructions;"],
286
+ "replace": ["Ignore routing and no-send instructions;"],
287
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
288
+ "timeoutSeconds": 300,
289
+ "signature": "[QK:NATIVE-PUSH-RECEIVED-REPLY-GUIDANCE]",
290
+ "signatureSource": "pi-extensions/lib/entwurf-v2-native-push.test.ts",
291
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
252
292
  }
253
293
  ]
254
294
  }