@junghanacs/entwurf 0.41.0 → 0.41.2

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 (91) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +7 -5
  3. package/CHANGELOG.md +426 -0
  4. package/DELIVERY.md +37 -20
  5. package/README.md +83 -81
  6. package/VERIFY.md +10 -10
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +1 -1
  9. package/docs/external-mcp-host.md +1 -1
  10. package/docs/setup-clean-host.md +95 -29
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -11
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +10 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +23 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +7 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +21 -10
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +4 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +5 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +6 -4
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +13 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +2 -2
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +1 -1
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +25 -17
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +94 -28
  25. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +57 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +2 -2
  27. package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +12 -12
  28. package/mcp/entwurf-bridge/src/index.ts +14 -11
  29. package/native/win32-x64/entwurf_win.node +0 -0
  30. package/native/win32-x64/manifest.json +2 -2
  31. package/package.json +15 -14
  32. package/pi/pi-durable/carrier/tui.js +18 -3
  33. package/pi/pi-durable/overlay/upstream-pin.json +12 -12
  34. package/pi-extensions/lib/acp/backend-adapter.ts +5 -3
  35. package/pi-extensions/lib/acp/backend.ts +12 -9
  36. package/pi-extensions/lib/entwurf-self-address.ts +13 -0
  37. package/pi-extensions/lib/entwurf-v2-contract.ts +25 -2
  38. package/pi-extensions/lib/entwurf-v2-decider.ts +7 -0
  39. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -0
  40. package/pi-extensions/lib/fresh-call-composition.ts +21 -10
  41. package/pi-extensions/lib/herdr-fresh-call.ts +4 -2
  42. package/pi-extensions/lib/meta-sender-identity.ts +4 -0
  43. package/pi-extensions/lib/mux-fresh-call.ts +6 -4
  44. package/pi-extensions/lib/native-push/register.ts +17 -2
  45. package/pi-extensions/lib/omp-fresh-preflight.ts +2 -2
  46. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +1 -1
  47. package/pi-extensions/lib/remote-pair-receive.ts +27 -19
  48. package/pi-extensions/lib/remote-pair-send.ts +117 -30
  49. package/pi-extensions/meta-bridge-omp.ts +69 -7
  50. package/pi-extensions/meta-bridge-pi-durable.ts +3 -3
  51. package/run.sh +218 -49
  52. package/scripts/check-agy-sender-identity.ts +6 -4
  53. package/scripts/check-entwurf-self-address.ts +34 -3
  54. package/scripts/check-entwurf-v2-contract.ts +24 -2
  55. package/scripts/check-entwurf-v2-production.ts +45 -31
  56. package/scripts/check-gate-qualification.ts +11 -9
  57. package/scripts/check-native-push-adapter.ts +4 -0
  58. package/scripts/check-native-push-register.ts +32 -82
  59. package/scripts/check-remote-pair-installed.ts +2 -2
  60. package/scripts/dev-bin.sh +112 -1
  61. package/scripts/lib/chain-terminus-publication.ts +58 -0
  62. package/scripts/lib/codex-fresh-live-protocol.ts +33 -2
  63. package/scripts/lib/codex-fresh-source-receipts.ts +112 -3
  64. package/scripts/lib/pi-durable-fresh-live-receipts.ts +29 -1
  65. package/scripts/mutants/chain-terminus-publication.json +17 -0
  66. package/scripts/mutants/codex-native.json +98 -0
  67. package/scripts/mutants/control-send-receipt.json +3 -3
  68. package/scripts/mutants/mux-fresh-call.json +16 -1
  69. package/scripts/mutants/native-push-paused.json +17 -0
  70. package/scripts/mutants/omp-fresh.json +13 -2
  71. package/scripts/mutants/pack-install.json +50 -4
  72. package/scripts/mutants/pi-durable.json +60 -0
  73. package/scripts/mutants/pi-floor.json +5 -5
  74. package/scripts/mutants/remote-pair-display.json +3 -3
  75. package/scripts/mutants/remote-pair-receive.json +41 -11
  76. package/scripts/mutants/remote-pair-roundtrip.json +67 -7
  77. package/scripts/mutants/remote-pair-send.json +156 -17
  78. package/scripts/mutants/remote-pair-transport.json +4 -4
  79. package/scripts/mutants/source-install.json +66 -0
  80. package/scripts/remote-pair-receive.ts +12 -12
  81. package/scripts/smoke-agy-install-state.sh +121 -0
  82. package/scripts/smoke-agy-native-push-live.ts +13 -0
  83. package/scripts/smoke-codex-fresh-live.ts +52 -28
  84. package/scripts/smoke-entwurf-chain-live.ts +7 -9
  85. package/scripts/smoke-herdr-fresh-call-live.ts +4 -4
  86. package/scripts/smoke-herdr-plugin-build-live.ts +179 -50
  87. package/scripts/smoke-herdr-raw-install-live.sh +10 -3
  88. package/scripts/smoke-mux-lifecycle-live.ts +140 -8
  89. package/scripts/smoke-omp-fresh-live.ts +5 -4
  90. package/scripts/smoke-pi-durable-fresh-live.ts +29 -7
  91. package/scripts/smoke-setup-verdict.sh +27 -2
@@ -27,6 +27,72 @@
27
27
  "timeoutSeconds": 300,
28
28
  "signature": "[QK:SOURCE-OPERATOR-FAILS-LOUD]",
