@junghanacs/entwurf 0.41.2 → 0.50.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 (122) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +328 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/ENTWURF-CLI.json +1229 -0
  6. package/ENTWURF.json +284 -0
  7. package/FAQ.md +4 -4
  8. package/README.md +125 -184
  9. package/VERIFY.md +2 -2
  10. package/docs/external-mcp-host.md +3 -3
  11. package/docs/setup-clean-host.md +37 -12
  12. package/mcp/entwurf-bridge/dist/ENTWURF.json +284 -0
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
  30. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  31. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  33. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  34. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  35. package/mcp/entwurf-bridge/src/index.ts +149 -327
  36. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  37. package/native/win32-x64/manifest.json +1 -1
  38. package/package.json +8 -4
  39. package/pi/pi-durable/bootstrap.mjs +179 -6
  40. package/pi/pi-durable/carrier/runtime.js +108 -9
  41. package/pi/pi-durable/carrier/session-name.js +11 -0
  42. package/pi/pi-durable/carrier/sessions.js +96 -20
  43. package/pi/pi-durable/overlay/upstream-pin.json +11 -5
  44. package/pi-extensions/lib/agent-guidance.ts +341 -0
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
  46. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
  47. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  48. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  49. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  50. package/pi-extensions/lib/meta-session.ts +13 -8
  51. package/pi-extensions/lib/mux-fresh-call.ts +23 -26
  52. package/pi-extensions/lib/mux-resume-call.ts +95 -23
  53. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
  54. package/pi-extensions/lib/pi-durable-location.ts +446 -0
  55. package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
  56. package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
  57. package/pi-extensions/lib/remote-pair-send.ts +7 -4
  58. package/pi-extensions/lib/resume-launch-identity.ts +31 -12
  59. package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
  60. package/run.sh +417 -654
  61. package/scripts/build-bridge.sh +10 -0
  62. package/scripts/check-capability-bundle-reach.ts +86 -0
  63. package/scripts/check-claude-launch.ts +341 -0
  64. package/scripts/check-entwurf-self-address.ts +3 -3
  65. package/scripts/check-entwurf-v2-surface.ts +94 -98
  66. package/scripts/check-fresh-call-dispatch.ts +70 -9
  67. package/scripts/check-fresh-cut-gate.sh +23 -9
  68. package/scripts/check-gate-qualification.ts +24 -17
  69. package/scripts/check-herdr-activation.ts +301 -23
  70. package/scripts/check-herdr-fresh-call.ts +161 -25
  71. package/scripts/check-herdr-plugin-build.ts +173 -1
  72. package/scripts/check-herdr-plugin.ts +28 -2
  73. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  74. package/scripts/check-install-surface.ts +1236 -6
  75. package/scripts/check-meta-doctor-oracle.sh +1 -1
  76. package/scripts/check-meta-facts.ts +20 -1
  77. package/scripts/check-mux-resume-call.ts +48 -3
  78. package/scripts/check-peer-facts.ts +39 -2
  79. package/scripts/cli-help.mjs +222 -0
  80. package/scripts/dev-bin.sh +39 -2
  81. package/scripts/herdr-activation.mjs +28 -6
  82. package/scripts/herdr-operator-link.mjs +389 -0
  83. package/scripts/herdr-plugin-activate.mjs +5 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  85. package/scripts/herdr-rails.mjs +43 -7
  86. package/scripts/herdr-runtime.mjs +27 -0
  87. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  88. package/scripts/meta-bridge-prune.ts +1 -1
  89. package/scripts/meta-facts.ts +1 -1
  90. package/scripts/mutants/agent-guidance.json +160 -0
  91. package/scripts/mutants/bridge-boot-resume.json +6 -8
  92. package/scripts/mutants/capability-bundle-reach.json +124 -0
  93. package/scripts/mutants/claude-launch.json +142 -0
  94. package/scripts/mutants/cli-help-source.json +420 -0
  95. package/scripts/mutants/control-send-receipt.json +21 -4
  96. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  97. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  98. package/scripts/mutants/fresh-cut.json +15 -0
  99. package/scripts/mutants/herdr-activation.json +128 -0
  100. package/scripts/mutants/herdr-fresh-call.json +278 -5
  101. package/scripts/mutants/herdr-operator-link.json +318 -0
  102. package/scripts/mutants/herdr-plugin-build.json +55 -0
  103. package/scripts/mutants/herdr-plugin.json +17 -0
  104. package/scripts/mutants/herdr-resume-call.json +288 -0
  105. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  106. package/scripts/mutants/meta-facts.json +15 -0
  107. package/scripts/mutants/mux-fresh-call.json +14 -3
  108. package/scripts/mutants/mux-resume-call.json +89 -3
  109. package/scripts/mutants/peer-facts.json +15 -0
  110. package/scripts/mutants/pi-durable.json +996 -6
  111. package/scripts/mutants/remote-pair-send.json +21 -2
  112. package/scripts/mutants/self-address.json +10 -10
  113. package/scripts/mutants/source-install.json +35 -0
  114. package/scripts/mutants/v2-surface.json +50 -64
  115. package/scripts/mutants/v2-visible-resume.json +266 -6
  116. package/scripts/peer-facts.ts +1 -1
  117. package/scripts/smoke-agy-install-state.sh +66 -0
  118. package/scripts/smoke-meta-prune.sh +23 -0
  119. package/scripts/smoke-omp-fresh-live.ts +12 -3
  120. package/scripts/smoke-omp-receive-live.ts +7 -1
  121. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  122. package/team.json.example +39 -0
@@ -21,6 +21,12 @@ import assert from "node:assert/strict";
21
21
  import * as fs from "node:fs/promises";
22
22
  import * as path from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
