@junghanacs/entwurf 0.39.0 → 0.41.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 (137) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +60 -5
  3. package/CHANGELOG.md +557 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +52 -2
  6. package/FAQ.md +8 -4
  7. package/README.md +37 -17
  8. package/VERIFY.md +23 -10
  9. package/docs/acp-backend-rail.md +5 -1
  10. package/docs/external-mcp-host.md +8 -6
  11. package/docs/fresh-cut-policy.md +9 -1
  12. package/docs/setup-clean-host.md +64 -9
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +100 -24
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +10 -4
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +96 -16
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +5 -29
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +28 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +26 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-retire-request.js +76 -0
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +123 -0
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +259 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js +80 -0
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +67 -17
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
  36. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
  37. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  38. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  39. package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +65 -0
  40. package/mcp/entwurf-bridge/src/index.ts +118 -24
  41. package/mcp/entwurf-bridge/test.sh +2 -2
  42. package/mcp/entwurf-bridge/tsconfig.build.json +17 -1
  43. package/package.json +5 -5
  44. package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
  45. package/pi-extensions/entwurf-control.ts +55 -20
  46. package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  47. package/pi-extensions/lib/cc-retire-request.ts +553 -0
  48. package/pi-extensions/lib/control-retire.ts +195 -0
  49. package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
  50. package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
  51. package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
  52. package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
  53. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
  54. package/pi-extensions/lib/fresh-call-composition.ts +15 -0
  55. package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
  56. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  57. package/pi-extensions/lib/meta-mailbox-body.ts +13 -4
  58. package/pi-extensions/lib/meta-session.ts +140 -16
  59. package/pi-extensions/lib/mux-fresh-call.ts +7 -27
  60. package/pi-extensions/lib/mux-launch.ts +29 -0
  61. package/pi-extensions/lib/mux-resume-call.ts +33 -12
  62. package/pi-extensions/lib/pi-retire-request.ts +111 -0
  63. package/pi-extensions/lib/remote-pair-receive.ts +184 -0
  64. package/pi-extensions/lib/remote-pair-send.ts +325 -0
  65. package/pi-extensions/lib/remote-pair-transport.ts +81 -0
  66. package/pi-extensions/lib/resume-launch-identity.ts +104 -18
  67. package/pi-extensions/lib/retirement-event.ts +253 -0
  68. package/pi-extensions/meta-bridge-hook.ts +142 -13
  69. package/pi-extensions/meta-bridge-pi-durable.ts +56 -2
  70. package/run.sh +122 -7
  71. package/scripts/agy-bridge-config.py +4 -1
  72. package/scripts/build-bridge.sh +14 -0
  73. package/scripts/check-capability-bundle-reach.ts +135 -2
  74. package/scripts/check-entwurf-bridge-boot.ts +2 -1
  75. package/scripts/check-entwurf-fact-provider.ts +1 -0
  76. package/scripts/check-entwurf-v2-production.ts +2 -0
  77. package/scripts/check-entwurf-v2-surface.ts +7 -0
  78. package/scripts/check-gate-qualification.ts +20 -8
  79. package/scripts/check-herdr-activation.ts +2 -2
  80. package/scripts/check-herdr-placement.ts +1 -0
  81. package/scripts/check-hook-launch-topology.ts +6 -15
  82. package/scripts/check-install-container.sh +2 -2
  83. package/scripts/check-meta-doctor-oracle.sh +27 -1
  84. package/scripts/check-meta-facts.ts +8 -3
  85. package/scripts/check-meta-hook-session-switch.ts +230 -13
  86. package/scripts/check-meta-identity-consumers.ts +1 -0
  87. package/scripts/check-meta-listing.ts +1 -0
  88. package/scripts/check-meta-manifest-schema.py +1 -1
  89. package/scripts/check-meta-session.ts +31 -0
  90. package/scripts/check-meta-v3-record.ts +55 -1
  91. package/scripts/check-mux-resume-call.ts +35 -9
  92. package/scripts/check-peer-facts.ts +1 -0
  93. package/scripts/check-pi-durable-contact.ts +3 -1
  94. package/scripts/check-pi-durable-send.ts +4 -2
  95. package/scripts/check-pi-mcp-bridge.ts +1 -0
  96. package/scripts/check-remote-pair-installed.ts +242 -0
  97. package/scripts/check-resume-launch-identity.ts +62 -23
  98. package/scripts/lib/meta-hook-bundle.ts +72 -0
  99. package/scripts/lib/remote-pair-installed-audit.ts +25 -0
  100. package/scripts/meta-bridge-claude-floor.sh +13 -9
  101. package/scripts/meta-bridge-doctor.sh +17 -0
  102. package/scripts/meta-bridge-install.sh +6 -0
  103. package/scripts/meta-facts.ts +1 -1
  104. package/scripts/mutants/bridge-boot-resume.json +5 -2
  105. package/scripts/mutants/cc-retire-pidfd.json +129 -0
  106. package/scripts/mutants/cc-retire-request.json +171 -0
  107. package/scripts/mutants/control-retire.json +105 -0
  108. package/scripts/mutants/control-send-receipt.json +2 -2
  109. package/scripts/mutants/copilot-receive.json +2 -6
  110. package/scripts/mutants/entwurf-v2-retire.json +476 -0
  111. package/scripts/mutants/fresh-call-dispatch.json +37 -0
  112. package/scripts/mutants/meta-hook-session-switch.json +88 -0
  113. package/scripts/mutants/meta-identity.json +57 -0
  114. package/scripts/mutants/mux-fresh-call.json +1 -1
  115. package/scripts/mutants/mux-resume-call.json +40 -4
  116. package/scripts/mutants/pi-durable.json +79 -0
  117. package/scripts/mutants/pi-retire-request.json +72 -0
  118. package/scripts/mutants/remote-pair-display.json +53 -0
  119. package/scripts/mutants/remote-pair-installed-audit.json +28 -0
  120. package/scripts/mutants/remote-pair-receive.json +126 -0
  121. package/scripts/mutants/remote-pair-roundtrip.json +83 -0
  122. package/scripts/mutants/remote-pair-send.json +194 -0
  123. package/scripts/mutants/remote-pair-transport.json +66 -0
  124. package/scripts/mutants/resume-launch-identity.json +35 -8
  125. package/scripts/mutants/retirement-event.json +195 -0
  126. package/scripts/mutants/v2-surface.json +16 -0
  127. package/scripts/mutants/v2-visible-resume.json +283 -13
  128. package/scripts/probe-bridge-command.ts +1 -0
  129. package/scripts/remote-pair-receive.ts +73 -0
  130. package/scripts/smoke-agy-install-state.sh +2 -2
  131. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  132. package/scripts/smoke-herdr-raw-install-live.sh +5 -4
  133. package/scripts/smoke-meta-install-state.sh +4 -4
  134. package/scripts/smoke-mux-lifecycle-live.ts +10 -10
  135. package/scripts/smoke-omp-mcp-state.sh +1 -1
  136. package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
  137. package/scripts/smoke-pi-provider-state.sh +3 -3
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-remote-pair-installed — #107 rail 3: the remote pair on an INSTALLED package.
4
+ *
5
+ * `./run.sh check-remote-pair-installed --tgz <absolute candidate tarball>`
6
+ *
7
+ * Why this is a scripts/ gate and not a source-adjacent vitest test: its subject is an ARTIFACT.
8
+ * It installs a caller-supplied tarball into a fresh consumer prefix with npm and then drives
9
+ * that install's own processes (its bridge `start.sh` → compiled dist, its `run.sh` → run_ts →
10
+ * compiled entry). A test beside the source can only reach the source.
11
+ *
12
+ * What it does, once, with every writable root (HOME, npm cache/userconfig, XDG, PI agent dir)
13
+ * inside one temp sandbox:
14
+ * 1. copies the given tarball into the sandbox and installs THAT copy (never repacks; the copy's
15
+ * sha256 is printed). npm reads the package's declared dependencies from the registry into the
16
+ * sandbox cache — a network read, but no model, no operator config and no ssh;
17
+ * 2. checks the installed package carries the receive entry's compiled twin and the remote libs;
18
+ * 3. runs the installed `entwurf remote-pair-receive` with wrong argv: usage on stderr, exit 2 —
19
+ * the compiled twin was reached through run_ts, not a TS crossing;
20
+ * 4. runs the existing T2 scene (pi-extensions/lib/remote-pair-roundtrip.test.ts) with its subject
21
+ * switched to this install: real installed bridge, stand-in ssh → installed run.sh → installed
22
+ * entry → production dispatch, with the same mailbox/provenance/receipt/reverse-dispatch oracles;
23
+ * 5. audits checkout invisibility: a preload resolve hook records every module each SUBJECT
24
+ * process resolves; every file must sit (by real path) inside the consumer prefix. The
25
+ * orchestrator (vitest + its fixture writer) is this checkout and is not audited;
26
+ * 6. control: the same hook on this checkout's own `run.sh remote-pair-receive` must report a
27
+ * checkout path, so the audit can see the thing it refuses.
28
+ *
29
+ * The subject switch (ENTWURF_REMOTE_PAIR_INSTALLED_PKG and the resolve-hook variables) is set
30
+ * here and only here: it changes what the T2 cells measure, so it must not be set ambiently.
31
+ *
32
+ * Mutation proof covers only the audit's acceptance primitive (scripts/lib/remote-pair-installed-audit.ts);
33
+ * the install, compiled-surface, twin and roundtrip cells are actual-runtime evidence with the
34
+ * checkout control, not mutation-qualified.
35
+ *
36
+ * Not claimed: the published npm artifact (this is whatever tarball the caller passes), real sshd,
37
+ * a real remote host, a durable host reading or replying, or the check-pack/check-pack-install floor.
38
+ */
39
+
40
+ import { spawnSync } from "node:child_process";
41
+ import { createHash } from "node:crypto";
42
+ import * as fs from "node:fs";
43
+ import * as os from "node:os";
44
+ import * as path from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+ import { filesOutsideRoot as outside } from "./lib/remote-pair-installed-audit.ts";
47
+
48
+ const LABEL = "check-remote-pair-installed";
49
+ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
50
+
51
+ const args = process.argv.slice(2);
52
+ if (args.length !== 2 || args[0] !== "--tgz" || !path.isAbsolute(args[1])) {
53
+ console.error(`usage: ./run.sh ${LABEL} --tgz <absolute candidate tarball>`);
54
+ process.exit(2);
55
+ }
56
+ const tgz = args[1];
57
+ if (!fs.lstatSync(tgz).isFile()) {
58
+ console.error(`${LABEL}: --tgz must be a regular file (${tgz})`);
59
+ process.exit(2);
60
+ }
61
+
62
+ let passed = 0;
63
+ let failed = 0;
64
+ function ok(label: string, cond: boolean, detail = ""): void {
65
+ if (cond) {
66
+ passed++;
67
+ console.log(` ok ${label}`);
68
+ } else {
69
+ failed++;
70
+ console.log(` FAIL ${label}${detail ? `\n ${detail}` : ""}`);
71
+ }
72
+ }
73
+
74
+ const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-remote-pair-installed-"));
75
+ const consumer = path.join(sandbox, "consumer");
76
+ const npmHome = path.join(sandbox, "npm-home");
77
+ const logs = path.join(sandbox, "resolve-logs");
78
+ const hook = path.join(sandbox, "resolve-hook.mjs");
79
+ for (const d of [consumer, npmHome, logs]) fs.mkdirSync(d, { recursive: true });
80
+
81
+ function sandboxEnv(home: string): NodeJS.ProcessEnv {
82
+ return {
83
+ PATH: process.env.PATH ?? "",
84
+ HOME: home,
85
+ XDG_CONFIG_HOME: path.join(home, ".config"),
86
+ XDG_DATA_HOME: path.join(home, ".local", "share"),
87
+ XDG_STATE_HOME: path.join(home, ".local", "state"),
88
+ XDG_CACHE_HOME: path.join(home, ".cache"),
89
+ PI_CODING_AGENT_DIR: path.join(home, ".pi", "agent"),
90
+ };
91
+ }
92
+
93
+ fs.writeFileSync(
94
+ hook,
95
+ [
96
+ 'import { appendFileSync } from "node:fs";',
97
+ 'import { registerHooks } from "node:module";',
98
+ "const log = process.env.ENTWURF_REMOTE_PAIR_RESOLVE_LOG;",
99
+ "if (log) {",
100
+ "\tregisterHooks({",
101
+ "\t\tresolve(specifier, context, next) {",
102
+ "\t\t\tconst r = next(specifier, context);",
103
+ '\t\t\tappendFileSync(log, r.url + "\\n");',
104
+ "\t\t\treturn r;",
105
+ "\t\t},",
106
+ "\t});",
107
+ "}",
108
+ "",
109
+ ].join("\n"),
110
+ );
111
+
112
+ function hookEnv(label: string): NodeJS.ProcessEnv {
113
+ return { NODE_OPTIONS: `--import=${hook}`, ENTWURF_REMOTE_PAIR_RESOLVE_LOG: path.join(logs, `${label}.log`) };
114
+ }
115
+
116
+ // The artifact is a snapshot: copy the caller's file into the sandbox once, then hash and install
117
+ // THAT copy, so what is measured is exactly what is installed. Never repacked.
118
+ const snapshot = path.join(sandbox, "artifact", path.basename(tgz));
119
+ fs.mkdirSync(path.dirname(snapshot), { recursive: true });
120
+ fs.copyFileSync(tgz, snapshot);
121
+ const sha = createHash("sha256").update(fs.readFileSync(snapshot)).digest("hex");
122
+ console.log(
123
+ `[${LABEL}] caller tarball ${tgz}\n[${LABEL}] snapshot ${snapshot}\n[${LABEL}] snapshot sha256 ${sha}\n[${LABEL}] sandbox ${sandbox}`,
124
+ );
125
+
126
+ try {
127
+ // 1. Install as-is.
128
+ fs.writeFileSync(
129
+ path.join(consumer, "package.json"),
130
+ JSON.stringify({ name: "remote-pair-consumer", private: true }),
131
+ );
132
+ const npm = spawnSync("npm", ["install", snapshot, "--no-audit", "--no-fund"], {
133
+ cwd: consumer,
134
+ encoding: "utf8",
135
+ env: {
136
+ ...sandboxEnv(npmHome),
137
+ npm_config_cache: path.join(sandbox, "npm-cache"),
138
+ npm_config_userconfig: path.join(npmHome, ".npmrc"),
139
+ },
140
+ });
141
+ ok(
142
+ "[QK:RPI-INSTALLED] npm installs the given tarball into a fresh consumer prefix",
143
+ npm.status === 0,
144
+ npm.stderr.slice(-800),
145
+ );
146
+ const pkg = path.join(consumer, "node_modules", "@junghanacs", "entwurf");
147
+ const pkgStat = fs.existsSync(pkg) ? fs.lstatSync(pkg) : null;
148
+ ok(
149
+ "[QK:RPI-INSTALLED] the package is a real directory inside the consumer (not a link to a checkout)",
150
+ pkgStat !== null && pkgStat.isDirectory() && !pkgStat.isSymbolicLink(),
151
+ );
152
+ if (failed) throw new Error("install failed; later cells would not measure the artifact");
153
+
154
+ // 2. The compiled surface is in the package.
155
+ for (const rel of [
156
+ "mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js",
157
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js",
158
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js",
159
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js",
160
+ ]) {
161
+ ok(`[QK:RPI-COMPILED-SURFACE] the installed package carries ${rel}`, fs.existsSync(path.join(pkg, rel)));
162
+ }
163
+
164
+ // 3. The installed bin reaches the compiled twin.
165
+ const home = path.join(sandbox, "usage-home");
166
+ const usage = spawnSync(path.join(pkg, "run.sh"), ["remote-pair-receive"], {
167
+ encoding: "utf8",
168
+ env: { ...sandboxEnv(home), ...hookEnv("installed-usage") },
169
+ });
170
+ ok(
171
+ "[QK:RPI-INSTALLED-ENTRY-TWIN] the installed `entwurf remote-pair-receive` answers wrong argv with usage on stderr, exit 2, no receipt",
172
+ usage.status === 2 && /usage: entwurf remote-pair-receive/.test(usage.stderr) && usage.stdout === "",
173
+ `status=${usage.status} stdout=${usage.stdout.slice(0, 200)} stderr=${usage.stderr.slice(0, 300)}`,
174
+ );
175
+
176
+ // 4. The T2 scene against the install.
177
+ const vitest = spawnSync(
178
+ path.join(REPO, "node_modules", ".bin", "vitest"),
179
+ ["run", "pi-extensions/lib/remote-pair-roundtrip.test.ts"],
180
+ {
181
+ cwd: REPO,
182
+ encoding: "utf8",
183
+ env: {
184
+ ...process.env,
185
+ ENTWURF_REMOTE_PAIR_INSTALLED_PKG: pkg,
186
+ ENTWURF_REMOTE_PAIR_RESOLVE_HOOK: hook,
187
+ ENTWURF_REMOTE_PAIR_RESOLVE_LOG_DIR: logs,
188
+ },
189
+ },
190
+ );
191
+ const summary = (vitest.stdout.match(/Tests\s+.*$/m) ?? [""])[0].trim();
192
+ ok(
193
+ "[QK:RPI-ROUNDTRIP-ON-INSTALL] the T2 cells (A→B, reverse dispatch, opt-in absent off, other host refuses, both socket dirs) pass against the installed bridge and entry",
194
+ vitest.status === 0 && /^Tests\s+5 passed\b/.test(summary),
195
+ `${summary}\n${vitest.stdout
196
+ .split("\n")
197
+ .filter((l) => /×|FAIL|Error/.test(l))
198
+ .slice(0, 10)
199
+ .join("\n")}`,
200
+ );
201
+
202
+ // 5. Checkout invisibility of the subject processes.
203
+ const all = fs.readdirSync(logs).map((n) => path.join(logs, n));
204
+ const bridgeLogs = all.filter((f) => path.basename(f).startsWith("bridge-"));
205
+ const entryLogs = all.filter((f) => path.basename(f).startsWith("entry-"));
206
+ const subjectLogs = [...bridgeLogs, ...entryLogs, ...all.filter((f) => path.basename(f) === "installed-usage.log")];
207
+ ok(
208
+ "[QK:RPI-AUDIT-SAW-SUBJECTS] the resolve hook recorded the installed bridge processes and the installed entry processes",
209
+ bridgeLogs.some((f) => fs.statSync(f).size > 0) && entryLogs.some((f) => fs.statSync(f).size > 0),
210
+ `bridge logs=${bridgeLogs.length} entry logs=${entryLogs.length}`,
211
+ );
212
+ const audit = outside(subjectLogs, consumer);
213
+ ok(
214
+ `[QK:RPI-CHECKOUT-INVISIBLE] every module the installed subject processes resolved (${audit.total}) sits inside the consumer prefix`,
215
+ audit.total > 0 && audit.outside.length === 0,
216
+ audit.outside.slice(0, 10).join("\n "),
217
+ );
218
+
219
+ // 6. Control: the audit sees a checkout when there is one.
220
+ const control = spawnSync(path.join(REPO, "run.sh"), ["remote-pair-receive"], {
221
+ encoding: "utf8",
222
+ env: { ...sandboxEnv(path.join(sandbox, "control-home")), ...hookEnv("control-checkout") },
223
+ });
224
+ const controlAudit = outside([path.join(logs, "control-checkout.log")], consumer);
225
+ const realRepo = fs.realpathSync(REPO);
226
+ ok(
227
+ "[QK:RPI-AUDIT-CONTROL] the same audit on this checkout's own entry reports checkout paths (the detector is live)",
228
+ control.status === 2 && controlAudit.outside.some((p) => p.startsWith(`${realRepo}${path.sep}`)),
229
+ `status=${control.status} outside=${controlAudit.outside.length}`,
230
+ );
231
+ } catch (err) {
232
+ ok(`[${LABEL}] gate ran to completion`, false, err instanceof Error ? err.message : String(err));
233
+ } finally {
234
+ // Synthetic fixture data only. Kept on failure so the resolve logs and npm output can be read.
235
+ if (failed === 0) fs.rmSync(sandbox, { recursive: true, force: true });
236
+ else console.log(`[${LABEL}] kept the sandbox for diagnosis: ${sandbox}`);
237
+ }
238
+
239
+ console.log(
240
+ `\n${LABEL}: ${passed} passed, ${failed} failed — installed artifact ${sha.slice(0, 12)}; no real ssh, no model`,
241
+ );
242
+ process.exit(failed === 0 ? 0 : 1);
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * check-resume-launch-identity — deterministic gate for `resume-launch-identity.ts`, the
3
- * record-authoritative leaf preserved when the visible-first cut deleted `spawn-bg` and every
4
- * caller it had.
3
+ * record-authoritative leaf of a visible same-id resume (its consumer is `entwurf-v2-visible-resume`).
5
4
  *