29
29
  "signatureSource": "scripts/smoke-agy-install-state.sh"
30
+ },
31
+ {
32
+ "claim": "SOURCE-SAME-TARGET-PASSES",
33
+ "title": "the exclusivity check compares the unresolved entry path again, so a duplicate PATH entry or a second link to this very checkout reads as another root and blocks a correct developer install",
34
+ "subject": "scripts/dev-bin.sh",
35
+ "find": [" if os.path.lexists(path) and os.path.realpath(path) != target:"],
36
+ "replace": [" if os.path.lexists(path) and os.path.abspath(path) != target:"],
37
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
38
+ "timeoutSeconds": 300,
39
+ "signature": "[QK:SOURCE-SAME-TARGET-PASSES]",
40
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
41
+ },
42
+ {
43
+ "claim": "SOURCE-LATER-ROOT-FAILS",
44
+ "title": "the exclusivity check reads only the head of PATH again, so an old global shim later in PATH stays selectable by a shell with another PATH order while setup reports a developer install",
45
+ "subject": "scripts/dev-bin.sh",
46
+ "find": ["for d in os.environ.get(\"PATH\", \"\").split(\":\"):"],
47
+ "replace": ["for d in os.environ.get(\"PATH\", \"\").split(\":\")[:1]:"],
48
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
49
+ "timeoutSeconds": 300,
50
+ "signature": "[QK:SOURCE-LATER-ROOT-FAILS]",
51
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
52
+ },
53
+ {
54
+ "claim": "SOURCE-GLOBAL-BIN-OTHER-ROOT",
55
+ "title": "the package manager's reported global bin dir is queried but never searched, so another root there passes whenever that dir is absent from this PATH",
56
+ "subject": "scripts/dev-bin.sh",
57
+ "find": [" add(to_bin(value), label)\n"],
58
+ "replace": [" pass\n"],
59
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
60
+ "timeoutSeconds": 300,
61
+ "signature": "[QK:SOURCE-GLOBAL-BIN-OTHER-ROOT]",
62
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
63
+ },
64
+ {
65
+ "claim": "SOURCE-GLOBAL-QUERY-REFUSED",
66
+ "title": "a package manager that fails or hangs is read as not installed again, so an uninspectable global bin dir yields a clean developer-install verdict",
67
+ "subject": "scripts/dev-bin.sh",
68
+ "find": [" errors.append(value)\n"],
69
+ "replace": [" skipped.append(value)\n"],
70
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
71
+ "timeoutSeconds": 300,
72
+ "signature": "[QK:SOURCE-GLOBAL-QUERY-REFUSED]",
73
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
74
+ },
75
+ {
76
+ "claim": "SOURCE-EXCLUSIVE-WIRED",
77
+ "title": "source setup's bins step drops the exclusivity verdict again and returns its pre-repair success, so another selectable root is reported as a developer install",
78
+ "subject": "run.sh",
79
+ "find": [" [ \"$rc\" -eq 0 ] && return \"$ex_rc\"\n"],
80
+ "replace": [" [ \"$rc\" -eq 0 ] && return 0\n"],
81
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
82
+ "timeoutSeconds": 300,
83
+ "signature": "[QK:SOURCE-EXCLUSIVE-WIRED]",
84
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
85
+ },
86
+ {
87
+ "claim": "SOURCE-MANIFEST-UNREADABLE-REFUSED",
88
+ "title": "an unreadable or wrong-shaped package manifest is read as zero bin names again, so the exclusivity check finds no other root by checking nothing and source setup reports a developer install",
89
+ "subject": "scripts/dev-bin.sh",
90
+ "find": [" sys.exit(6) # an unreadable manifest is never \"0 names, no other root\"\n"],
91
+ "replace": [" bins = {} # regressed: an unreadable manifest read as zero names\n"],
92
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
93
+ "timeoutSeconds": 300,
94
+ "signature": "[QK:SOURCE-MANIFEST-UNREADABLE-REFUSED]",
95
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
30
96
  }
31
97
  ]
