@junghanacs/entwurf 0.31.0 → 0.33.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 (58) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +128 -0
  3. package/README.md +62 -117
  4. package/VERIFY.md +40 -21
  5. package/demo/README.md +1 -1
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/setup-clean-host.md +112 -95
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +1 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +1 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +8 -10
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +3 -3
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +153 -205
  13. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +6 -4
  14. package/mcp/entwurf-bridge/src/index.ts +1 -1
  15. package/package.json +14 -17
  16. package/pi/pi-durable/bootstrap.mjs +142 -19
  17. package/pi/pi-durable/carrier/LICENSE +21 -0
  18. package/pi/pi-durable/carrier/harness-setup.js +123 -0
  19. package/pi/pi-durable/carrier/prompt.js +74 -0
  20. package/pi/pi-durable/carrier/runtime.js +402 -0
  21. package/pi/pi-durable/carrier/sessions.js +56 -0
  22. package/pi/pi-durable/carrier/subagent.js +55 -0
  23. package/pi/pi-durable/carrier/tui.js +655 -0
  24. package/pi/pi-durable/carrier-relocation.mjs +160 -0
  25. package/pi/pi-durable/carrier-resolver.mjs +38 -0
  26. package/pi/pi-durable/overlay/upstream-pin.json +103 -11
  27. package/pi-extensions/lib/fresh-call-composition.ts +1 -1
  28. package/pi-extensions/lib/mux-fresh-call.ts +8 -10
  29. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +3 -3
  30. package/pi-extensions/lib/pi-durable-runtime.ts +197 -263
  31. package/pi-extensions/meta-bridge-pi-durable.ts +13 -7
  32. package/run.sh +519 -248
  33. package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
  34. package/scripts/check-fresh-call-dispatch.ts +1 -1
  35. package/scripts/check-gate-qualification.ts +86 -34
  36. package/scripts/check-herdr-activation.ts +5 -4
  37. package/scripts/check-pi-durable-contact.ts +26 -93
  38. package/scripts/check-pi-durable-receive.ts +22 -59
  39. package/scripts/check-pi-durable-send.ts +235 -75
  40. package/scripts/check-release-gate-outcomes.ts +21 -17
  41. package/scripts/emit-pi-durable-carrier.ts +355 -0
  42. package/scripts/lib/mutation-qualify.ts +21 -45
  43. package/scripts/lib/pi-durable-operator-guard.ts +89 -0
  44. package/scripts/lib/qualification-receipt.ts +7 -11
  45. package/scripts/mutants/control-send-receipt.json +1 -1
  46. package/scripts/mutants/gate-qualification.json +13 -0
  47. package/scripts/mutants/mux-fresh-call.json +13 -13
  48. package/scripts/mutants/pack-install.json +3 -3
  49. package/scripts/mutants/pi-durable.json +442 -109
  50. package/scripts/mutants/pi-floor.json +24 -2
  51. package/scripts/mutants/release-gate.json +5 -3
  52. package/scripts/pi-durable-contact-host.mjs +3 -3
  53. package/scripts/pi-durable-receive-host.mjs +3 -3
  54. package/scripts/pi-durable-send-host.mjs +76 -5
  55. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  56. package/scripts/smoke-pi-durable-fresh-live.ts +3 -3
  57. package/scripts/smoke-setup-verdict.sh +15 -15
  58. package/scripts/smoke-user-scope-citizen.sh +1 -1
@@ -1,7 +1,7 @@
1
- // G-receive host half (#129 L-receive). Runs AS the durable host, under the pinned overlay checkout's
2
- // source resolver, and is spawned only by scripts/check-pi-durable-receive.ts:
1
+ // G-receive host half (#129 L-receive). Runs AS the durable host, under this checkout's pi-durable
2
+ // carrier resolver (#130), and is spawned only by scripts/check-pi-durable-receive.ts:
3
3
  //