6
- * WHY A GATE FOR A CONSUMER-ZERO MODULE. Identity authority is a fail-closed risk class: this
7
- * leaf's whole job is deciding WHICH being a resume turn lands in. A preserved leaf whose only
8
- * guarantee is its own header comment is exactly the rot preservation was meant to prevent — it
9
- * would be re-adopted, months later, by a visible-resume composition that trusts prose. The cells
5
+ * WHY A GATE FOR AN IDENTITY LEAF. Identity authority is a fail-closed risk class: this leaf's whole
6
+ * job is deciding WHICH being a resume lands in. A leaf whose only guarantee is its own header
7
+ * comment would be trusted as prose by every composition that calls it. The cells
10
8
  * below are transplanted from the deleted `check-entwurf-v2-spawn-production` §9 (the plan
11
9
  * parameter became a bare garden id; nothing else about the identity contract changed), plus one
12
10
  * new cell for the #52 read the leaf's header claims and the deleted gate never isolated.
@@ -25,14 +23,18 @@
25
23
  * backend, no recorded transcript, recorded-but-missing file, no model, headerless file. The
26
24
  * missing-file cell is a regression pin: before the existence check that case fell through
27
25
  * `readSessionIdentity`'s ENOENT swallow and lied "no recorded model".
28
- * 4b. THE ONE EXPECTED REFUSAL — a backend with no same-id resume is not a defect at all: the
29
- * record is fine and the operator reached past a capability boundary. It alone carries a
30
- * TYPED reason (`target-not-pi`) so the visible-resume composition can match a FIELD instead
31
- * of parsing a sentence or re-reading the record. Everything else here stays a bare throw.
26
+ * 4b. THE ONE EXPECTED REFUSAL — a backend with no same-id resume (since #147 L2: anything but pi
27
+ * and claude-code) is not a defect at all: the record is fine and the operator reached past a
28
+ * capability boundary. It alone carries a TYPED reason (`target-backend-unsupported`) so the
29
+ * visible-resume composition can match a FIELD instead of parsing a sentence or re-reading the
30
+ * record. Everything else here stays a bare throw.
31
+ * 4e. CLAUDE CODE (#147 L2) — a claude-code record resolves to its own session id, the record's
32
+ * cwd and createdAt, reading no transcript; a session id outside the lowercase-UUID grammar and
33
+ * a recorded transcript that is not a file on disk are each refused with their own cause.
32
34
  * 4c. ABSOLUTE TRANSCRIPT — a relative recorded path would resolve against the WINDOW's own cwd
33
35
  * at launch, not against this process's, so it names a different file than the one checked
34
36
  * here. Refused before anything opens.
35
- * 5. SSOT — the module header names THIS gate, so the preserved leaf cannot advertise a
37
+ * 5. SSOT — the module header names THIS gate and its consumer, so the leaf cannot advertise a
36
38
  * certification that does not exist.
37
39
  *
38
40
  * No spawn, no socket, no timer: the launch this identity feeds is deliberately not in scope.
@@ -91,12 +93,49 @@ function main(): void {
91
93
  ok(label, msg.includes(needle));
92
94
  };
93
95
 
96
+ // ── 4e. CLAUDE CODE (#147 L2) ─────────────────────────────────────────────────
97
+ {
98
+ const native = "0b5e2a4c-1f3d-4e8a-9c7b-2d6f8e1a3c5d";
99
+ const transcript = writeTranscript("cc-session.jsonl", "vendor-owned bytes this leaf never parses\n");
100
+ const gidCc = mintRecord(native, transcript, "claude-code");
101
+ const raw = JSON.parse(fs.readFileSync(path.join(storeDir, `${gidCc}.meta.json`), "utf8")) as {
102
+ createdAt: string;
103
+ };
104
+ let cc: ReturnType<typeof resolveResumeLaunchIdentity> | null = null;
105
+ try {
106
+ cc = resolveResumeLaunchIdentity(gidCc);
107
+ } catch {
108
+ cc = null;
109
+ }
110
+ ok(
111
+ "4e: a claude-code record resolves to its own session id, the record's cwd and createdAt [QK:RESUME-ID-CC-RESOLVES]",
112
+ cc?.backend === "claude-code" &&
113
+ cc.nativeSessionId === native &&
114
+ cc.cwd === cwd &&
115
+ cc.createdAt === raw.createdAt,
116
+ );
117
+ rejects(
118
+ mintRecord("--dangerously-skip-permissions", transcript, "claude-code"),
119
+ "is not a Claude Code session id",
120
+ "4e: a recorded session id outside the UUID grammar is refused before it can become a claude flag [QK:RESUME-ID-CC-SESSION-GRAMMAR]",
121
+ );
122
+ rejects(
123
+ mintRecord("0b5e2a4c-1f3d-4e8a-9c7b-2d6f8e1a3c5e", path.join(world, "gone.jsonl"), "claude-code"),
124
+ "is not a file on disk",
125
+ "4e: a recorded transcript that is gone is refused — claude --resume would open nothing [QK:RESUME-ID-CC-TRANSCRIPT-PRESENT]",
126
+ );
127
+ }
128
+
94
129
  // ── 1. HAPPY ──────────────────────────────────────────────────────────────
95
130
  const nativeOk = "0199aaaa-1111-4222-8333-444455556666";
96
131
  const fileOk = writeTranscript("own.jsonl", sessionLine(nativeOk) + modelLine);
97
132
  const gidOk = mintRecord(nativeOk, fileOk);
98
133
  const launch = resolveResumeLaunchIdentity(gidOk);
99
- ok("1: record-backed resume resolves the recorded transcript", launch.sessionFile === fileOk);
134
+ assert.ok(launch.backend === "pi", "1: a pi record resolves as pi");
135
+ ok(
136
+ "1: record-backed resume resolves the recorded transcript",
137
+ launch.backend === "pi" && launch.sessionFile === fileOk,
138
+ );
100
139
  ok("1: resume cwd = transcript header authority (#9 cold-resume)", launch.cwd === cwd);
101
140
  ok(
102
141
  "1: resume model/provider = first model_change",
@@ -122,29 +161,29 @@ function main(): void {
122
161
  // ── 4. CAUSE FIDELITY ─────────────────────────────────────────────────────
123
162
  rejects("20260101T000000-facade", "not a garden citizen", "4: recordless gid → not a citizen");
124
163
 
125
- const gidClaude = mintRecord("claude-native-1", null, "claude-code");
164
+ const gidCodex = mintRecord("codex-thread-1", null, "codex");
126
165
  rejects(
127
- gidClaude,
128
- "is a claude-code citizen",
166
+ gidCodex,
167
+ "is a codex citizen",
129
168
  "4: out-of-domain citizen → refused as a capability-domain miss, not as citizen rank",
130
169
  );
131
170
  // This one refusal is EXPECTED, not a defect: the record is perfectly good and the operator
132
171
  // has reached past a capability boundary. So it is the only failure here that carries a
133
172
  // typed `reason` a caller can match on a FIELD — the visible-resume composition turns it
134
- // into `target-not-pi`, and if it degraded to a bare Error that caller would either parse
173
+ // into `target-backend-unsupported`, and if it degraded to a bare Error that caller would either parse
135
174
  // this sentence or read the record a second time to ask which backend it was.
136
175
  {
137
176
  let caught: Error | null = null;
138
177
  try {
139
- resolveResumeLaunchIdentity(gidClaude);
178
+ resolveResumeLaunchIdentity(gidCodex);
140
179
  } catch (err) {
141
180
  caught = err as Error;
142
181
  }
143
182
  ok(
144
183
  "4: the backend miss is a TYPED refusal carrying reason + backend, not a bare Error [QK:RESUME-ID-BACKEND-TYPED-REFUSAL]",
145
184
  caught instanceof ResumeBackendUnsupportedError &&
146
- caught.reason === "target-not-pi" &&
147
- caught.backend === "claude-code",
185
+ caught.reason === "target-backend-unsupported" &&
186
+ caught.backend === "codex",
148
187
  );
149
188
  }
150
189
 
@@ -195,7 +234,8 @@ function main(): void {
195
234
  retired.err.model === "claude-sonnet-5" &&
196
235
  routed.err instanceof AcpModelNotCuratedError &&
197
236
  routed.err.model === "claude-opus-5" &&
198
- current.identity?.provider === "entwurf" &&
237
+ current.identity?.backend === "pi" &&
238
+ current.identity.provider === "entwurf" &&
199
239
  current.identity.model === "claude-sonnet-5-5",
200
240
  );
201
241
  }
@@ -243,6 +283,7 @@ function main(): void {
243
283
  transcriptPath: fileOk,
244
284
  createdAt: "2026-03-01T12:00:00.000Z",
245
285
  recordUpdatedAt: "2026-03-01T12:30:00.000Z",
286
+ attachIncarnation: null,
246
287
  }),
247
288
  );