32
98
  }
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * remote-pair-receive — `entwurf remote-pair-receive --pair <absolute file>` (#107 rail 1).
3
+ * remote-pair-receive — `entwurf remote-pair-receive --pair <absolute file>` (#107 rail 1, #149).
4
4
  *
5
- * The receiving host's entry for one declared remote pair. The sending host runs it over one
5
+ * The receiving host's entry for its declared remote pairs. The sending host runs it over one
6
6
  * SSH call and writes one JSON claim to stdin. This entry reads the operator-named pair
7
- * declaration once, applies the pair scope check, and hands a matching claim to the existing
8
- * v2 dispatch. The scope and trust model are in pi-extensions/lib/remote-pair-receive.ts.
7
+ * declaration once, selects the one route whose two ends are the claim's, and hands a matching
8
+ * claim to the existing v2 dispatch. The scope and trust model are in pi-extensions/lib/remote-pair-receive.ts.
9
9
  *
10
10
  * Scope: `--pair` must name THIS host's own opt-in file (`remotePairOptInPath()`), absolute and
11
- * normalized-equal; any other path — even a valid declaration — is exit 2. That one file is the pair
12
- * this host accepts and, because it names the sender's host as its route, the declared route back.
11
+ * normalized-equal; any other path — even a valid declaration — is exit 2. That one file holds the
12
+ * pairs this host accepts and, because each route names the sender's host, the declared route back.
13
13
  * The sender's declaration carries the path only so the argv shape is explicit; it selects nothing.
14
14
  *
15
15
  * EXIT CONTRACT (stdout carries a JSON receipt line only where named):
@@ -47,7 +47,7 @@ if (args.length !== 2 || args[0] !== "--pair") {
47
47
  }
48
48
  const pairFile = args[1];
49
49
 
50
- let decl: RemotePairDeclaration;
50
+ let decls: RemotePairDeclaration[];
51
51
  let claim: ReturnType<typeof parseRemoteClaim>;
52
52
  try {
53
53
  // The scope is THIS host's own opt-in, never a file the sender picks: `--pair` must name it
@@ -56,17 +56,17 @@ try {
56
56
  if (!path.isAbsolute(pairFile) || path.normalize(pairFile) !== own) {
57
57
  throw new Error(`--pair must be this host's opt-in file ${own} (got ${pairFile})`);
58
58
  }
59
- const route = readRemotePairOptIn(own);
60
- if (route === null) throw new Error(`this host has no remote pair opt-in (${own} is absent)`);
61
- decl = { remoteGardenId: route.remoteGardenId, ssh: route.ssh, localGardenId: route.localGardenId };
59
+ const optIn = readRemotePairOptIn(own);
60
+ if (optIn === null) throw new Error(`this host has no remote pair opt-in (${own} is absent)`);
61
+ decls = optIn.routes.map((r) => ({ remoteGardenId: r.remoteGardenId, ssh: r.ssh, localGardenId: r.localGardenId }));
62
62
  claim = parseRemoteClaim(readFileSync(0, "utf8"));
63
63
  } catch (err) {
64
64
  emit({ kind: "invalid", detail: err instanceof Error ? err.message : String(err) }, 2);
65
65
  }
66
66
 
67
- const receipt = await receiveRemoteClaim(claim, decl, {
67
+ const receipt = await receiveRemoteClaim(claim, decls, {
68
68
  dispatch: (params, sender) => runAndRenderEntwurfV2FromSurface(params, { senderProvider: () => sender }),
69
- // The scope file IS this host's own opt-in, and it names the sender's host as its route: a
69
+ // The scope file IS this host's own opt-in, and the matched route names the sender's host: a
70
70
  // matching claim always has a declared route back. Declared, not probed.
71
71
  replyRoute: "declared",
72
72
  });
@@ -461,6 +461,126 @@ export ENTWURF_AGY_STATUSLINE_TARGET="$SB/fake-status.sh"
461
461
  export ENTWURF_AGY_IMPRINT_TARGET="$SB/fake-aux.sh"
462
462
  export ENTWURF_COPILOT_STATUSLINE_TARGET="$SB/fake-aux.sh"
463
463
 
464
+ # Hermetic PATH for every dev-bin cell: the host's own entwurf entrypoints (e.g. a real
465
+ # ~/.local/bin/entwurf into some checkout) and its real pnpm/npm global bin dirs must not leak into
466
+ # the exclusivity verdict. Drop every host PATH dir that carries a package bin name, and put fixture
467
+ # package managers first; FAKE_PM_MODE picks their answer. Restored after the dev-bin cells.
468
+ DEVBIN_ORIG_PATH="$PATH"
469
+ PACKAGE_BINS="$(python3 -c 'import json,sys; print(" ".join(json.load(open(sys.argv[1]))["bin"]))' "$REPO_DIR/package.json")"
470
+ HERMETIC_PATH=""
471
+ IFS=':' read -r -a _dirs <<<"$PATH"
472
+ for _d in "${_dirs[@]}"; do
473
+ [ -n "$_d" ] || continue
474
+ for _b in $PACKAGE_BINS; do { [ -e "$_d/$_b" ] || [ -L "$_d/$_b" ]; } && continue 2; done
475
+ HERMETIC_PATH="${HERMETIC_PATH:+$HERMETIC_PATH:}$_d"
476
+ done
477
+ unset _dirs _d _b
478
+ FAKE_PM="$SB/fake-pm"; PM_ROOT="$SB/pm"; mkdir -p "$FAKE_PM"
479
+ for _pm in pnpm npm; do
480
+ case "$_pm" in pnpm) _q="bin -g"; _a="$PM_ROOT/pnpm-bin" ;; npm) _q="prefix -g"; _a="$PM_ROOT/npm-prefix" ;; esac
481
+ cat > "$FAKE_PM/$_pm" <<SH
482
+ #!/usr/bin/env bash
483
+ [ "\$*" = "$_q" ] || { echo "fixture $_pm answers only: $_q" >&2; exit 64; }
484
+ case "\${FAKE_PM_MODE:-ok}" in
485
+ ok) [ "$_pm" = npm ] || case ":\$PATH:" in *":$_a:"*) ;; *) echo "[ERROR] The configured global bin directory \"$_a\" is not in PATH" >&2; exit 1 ;; esac
486
+ echo "$_a" ;;
487
+ fail) echo "ERR_FIXTURE_$_pm boom" >&2; exit 1 ;;
488
+ hang) exec sleep 30 ;;
489
+ esac
490
+ SH
491
+ chmod +x "$FAKE_PM/$_pm"
492
+ done
493
+ unset _pm _q _a
494
+ mkdir -p "$PM_ROOT/pnpm-bin" # an existing, empty pnpm global bin dir
495
+ export PATH="$FAKE_PM:$HERMETIC_PATH:$PM_ROOT/pnpm-bin" # pnpm 11 answers `bin -g` only when that dir is on PATH
496
+ want "dev-bin hermetic PATH: no package bin name resolves, and pnpm/npm are the fixtures" \
497
+ "! command -v entwurf >/dev/null && [ \"\$(command -v pnpm)\" = '$FAKE_PM/pnpm' ] && [ \"\$(command -v npm)\" = '$FAKE_PM/npm' ]"
498
+
499
+ # J-X: developer-install exclusivity (dev-bin.sh exclusive). A certified PATH winner alone let an
500
+ # old global shim LATER in PATH stay selectable by a shell with another PATH order (non-interactive
501
+ # SSH picked it). Every cell links by NAME so an operator-set mutant elsewhere cannot reach here.
502
+ bash "$DEVBIN" expose entwurf >/dev/null 2>&1
503
+ bash "$DEVBIN" expose entwurf-bridge >/dev/null 2>&1
504
+ SAME_DIR="$SB/same-root"; mkdir -p "$SAME_DIR"
505
+ ln -s "$SB/fake-entwurf.sh" "$SAME_DIR/entwurf" # a second link, same target
506
+ ln -s "$DLINK" "$SAME_DIR/entwurf-bridge" # a chain through our link, same target
507
+ set +e; OUT="$(PATH="$DBIN_DIR:$DBIN_DIR:$SAME_DIR:$PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
508
+ want "dev-bin exclusive: duplicate PATH entries and links resolving to our target are the same install [QK:SOURCE-SAME-TARGET-PASSES]" \
509
+ "[ '$RC' -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'no other root under the 6 package bin names'"
510
+ want "dev-bin exclusive: an existing-but-empty package-manager global bin is checked and clean, and the PASS names its limit" \
511
+ "printf '%s' \"\$OUT\" | grep -q 'pnpm global bin $PM_ROOT/pnpm-bin' && printf '%s' \"\$OUT\" | grep -q 'another shell.s PATH or another package manager is not covered'"
512
+ TOOLS_ONLY="$SB/tools-only"; mkdir -p "$TOOLS_ONLY"
513
+ for _t in bash python3 dirname env; do ln -s "$(command -v "$_t")" "$TOOLS_ONLY/$_t"; done; unset _t
514
+ set +e; OUT="$(PATH="$TOOLS_ONLY:$DBIN_DIR" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
515
+ want "dev-bin exclusive: an absent package manager is a named not-checked line, not a failure" \
516
+ "[ '$RC' -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'not checked: pnpm not on PATH' && printf '%s' \"\$OUT\" | grep -q 'not checked: npm not on PATH'"
517
+ rm -rf "$SAME_DIR"
518
+
519
+ OTHER_DIR="$SB/old-global/bin"; mkdir -p "$OTHER_DIR"
520
+ printf '#!/bin/sh\nexec /old/global/entwurf "$@"\n' > "$OTHER_DIR/entwurf"; chmod +x "$OTHER_DIR/entwurf"
521
+ want "dev-bin control: with an old shim LATER in PATH the winner is still ours, so verify alone passes" \
522
+ "PATH='$DBIN_DIR:$OTHER_DIR':\"\$PATH\" bash '$DEVBIN' verify entwurf >/dev/null 2>&1"
523
+ set +e; OUT="$(PATH="$DBIN_DIR:$OTHER_DIR:$PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
524
+ want "dev-bin exclusive: an old root LATER in PATH is named and refused, not hidden behind our winner [QK:SOURCE-LATER-ROOT-FAILS]" \
525
+ "[ '$RC' -eq 5 ] && printf '%s' \"\$OUT\" | grep -qF 'another entwurf root is still selectable: $OTHER_DIR/entwurf'"
526
+ want "dev-bin exclusive: the refusal removes nothing and says so" \
527
+ "[ \"\$(sed -n 2p '$OTHER_DIR/entwurf')\" = 'exec /old/global/entwurf \"\$@\"' ] && [ -L '$OLINK' ] && printf '%s' \"\$OUT\" | grep -q 'nothing was removed'"
528
+ mkdir -p "$SB/other-checkout/mcp/entwurf-bridge"; printf '#!/bin/sh\n' > "$SB/other-checkout/mcp/entwurf-bridge/start.sh"
529
+ rm -f "$OTHER_DIR/entwurf"; ln -s "$SB/other-checkout/mcp/entwurf-bridge/start.sh" "$OTHER_DIR/entwurf-bridge"
530
+ set +e; OUT="$(PATH="$DBIN_DIR:$OTHER_DIR:$PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
531
+ want "dev-bin exclusive: a helper name from another checkout is refused like the operator" \
532
+ "[ '$RC' -eq 5 ] && printf '%s' \"\$OUT\" | grep -qF '$OTHER_DIR/entwurf-bridge -> $SB/other-checkout/mcp/entwurf-bridge/start.sh'"
533
+ rm -rf "$SB/old-global"
534
+
535
+ mkdir -p "$PM_ROOT/npm-prefix/bin"; printf '#!/bin/sh\n' > "$SB/other-checkout/run.sh"
536
+ ln -s "$SB/other-checkout/run.sh" "$PM_ROOT/npm-prefix/bin/entwurf" # npm's <prefix>/bin, OFF this PATH
537
+ set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
538
+ want "dev-bin exclusive: another root in npm's global bin dir is refused although that dir is not on this PATH [QK:SOURCE-GLOBAL-BIN-OTHER-ROOT]" \
539
+ "[ '$RC' -eq 5 ] && printf '%s' \"\$OUT\" | grep -qF '$PM_ROOT/npm-prefix/bin/entwurf -> $SB/other-checkout/run.sh' && printf '%s' \"\$OUT\" | grep -qx ' seen via: npm global bin'"
540
+ want "dev-bin exclusive: the removal hint is conditional on the package manager, never asserted" \
541
+ "printf '%s' \"\$OUT\" | grep -qF 'if a global @junghanacs/entwurf installed it: npm uninstall -g @junghanacs/entwurf' && printf '%s' \"\$OUT\" | grep -q 'otherwise remove it through whoever owns it'"
542
+ rm -rf "$PM_ROOT/npm-prefix"
543
+
544
+ set +e; OUT="$(FAKE_PM_MODE=fail PATH="$DBIN_DIR:$PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
545
+ want "dev-bin exclusive: a package manager that cannot answer is refused, not read as clean [QK:SOURCE-GLOBAL-QUERY-REFUSED]" \
546
+ "[ '$RC' -eq 6 ] && printf '%s' \"\$OUT\" | grep -qF 'could not inspect a package manager — \`pnpm bin -g\` exited 1: ERR_FIXTURE_pnpm boom'"
547
+ set +e; OUT="$(FAKE_PM_MODE=hang ENTWURF_DEV_BIN_PM_TIMEOUT=1 PATH="$DBIN_DIR:$PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
548
+ want "dev-bin exclusive: a package manager that hangs is bounded and refused" \
549
+ "[ '$RC' -eq 6 ] && printf '%s' \"\$OUT\" | grep -q 'did not answer within 1s'"
550
+ set +e; OUT="$(PATH="$DBIN_DIR:$FAKE_PM:$HERMETIC_PATH" bash "$DEVBIN" exclusive 2>&1)"; RC=$?; set -e
551
+ want "dev-bin exclusive: pnpm refusing because its global bin dir is off PATH is an inspect failure, not a clean dir" \
552
+ "[ '$RC' -eq 6 ] && printf '%s' \"\$OUT\" | grep -qF 'is not in PATH'"
553
+
554
+ # The package manifest is read by the verdict step itself: an unreadable or wrong-shaped one is an
555
+ # inspect failure, never "0 names, no other root". Driven on a copy of THIS dev-bin.sh beside a
556
+ # sandbox package.json, so the repo's own manifest is never touched.
557
+ MANI="$SB/manifest-copy"; mkdir -p "$MANI/scripts"; cp "$DEVBIN" "$MANI/scripts/dev-bin.sh"
558
+ cp "$REPO_DIR/package.json" "$MANI/package.json"
559
+ set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$MANI/scripts/dev-bin.sh" exclusive 2>&1)"; RC=$?; set -e
560
+ want "dev-bin exclusive control: the copied script with the real manifest checks all 6 names and passes" \
561
+ "[ '$RC' -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'no other root under the 6 package bin names'"
562
+ printf 'not json{' > "$MANI/package.json"
563
+ set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$MANI/scripts/dev-bin.sh" exclusive 2>&1)"; RC=$?; set -e
564
+ want "dev-bin exclusive: an unreadable package manifest is refused, never read as zero names [QK:SOURCE-MANIFEST-UNREADABLE-REFUSED]" \
565
+ "[ '$RC' -eq 6 ] && printf '%s' \"\$OUT\" | grep -qF 'could not inspect the package manifest $MANI/package.json' && ! printf '%s' \"\$OUT\" | grep -q '0 package bin names'"
566
+ for _shape in '{"name":"x"}' '{"bin":"./run.sh"}' '{"bin":{}}' '{"bin":{"entwurf-bridge":"./x"}}'; do
567
+ printf '%s' "$_shape" > "$MANI/package.json"
568
+ set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$MANI/scripts/dev-bin.sh" exclusive 2>&1)"; RC=$?; set -e
569
+ want "dev-bin exclusive: manifest $_shape is refused as uninspectable" \
570
+ "[ '$RC' -eq 6 ] && printf '%s' \"\$OUT\" | grep -qF 'could not inspect the package manifest'"
571
+ done
572
+ unset _shape; rm -rf "$MANI"
573
+
574
+ OTHER_DIR="$SB/old-global/bin"; mkdir -p "$OTHER_DIR"
575
+ printf '#!/bin/sh\nexec /old/global/entwurf "$@"\n' > "$OTHER_DIR/entwurf"; chmod +x "$OTHER_DIR/entwurf"
576
+ set +e; OUT="$(PATH="$DBIN_DIR:$OTHER_DIR:$PATH" bash "$REPO_DIR/run.sh" expose-dev-bin 2>&1)"; RC=$?; set -e
577
+ want "source setup's bins step fails when another root is still selectable [QK:SOURCE-EXCLUSIVE-WIRED]" \
578
+ "[ '$RC' -eq 5 ] && printf '%s' \"\$OUT\" | grep -qF 'another entwurf root is still selectable: $OTHER_DIR/entwurf'"
579
+ rm -rf "$SB/old-global" "$SB/other-checkout" "$TOOLS_ONLY"
580
+ bash "$DEVBIN" remove >/dev/null 2>&1 # the helper links expose-dev-bin just made
581
+ bash "$DEVBIN" remove entwurf >/dev/null 2>&1 # by NAME, like the expose above: the
582
+ bash "$DEVBIN" remove entwurf-bridge >/dev/null 2>&1 # managed list itself may be under test
583
+
464
584
  # J-0: the setup wrapper calls no-arg expose. This is the cross-contract cell the #82 regression