4
- // node --import <checkout>/packages/coding-agent/src/experimental/source-resolver.ts \
4
+ // node --import <checkout>/pi/pi-durable/carrier-resolver.mjs \
5
5
  // scripts/pi-durable-receive-host.mjs <bridge entry>
6
6
  //
7
7
  // It opens the durable app through the production bootstrap composition — which arms the contact's
@@ -1,7 +1,7 @@
1
- // G-send host half (#129 L-send first cell). Runs AS the durable host, under the pinned overlay
2
- // checkout's source resolver, and is spawned only by scripts/check-pi-durable-send.ts:
1
+ // G-send host half (#129 L-send first cell). Runs AS the durable host, under this checkout's
2
+ // pi-durable carrier resolver (#130), and is spawned only by scripts/check-pi-durable-send.ts:
3
3
  //
4
- // node --import <checkout>/packages/coding-agent/src/experimental/source-resolver.ts \
4
+ // node --import <checkout>/pi/pi-durable/carrier-resolver.mjs \
5
5
  // scripts/pi-durable-send-host.mjs <bridge entry> <prompt>
6
6
  //
7
7
  // It opens the durable app through the production bootstrap composition, then gives the app ONE
@@ -16,11 +16,16 @@
16
16
  // holds. It prints one JSON report line, holds the host alive until the gate writes `close`,
17
17
  // closes, prints `{"closed":true}` and exits. With ENTWURF_PI_DURABLE_SEND_FAULT=malformed-report
18
18
  // (the gate's control) it gives the app no input and sends a non-JSON report line instead.
19
+ //
20
+ // --native-module <file> (#130 P2): the whole packaged `main` runs instead — its argument parse, the
21
+ // operator module's initialization, the TUI load and the open, in main's own order — with ONE seam:
22
+ // the TUI runner is this driver, which gives the app the one input through the controller it is
23
+ // handed and reports the view as above. Nothing else of main is replaced.
19
24
  import { createInterface } from "node:readline";
20
- import { openDurableCitizen } from "../plugins/pi-durable/bootstrap.mjs";
25
+ import { main, openDurableCitizen } from "../plugins/pi-durable/bootstrap.mjs";
21
26
 
22
27
  const USAGE =
23
- "usage: pi-durable-send-host.mjs <bridge entry> <prompt> [--await-go] [--report-intent <callId> | --recover <callId>]";
28
+ "usage: pi-durable-send-host.mjs <bridge entry> <prompt> [--await-go] [--report-intent <callId> | --recover <callId> | --native-module <file>]";
24
29
  const [bridgeEntry, prompt, ...flags] = process.argv.slice(2);
25
30
  if (bridgeEntry === undefined || prompt === undefined) throw new Error(USAGE);
26
31
  // --await-go: report `born` once the citizen exists, and give the app its one input only after the
@@ -36,13 +41,18 @@ if (bridgeEntry === undefined || prompt === undefined) throw new Error(USAGE);
36
41
  let awaitGo = false;
37
42
  let reportIntent;
38
43
  let recoverCall;
44
+ let nativeModule;
39
45
  for (let i = 0; i < flags.length; i++) {
40
46
  if (flags[i] === "--await-go") awaitGo = true;
41
47
  else if (flags[i] === "--report-intent" && flags[i + 1] !== undefined) reportIntent = flags[++i];
42
48
  else if (flags[i] === "--recover" && flags[i + 1] !== undefined) recoverCall = flags[++i];
49
+ else if (flags[i] === "--native-module" && flags[i + 1] !== undefined) nativeModule = flags[++i];
43
50
  else throw new Error(USAGE);
44
51
  }
45
52
  if (reportIntent !== undefined && recoverCall !== undefined) throw new Error(USAGE);
53
+ if (nativeModule !== undefined && (awaitGo || reportIntent !== undefined || recoverCall !== undefined)) {
54
+ throw new Error(USAGE);
55
+ }
46
56
 
47
57
  const stdinLines = createInterface({ input: process.stdin })[Symbol.asyncIterator]();