248
289
  // The happy-path id resolved a moment ago; the ONLY thing that changed is a second record
@@ -271,9 +312,7 @@ function main(): void {
271
312
  "5: the leaf's header names check-resume-launch-identity (no certification it does not have) [QK:RESUME-ID-GATE-SSOT]",
272
313
  src.includes("`check-resume-launch-identity`"),
273
314
  );
274
- // The preservation is deliberate and stated: a reader who finds an uncalled export must
275
- // be able to tell "kept on purpose, for the visible lane" from "forgotten".
276
- ok("5: the leaf states it is preserved with no consumer, on purpose", /no consumer in the shipped tree/.test(src));
315
+ ok("5: the leaf names its consumer", /`entwurf-v2-visible-resume` is its consumer/.test(src));
277
316
  }
278
317
 
279
318
  console.log(`\ncheck-resume-launch-identity: ${passed} checks passed`);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * meta-hook-bundle — the Claude Code hook bundle a gate stages, derived from the hook's OWN
3
+ * import graph rather than from a hand-kept file list (#147 L2 B1).
4
+ *
5
+ * The installed bundle is the entry plus exactly the `lib/` files `meta-bridge-install.sh`
6
+ * copies. A gate that hand-copies its own list can pass while the installer misses a file the
7
+ * hook now imports — the false green this module exists to remove. So a staged bundle carries
8
+ * the entry's import closure, and `installerCopiedLibFiles` reads what the installer actually
9
+ * copies, for the gate to compare the two.
10
+ *
11
+ * SCOPE, deliberately narrow: only static `from "./…"` / `from "../…"` specifiers in DOUBLE
12
+ * quotes — the one form the hook's closure (entry, meta-session, entwurf-v2-lock, session-id)
13
+ * uses today. A side-effect `import "./x"`, a dynamic `import("./x")`, a `require`, or a
14
+ * single-quoted specifier is NOT followed, so the comparison says nothing about them; this is
15
+ * not a general module resolver and must not grow into one. `meta-session`'s win32 addon is
16
+ * one such unfollowed load (`createRequire`, win32 only, outside the Linux/macOS bundle). The
17
+ * independent proof that the closure is complete is the gate RUNNING the staged compiled hook.
18
+ */
19
+
20
+ import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
21
+ import * as path from "node:path";
22
+
23
+ const IMPORT_RE = /\bfrom\s+"(\.{1,2}\/[^"]+)"/g;
24
+
25
+ /**
26
+ * The entry's relative-import closure, as paths relative to the entry's directory
27
+ * (`lib/meta-session.ts`, …). The entry itself is not included.
28
+ */
29
+ export function hookImportClosure(entryFile: string): string[] {
30
+ const root = path.dirname(entryFile);
31
+ const seen = new Set<string>();
32
+ const visit = (file: string): void => {
33
+ const source = readFileSync(file, "utf8");
34
+ for (const match of source.matchAll(IMPORT_RE)) {
35
+ const target = path.resolve(path.dirname(file), match[1] as string);
36
+ const rel = path.relative(root, target);
37
+ if (seen.has(rel)) continue;
38
+ seen.add(rel);
39
+ visit(target);
40
+ }
41
+ };
42
+ visit(entryFile);
43
+ return [...seen].sort();
44
+ }
45
+
46
+ /** The `lib/<name>` destinations `meta-bridge-install.sh` copies into the assembled plugin. */
47
+ export function installerCopiedLibFiles(installerText: string): string[] {
48
+ const out = new Set<string>();
49
+ for (const match of installerText.matchAll(/^cp\s.*"\$ASM\/\$PLUGIN\/lib\/([^"]+)"\s*$/gm)) {
50
+ out.add(match[1] as string);
51
+ }
52
+ return [...out].sort();
53
+ }
54
+
55
+ /**
56
+ * Stage entry + its import closure + the launcher under `root`, exactly as installed. `entryFile`
57
+ * is the source (`pi-extensions/meta-bridge-hook.ts`) or the compiled entry
58
+ * (`mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js`); the closure follows whichever.
59
+ */
60
+ export function stageMetaHookBundle(root: string, entryFile: string, launcher: string): string[] {
61
+ const closure = hookImportClosure(entryFile);
62
+ mkdirSync(path.join(root, "scripts"), { recursive: true });
63
+ copyFileSync(entryFile, path.join(root, path.basename(entryFile)));
64
+ for (const rel of closure) {
65
+ mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
66
+ copyFileSync(path.join(path.dirname(entryFile), rel), path.join(root, rel));
67
+ }
68
+ const staged = path.join(root, "scripts", "hook-launch.sh");
69
+ copyFileSync(launcher, staged);
70
+ chmodSync(staged, 0o755);
71
+ return closure;
72
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The acceptance primitive of `check-remote-pair-installed`'s checkout-invisibility cell (#107 rail 3),
3
+ * as a pure function over resolve-hook logs so it can be proven on tiny synthetic files
4
+ * (test/remote-pair-installed-audit.test.ts). Each log line is one URL a subject process resolved.
5
+ * A `file:` URL counts and is judged by its REAL path; anything else (node: builtins) is ignored.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ /** Every file the logs name that is NOT inside `root` (both compared by real path), and how many file URLs were read. */
12
+ export function filesOutsideRoot(logFiles: readonly string[], root: string): { total: number; outside: string[] } {
13
+ const realRoot = fs.realpathSync(root);
14
+ const out = new Set<string>();
15
+ let total = 0;
16
+ for (const f of logFiles) {
17
+ for (const url of fs.readFileSync(f, "utf8").split("\n").filter(Boolean)) {
18
+ if (!url.startsWith("file:")) continue;
19
+ total++;
20
+ const real = fs.realpathSync(fileURLToPath(url));
21
+ if (real !== realRoot && !real.startsWith(`${realRoot}${path.sep}`)) out.add(real);
22
+ }
23
+ }
24
+ return { total, outside: [...out].sort() };
25
+ }
@@ -6,7 +6,8 @@
6
6
  #
7
7
  # The floor is a full three-part version, not a major lane: the discriminator sits
8
8
  # in the PATCH position (2.1.138 drops `args`, 2.1.139 introduced the exec form,
9
- # 2.1.217 is the version this repo actually proved in a live session). A major-only
9
+ # 2.1.217 is the version this repo proved the exec form on in a live session, 2.1.223
10
+ # is where `--resume <id>` started finding a session outside the current project). A major-only
10
11
  # comparison like the Node floor's `-lt 24` cannot express it, so a real semver
11
12
  # compare has to exist — and it must exist exactly ONCE. Two hand-rolled compares in
12
13
  # two shell scripts is how "the installer blessed a version the doctor refuses"
@@ -16,15 +17,18 @@
16
17
  # `entwurf.claudeCodeFloor` is the SSOT and everything here reads it. There is one
17
18
  # number to move. `check-claude-floor-coherence` binds every other spelling of it.
18
19
  #
19
- # WHY THE FLOOR IS 2.1.217 AND NOT 2.1.139
20
+ # WHY THE FLOOR IS 2.1.306
20
21
  #
21
- # 2.1.139 is where the exec-form strings enter the binary, but no session was ever
22
- # run on it — the floor would have been an inference. 2.1.217 is the version whose
23
- # runtime behavior was actually observed end to end (#51 B2: `args` honored per
24
- # element, no shell on the launch path, `asyncRewake` exit 2 waking an idle session).
25
- # GLG's decision (2026-07-22): Claude Code auto-updates, so entwurf does not carry
26
- # back-compat for versions it never verified. Declare the proven floor, not the
27
- # earliest plausible one.
22
+ # 2.1.217 is the version whose exec-form runtime behavior was observed end to end
23
+ # (#51 B2: `args` honored per element, no shell on the launch path, `asyncRewake` exit 2
24
+ # waking an idle session); those observations stand. #147 added same-id visible resume
25
+ # of a Claude Code citizen, which runs `claude --resume <nativeSessionId>` from the
26
+ # record's LAST OBSERVED cwd. The vendor CLI reference (code.claude.com cli-reference,
27
+ # read 2026-10-21, unversioned page) says the session-id search covers every project on
28
+ # the machine only from v2.1.223; below it a session whose cwd moved may not be found.
29
+ # GLG's decision (2026-10-21): the floor follows what the direction needs, with no
30
+ # back-compat — 2.1.306, the version GLG relayed as running here. That number is a
31
+ # declared floor: no LIVE receipt has been taken at it yet.
28
32
  #
29
33
  # Requires: python3 (already a hard dependency of both callers).
30
34
 
@@ -53,8 +53,10 @@ esac
53
53
  # a second derivation is a second thing to forget when the layout moves.
54
54
  if [ "$LIB_EXT" = "js" ]; then
55
55
  SRC_MS="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
56
+ SRC_LOCK="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js"
56
57
  else
57
58
  SRC_MS="$REPO/pi-extensions/lib/meta-session.ts"
59
+ SRC_LOCK="$REPO/pi-extensions/lib/entwurf-v2-lock.ts"
58
60
  fi
59
61
  # shellcheck source=scripts/meta-bridge-hook-log.sh
60
62
  source "$REPO/scripts/meta-bridge-hook-log.sh"
@@ -1276,6 +1278,21 @@ fi
1276
1278
  if [ -f "$ASM_MS" ] && [ "$asm_h" != "$src_h" ]; then
1277
1279
  warn "assembled bundle differs from source ($asm_v vs $src_v) — stale .assembled; install re-assembles it"
1278
1280
  fi
1281
+ # The hook's record-arm lock (#147 L2 B1) is part of the same writer closure: a writer that
1282
+ # commits an arm binding without the lock module it imports cannot load at all, and a stale one
1283
+ # serializes against a different contract. Same authority as the writer: the INSTALLED bundle.
1284
+ src_lock_h="$(hash12 "$SRC_LOCK")"
1285
+ if [ -n "${INST_MS:-}" ]; then
1286
+ INST_LOCK="$(dirname "$INST_MS")/entwurf-v2-lock.$LIB_EXT"
1287
+ inst_lock_h="$(hash12 "$INST_LOCK")"
1288
+ if [ ! -f "$INST_LOCK" ]; then
1289
+ bad "deployed hook lock module MISSING ($INST_LOCK) — the hook imports it, so the installed writer cannot load. Run ./run.sh install-meta-bridge to redeploy."
1290
+ elif [ "$inst_lock_h" != "$src_lock_h" ]; then
1291
+ bad "deployed hook lock module STALE: installed=$inst_lock_h vs source=$src_lock_h ($INST_LOCK). Run ./run.sh install-meta-bridge to redeploy."
1292
+ else
1293
+ ok "deployed hook lock module matches source ($inst_lock_h)"
1294
+ fi
1295
+ fi
1279
1296
 
1280
1297
  # Identity-writer dependency: loadMetaCapabilityRegistry reads
1281
1298
  # entwurf-capabilities.json at runtime. In the bundle layout it must sit at the
@@ -58,11 +58,13 @@ case "$REPO" in
58
58
  HOOK_ENTRY="meta-bridge-hook.js"
59
59
  HOOK_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
60
60
  LIB_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
61
+ LOCK_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js"
61
62
  LIB_EXT="js" ;;
62
63
  *)