+ import {
25
+ ENTWURF_DELIVERY_RESULT_CLAUSES,
26
+ ENTWURF_REJECT_HINTS,
27
+ ENTWURF_SLOTTED_REJECT_HINTS,
28
+ ENTWURF_TOOL_GUIDANCE,
29
+ } from "../pi-extensions/lib/agent-guidance.ts";
24
30
  import type { EntwurfV2RunResult } from "../pi-extensions/lib/entwurf-v2-runner.ts";
25
31
  import {
26
32
  actionableRejectHint,
@@ -28,17 +34,7 @@ import {
28
34
  type SurfaceEntwurfV2Params,
29
35
  toDispatchInput,
30
36
  } from "../pi-extensions/lib/entwurf-v2-surface.ts";
31
-
32
- /** Cut ONE model-facing string out of a registration block so two descriptions in the same
33
- * block cannot satisfy an assertion for each other (round-4 masking, 2026-07-27). Returns ""
34
- * when either marker is missing, which the callers' `length > 120` guard turns into a loud
35
- * failure rather than a silent pass. */
36
- function sliceDescription(block: string, startMarker: string, endMarker: string): string {
37
- const s = block.indexOf(startMarker);
38
- if (s === -1) return "";
39
- const e = block.indexOf(endMarker, s + startMarker.length);
40
- return e === -1 ? "" : block.slice(s, e);
41
- }
37
+ import { FRESH_CUT_PRESCRIPTION } from "../pi-extensions/lib/meta-session.ts";
42
38
 
43
39
  let passed = 0;
44
40
  function ok(label: string, cond: boolean): void {
@@ -47,17 +43,6 @@ function ok(label: string, cond: boolean): void {
47
43
  passed++;
48
44
  }
49
45
 
50
- /**
51
- * Collapse a model-facing string's SOURCE form into the text the model actually reads:
52
- * drop TS string-concat glue between adjacent literals and normalize whitespace.
53
- */
54
- function modelText(slice: string): string {
55
- return slice
56
- .replace(/["']\s*\+\s*["']/g, "")
57
- .replace(/\s+/g, " ")
58
- .trim();
59
- }
60
-
61
46
  /**
62
47
  * F-2 (2026-07-27): the two model-facing surfaces described the lock, `mode`, and the
63
48
  * unsupported-citizen outcome set in ways the code does not support. Measured against
@@ -77,13 +62,11 @@ function modelText(slice: string): string {
77
62
  * Applied to BOTH surfaces from one predicate so they cannot drift apart again.
78
63
  */
79
64
  function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: string, modeDescRaw: string): void {
80
- // Compare the text the MODEL reads, not the shape the author happened to type. pi-native
81
- // wraps inside one template literal; the MCP surface wraps by `+`-concatenating literals.
82
- // Without this normalization the same sentence matches on one surface and not the other,
83
- // and the assertion silently measures line-wrapping instead of meaning.
84
- const longDesc = modelText(longDescRaw);
85
- const intentDesc = modelText(intentDescRaw);
86
- const modeDesc = modelText(modeDescRaw);
65
+ // Inputs are decoded product strings, not TS scaffolding. Keep these semantic predicates
66
+ // separate from the exact, unnormalized pre-extraction byte oracle beside the reader.
67
+ const longDesc = longDescRaw;
68
+ const intentDesc = intentDescRaw;
69
+ const modeDesc = modeDescRaw;
87
70
  ok(
88
71
  `${tag} — long description scopes the per-target lock to the control-socket DOMAIN [QK:V2SURF-MCP-LOCK-DOMAIN]`,
89
72
  /control-socket-DOMAIN\s+dispatch/.test(longDesc) && /lock-free/.test(longDesc),
@@ -155,7 +138,7 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
155
138
  * DORMANT HONESTY — the model-facing replacement for the retired `V2SURF-MERGED-REJECT` claim,
156
139
  * whose subject (two distinct OWNED rejects) left with the `owned-outcome` intent.
157
140
  *
158
- * After the visible-first cut a dormant socket-domain citizen is unreachable by every verb, and
141
+ * After the visible-first cut a dormant socket-domain citizen rejects delivery by v2, and
159
142
  * these four strings are the only place a caller learns that BEFORE spending a dispatch. So each
160
143
  * one must, on its own:
161
144
  * - name the reason it will actually get back (`dormant-fire-forget-unsupported`);
@@ -175,7 +158,7 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
175
158
  * run unlabelled behind it — still real assertions, just not this claim's signature.
176
159
  */
177
160
  function assertDormantHonesty(tag: string, rawText: string): void {
178
- const text = modelText(rawText);
161
+ const text = rawText;
179
162
  const at = text.indexOf("owned-outcome");
180
163
  ok(
181
164
  `${tag} — names the withdrawn intent owned-outcome and says "withdrawn" beside it (not as an offer) [QK:V2SURF-DORMANT-HONESTY]`,
@@ -192,18 +175,23 @@ function assertDormantHonesty(tag: string, rawText: string): void {
192
175
  * PEERS DEAD-ROW HONESTY. `entwurf_peers` is a FACT surface and stays one: this claim must never
193
176
  * be satisfiable by adding a per-row action/routing field, which is why it is asserted on the
194
177
  * DESCRIPTION and paired with a negative that forbids the row-level fix. What the description owes
195
- * a caller is that `dead` is a reported fact carrying no invitation — the citizen is dormant and,
196
- * since the visible-first cut, reachable by no verb at all.
178
+ * a caller is that `dead` is a reported fact carrying no delivery invitation. A separately
179
+ * requested, proven-dormant Pi/pi-durable/Claude lifecycle can reopen the same id without submitting a prompt.
197
180
  */
198
181
  function assertPeersDeadRowHonesty(tag: string, rawBlock: string): void {
199
- const text = modelText(rawBlock);
182
+ const text = rawBlock;
200
183
  ok(
201
- `${tag} — entwurf_peers says a dead/dormant row is a REPORTED FACT that grants no action [QK:V2SURF-PEERS-DEAD-ROW]`,
202
- /REPORTED FACT/.test(text) && /grants no action/.test(text) && /not an invitation to dispatch/.test(text),
184
+ `${tag} — entwurf_peers reports dead delivery unavailable, no action grant, and separate explicit-operator promptless resume [QK:V2SURF-PEERS-DEAD-ROW]`,
185
+ /REPORTED FACT/.test(text) &&
186
+ /grants no action/.test(text) &&
187
+ /not an invitation to dispatch/.test(text) &&
188
+ /no entwurf_v2 delivery is available while it is dormant/.test(text) &&
189
+ /Only on an explicit operator request, the separate entwurf_resume_call/.test(text) &&
190
+ /proven-dormant Pi, pi-durable or Claude Code citizen under the same id, submitting no prompt/.test(text),
203
191
  );
204
192
  ok(
205
- `${tag} — entwurf_peers calls a dead row currently unreachable by any verb`,
206
- /dormant/.test(text) && /unreachable by any verb/.test(text),
193
+ `${tag} — entwurf_peers does not turn delivery dormancy into a ban on every lifecycle verb`,
194
+ /dormant/.test(text) && !/unreachable by any verb/.test(text),
207
195
  );
208
196
  // The forbidden repair: teaching the row itself to carry a verb. The facts-only rule is what
209
197
  // keeps this surface from becoming a second dispatch table, so a description that promised a
@@ -214,14 +202,6 @@ function assertPeersDeadRowHonesty(tag: string, rawBlock: string): void {
214
202
  );
215
203
  }
216
204
 
217
- /**
218
- * Containment-disjointness is NOT enough to prove a description slice is tight. Round 5
219
- * (2026-07-27) shipped a "long description" slice of 4,468 chars that SPANNED the parameter
220
- * entries because a bare `description:` marker matched the `target` param first — and two
221
- * over-wide slices can still be mutually non-containing. So pin the ABSENCE of every
222
- * parameter marker in the long-description slice: if the slice ever swallows the schema
223
- * again, this fails instead of silently satisfying the schema's own text.
224
- */
225
205
  /**
226
206
  * HOST TRUNCATION CAP. Measured 2026-07-30 on both Claude ACP and stock Claude Code: a tool
227
207
  * description longer than this is cut mid-sentence and the tail is replaced with `… [truncated]`.
@@ -230,30 +210,12 @@ function assertPeersDeadRowHonesty(tag: string, rawBlock: string): void {
230
210
  * to choose an intent. Every other assertion in this file checks that a sentence is PRESENT in
231
211
  * source; none of them can see that the host threw half of it away. This one can.
232
212
  *
233
- * Judged on `modelText` (the rendered string), not the source slice: pi-native wraps inside one
234
- * template literal and the MCP surface `+`-concatenates, so only the normalized text has the
235
- * length the host actually measures.
213
+ * Judged on the decoded JSON string, with no source scaffolding or whitespace normalization.
214
+ * Exact current fields are checked separately against frozen originals plus reviewed literal amendments.
236
215
  */
237
216
  const HOST_DESCRIPTION_CAP = 2048;
238
217
 
239
- /**
240
- * The string the HOST measures, recovered from either surface's source scaffolding: drop the
241
- * `description:` key and the outer template-literal / string-literal fence, and unescape `\"`.
242
- * Counting the raw slice instead would charge the text for punctuation the model never sees —
243
- * and the two surfaces carry DIFFERENT punctuation, so the cap would mean two different things.
244
- */
245
- function renderedDescription(raw: string): string {
246
- const flat = modelText(raw);
247
- // Anchor on the description's own first word rather than on scaffolding: the two surfaces put
248
- // DIFFERENT preludes in front of it (the MCP slice starts at the tool name), and counting those
249
- // would report a number the host never measures — a cap gate that cites the wrong length gets
250
- // quoted as if it were the real one.
251
- const from = flat.indexOf("CANONICAL");
252
- return (from >= 0 ? flat.slice(from) : flat).replace(/[`"],?$/, "").replace(/\\"/g, '"');
253
- }
254
-
255
- function assertDescriptionFitsHostCap(tag: string, longDescRaw: string): void {
256
- const rendered = renderedDescription(longDescRaw);
218
+ function assertDescriptionFitsHostCap(tag: string, rendered: string): void {
257
219
  ok(
258
220
  `${tag} — long description fits the host's ${HOST_DESCRIPTION_CAP}-char cap (${rendered.length}) ` +
259
221
  "[QK:V2SURF-DESC-FITS-HOST-CAP]",
@@ -261,12 +223,6 @@ function assertDescriptionFitsHostCap(tag: string, longDescRaw: string): void {
261
223
  );
262
224
  }
263
225
 
264
- function assertLongDescExcludesParams(tag: string, longDesc: string, markers: readonly string[]): void {
265
- for (const marker of markers) {
266
- ok(`${tag} — long description slice excludes the \`${marker}\` parameter marker`, !longDesc.includes(marker));
267
- }
268
- }
269
-
270
226
  const HERE = path.dirname(fileURLToPath(import.meta.url));
271
227
  const REPO = path.join(HERE, "..");
272
228
  const SURFACE_SRC = path.join(REPO, "pi-extensions/lib/entwurf-v2-surface.ts");
@@ -587,9 +543,24 @@ async function main(): Promise<void> {
587
543
  !/DELIVERED|\bdelivered\b/.test(dirtyRefused.text) &&
588
544
  // and every one of them keeps the thing the operator must act on
589
545
  [dirtyBusy, dirtyMailbox, dirtyRefused].every(
590
- (r) => r.isError && r.text.includes("DIRTY") && r.text.includes("do NOT retry"),
546
+ (r) =>
547
+ r.isError &&
548
+ r.text.includes("DIRTY") &&
549
+ r.text.includes("do NOT retry automatically") &&
550
+ r.text.includes("operator resolve the lock") &&
551
+ !r.text.includes("would double-deliver"),
591
552
  ),
592
553
  );
554
+ // #154 E1a: the two static clauses of this receipt are authored in root ENTWURF.json; the
555
+ // syntax around them stays here, so the whole rendered line is compared, not a fragment.
556
+ const dirtyClause = ENTWURF_DELIVERY_RESULT_CLAUSES["dirty-lock"];
557
+ ok(
558
+ "[QK:V2SURF-DELIVERY-RESULT-CLAUSES-FROM-ROOT] the dirty-lock receipt renders the root finalized-rejected and dirty-lock clauses byte for byte inside its code-owned syntax",
559
+ dirtyBusy.text ===
560
+ `entwurf_v2 control-socket finalized as queued-steer, and ${dirtyClause}\n error: release boom` &&
561
+ dirtyRefused.text ===
562
+ `entwurf_v2 control-socket finalized as rejected (compacting) — ${ENTWURF_DELIVERY_RESULT_CLAUSES["finalized-rejected"]}, and ${dirtyClause}\n error: release boom`,
563
+ );
593
564
 
594
565
  // plain execution-failed → error
595
566
  const failed: EntwurfV2RunResult = {
@@ -683,6 +654,25 @@ async function main(): Promise<void> {
683
654
  rls.includes("restart it under the current runtime") &&
684
655
  rls.includes("meta-bridge-fresh-cut"),
685
656
  );
657
+ // #154 E1b: the text on either side of the fix command is authored in root ENTWURF.json and the
658
+ // command comes from its owner, so the WHOLE hint is compared against that composition — the
659
+ // phrase checks above stay, and a hand-kept copy of the text that still carries them dies here.
660
+ const rlsRoot = ENTWURF_SLOTTED_REJECT_HINTS["record-less-socket"];
661
+ ok(
662
+ "[QK:V2SURF-RECORD-LESS-SOCKET-HINT-FROM-ROOT] the record-less-socket hint is exactly the root text before the fix command, the command from its meta-session owner, and the root text after it",
663
+ rls === `${rlsRoot.beforeFreshCutPrescription}${FRESH_CUT_PRESCRIPTION}${rlsRoot.afterFreshCutPrescription}`,
664
+ );
665
+ // #154 C1-B: the five static hints are authored in root ENTWURF.json. This is the WIRING
666
+ // claim — what the surface returns is the Root value for every declared reason — compared
667
+ // against the loaded Root, never a fixture, so a Root prose mutant still dies at its own
668
+ // semantic QK above. The C0 byte oracle lives beside the reader (GUIDANCE-REJECT-HINTS-ORIGINAL).
669
+ const unwired = Object.entries(ENTWURF_REJECT_HINTS)
670
+ .filter(([reason, text]) => actionableRejectHint(reason) !== text)
671
+ .map(([reason]) => reason);
672
+ ok(
673
+ `2D: every static reject hint the surface renders is the root ENTWURF.json value [QK:V2SURF-REJECT-HINTS-FROM-ROOT] (unwired: ${JSON.stringify(unwired)})`,
674
+ Object.keys(ENTWURF_REJECT_HINTS).length === 5 && unwired.length === 0,
675
+ );
686
676
  }
687
677
 
688
678
  // ── 6: RETIRED — parseEntwurfPrefixRootsEnv / ENTWURF_PREFIX_ROOTS ────────
@@ -776,14 +766,10 @@ async function main(): Promise<void> {
776
766
  // Same shipped-rail completeness rule as the pi-native surface (4), and split the SAME
777
767
  // two ways: the long tool description and the `intent` param description must EACH be
778
768
  // true on their own (see the round-4 masking note in block 4).
779
- // Same fail-loud rule as block 4: a missing marker would make `slice(0, -1)` return
780
- // nearly the whole block and sail past the length guard, so assert the boundary first.
781
- const mcpSchemaStart = v2Block.indexOf("\t{");
782
- ok("5: MCP — schema boundary marker is present (slice cannot silently widen)", mcpSchemaStart > 0);
783
- const mcpLongDesc = v2Block.slice(0, mcpSchemaStart);
784
- const mcpIntentDesc = sliceDescription(v2Block, "intent: z", "message: z");
769
+ const mcpLongDesc = ENTWURF_TOOL_GUIDANCE.entwurf_v2.descriptions.mcp;
770
+ const mcpIntentDesc = ENTWURF_TOOL_GUIDANCE.entwurf_v2.parameters.intent;
785
771
  ok(
786
- "5: MCP — long/intent slices are disjoint (neither can satisfy the other's claim)",
772
+ "5: MCP — long/intent strings are disjoint (neither can satisfy the other's claim)",
787
773
  mcpLongDesc.length > 0 &&
788
774
  mcpIntentDesc.length > 0 &&
789
775
  !mcpLongDesc.includes(mcpIntentDesc) &&
@@ -799,11 +785,12 @@ async function main(): Promise<void> {
799
785
  }
800
786
  ok("5: MCP — long description denies native-push a mailbox", /NO mailbox/.test(mcpLongDesc));
801
787
  ok(
802
- "5: MCP — long description steers live/alive peer → fire-and-forget",
803
- /liveness=alive/.test(mcpLongDesc) && /fire-and-forget/.test(mcpLongDesc),
788
+ "5: MCP — long description says a live socket can receive fire-and-forget, and unsupported is not reachability",
789
+ /A live socket can receive fire-and-forget/.test(mcpLongDesc) &&
790
+ /not reachability/.test(mcpLongDesc) &&
791
+ !/liveness=alive\s*→\s*fire-and-forget/.test(mcpLongDesc),
804
792
  );
805
- assertLongDescExcludesParams("5: MCP", mcpLongDesc, ["target: z", "intent: z", "mode: z", "wants_reply: z"]);
806
- const mcpModeDesc = sliceDescription(v2Block, "mode: z", "wants_reply: z");
793
+ const mcpModeDesc = ENTWURF_TOOL_GUIDANCE.entwurf_v2.parameters.mode;
807
794
  assertRailSemantics("5: MCP", mcpLongDesc, mcpIntentDesc, mcpModeDesc);
808
795
  assertDescriptionFitsHostCap("5: MCP", mcpLongDesc);
809
796
 
@@ -817,21 +804,20 @@ async function main(): Promise<void> {
817
804
  "5: MCP — entwurf_peers block has a REAL end boundary (next server.tool present)",
818
805
  peersStart !== -1 && peersAfter > peersStart,
819
806
  );
820
- const peersBlock = src.slice(peersStart, peersAfter);
821
- ok("5: MCP — entwurf_peers block is isolated (non-vacuous)", peersBlock.length > 200);
807
+ const peersText = ENTWURF_TOOL_GUIDANCE.entwurf_peers.descriptions.mcp;
822
808
  ok(
823
809
  "5: MCP — entwurf_peers names the THIRD answer (a reject), not just the two allow rails",
824
- /mailbox-undeliverable/.test(peersBlock) && /native-push-target-dead/.test(peersBlock),
810
+ /mailbox-undeliverable/.test(peersText) && /native-push-target-dead/.test(peersText),
825
811
  );
826
812
  ok(
827
813
  "5: MCP — entwurf_peers no longer claims `unsupported` does NOT mean unreachable",
828
- !/does NOT mean unreachable/.test(peersBlock),
814
+ !/does NOT mean unreachable/.test(peersText),
829
815
  );
830
816
  // Visible-first cut, operator-surface half: a dormant citizen still gets a row here, and a
831
817
  // caller who reads "it is in the list" as "I can dispatch to it" spends a dispatch to find
832
818
  // out otherwise. The FACT stays a fact — no per-row action field, no routing verb, which is
833
819
  // the rule this surface exists under — so the correction has to live in the description.
834
- assertPeersDeadRowHonesty("5: MCP", peersBlock);
820
+ assertPeersDeadRowHonesty("5: MCP", peersText);
835
821
 
836
822
  // F-7: entwurf_inbox_read described itself as draining "your own" inbox while the handler
837
823
  // passes the CALLER-SUPPLIED gardenId straight to readMetaInbox with no comparison against
@@ -845,7 +831,7 @@ async function main(): Promise<void> {
845
831
  inboxStart !== -1 && inboxAfter > inboxStart,
846
832
  );
847
833
  const inboxBlock = src.slice(inboxStart, inboxAfter);
848
- const inboxText = modelText(inboxBlock);
834
+ const inboxText = ENTWURF_TOOL_GUIDANCE.entwurf_inbox_read.descriptions.mcp;
849
835
  ok("5: MCP — entwurf_inbox_read block is isolated (non-vacuous)", inboxText.length > 200);
850
836
  ok(
851
837
  "5: MCP — entwurf_inbox_read states the garden id is CALLER-SUPPLIED and unverified [QK:V2SURF-INBOX-SCOPE-HONESTY]",
@@ -971,20 +957,27 @@ async function main(): Promise<void> {
971
957
  // rail's four boundaries are the ONLY receiver-acceptance values; a mailbox answers with an
972
958
  // ENQUEUE receipt and native-push with an INJECTION, and folding those into the boundary union
973
959
  // would tell a caller that `mailbox-enqueued` is something a receiver observed about its own
974
- // queues. Missing either rail is red — the earlier draft listed `mailbox-enqueued` beside the
960
+ // queues. The socket dispatch may re-resolve to a mailbox leg and report mailbox-enqueued;
961
+ // that exception must name the leg, not silently invent a fifth receiver queue boundary.
962
+ // Missing either rail is red — the earlier draft listed `mailbox-enqueued` beside the
975
963
  // four and left native-push out entirely, and nothing noticed.
976
964
  {
977
- const outcomeSites = ["mcp/entwurf-bridge/src/index.ts"];
965
+ const outcomeSites = [
966
+ {
967
+ rel: "ENTWURF.json tools.entwurf_v2.descriptions.mcp",
968
+ text: ENTWURF_TOOL_GUIDANCE.entwurf_v2.descriptions.mcp,
969
+ },
970
+ ];
978
971
  const missing: string[] = [];
979
- for (const rel of outcomeSites) {
980
- const text = await fs.readFile(path.join(REPO, rel), "utf8");
972
+ for (const { rel, text } of outcomeSites) {
981
973
  const at = text.indexOf("reports ONE");
982
974
  const blurb = at < 0 ? "" : text.slice(at, at + 900);
983
975
  if (!/acceptance boundary/i.test(blurb)) missing.push(`${rel}: control receiver acceptance boundary`);
984
976
  for (const b of ["sent", "queued-steer", "queued-follow-up", "accepted-unknown-boundary"]) {
985
977
  if (!blurb.includes(b)) missing.push(`${rel}: boundary ${b}`);
986
978
  }
987
- if (!/those four only/i.test(blurb)) missing.push(`${rel}: the four-only scope of the boundary union`);
979
+ if (!blurb.includes("those four, or `mailbox-enqueued` on a re-resolved mailbox leg"))
980
+ missing.push(`${rel}: the four receiver boundaries and the explicitly re-resolved mailbox leg`);
988
981
  if (!/mailbox ENQUEUE/i.test(blurb)) missing.push(`${rel}: mailbox enqueue receipt`);
989
982
  if (!/native-push INJECTION/i.test(blurb)) missing.push(`${rel}: native-push injection`);
990
983
  if (!/rejection|rejected/i.test(blurb)) missing.push(`${rel}: rejection`);
@@ -994,7 +987,7 @@ async function main(): Promise<void> {
994
987
  }
995
988
  }
996
989
  ok(
997
- `[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL] the tool description names each rail's own result — the control receiver's four acceptance boundaries (and that those four are the whole union), the mailbox ENQUEUE receipt, the native-push INJECTION, a rejection, and accepted-but-lock-dirty — so a caller is never told that one rail's vocabulary describes another's (missing: ${JSON.stringify(missing)})`,
990
+ `[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL] the tool description names each rail's own result — the control receiver's four acceptance boundaries (or mailbox-enqueued on an explicitly re-resolved mailbox leg), the mailbox ENQUEUE receipt, the native-push INJECTION, a rejection, and accepted-but-lock-dirty — so a caller is never told that one rail's vocabulary describes another's (missing: ${JSON.stringify(missing)})`,
998
991
  missing.length === 0,
999
992
  );
1000
993
  }
@@ -1029,7 +1022,10 @@ async function main(): Promise<void> {
1029
1022
  ];
1030
1023
  const gaps: string[] = [];
1031
1024
  for (const rel of sites) {
1032
- const text = await fs.readFile(path.join(REPO, rel), "utf8");
1025
+ const text =
1026
+ rel === "mcp/entwurf-bridge/src/index.ts"
1027
+ ? ENTWURF_TOOL_GUIDANCE.entwurf_v2.parameters.mode
1028
+ : await fs.readFile(path.join(REPO, rel), "utf8");
1033
1029
  for (const axis of axes) if (!axis.holds(text)) gaps.push(`${rel}: ${axis.name}`);
1034
1030
  if (/steer \((?:interrupt|immediate)|interrupt the current turn|steer = interrupt/i.test(text)) {
1035
1031
  gaps.push(`${rel}: still claims steer interrupts`);
@@ -18,6 +18,11 @@
18
18
  * FCD-NO-RAIL-PARAMETER no caller-facing input can choose a rail
19
19
  * FCD-NO-FALLBACK an incomplete herdr context refuses by its herdr reason
20
20
  * FCD-HERDR-BACKEND-FIRST a non-pilot backend refuses before any preflight and any mutation
21
+ * FCD-HERDR-PI-DURABLE-LAYOUT inside herdr pi-durable reaches the layout branch, which is told
22
+ * only WHETHER the caller is a Codex citizen and never falls back from
23
+ * its missing record cwd to this process's own (#157 A-L1)
24
+ * FCD-HERDR-NO-THREAD-ID the herdr branch builds its request from an explicit field list in
25
+ * which the Codex thread id appears only as that boolean (#157 A-L1 D5)
21
26
  * FCD-CODEX-PREFLIGHT-TMUX the Codex preflight still gates the tmux rail, and only that rail
22
27
  * FCD-ONE-NONCE one nonce is minted per call and reaches the selected rail
23
28
  * FCD-SURFACE-ROOT-ONLY the one public surface (the bridge; pi reaches it through Pi's
@@ -28,6 +33,8 @@
28
33
  * FCD-RECOVERY-VISIBLE a post-split failure shows closed vs orphan-unreclaimed:reason
29
34
  * FCD-ACP-MODEL-EXACT a pi `entwurf/<id>` that is not an exact curated ACP id refuses on
30
35
  * either rail before anything runs (#128)
36
+ * FCD-RESUME-RAIL-SHARED a same-id visible resume reaches the same exact rail choice through
37
+ * its own entry point, with no fallback in either direction (#157 G1a)
31
38
  */
32
39
 
33
40
  import assert from "node:assert/strict";
@@ -36,6 +43,7 @@ import * as path from "node:path";
36
43
  import { fileURLToPath } from "node:url";
37
44
  import {
38
45
  dispatchFreshCall,
46
+ dispatchResumeLaunch,
39
47
  renderDispatchedFreshCall,
40
48
  selectFreshCallRail,
41
49
  } from "../pi-extensions/lib/fresh-call-dispatch.ts";
@@ -180,25 +188,45 @@ async function main(): Promise<void> {
180
188
  !herdrBranch.includes("codexCallerFreshPreflight"),
181
189
  );
182
190
 
183
- // #129: pi-durable joined the tmux fresh set, NOT the herdr pilot set (GLG, 2026-10-06: no herdr
184
- // pi-durable support in 0.31). Same axis as the codex cell: refused by the herdr reason, nothing
185
- // spawned, no tmux fallback, and its installed-carrier preflight is never asked on this branch.
191
+ // #157 A-L1 (main 0a6f9b relaying GLG's #157 grant; supersedes the #129 "not in herdr" cell):
192
+ // inside herdr pi-durable reaches the herdr LAYOUT branch, which never runs the herdr CLI. The
193
+ // root hands that branch one boolean — the caller IS a record-backed Codex citizen — and the
194
+ // branch, not this root, owns the pi-durable preflight. Both calls below are refused before any
195
+ // socket is opened (this env names none), so nothing here can reach a herdr server.
186
196
  const durable = recordingSpawn();
187
- const durableRefused = await dispatchFreshCall(
188
- { ...REQUEST, backend: "pi-durable", model: "loopback/scripted" },
197
+ const durableRequest = { ...REQUEST, backend: "pi-durable", model: "loopback/scripted" };
198
+ const durablePlain = await dispatchFreshCall(durableRequest, HERDR_ENV, durable.spawn);
199
+ const durableCodexCaller = await dispatchFreshCall(
200
+ { ...durableRequest, callerNativeSessionId: "019a0000-0000-7000-8000-000000000000" },
189
201
  HERDR_ENV,
190
202
  durable.spawn,
191
203
  );
192
204
  ok(
193
- "[QK:FCD-HERDR-PI-DURABLE-UNSUPPORTED] inside herdr pi-durable is not a pilot backend: it refuses by the herdr reason with nothing spawned, never falls back to a tmux window, and its runtime preflight is never consulted there",
194
- durableRefused.rail === "herdr" &&
195
- !durableRefused.result.ok &&
196
- durableRefused.result.reason === "herdr-backend-unsupported" &&
205
+ "[QK:FCD-HERDR-PI-DURABLE-LAYOUT] inside herdr pi-durable is no longer refused as unsupported: with the caller's own process cwd it reaches the layout branch's socket-path refusal, while a known Codex caller with no record cwd is refused as codex-caller-cwd-unavailable instead of falling back to this process's cwd — the root passes Codex-ness as a boolean, nothing spawns the herdr CLI, no tmux fallback, and the root's herdr branch names no pi-durable preflight",
206
+ durablePlain.rail === "herdr" &&
207
+ !durablePlain.result.ok &&
208
+ durablePlain.result.reason === "herdr-socket-path-missing" &&
209
+ durableCodexCaller.rail === "herdr" &&
210
+ !durableCodexCaller.result.ok &&
211
+ durableCodexCaller.result.reason === "codex-caller-cwd-unavailable" &&
197
212
  durable.calls.length === 0 &&
198
213
  herdrBranchLocated &&
199
214
  !herdrBranch.includes("piDurable"),
200
215
  );
201
216
 
217
+ // #157 A-L1 D5: the Codex thread id is a tmux seat input and STOPS at this root. The herdr branch
218
+ // therefore spells its request field by field — a spread of the whole request would carry the id
219
+ // along even if no herdr code read it today — and names the id once, inside the boolean.
220
+ const threadIdMentions = herdrBranch.split("callerNativeSessionId").length - 1;
221
+ ok(
222
+ "[QK:FCD-HERDR-NO-THREAD-ID] the herdr branch hands its rail an explicit field list, not a spread of the request, and the Codex thread id appears in it exactly once — as `callerIsCodex: request.callerNativeSessionId !== undefined` — so only a boolean and the record cwd cross to herdr while the tmux request stays whole",
223
+ herdrBranchLocated &&
224
+ !/\.\.\.request\b/.test(herdrBranch) &&
225
+ threadIdMentions === 1 &&
226
+ herdrBranch.includes("callerIsCodex: request.callerNativeSessionId !== undefined,") &&
227
+ herdrBranch.includes("callerCwd: request.callerCwd"),
228
+ );
229
+
202
230
  // ── the tmux rail keeps its pre-existing ordering ────────────────────────────────────
203
231
  // The import line names the preflight too, so the body is what gets counted: the claim is
204
232
  // "called once, on one path", not "the word appears once".
@@ -292,6 +320,8 @@ async function main(): Promise<void> {
292
320
  backend: "pi",
293
321
  requestedKind: "pi",
294
322
  model: "m/1",
323
+ cwd: "/repo/dir",
324
+ cwdSource: "caller-process",
295
325
  herdrAgentName: "entwurf-abc",
296
326
  herdrWorkspaceId: "w7",
297
327
  herdrTabId: "w7:t3",
@@ -354,6 +384,37 @@ async function main(): Promise<void> {
354
384
  !unknown.text.includes("orphan-unreclaimed"),
355
385
  );
356
386
 
387
+ // ── #157 G1a: a visible resume reaches the SAME rail choice ──────────────────────────
388
+ // The root's second entry point owns only the rail choice for a same-id visible resume. Its tmux
389
+ // half is whatever the surface hands in, so a recording stub stands for it; its herdr half is the
390
+ // real herdr resume launch, driven with an incomplete herdr context so it refuses before any
391
+ // runner exists. No existing cell exercises this entry point: a resume entry that read the marker
392
+ // loosely, or fell back to the surface's tmux mapping, would leave every cell above green.
393
+ {
394
+ const RESUME_INPUT = { backend: "pi", cwd: "/x/project", runtimeArgs: ["--session", "/x/s.jsonl"] } as const;
395
+ const tmuxCalls: unknown[] = [];
396
+ const tmuxLaunch = (input: typeof RESUME_INPUT) => {
397
+ tmuxCalls.push(input);
398
+ return { ok: false as const, reason: "tmux-stub" };
399
+ };
400
+ const resumeSpawn = recordingSpawn();
401
+ const inHerdr = await dispatchResumeLaunch(RESUME_INPUT, tmuxLaunch, { HERDR_ENV: "1" }, resumeSpawn.spawn);
402
+ const tmuxCallsInHerdr = tmuxCalls.length;
403
+ const elsewhere = [{ HERDR_ENV: "0" }, { HERDR_ENV: "true" }, { HERDR_ENV: "" }, { HERDR_ENV: " 1" }, {}].map(
404
+ (env) => dispatchResumeLaunch(RESUME_INPUT, tmuxLaunch, env, resumeSpawn.spawn),
405
+ );
406
+ ok(
407
+ "[QK:FCD-RESUME-RAIL-SHARED] a same-id visible resume reaches the SAME rail choice fresh call does, through its own entry point: exactly `HERDR_ENV=1` goes to the herdr resume launch and, with that herdr context incomplete, refuses by the herdr reason without ever calling the surface's tmux mapping; every other value, and an absent variable, goes to that tmux mapping with the record-derived input unchanged — a second, looser predicate or a fallback either way would reopen a citizen where the operator is not looking",
408
+ !inHerdr.ok &&
409
+ inHerdr.reason === "herdr-context-missing" &&
410
+ tmuxCallsInHerdr === 0 &&
411
+ resumeSpawn.calls.length === 0 &&
412
+ elsewhere.every((r) => !("then" in r) && r.reason === "tmux-stub") &&
413
+ tmuxCalls.length === elsewhere.length &&
414
+ tmuxCalls.every((input) => input === RESUME_INPUT),
415
+ );
416
+ }
417
+
357
418
  console.log(`\n[check-fresh-call-dispatch] ${passed} assertions ok`);
358
419
  }
359
420
 
@@ -63,6 +63,7 @@ export HOME="$SANDBOX/home"
63
63
  export XDG_DATA_HOME="$SANDBOX/home/.local/share"
64
64
  export XDG_STATE_HOME="$SANDBOX/home/.local/state"
65
65
  export XDG_CACHE_HOME="$SANDBOX/home/.cache"
66
+ export XDG_CONFIG_HOME="$SANDBOX/home/.config"
66
67
  export PI_CODING_AGENT_DIR="$SANDBOX/home/.pi/agent"
67
68
  export ENTWURF_META_SESSIONS_DIR="$SANDBOX/store"
68
69
  export ENTWURF_META_MAILBOX_DIR="$SANDBOX/mailbox"
@@ -1460,6 +1461,11 @@ fake_pkg="$SANDBOX/nm/node_modules/@junghanacs/entwurf"
1460
1461
  mkdir -p "$fake_pkg/scripts"
1461
1462
  cp "$REPO/run.sh" "$fake_pkg/run.sh"
1462
1463
  cp "$REPO/scripts/meta-bridge-store-doctor.ts" "$fake_pkg/scripts/"
1464
+ # Sourcing run.sh with no argv still walks its dispatch to the root usage, which renders
1465
+ # ENTWURF-CLI.json through scripts/cli-help.mjs (#153). Without those two files that render fails
1466
+ # first, under run.sh's own errexit, and the doctor branch below is never reached.
1467
+ cp "$REPO/ENTWURF-CLI.json" "$fake_pkg/ENTWURF-CLI.json"
1468
+ cp "$REPO/scripts/cli-help.mjs" "$fake_pkg/scripts/"
1463
1469
  set +e
1464
1470
  out="$(RUNSH="$fake_pkg/run.sh" bash -c 'set --; source "$RUNSH" >/dev/null; preflight_v3_store install' 2>&1)"; rc=$?
1465
1471
  set -e
@@ -1485,19 +1491,27 @@ rm -rf "$SANDBOX/nm"
1485
1491
  # names — the point is not that failures are nonzero, it is that a caller can BRANCH.
1486
1492
  echo "[check-fresh-cut-gate] J. exit contract"
1487
1493
 
1488
- # J1/J2 — the two states that need no host at all.
1494
+ # J1/J2 — the two states that need no host at all. Streams are kept apart: the leaf's own
1495
+ # usage line is now only the address of the description (#153), and the contract text itself —
1496
+ # exit numbers included — is the public page `entwurf meta-bridge-fresh-cut --help` prints, which
1497
+ # check-install-surface S9u holds against this verb's ce92dc7 text and FRESH_CUT_EXIT.
1498
+ fc_pointer='meta-bridge-fresh-cut: see `entwurf help meta-bridge-fresh-cut` (from that checkout'"'"'s root: `./run.sh help meta-bridge-fresh-cut`)'
1489
1499
  set +e
1490
- help_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" --help 2>&1)"; rc=$?
1500
+ node --experimental-strip-types "${FRESH_CUT[@]}" --help >"$SANDBOX/j1.out" 2>"$SANDBOX/j1.err"; rc=$?
1491
1501
  set -e
1492
- if [ "$rc" = 0 ]; then ok "J1 --help exits 0"; else bad "J1 --help exited $rc" "$help_out"; fi
1493
- case "$help_out" in
1494
- *"EXIT CONTRACT"*) ok "J1b --help PRINTS the contract (an operator can read it where they hit it)" ;;
1495
- *) bad "J1b --help never printed the exit contract" "$help_out" ;;
1496
- esac
1502
+ if [ "$rc" = 0 ]; then ok "J1 --help exits 0"; else bad "J1 --help exited $rc" "$(cat "$SANDBOX/j1.err")"; fi
1497
1503
  set +e
1498
- bad_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" --wat 2>&1)"; rc=$?
1504
+ node --experimental-strip-types "${FRESH_CUT[@]}" --wat >"$SANDBOX/j2.out" 2>"$SANDBOX/j2.err"; rc2=$?
1499
1505
  set -e
1500
- if [ "$rc" = 2 ]; then ok "J2 an unknown argument is USAGE (exit 2), never a store verdict"; else bad "J2 unknown arg exited $rc, want 2" "$bad_out"; fi
1506
+ if [ "$rc2" = 2 ]; then ok "J2 an unknown argument is USAGE (exit 2), never a store verdict"; else bad "J2 unknown arg exited $rc2, want 2" "$(cat "$SANDBOX/j2.err")"; fi
1507
+ if [ "$rc" = 0 ] && [ "$rc2" = 2 ] && [ ! -s "$SANDBOX/j1.out" ] && [ ! -s "$SANDBOX/j2.out" ] \
1508
+ && [ "$(cat "$SANDBOX/j1.err")" = "$fc_pointer" ] && [ "$(wc -l <"$SANDBOX/j1.err")" = 1 ] \
1509
+ && cmp -s "$SANDBOX/j1.err" "$SANDBOX/j2.err"; then
1510
+ ok "J1b --help (exit 0) and an unknown argument (exit 2) print nothing on stdout and one stderr line pointing at \`entwurf help meta-bridge-fresh-cut\`"
1511
+ else
1512
+ bad "[QK:FRESHCUT-HELP-POINTER] J1b the leaf's usage is not exactly the one pointer line on stderr with an empty stdout" \
1513
+ "help rc=$rc stdout=$(head -c 200 "$SANDBOX/j1.out") stderr=$(head -c 400 "$SANDBOX/j1.err") | wat rc=$rc2 stdout=$(head -c 200 "$SANDBOX/j2.out") stderr=$(head -c 400 "$SANDBOX/j2.err")"
1514
+ fi
1501
1515
 
1502
1516
  # J3 — the success state, distinct from every failure below.
1503
1517
  reset_world prevgen