48
58
  /** Wait for one exact line from the gate; the gate closing stdin first is a failure. */
@@ -80,6 +90,67 @@ function summarize(entry) {
80
90
  };
81
91
  }
82
92
 
93
+ /** Give the app one input through `controller` and resolve once `source` shows the turn settled. */
94
+ function submitAndSettle(source, controller) {
95
+ const before = source.current();
96
+ const noticesBefore = before.notices.length;
97
+ let submitted = false;
98
+ return new Promise((resolve) => {
99
+ const check = () => {
100
+ if (!submitted) return;
101
+ const view = source.current();
102
+ const busy = view.conversation.docs["pi.live"]?.run !== undefined;
103
+ const answered = view.conversation.entries.some((e) => e.kind === "pi.assistant");
104
+ const failed = view.notices.slice(noticesBefore).some((n) => n.level === "error");
105
+ if ((!busy && answered) || failed) {
106
+ unsubscribe();
107
+ resolve(before);
108
+ }
109
+ };
110
+ const unsubscribe = source.subscribe(check);
111
+ controller.submit(prompt, "followUp").then(() => {
112
+ submitted = true;
113
+ check();
114
+ });
115
+ });
116
+ }
117
+
118
+ if (nativeModule !== undefined) {
119
+ let code = 0;
120
+ try {
121
+ await main(
122
+ ["--provider", "loopback", "--model", "scripted", "--width", "task-wide", "--native-module", nativeModule],
123
+ {
124
+ bridgeEntry,
125
+ loadTui: async () => ({
126
+ runDurableTui: async (source, controller) => {
127
+ const before = await submitAndSettle(source, controller);
128
+ const view = source.current();
129
+ process.stdout.write(
130
+ `${JSON.stringify({
131
+ native: true,
132
+ hostPid: process.pid,
133
+ session: view.session,
134
+ conversationIdBefore: before.conversation.conversation.id,
135
+ conversationIdAfter: view.conversation.conversation.id,
136
+ entries: view.conversation.entries.filter((e) => e.kind.startsWith("pi.")).map(summarize),
137
+ busy: view.conversation.docs["pi.live"]?.run !== undefined,
138
+ notices: view.notices.map((n) => `${n.level}: ${n.message}`),
139
+ })}\n`,
140
+ );
141
+ await waitFor("close");
142
+ },
143
+ }),
144
+ },
145
+ );
146
+ } catch (error) {
147
+ code = 1;
148
+ process.stdout.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`);
149
+ }
150
+ process.stdout.write(`${JSON.stringify({ closed: true })}\n`);
151
+ process.exit(code);
152
+ }
153
+
83
154
  const citizen = await openDurableCitizen({
84
155
  bridgeEntry,
85
156
  cwd: process.cwd(),
@@ -108,7 +108,7 @@ if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then
108
108
  exit 0
109
109
  fi
110
110
 
111
- PI_SPEC="$(node -p "require('$REPO/package.json').devDependencies['@earendil-works/pi-coding-agent']")"
111
+ PI_SPEC="$(node -p "require('$REPO/package.json').dependencies['@earendil-works/pi-coding-agent']")"
112
112
  # Pinned, not floating: a receipt that cannot say WHICH Claude Code it ran is not
113
113
  # reproducible. This is the version measured in the container on 2026-09-19. Claude Code
114
114
  # is scaffolding here (Rule 17) — the product face starts at `herdr integration install`.
@@ -214,11 +214,11 @@ for (const bin of ["pi", "entwurf", "tmux"]) {
214
214
  }
215
215
  const layout = piDurablePackageLayout();
216
216
  ok("this package closure carries the pi-durable units", layout !== null);
217
- const runtime = checkPiDurableRuntime(process.env, layout);
217
+ const runtime = checkPiDurableRuntime(layout);
218
218
  ok(
219
- "the operator-provided durable runtime is present and is the pin (the 0.31.0 supply contract)",
219
+ "this package's pi-durable carrier and its declared SDK set verify (the 0.32.0 supply contract)",
220
220
  runtime.ok,
221
- runtime.ok ? "" : `${runtime.reason}`,
221
+ runtime.ok ? "" : `${runtime.reason}: ${runtime.detail}`,
222
222
  );
223
223
  receipts["0-runtime"] = runtime.ok ? JSON.stringify(runtime) : "";
224
224
 
@@ -140,8 +140,8 @@ want "S-1: claude absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\"
140
140
  want "S-1: agy absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'agy: SKIP'"
141
141
  want "S-1: copilot absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP'"
142
142
  want "S-1: omp absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'omp: SKIP'"
143
- want "S-1: an absent operator durable runtime is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'pi-durable: SKIP'"
144
- want "S-1: setup wrote no durable runtime (Entwurf never installs it)" "[ ! -e '$XDG_DATA_HOME/entwurf/pi-durable' ]"
143
+ want "S-1: the pi-durable row is the package's own carrier verdict (PASS: carrier + pinned SDK set verify; never a SKIP)" "printf '%s' \"\$OUT\" | grep -q 'pi-durable: PASS'"
144
+ want "S-1: setup writes nothing for pi-durable (the carrier ships in the package)" "[ ! -e '$XDG_DATA_HOME/entwurf/pi-durable' ]"
145
145
  want "S-1: core bridge boundary validated (PASS)" "printf '%s' \"\$OUT\" | grep -q 'core: PASS'"
146
146
  want "S-1: final verdict is computed, not unconditional" \
147
147
  "printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
@@ -151,20 +151,20 @@ want "S-1: zero Pi wiring written for an absent pi" "[ ! -e '$PROJ1/.pi' ]"
151
151
  want "S-1: no user-scope pi settings were created" "[ ! -e '$PI_CODING_AGENT_DIR/settings.json' ]"
152
152
  want "S-1: no agy config was created" "[ ! -e '$HOME/.gemini' ]"
153
153
 
154
- # ── S-PD: a durable runtime PRESENT at the fixed place but not the pin → named FAIL, non-green ──
155
- # #129 W (L5). Presence is the locator directory under this sandbox's XDG_DATA_HOME; an empty
156
- # directory is not a git checkout, so the shared verifier answers pi-durable-runtime-unverifiable.
157
- # A drifted runtime must never read as SKIP (not installed) or PASS: it is detected and broken.
158
- echo "[smoke-setup-verdict] S-PD present-but-not-the-pin durable runtime"
154
+ # ── S-PD: the retired source-runtime location decides nothing (#130 hard cut, no legacy reader) ──
155
+ # 0.31.0 read an operator runtime at $XDG_DATA_HOME/entwurf/pi-durable/runtime. Since #130 the
156
+ # carrier ships in the package and that location is not a fallback: debris left there (here, an
157
+ # empty non-checkout directory, which 0.31.0 refused as unverifiable) must neither turn the row
158
+ # into a FAIL nor be touched. The row stays the package's own carrier verdict.
159
+ echo "[smoke-setup-verdict] S-PD debris at the retired durable runtime location"
159
160
  PROJPD="$SB/projpd"; mkdir -p "$PROJPD"
160
161
  PD_RUNTIME="$XDG_DATA_HOME/entwurf/pi-durable/runtime"; mkdir -p "$PD_RUNTIME"
161
162
  seed_auth
162
163
  set +e; OUT="$(bash "$REPO_DIR/run.sh" setup "$PROJPD" 2>&1)"; RC=$?; set -e
163
- want "S-PD: a present non-pin runtime is a component FAIL that owns a nonzero exit" \
164
- "[ '$RC' -ne 0 ] && printf '%s' \"\$OUT\" | grep -q 'pi-durable: FAIL'"
165
- want "S-PD: the FAIL names the verifier's reason" "printf '%s' \"\$OUT\" | grep -q 'pi-durable-runtime-unverifiable'"
166
- want "S-PD: the computed summary names the durable component" "printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: pi-durable)'"
167
- want "S-PD: setup changed nothing in the operator's runtime directory" "[ -z \"\$(ls -A '$PD_RUNTIME')\" ]"
164
+ want "S-PD: the retired location does not decide the row — the carrier verdict is still PASS, setup green" \
165
+ "[ '$RC' -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'pi-durable: PASS'"
166
+ want "S-PD: no 0.31.0 runtime refusal is named" "! printf '%s' \"\$OUT\" | grep -q 'pi-durable-runtime-'"
167
+ want "S-PD: setup changed nothing at the retired location" "[ -z \"\$(ls -A '$PD_RUNTIME')\" ]"
168
168
  want_auth_untouched "S-PD"
169
169
  rm -rf "$XDG_DATA_HOME/entwurf/pi-durable"
170
170
  want "S-1: no Copilot config/units were created" "[ ! -e '$HOME/.copilot' ]"
@@ -212,7 +212,7 @@ want_auth_untouched "S-2b"
212
212
  # ── S-3: pi at the pinned floor → presence completes project + user wiring ──
213
213
  echo "[smoke-setup-verdict] S-3 pi present at floor"
214
214
  PROJ3="$SB/proj3"; mkdir -p "$PROJ3"
215
- PI_PIN="$(node -e 'console.log(require(process.argv[1]).devDependencies["@earendil-works/pi-coding-agent"])' "$REPO_DIR/package.json")"
215
+ PI_PIN="$(node -e 'console.log(require(process.argv[1]).dependencies["@earendil-works/pi-coding-agent"])' "$REPO_DIR/package.json")"
216
216
  printf '#!/usr/bin/env bash\necho %s\n' "$PI_PIN" > "$SB/harness/pi-ok"; chmod +x "$SB/harness/pi-ok"
217
217
  seed_auth
218
218
  set +e; OUT="$(PI_BIN="$SB/harness/pi-ok" bash "$REPO_DIR/run.sh" setup "$PROJ3" 2>&1)"; RC=$?; set -e
@@ -529,8 +529,8 @@ PROJ9C="$SB/proj9c"; mkdir -p "$PROJ9C"
529
529
  seed_auth
530
530
  set +e; OUT="$(PATH="$FAKE_UNAME:$PATH" bash "$REPO_DIR/run.sh" setup "$PROJ9C" 2>&1)"; RC=$?; set -e
531
531
  want "S-9c: a harness-free uncertified host still exits 0" "[ '$RC' -eq 0 ]"
532
- want "S-9c: all seven harness probes are still zero-state SKIPs" \
533
- "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && printf '%s' \"\$OUT\" | grep -q 'codex: SKIP' && printf '%s' \"\$OUT\" | grep -q 'pi-durable: SKIP'"
532
+ want "S-9c: all six harness probes are still zero-state SKIPs, and the packaged pi-durable carrier still verifies" \
533
+ "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && printf '%s' \"\$OUT\" | grep -q 'codex: SKIP' && printf '%s' \"\$OUT\" | grep -q 'pi-durable: PASS'"
534
534
  want "S-9c: core still PASSes and the computed summary is still green" \
535
535
  "printf '%s' \"\$OUT\" | grep -q 'core: PASS' && printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
536
536
  want "S-9c: an absent harness is never given a rail verdict it did not earn" \
@@ -18,7 +18,7 @@
18
18
  #
19
19
  # Cell 15 closes the third (#110), and it is the one cell here that SPAWNS pi: the
20
20
  # defect is not a byte, it is which of two checkouts wins `--entwurf-control`, and
21
- # only the real loader can say. It uses the pinned devDep pi, never PATH.
21
+ # only the real loader can say. It uses the pinned dependency pi, never PATH.
22
22
  set -euo pipefail
23
23
  export PYTHONDONTWRITEBYTECODE=1 # snapshot purity: no ignored scripts/__pycache__ writes under qualification
24
24