63
64
  HOOK_ENTRY="meta-bridge-hook.ts"
64
65
  HOOK_SRC="$REPO/pi-extensions/meta-bridge-hook.ts"
65
66
  LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
67
+ LOCK_SRC="$REPO/pi-extensions/lib/entwurf-v2-lock.ts"
66
68
  LIB_EXT="ts" ;;
67
69
  esac
68
70
 
@@ -187,9 +189,13 @@ cp -r "$SRC/$PLUGIN" "$ASM/$PLUGIN"
187
189
  # Installed → tsc-emitted JS (dist, node_modules-safe); dev clone → strip-types .ts.
188
190
  [ -f "$HOOK_SRC" ] || die "hook artifact missing: $HOOK_SRC (installed package ships it via prepack build-bridge → dist; reinstall @junghanacs/entwurf, or run 'pnpm run build-bridge' in a dev clone)."
189
191
  [ -f "$LIB_SRC" ] || die "hook lib artifact missing: $LIB_SRC (same build-bridge dist closure)."
192
+ [ -f "$LOCK_SRC" ] || die "hook lock artifact missing: $LOCK_SRC (same build-bridge dist closure)."
190
193
  cp "$HOOK_SRC" "$ASM/$PLUGIN/$HOOK_ENTRY"
191
194
  mkdir -p "$ASM/$PLUGIN/lib"