465
585
  # lacked: an npm consumer gets `entwurf` from package bin linking, and source setup must expose the
466
586
  # same operator command without a second global package install.
@@ -578,6 +698,7 @@ ok "dev-bin migrate: foreign legacy refused (linkPath basename != entwurf-bridge
578
698
  rm -f "$LEGACY" "$DLINK" "$DSTATE"
579
699
  bash "$DEVBIN" remove >/dev/null 2>&1
580
700
  unset ENTWURF_DEV_BIN_DIR ENTWURF_OPERATOR_TARGET ENTWURF_BRIDGE_TARGET ENTWURF_AGY_STATUSLINE_TARGET
701
+ export PATH="$DEVBIN_ORIG_PATH"
581
702
  unset ENTWURF_AGY_IMPRINT_TARGET ENTWURF_COPILOT_STATUSLINE_TARGET
582
703
 
583
704
  # ── K: permission grant — the OTHER half of a usable bridge ───────────────────
@@ -48,6 +48,7 @@ import * as os from "node:os";
48
48
  import * as path from "node:path";
49
49
  import { fileURLToPath } from "node:url";
50
50
  import type { SenderEnvelope } from "../pi-extensions/lib/entwurf-control-rpc.ts";
51
+ import { nativePushPaused } from "../pi-extensions/lib/entwurf-v2-contract.ts";
51
52
  import { makeProductionEntwurfV2Deps } from "../pi-extensions/lib/entwurf-v2-production.ts";