192
195
  cp "$LIB_SRC" "$ASM/$PLUGIN/lib/meta-session.$LIB_EXT"
196
+ # The record-arm lock (#147 L2 B1): the hook imports the shared per-gid lock primitive, so it
197
+ # travels in the same closure. The doctor's writer-version parity hashes it beside the writer.
198
+ cp "$LOCK_SRC" "$ASM/$PLUGIN/lib/entwurf-v2-lock.$LIB_EXT"
193
199
  cp "$REPO/pi-extensions/lib/session-id.js" "$ASM/$PLUGIN/lib/session-id.js"
194
200
  # v2 writer (3D-3+) reads the capability registry at runtime
195
201
  # (loadMetaCapabilityRegistry). It MUST travel at the plugin ROOT — meta-session's
@@ -15,7 +15,7 @@
15
15
  * {
16
16
  * "schemaVersion": 1, // of THIS projection, not of the records
17
17
  * "storeDir": "/abs/path",
18
- * "citizens": [ <full verbatim strict 9-field v3 records, sorted by gardenId> ],
18
+ * "citizens": [ <full verbatim strict v3 records, sorted by gardenId> ],
19
19
  * "defects": [ { "filename", "message" }, … sorted by filename ]
20
20
  * }
21
21
  *
@@ -19,9 +19,12 @@
19
19
  "claim": "BRIDGEBOOT-RESUME-CALL-TARGET-ONLY",
20
20
  "title": "a model knob is added to the resume schema, giving the caller a second way to decide what a resumed citizen is after the record already decided",
21
21
  "subject": "mcp/entwurf-bridge/src/index.ts",
22
- "find": ["\t\t\t.describe(\"Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).\"),"],
22
+ "find": [
23
+ "\t\t\t.describe(\"Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers).\"),"
24
+ ],
23
25
  "replace": [
24
- "\t\t\t.describe(\"Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).\"),\n\t\tmodel: z.string().optional().describe(\"Override the recorded model.\"),"
26
+ "\t\t\t.describe(\"Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers).\"),",
27
+ "\t\tmodel: z.string().optional().describe(\"Override the recorded model.\"),"
25
28
  ],
26
29
  "gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
27
30
  "timeoutSeconds": 120,