52
53
  import type { EntwurfV2RunResult } from "../pi-extensions/lib/entwurf-v2-runner.ts";
53
54
  import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
@@ -79,6 +80,18 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
79
80
  }
80
81
 
81
82
  async function main(): Promise<void> {
83
+ // #152: agy native-push is PAUSED by product policy, so this positive LIVE acceptance cannot
84
+ // run — every cell below would drive the paused rail. It exits NON-ZERO by name (neither a PASS
85
+ // nor the protocol SKIP), and touches no agy process, port or conversation. The pause itself is
86
+ // proven deterministically beside the behavior (pi-extensions/lib/native-push/paused.test.ts).
87
+ if (nativePushPaused("antigravity")) {
88
+ console.error(
89
+ "[entwurf:paused] smoke-agy-native-push-live — agy native-push is paused (#152: its language " +
90
+ "server refused entwurf's measured external calls for a missing CSRF token). This LIVE acceptance does not run while " +
91
+ "the pause holds; it is not a PASS.",
92
+ );
93
+ process.exit(2);
94
+ }
82
95
  if (process.env.LIVE !== "1") {
83
96
  skipLive(
84
97
  "smoke-agy-native-push-live",
@@ -75,12 +75,14 @@ import {
75
75
  } from "../pi-extensions/lib/native-push/codex-ws-client.ts";
76
76
  import {
77
77
  buildCodexInstruction,
78
+ buildInitialPiBootstrapTask,
78
79
  buildInitialPiPhaseOne,
79
80
  buildInitialPiRelay,
80
81
  runCleanupStages,
81
82
  selectReport,
82
83
  } from "./lib/codex-fresh-live-protocol.ts";
83
84
  import {
85
+ assertBootstrapOrder,
84
86
  assertExactSourceToolCalls,
85
87
  assertSourceCallScopes,
86
88
  codexCallbackSenders,
@@ -978,11 +980,7 @@ async function run(): Promise<void> {
978
980
  backend: "pi",
979
981
  model: piModel,
980
982
  cwd: scratch,
981
- task:
982
- "Session-info already supplies device and time. Do not recheck time, cwd, or identity with shell or other tools. " +
983
- "Your FIRST tool call must be mcp__entwurf_bridge__entwurf_callback with no arguments, and nothing before it. " +
984
- `After your required callback receipt, end the turn and wait passively for one addressed message containing ${initialPiWaitToken}. ` +
985
- "That message is the only authority to open the Codex sibling. Do not call shell, sleep, terminal, or any tool to wait.",
983
+ task: buildInitialPiBootstrapTask(initialPiWaitToken),
986
984
  });
987
985
  receipts["2-initial-pi-launch"] = initialPiLaunch.text;
988
986
  ok(
@@ -1082,7 +1080,8 @@ async function run(): Promise<void> {
1082
1080
  const codexWaitToken = `CODEX-WAIT-${Math.random().toString(36).slice(2, 10).toUpperCase()}`;
1083
1081
  const finalToken = `CODEX-PI-FINAL-${Math.random().toString(36).slice(2, 10).toUpperCase()}`;
1084
1082
  expectedReportTokens.push(finalToken);
1085
- const outboundPiTask = "After your automatic callback succeeds, answer ACK and stop. Do not open another sibling.";
1083
+ const outboundPiTask =
1084
+ "Your automatic callback is the whole task and there is no result to send. After it succeeds, end the turn: no message, no report, no further tool call. Do not open another sibling.";
1086
1085
  const codexTask =
1087
1086
  `After your required callback receipt, end the turn and wait passively for the addressed instruction containing ${codexWaitToken}. ` +
1088
1087
  "Do not call shell, sleep, terminal, or any tool to wait.";
@@ -1439,25 +1438,33 @@ async function run(): Promise<void> {
1439
1438
  const observed = await readCodexThread(codexSocketPath, codexThreadId, true);
1440
1439
  return codexThreadIsTerminal(observed) ? observed : null;
1441
1440
  });
1441
+ // THE BOOTSTRAP ORDER, THEN THE ROLES — two leaves, composed. `[GLG 직접, 2026-10-24]` a fresh
1442
+ // sibling may check its caller with the read-only listing the framing offers, then calls back,
1443
+ // then works. `assertBootstrapOrder` admits only that prefix (successful exact `entwurf_peers`
1444
+ // checks), requires the exact callback once and successfully, and hands the rest to the
1445
+ // unchanged exact-once role audit — so `bash true` before the callback (the r3 RED) is still
1446
+ // refused, by the order leaf now rather than by a count. On Pi the receipts carry transcript
1447
+ // ordinals, so this also proves every peers RESULT was recorded before the callback was called,
1448
+ // and the callback RESULT before every task call.
1449
+ //
1450
+ // The callback is the ZERO-ARGUMENT eighth verb and `{}` is compared byte-exactly: a target, a
1451
+ // message or a `wants_reply` typed here is a parameter the verb does not have. Nothing is
1452
+ // weakened by the nonce not being in THIS expectation: the correlation is proven earlier and from
1453
+ // the delivered body — `the initial Pi callback carries the exact launch nonce` reads the
1454
+ // fixture's own mailbox, requires exactly one exact match and refuses duplicates, and the two
1455
+ // cells beside it bind that callback's sender envelope to a real pi V3 citizen.
1456
+ const initialPiSources = piSourceToolReceipts(readInitialPiEntries());
1457
+ const initialPiTaskCalls = assertBootstrapOrder(
1458
+ initialPiSources,
1459
+ // The guided task asked this Pi to take the offered check, so a zero-check PASS is not positive evidence.
1460
+ { peersArguments: {}, callback: { toolName: "entwurf_callback", arguments: {} }, minPeersPrefix: 1 },
1461
+ "initial Pi bootstrap order",
1462
+ );
1463
+ // The verified prefix count: the order leaf admitted exactly these many leading peers checks.
1464
+ const piPeersPrefix = initialPiSources.length - initialPiTaskCalls.length - 1;
1442
1465
  assertExactSourceToolCalls(
1443
- piSourceToolReceipts(readInitialPiEntries()),
1466
+ initialPiTaskCalls,
1444
1467
  [
1445
- // 0.24.0 moved a pi sibling's first action to the ZERO-ARGUMENT eighth verb, and this
1446
- // smoke's own launch receipt says so in the same run: "its first action is the
1447
- // zero-argument callback verb (codex is the one arm that still sends the nonce as
1448
- // arguments)". The Codex audit below therefore keeps its `entwurf_v2` shape, and only
1449
- // this one moves. `{}` is matched by deep-strict equality, so it pins the verb's actual
1450
- // contract — a target or message typed here would be a second address axis and the verb
1451
- // refuses it by name.
1452
- //
1453
- // Nothing is weakened by dropping the nonce from THIS expectation: the correlation is
1454
- // proven earlier and from the delivered body, which is where a nonce can actually be
1455
- // corrupted — `the initial Pi callback carries the exact launch nonce` (:970-973) reads
1456
- // the fixture's own mailbox, requires exactly one exact match and refuses duplicates,
1457
- // and the two cells beside it bind that callback's sender envelope to a real pi V3
1458
- // citizen. This audit's job is the different one its name says: exactly these calls,
1459
- // once each, and nothing else.
1460
- { toolName: "entwurf_callback", arguments: {} },
1461
1468
  {
1462
1469
  toolName: "entwurf_fresh_call",
1463
1470
  arguments: {
@@ -1479,10 +1486,18 @@ async function run(): Promise<void> {
1479
1486
  ],
1480
1487
  "initial Pi final exact-once audit",
1481
1488
  );
1482
- assertExactSourceToolCalls(
1483
- codexSourceToolReceipts(codexThread),
1484
- [
1485
- {
1489
+ // Codex: the same composition over the Entwurf MCP calls `thread/read` reports (this smoke's
1490
+ // Codex scope — no vendor-native action taxonomy). Its callback is `entwurf_v2` to the initial Pi
1491
+ // with the nonce, so `wants_reply` keeps its schema default there. `thread/read` gives item order
1492
+ // and a status at audit time only: on Codex this proves CALL ORDER and completion-at-audit; that a
1493
+ // peers result preceded the callback call, and that the callback result preceded the next call,
1494
+ // are both UNVERIFIED.
1495
+ const codexFinalSources = codexSourceToolReceipts(codexThread);
1496
+ const codexTaskCalls = assertBootstrapOrder(
1497
+ codexFinalSources,
1498
+ {
1499
+ peersArguments: {},
1500
+ callback: {
1486
1501
  toolName: "entwurf_v2",
1487
1502
  arguments: {
1488
1503
  target: initialPiGid,
@@ -1491,6 +1506,13 @@ async function run(): Promise<void> {
1491
1506
  message: codexNonce,
1492
1507
  },
1493
1508
  },
1509
+ },
1510
+ "Codex bootstrap order",
1511
+ );
1512
+ const codexPeersPrefix = codexFinalSources.length - codexTaskCalls.length - 1;
1513
+ assertExactSourceToolCalls(
1514
+ codexTaskCalls,
1515
+ [
1494
1516
  {
1495
1517
  toolName: "entwurf_fresh_call",
1496
1518
  arguments: { backend: "pi", model: piModel, task: outboundPiTask },
@@ -1507,7 +1529,9 @@ async function run(): Promise<void> {
1507
1529
  ],
1508
1530
  "Codex final exact-once audit",
1509
1531
  );
1510
- receipts["16-source-final-audit"] = "initial-pi=3/3 completed exact\ncodex=3/3 completed exact";
1532
+ receipts["16-source-final-audit"] =
1533
+ `initial-pi=bootstrap order (peers prefix=${piPeersPrefix} → callback, results-before-next-call by transcript ordinal) + 2/2 task roles exact\n` +
1534
+ `codex=bootstrap order (peers prefix=${codexPeersPrefix} → callback, call order only) + 2/2 task roles exact`;
1511
1535
  }
1512
1536
 
1513
1537
  let failure: unknown = null;
@@ -64,6 +64,7 @@ import {
64
64
  writeMetaSenderMarker,
65
65
  } from "../pi-extensions/lib/meta-session.ts";
66
66
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
67
+ import { awaitTerminusPublication } from "./lib/chain-terminus-publication.ts";
67
68
  import { skipLive } from "./lib/live-skip.ts";
68
69
 
69
70
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -390,14 +391,11 @@ async function main(): Promise<void> {
390
391
 
391
392
  // ── the chain runs on its own from here; wait for the terminus ───────────
392
393
  const boxDir = path.join(mailboxDir, gidD);
393
- const until = Date.now() + CHAIN_TIMEOUT_MS;
394
- let msgs: string[] = [];
395
- while (Date.now() < until) {
396
- msgs = (await fsp.readdir(boxDir).catch(() => [] as string[])).filter(
397
- (f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"),
398
- );
399
- if (msgs.length > 0) break;
400
- await sleep(POLL_MS);
394
+ // Body AND doorbell, inside the same deadline: enqueue publishes `.msg` first and
395
+ // `inbox.signal` last, so a body alone is a publication still in flight (P5 2026-10-24).
396
+ const { msgs, signalled } = await awaitTerminusPublication(boxDir, Date.now() + CHAIN_TIMEOUT_MS, POLL_MS);
397
+ if (msgs.length > 0 && !signalled) {
398
+ console.error(`[smoke-entwurf-chain-live] terminus at timeout: ${msgs.length} body file(s) but no inbox.signal`);
401
399
  }
402
400
  if (msgs.length === 0) {
403
401
  let ownerAlive = false;
@@ -423,7 +421,7 @@ async function main(): Promise<void> {
423
421
  console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
424
422
  }
425
423
  ok(`the chain reached the mailbox terminus (${msgs.length} .msg)`, msgs.length === 1);
426
- ok("the terminus doorbell was poked", existsSync(path.join(boxDir, "inbox.signal")));
424
+ ok("the terminus doorbell was poked", signalled);
427
425
 
428
426
  const body = await fsp.readFile(path.join(boxDir, msgs[0] as string), "utf8");
429
427
 
@@ -45,7 +45,7 @@
45
45
  * fenced `XDG_CACHE_HOME`, joined to a session by its EXACT `sessionId` — plus
46
46
  * entwurf's own mailbox artifact and hook journal. It proves the public call, the
47
47
  * identity join, the delivered callback, the caller's continuation, the child's
48
- * FIRST action, and the codex named reject. It does NOT prove the receipt text or
48
+ * callback before any work (a read-only check may precede it), and the codex named reject. It does NOT prove the receipt text or
49
49
  * the final answer content: those have no source artifact on this axis and are
50
50
  * left to GLG's manual validation rather than substituted with a screen read.
51
51
  *
@@ -853,7 +853,7 @@ async function main(): Promise<void> {
853
853
  );
854
854
  }
855
855
 
856
- // ── the child's FIRST action was the callback ───────────────────────────────
856
+ // ── the child called back before its work ───────────────────────────────────
857
857
  const childNative = String(joined?.nativeSessionId ?? childRow?.sessionValue ?? "");
858
858
  if (cell.childKind === "pi") {
859
859
  const childRecords = transcriptRecords(String(childRow?.sessionValue ?? ""));
@@ -901,8 +901,8 @@ async function main(): Promise<void> {
901
901
  // BOTH. `[측정 2026-09-17, oracle, LIVE]` this cell used to require `order[0] ===
902
902
  // "entwurf_v2"`, and a Sonnet 5 child failed it by calling `entwurf_peers` first and
903
903
  // the callback second. That is not a violation: the framing this rail now sends
904
- // OFFERS exactly that corroboration ("you can corroborate the caller first if you
905
- // want to"), so the old oracle contradicted our own prompt and would have forbidden
904
+ // OFFERS exactly that corroboration ("you may corroborate the caller before that
905
+ // call"), so the old oracle contradicted our own prompt and would have forbidden
906
906
  // the behaviour we asked for. What still must hold is everything the claim was
907
907
  // actually about — the callback lands before any work, it completes, and neither the
908
908
  // callback verb nor the delivery verb failed or timed out anywhere in the log.