@junghanacs/entwurf 0.41.2 → 0.45.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 (41) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +159 -0
  3. package/ENTWURF.json +236 -0
  4. package/README.md +12 -7
  5. package/docs/setup-clean-host.md +8 -8
  6. package/mcp/entwurf-bridge/dist/ENTWURF.json +236 -0
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -188
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +15 -11
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +18 -16
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
  14. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +23 -45
  15. package/mcp/entwurf-bridge/src/index.ts +121 -319
  16. package/package.json +3 -2
  17. package/pi-extensions/lib/agent-guidance.ts +341 -0
  18. package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
  19. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +15 -16
  20. package/pi-extensions/lib/mux-fresh-call.ts +23 -26
  21. package/pi-extensions/lib/mux-resume-call.ts +18 -19
  22. package/pi-extensions/lib/remote-pair-send.ts +7 -4
  23. package/pi-extensions/meta-bridge-pi-durable.ts +23 -64
  24. package/run.sh +9 -0
  25. package/scripts/build-bridge.sh +10 -0
  26. package/scripts/check-capability-bundle-reach.ts +86 -0
  27. package/scripts/check-entwurf-self-address.ts +3 -3
  28. package/scripts/check-entwurf-v2-surface.ts +94 -98
  29. package/scripts/check-gate-qualification.ts +9 -7
  30. package/scripts/check-mux-resume-call.ts +20 -3
  31. package/scripts/mutants/agent-guidance.json +160 -0
  32. package/scripts/mutants/bridge-boot-resume.json +6 -8
  33. package/scripts/mutants/capability-bundle-reach.json +124 -0
  34. package/scripts/mutants/control-send-receipt.json +21 -4
  35. package/scripts/mutants/mux-fresh-call.json +14 -3
  36. package/scripts/mutants/mux-resume-call.json +28 -3
  37. package/scripts/mutants/pi-durable.json +17 -5
  38. package/scripts/mutants/remote-pair-send.json +21 -2
  39. package/scripts/mutants/self-address.json +10 -10
  40. package/scripts/mutants/v2-surface.json +50 -64
  41. package/scripts/mutants/v2-visible-resume.json +11 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Product-owned natural-language logic, authored only in root ENTWURF.json (#154).
3
+ * One module-relative file, read once. The compiled layout carries an exact copy at
4
+ * dist/ENTWURF.json; no cwd/HOME search, override, old-text fallback or hot reload.
5
+ * Existing operator engraving and user-configuration contracts are unrelated.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ const TOOL_SPEC = {
9
+ entwurf_v2: { durable: true, parameters: ["target", "intent", "message", "mode", "wants_reply"] },
10
+ entwurf_self: { durable: true, parameters: [] },
11
+ entwurf_peers: { durable: true, parameters: [] },
12
+ entwurf_inbox_read: { durable: true, parameters: ["gardenId"] },
13
+ entwurf_register_native: { durable: false, parameters: ["backend", "nativeSessionId", "cwd"] },
14
+ entwurf_fresh_call: {
15
+ durable: true,
16
+ parameters: ["backend", "model", "task", "cwd", "placement.tmuxSession", "placement"],
17
+ },
18
+ entwurf_resume_call: { durable: true, parameters: ["target"] },
19
+ entwurf_retire_call: { durable: true, parameters: ["target"] },
20
+ entwurf_callback: { durable: true, parameters: [] },
21
+ };
22
+ /** Frozen entwurf_v2 reject wire ids whose static next-step hint text lives in the root file. */
23
+ const REJECT_HINT_REASONS = [
24
+ "dormant-fire-forget-unsupported",
25
+ "indeterminate-no-spawn",
26
+ "native-push-target-dead",
27
+ "native-push-probe-indeterminate",
28
+ "native-push-paused",
29
+ ];
30
+ /** Frozen entwurf_resume_call record/liveness reject wire ids whose static hint text lives in the root file. */
31
+ const RESUME_REJECT_HINT_REASONS = [
32
+ "target-invalid",
33
+ "target-backend-unsupported",
34
+ "target-locked",
35
+ "target-live",
36
+ "target-indeterminate",
37
+ "target-receiver-absent",
38
+ "target-receiver-corrupt",
39
+ "target-receiver-mismatch",
40
+ "target-receiver-unbound",
41
+ "target-address-conflict",
42
+ "launch-refused",
43
+ ];
44
+ /** Frozen entwurf_fresh_call tmux launch-stage reject wire ids whose static hint text lives in the root file. */
45
+ const FRESH_LAUNCH_REJECT_HINT_REASONS = [
46
+ "no-tmux-context",
47
+ "anchor-malformed",
48
+ "anchor-unresolved",
49
+ "anchor-mismatch",
50
+ "caller-identity-unavailable",
51
+ "cwd-not-absolute",
52
+ "cwd-format-token",
53
+ "cwd-missing",
54
+ "cwd-not-directory",
55
+ "tmux-session-name-invalid",
56
+ "tmux-session-missing",
57
+ "model-empty",
58
+ "task-empty",
59
+ "runtime-unresolved",
60
+ "runtime-not-absolute",
61
+ "runtime-path-whitespace",
62
+ "runtime-missing",
63
+ "runtime-not-regular-file",
64
+ "runtime-not-executable",
65
+ ];
66
+ /** Frozen entwurf_resume_call tmux launch-stage reject wire ids whose static hint text lives in the root file. */
67
+ const RESUME_LAUNCH_REJECT_HINT_REASONS = [
68
+ "no-tmux-context",
69
+ "anchor-malformed",
70
+ "anchor-unresolved",
71
+ "anchor-mismatch",
72
+ "cwd-not-absolute",
73
+ "cwd-format-token",
74
+ "cwd-missing",
75
+ "cwd-not-directory",
76
+ "herdr-resume-unsupported",
77
+ "runtime-unresolved",
78
+ "runtime-not-absolute",
79
+ "runtime-path-whitespace",
80
+ "runtime-missing",
81
+ "runtime-not-regular-file",
82
+ "runtime-not-executable",
83
+ ];
84
+ /** Frozen keys of the static delivery-result clauses whose text lives in the root file; the
85
+ * receipts around them (syntax, branch conditions, slots, punctuation) stay code-owned. */
86
+ const DELIVERY_RESULT_CLAUSE_KEYS = [
87
+ "finalized-rejected",
88
+ "dirty-lock",
89
+ "remote-accepted",
90
+ "remote-reported-error",
91
+ "remote-indeterminate",
92
+ ];
93
+ /** Frozen wire ids of the reject hints whose authored text sits on either side of ONE code-owned
94
+ * slot, and each one's exact segment keys. The slot value, the reason branch and the
95
+ * concatenation stay code-owned; the root file holds only the text around the slot. */
96
+ const SLOTTED_REJECT_HINT_SEGMENTS = {
97
+ "record-less-socket": ["beforeFreshCutPrescription", "afterFreshCutPrescription"],
98
+ };
99
+ function record(value, at) {
100
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
101
+ throw new Error(`ENTWURF.json ${at}: expected an object`);
102
+ }
103
+ }
104
+ function keys(value, expected, at) {
105
+ const actual = Object.keys(value).sort();
106
+ const wanted = [...expected].sort();
107
+ if (actual.length !== wanted.length || actual.some((key, i) => key !== wanted[i])) {
108
+ throw new Error(`ENTWURF.json ${at}: keys must be exactly ${wanted.join(", ")}`);
109
+ }
110
+ }
111
+ function text(value, at) {
112
+ if (typeof value !== "string" || value.length === 0) {
113
+ throw new Error(`ENTWURF.json ${at}: expected a non-empty string`);
114
+ }
115
+ }
116
+ /**
117
+ * JSON.parse owns strict grammar, but its last-key-wins value is never enough.
118
+ * Walk the ORIGINAL, already grammar-checked tokens and reject duplicate DECODED
119
+ * object keys before returning data. Object scopes are independent; strings that
120
+ * are values (including punctuation and escaped quotes) are never key tokens.
121
+ */
122
+ function rejectDuplicateKeys(source) {
123
+ let pos = 0;
124
+ const space = () => {
125
+ while (pos < source.length && /[\x20\t\r\n]/.test(source[pos]))
126
+ pos++;
127
+ };
128
+ const string = () => {
129
+ const start = pos++;
130
+ while (pos < source.length) {
131
+ if (source[pos] === "\\")
132
+ pos += 2;
133
+ else if (source[pos++] === '"')
134
+ return JSON.parse(source.slice(start, pos));
135
+ }
136
+ throw new Error("ENTWURF.json: unterminated string");
137
+ };
138
+ const value = () => {
139
+ space();
140
+ if (source[pos] === "{") {
141
+ pos++;
142
+ space();
143
+ const seen = new Set();
144
+ if (source[pos] !== "}") {
145
+ for (;;) {
146
+ space();
147
+ const key = string();
148
+ if (seen.has(key))
149
+ throw new Error(`ENTWURF.json: duplicate object key ${JSON.stringify(key)}`);
150
+ seen.add(key);
151
+ space();
152
+ pos++; // colon; JSON.parse already certified the grammar
153
+ value();
154
+ space();
155
+ if (source[pos] !== ",")
156
+ break;
157
+ pos++;
158
+ }
159
+ }
160
+ pos++;
161
+ }
162
+ else if (source[pos] === "[") {
163
+ pos++;
164
+ space();
165
+ if (source[pos] !== "]") {
166
+ for (;;) {
167
+ value();
168
+ space();
169
+ if (source[pos] !== ",")
170
+ break;
171
+ pos++;
172
+ }
173
+ }
174
+ pos++;
175
+ }
176
+ else if (source[pos] === '"') {
177
+ string();
178
+ }
179
+ else {
180
+ while (pos < source.length && !/[\x20\t\r\n,}\]]/.test(source[pos]))
181
+ pos++;
182
+ }
183
+ };
184
+ value();
185
+ }
186
+ function freeze(value) {
187
+ if (value === null || typeof value !== "object")
188
+ return;
189
+ for (const child of Object.values(value))
190
+ freeze(child);
191
+ Object.freeze(value);
192
+ }
193
+ /** Pure data-validation seam; production always reads the one fixed module URL below. */
194
+ export function parseEntwurfGuidance(source) {
195
+ const data = JSON.parse(source);
196
+ rejectDuplicateKeys(source);
197
+ record(data, "root");
198
+ if (data.schemaVersion !== 6)
199
+ throw new Error("ENTWURF.json: unsupported schemaVersion");
200
+ keys(data, [
201
+ "_about",
202
+ "schemaVersion",
203
+ "revision",
204
+ "history",
205
+ "tools",
206
+ "rejectHints",
207
+ "resumeRejectHints",
208
+ "freshLaunchRejectHints",
209
+ "resumeLaunchRejectHints",
210
+ "deliveryResultClauses",
211
+ "slottedRejectHints",
212
+ ], "root");
213
+ if (!Number.isSafeInteger(data.revision) || data.revision < 1) {
214
+ throw new Error("ENTWURF.json: revision must be a positive safe integer");
215
+ }
216
+ record(data._about, "_about");
217
+ keys(data._about, ["purpose", "ownership", "scope", "editing"], "_about");
218
+ for (const [key, value] of Object.entries(data._about))
219
+ text(value, `_about.${key}`);
220
+ if (!Array.isArray(data.history) || data.history.length === 0) {
221
+ throw new Error("ENTWURF.json: history must be a non-empty array");
222
+ }
223
+ for (const entry of data.history) {
224
+ record(entry, "history entry");
225
+ keys(entry, ["revision", "source", "issue", "change"], "history entry");
226
+ if (!Number.isSafeInteger(entry.revision) ||
227
+ entry.revision < 1 ||
228
+ entry.revision > data.revision) {
229
+ throw new Error("ENTWURF.json: history revision must name an existing payload revision");
230
+ }
231
+ for (const key of ["source", "issue", "change"])
232
+ text(entry[key], `history.${key}`);
233
+ }
234
+ record(data.tools, "tools");
235
+ keys(data.tools, Object.keys(TOOL_SPEC), "tools");
236
+ for (const name of Object.keys(TOOL_SPEC)) {
237
+ const tool = data.tools[name];
238
+ record(tool, name);
239
+ keys(tool, ["descriptions", "parameters"], name);
240
+ record(tool.descriptions, `${name}.descriptions`);
241
+ keys(tool.descriptions, TOOL_SPEC[name].durable ? ["mcp", "durable"] : ["mcp"], `${name}.descriptions`);
242
+ for (const [rail, value] of Object.entries(tool.descriptions))
243
+ text(value, `${name}.descriptions.${rail}`);
244
+ record(tool.parameters, `${name}.parameters`);
245
+ keys(tool.parameters, TOOL_SPEC[name].parameters, `${name}.parameters`);
246
+ for (const [field, value] of Object.entries(tool.parameters))
247
+ text(value, `${name}.parameters.${field}`);
248
+ }
249
+ record(data.rejectHints, "rejectHints");
250
+ keys(data.rejectHints, REJECT_HINT_REASONS, "rejectHints");
251
+ for (const [reason, value] of Object.entries(data.rejectHints))
252
+ text(value, `rejectHints.${reason}`);
253
+ record(data.resumeRejectHints, "resumeRejectHints");
254
+ keys(data.resumeRejectHints, RESUME_REJECT_HINT_REASONS, "resumeRejectHints");
255
+ for (const [reason, value] of Object.entries(data.resumeRejectHints))
256
+ text(value, `resumeRejectHints.${reason}`);
257
+ record(data.freshLaunchRejectHints, "freshLaunchRejectHints");
258
+ keys(data.freshLaunchRejectHints, FRESH_LAUNCH_REJECT_HINT_REASONS, "freshLaunchRejectHints");
259
+ for (const [reason, value] of Object.entries(data.freshLaunchRejectHints))
260
+ text(value, `freshLaunchRejectHints.${reason}`);
261
+ record(data.resumeLaunchRejectHints, "resumeLaunchRejectHints");
262
+ keys(data.resumeLaunchRejectHints, RESUME_LAUNCH_REJECT_HINT_REASONS, "resumeLaunchRejectHints");
263
+ for (const [reason, value] of Object.entries(data.resumeLaunchRejectHints))
264
+ text(value, `resumeLaunchRejectHints.${reason}`);
265
+ record(data.deliveryResultClauses, "deliveryResultClauses");
266
+ keys(data.deliveryResultClauses, DELIVERY_RESULT_CLAUSE_KEYS, "deliveryResultClauses");
267
+ for (const [key, value] of Object.entries(data.deliveryResultClauses))
268
+ text(value, `deliveryResultClauses.${key}`);
269
+ record(data.slottedRejectHints, "slottedRejectHints");
270
+ keys(data.slottedRejectHints, Object.keys(SLOTTED_REJECT_HINT_SEGMENTS), "slottedRejectHints");
271
+ for (const reason of Object.keys(SLOTTED_REJECT_HINT_SEGMENTS)) {
272
+ const segments = data.slottedRejectHints[reason];
273
+ record(segments, `slottedRejectHints.${reason}`);
274
+ keys(segments, SLOTTED_REJECT_HINT_SEGMENTS[reason], `slottedRejectHints.${reason}`);
275
+ for (const [key, value] of Object.entries(segments))
276
+ text(value, `slottedRejectHints.${reason}.${key}`);
277
+ }
278
+ freeze(data);
279
+ return data;
280
+ }
281
+ const guidance = parseEntwurfGuidance(readFileSync(new URL("../../ENTWURF.json", import.meta.url), "utf8"));
282
+ /** Only these exact payload strings enter tool schemas; _about/history never do. */
283
+ export const ENTWURF_TOOL_GUIDANCE = guidance.tools;
284
+ /** Static next-step text appended to these entwurf_v2 rejections; the decider and wire ids stay code-owned. */
285
+ export const ENTWURF_REJECT_HINTS = guidance.rejectHints;
286
+ /** Static next-step text in these entwurf_resume_call record/liveness rejections; the verb and the shared ACP model hint stay code-owned. */
287
+ export const ENTWURF_RESUME_REJECT_HINTS = guidance.resumeRejectHints;
288
+ /** Static tmux launch-stage hints of entwurf_fresh_call; slot-bearing, ACP and preflight-leaf hints stay code-owned. */
289
+ export const ENTWURF_FRESH_LAUNCH_REJECT_HINTS = guidance.freshLaunchRejectHints;
290
+ /** Static tmux launch-stage hints of entwurf_resume_call, kept apart from the fresh verb even where the text matches. */
291
+ export const ENTWURF_RESUME_LAUNCH_REJECT_HINTS = guidance.resumeLaunchRejectHints;
292
+ /** Static delivery-result clauses of the entwurf_v2 dirty-lock and remote-pair receipts; the receipt syntax around them stays code-owned. */
293
+ export const ENTWURF_DELIVERY_RESULT_CLAUSES = guidance.deliveryResultClauses;
294
+ /** Text on either side of one code-owned slot per reject reason; the slot value and the concatenation stay with the consumer. */
295
+ export const ENTWURF_SLOTTED_REJECT_HINTS = guidance.slottedRejectHints;
296
+ export const ENTWURF_GUIDANCE_VERSION = Object.freeze({
297
+ schemaVersion: guidance.schemaVersion,
298
+ revision: guidance.revision,
299
+ });
@@ -17,6 +17,7 @@
17
17
  * surface never needs to name the `EntwurfV2RunResult` union (it only sees `{text,isError}`).
18
18
  */
19
19
  import { basename } from "node:path";
20
+ import { ENTWURF_DELIVERY_RESULT_CLAUSES, ENTWURF_REJECT_HINTS, ENTWURF_SLOTTED_REJECT_HINTS, } from "./agent-guidance.js";
20
21
  import { makeProductionEntwurfV2Deps } from "./entwurf-v2-production.js";
21
22
  import { runEntwurfV2 } from "./entwurf-v2-runner.js";
22
23
  import { FRESH_CUT_PRESCRIPTION } from "./meta-session.js";
@@ -36,7 +37,9 @@ export function toDispatchInput(params) {
36
37
  * Detour B (B-a) — actionable rendering of an honest reject. The decider is UNCHANGED:
37
38
  * a reject stays a reject (Hard Rule 3). This only appends a one-line "what to do instead"
38
39
  * to the reject TEXT, so an honest reject stops reading as "delivery impossible". Returns
39
- * undefined for rejects with no useful next step.
40
+ * undefined for rejects with no useful next step. Hint English is authored in root ENTWURF.json
41
+ * (#154); the reason switch stays here, and so do the record-less-socket slot value and its
42
+ * concatenation (#154 E1b).
40
43
  */
41
44
  export function actionableRejectHint(reason) {
42
45
  switch (reason) {
@@ -45,11 +48,7 @@ export function actionableRejectHint(reason) {
45
48
  // state — the citizen exists and is not running — matters more than ever now that
46
49
  // there is no verb behind it: a caller that reads "reject" as "wrong id" goes
47
50
  // looking in the wrong place.
48
- return ("this citizen's record is intact but its session is not running, and delivery has no way to wake it. " +
49
- "The resume that used to answer here launched a hidden background child and was withdrawn under the " +
50
- "visible-first rule; the visible same-id resume is the separate lifecycle verb entwurf_resume_call. " +
51
- "Only on an explicit operator request, use entwurf_resume_call {target} for Pi or Claude Code; " +
52
- "it submits no prompt. This capability hint is not permission to resume automatically.");
51
+ return ENTWURF_REJECT_HINTS["dormant-fire-forget-unsupported"];
53
52
  case "indeterminate-no-spawn":
54
53
  // The socket probe did not settle, so liveness is UNKNOWN — an unestablished probe
55
54
  // is not a measured death, and dispatching into it could double-deliver. Say
@@ -60,31 +59,27 @@ export function actionableRejectHint(reason) {
60
59
  // (never dispatch into an indeterminate target), not a capability that still
61
60
  // exists. So the hint's job is to say plainly what did NOT happen: nothing
62
61
  // delivered, nothing started.
63
- return ("the control-socket probe was inconclusive, so the target's liveness is UNKNOWN — this is not a " +
64
- "measured death. NOTHING was delivered and NO process was started (the reason id keeps its frozen " +
65
- "'-no-spawn' wire spelling from an era when one could be; entwurf_v2 starts nothing on any rail). " +
66
- "Re-run entwurf_peers to re-probe; if it stays indeterminate, check for a stale socket file left at " +
67
- "that garden id by a session that died without cleaning up.");
62
+ return ENTWURF_REJECT_HINTS["indeterminate-no-spawn"];
68
63
  case "native-push-target-dead":
69
64
  // The adapter probe found no live native host serving this conversation/thread. Stays
70
65
  // backend-neutral: agy speaks conversations over an LS port, Codex threads over an
71
66
  // app-server socket, and this hint is read by callers of both.
72
- return "the native-push conversation/thread is not live — no native host (harness process or app-server) answered the probe for it. Re-open that conversation/thread in its own harness, then retry; there is nothing to inject into.";
67
+ return ENTWURF_REJECT_HINTS["native-push-target-dead"];
73
68
  case "native-push-probe-indeterminate":
74
69
  // Native host up, but nothing served this conversation/thread — inconclusive, not a
75
70
  // hard dead. Backend-neutral for the same reason as the dead cell above.
76
- return "the native host (harness process or app-server) is up, but nothing served this conversation/thread, so the probe is INCONCLUSIVE — not a measured death. Retry once it is loaded, or verify the conversation/thread id.";
71
+ return ENTWURF_REJECT_HINTS["native-push-probe-indeterminate"];
77
72
  case "native-push-paused":
78
73
  // #152: a policy refusal, not a liveness verdict — this dispatch probed and sent nothing.
79
- return "native-push for this backend is paused (#152): its language server refused entwurf's measured external calls for a missing CSRF token, so this dispatch probed and sent nothing. The citizen and its record are kept; reach it from inside its own harness instead.";
80
- case "record-less-socket":
74
+ return ENTWURF_REJECT_HINTS["native-push-paused"];
75
+ case "record-less-socket": {
81
76
  // #50 C4: name the true cause AND the fix — a bare socket is a diagnostic
82
- // state, not an addressable citizen (the record is the address).
83
- return ("a control socket exists at this garden id but NO meta-record claims it — the record is the " +
84
- "sole address authority, so a bare socket is not an addressable citizen. If a pre-record-era " +
85
- "resident owns that socket, restart it under the current runtime (session_start births its " +
86
- `record); if the store predates the current generation, quiesce and archive it with ` +
87
- `${FRESH_CUT_PRESCRIPTION}. A stale/forged socket should be removed.`);
77
+ // state, not an addressable citizen (the record is the address). The text on either
78
+ // side of the fix command is authored in root ENTWURF.json; the command itself stays
79
+ // with its owner (meta-session.ts), so the root file never copies it.
80
+ const hint = ENTWURF_SLOTTED_REJECT_HINTS["record-less-socket"];
81
+ return `${hint.beforeFreshCutPrescription}${FRESH_CUT_PRESCRIPTION}${hint.afterFreshCutPrescription}`;
82
+ }
88
83
  default:
89
84
  return undefined;
90
85
  }
@@ -185,13 +180,14 @@ export function renderEntwurfV2Result(result) {
185
180
  // the same acceptance vocabulary the ordinary receipt uses is named here too — a
186
181
  // dirty lock is a reason to be MORE precise about what happened, not less, because
187
182
  // the operator is about to decide by hand whether anything needs re-sending.
183
+ // The two static clauses are authored in root ENTWURF.json (#154 E1a); this receipt's
184
+ // syntax, branches and slots stay here.
188
185
  const what = result.finalizedOutcome === "rejected"
189
- ? `rejected${result.finalizedRejectReason ? ` (${result.finalizedRejectReason})` : ""} — the receiver refused it; nothing was accepted`
186
+ ? `rejected${result.finalizedRejectReason ? ` (${result.finalizedRejectReason})` : ""} — ${ENTWURF_DELIVERY_RESULT_CLAUSES["finalized-rejected"]}`
190
187
  : `${result.finalizedBoundary ?? "mailbox-enqueued"}${result.finalizedMessagePath ? ` (enqueued ${basename(result.finalizedMessagePath)})` : ""}`;
191
188
  return {
192
- text: `entwurf_v2 ${result.transport} finalized as ${what}, and the lock is DIRTY ` +
193
- `(release failed) — do NOT retry, a re-send would double-deliver. Clear the lock by hand.` +
194
- `\n error: ${result.error}`,
189
+ text: `entwurf_v2 ${result.transport} finalized as ${what}, and ` +
190
+ `${ENTWURF_DELIVERY_RESULT_CLAUSES["dirty-lock"]}\n error: ${result.error}`,
195
191
  isError: true,
196
192
  };
197
193
  }
@@ -73,6 +73,7 @@
73
73
  * half reads the receiver marker and the record instead.
74
74
  */
75
75
  import { ACP_MODEL_NOT_CURATED_HINT, AcpModelNotCuratedError } from "./acp/model-ids.js";
76
+ import { ENTWURF_RESUME_REJECT_HINTS } from "./agent-guidance.js";
76
77
  import { buildResumePiArgs } from "./entwurf-resume-args.js";
77
78
  import { ENTWURF_V2_LOCK_DIR, acquireLock as realAcquireLock, releaseLock as realReleaseLock, } from "./entwurf-v2-lock.js";
78
79
  import { classifyMarkerOwner, metaReceiverMarkerPath, parseMetaReceiverMarker, probePidExistence, processStartKey, readStoreRecordFile, } from "./meta-session.js";
@@ -441,19 +442,22 @@ export async function observeSameGidSocket(targetGardenId, socketPath, deps, tim
441
442
  await deps.sleep(intervalMs);
442
443
  }
443
444
  }
445
+ // The static record/liveness hint text is authored in root ENTWURF.json (#154 C1-C); the reasons,
446
+ // this total table and the renderer stay here. acp-model-not-curated keeps the shared ACP constant,
447
+ // and the launch-stage mux hints folded into `detail` stay behind the mux import fence.
444
448
  const REJECT_HINT = {
445
- "target-invalid": "no citizen has that id; discover targets with entwurf_peers",
446
- "target-backend-unsupported": "that citizen's backend has no same-id resume (only pi and Claude Code do) — reach it with entwurf_v2, or open a fresh sibling",
449
+ "target-invalid": ENTWURF_RESUME_REJECT_HINTS["target-invalid"],
450
+ "target-backend-unsupported": ENTWURF_RESUME_REJECT_HINTS["target-backend-unsupported"],
447
451
  "acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
448
- "target-locked": "another resume or dispatch holds this citizen's lock right now",
449
- "target-live": "that citizen is already running — this verb only reopens dormant ones",
450
- "target-indeterminate": "the citizen could not be proven dormant (an inconclusive socket probe or receiver owner), so nothing was started",
451
- "target-receiver-absent": "a Claude Code citizen is proven dormant only by its receiver marker, and it has none — if its window is closed, reopen it yourself",
452
- "target-receiver-corrupt": "the citizen's receiver marker cannot be read as a marker, so its owner cannot be judged",
453
- "target-receiver-mismatch": "the receiver marker under this garden id names another backend or native session than the record — look at the store before anything reopens",
454
- "target-receiver-unbound": "the dead receiver owner is not bound to the record's last arm (none observed, a later attach, or the record moved), so a Claude Code process may still hold the session — if its window is closed, reopen it yourself",
455
- "target-address-conflict": "that citizen's control-socket path is corrupt",
456
- "launch-refused": "tmux was never asked to open a window",
452
+ "target-locked": ENTWURF_RESUME_REJECT_HINTS["target-locked"],
453
+ "target-live": ENTWURF_RESUME_REJECT_HINTS["target-live"],
454
+ "target-indeterminate": ENTWURF_RESUME_REJECT_HINTS["target-indeterminate"],
455
+ "target-receiver-absent": ENTWURF_RESUME_REJECT_HINTS["target-receiver-absent"],
456
+ "target-receiver-corrupt": ENTWURF_RESUME_REJECT_HINTS["target-receiver-corrupt"],
457
+ "target-receiver-mismatch": ENTWURF_RESUME_REJECT_HINTS["target-receiver-mismatch"],
458
+ "target-receiver-unbound": ENTWURF_RESUME_REJECT_HINTS["target-receiver-unbound"],
459
+ "target-address-conflict": ENTWURF_RESUME_REJECT_HINTS["target-address-conflict"],
460
+ "launch-refused": ENTWURF_RESUME_REJECT_HINTS["launch-refused"],
457
461
  };
458
462
  /**
459
463
  * ONE renderer — the same reason `mux-fresh-call` has one: it keeps the operator-visible answer
@@ -86,6 +86,7 @@
86
86
  * there is no "session created" field because nothing here creates one.
87
87
  */
88
88
  import { ACP_MODEL_NOT_CURATED_HINT } from "./acp/model-ids.js";
89
+ import { ENTWURF_FRESH_LAUNCH_REJECT_HINTS } from "./agent-guidance.js";
89
90
  import { callbackEnvAssignments } from "./callback-env.js";
90
91
  import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
91
92
  import { CODEX_CALLER_SEAT_HINT, resolveCodexCallerSeat, } from "./codex-caller-seat.js";
@@ -442,7 +443,9 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
442
443
  };
443
444
  }
444
445
  /** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
445
- * they will guess about. */
446
+ * they will guess about. The static tmux launch-stage text is authored in root
447
+ * ENTWURF.json (#154 C1-D) and bound key by key, never spread, so a duplicate or missing reason
448
+ * still fails typecheck; slot-bearing, ACP and preflight-leaf hints stay code-owned. */
446
449
  const REJECT_HINT = {
447
450
  // The Copilot capability reasons keep their repair text on the leaf that decides them, so
448
451
  // the sentence an operator reads cannot drift away from the predicate that produced it.
@@ -454,28 +457,28 @@ const REJECT_HINT = {
454
457
  ...CODEX_CALLER_PREFLIGHT_HINT,
455
458
  ...CODEX_CALLER_SEAT_HINT,
456
459
  ...FRESH_CALL_PROVIDER_HINT,
457
- "no-tmux-context": "this agent is not running inside tmux, so there is no session to open a sibling beside",
458
- "anchor-malformed": "TMUX_PANE is not a native pane id",
459
- "anchor-unresolved": "tmux resolved no pane for this agent's anchor",
460
- "anchor-mismatch": "tmux answered about a different pane than the one asked about",
461
- "caller-identity-unavailable": "this surface has no record-backed garden id for the caller, so the sibling would have no address to call back to",
462
- "cwd-not-absolute": "the requested cwd is not an absolute path (the value is taken literally — nothing trims or resolves it)",
463
- "cwd-format-token": "the requested cwd contains '#', which tmux expands as a format inside -c — it would silently rewrite the path or run a command",
464
- "cwd-missing": "the requested cwd does not exist; tmux would not report this, it would open the window in $HOME and look successful",
465
- "cwd-not-directory": "the requested cwd exists but is not a directory",
466
- "tmux-session-name-invalid": "the requested tmux session name is outside the shape this rail addresses (start with a letter or digit, then letters, digits, '_' or '-') — some other shapes tmux cannot resolve at all ('#' is expanded when the name is stored; '.' and ':' are its own pane/window separators inside a target; a name like '$0' loses to the session id '$0'), and the rest are declined to keep one narrow grammar, so rename the session or open one whose name fits",
467
- "tmux-session-missing": "no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again",
468
- "model-empty": "model is empty after trimming; fresh calls require an explicit model",
460
+ "no-tmux-context": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["no-tmux-context"],
461
+ "anchor-malformed": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-malformed"],
462
+ "anchor-unresolved": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-unresolved"],
463
+ "anchor-mismatch": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-mismatch"],
464
+ "caller-identity-unavailable": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["caller-identity-unavailable"],
465
+ "cwd-not-absolute": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-not-absolute"],
466
+ "cwd-format-token": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-format-token"],
467
+ "cwd-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-missing"],
468
+ "cwd-not-directory": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-not-directory"],
469
+ "tmux-session-name-invalid": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["tmux-session-name-invalid"],
470
+ "tmux-session-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["tmux-session-missing"],
471
+ "model-empty": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["model-empty"],
469
472
  "model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
470
473
  "acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
471
- "task-empty": "task is empty after trimming",
474
+ "task-empty": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["task-empty"],
472
475
  "task-too-long": `task exceeds ${TASK_MAX_CHARS} characters`,
473
- "runtime-unresolved": "the backend's runtime is not installed on PATH",
474
- "runtime-not-absolute": "the resolved runtime path is not absolute",
475
- "runtime-path-whitespace": "the resolved runtime path contains whitespace, which tmux would re-split",
476
- "runtime-missing": "nothing exists at the resolved runtime path",
477
- "runtime-not-regular-file": "the resolved runtime path is not a regular file",
478
- "runtime-not-executable": "the resolved runtime path is not executable",
476
+ "runtime-unresolved": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-unresolved"],
477
+ "runtime-not-absolute": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-absolute"],
478
+ "runtime-path-whitespace": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-path-whitespace"],
479
+ "runtime-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-missing"],
480
+ "runtime-not-regular-file": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-regular-file"],
481
+ "runtime-not-executable": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-executable"],
479
482
  };
480
483
  /**
481
484
  * ONE renderer, outside the registration. Not a convenience: it keeps the operator-visible answer
@@ -48,6 +48,7 @@
48
48
  * where the pane actually landed is a separate query against the stable pane id, and it belongs
49
49
  * to acceptance, not to the product's launch receipt.
50
50
  */
51
+ import { ENTWURF_RESUME_LAUNCH_REJECT_HINTS } from "./agent-guidance.js";
51
52
  import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
52
53
  import { assertLaunchTarget, LaunchPreconditionError, resolveRuntimeOnPath, SCRUBBED_INHERITED_ENV, } from "./mux-launch.js";
53
54
  import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWindowFields, requireSameContext, runTmux, } from "./mux-placement.js";
@@ -136,21 +137,22 @@ export function resumeCall(params, env = process.env) {
136
137
  };
137
138
  }
138
139
  /** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
139
- * they will guess about. */
140
+ * they will guess about. The text is authored in root ENTWURF.json (#154 C1-D); each
141
+ * key is bound explicitly so the reason union stays exhaustive here. */
140
142
  export const RESUME_CALL_REJECT_HINT = {
141
- "no-tmux-context": "this agent is not running inside tmux, so there is no session to reopen the citizen in",
142
- "anchor-malformed": "TMUX_PANE is not a native pane id",
143
- "anchor-unresolved": "tmux resolved no pane for this agent's anchor",
144
- "anchor-mismatch": "tmux answered about a different pane than the one asked about",
145
- "cwd-not-absolute": "the recorded cwd is not an absolute path",
146
- "cwd-format-token": "the recorded cwd contains '#', which tmux expands as a format inside -c — it would silently rewrite the path or run a command",
147
- "cwd-missing": "the recorded cwd no longer exists; tmux would not report this, it would open the window in $HOME and look successful",
148
- "cwd-not-directory": "the recorded cwd exists but is not a directory",
149
- "herdr-resume-unsupported": "this agent runs inside herdr, and same-id resume has only a tmux rail — nothing was opened in any ambient tmux session",
150
- "runtime-unresolved": "the citizen's runtime (pi or claude) is not installed on PATH",
151
- "runtime-not-absolute": "the resolved runtime path is not absolute",
152
- "runtime-path-whitespace": "the resolved runtime path contains whitespace, which tmux would re-split",
153
- "runtime-missing": "nothing exists at the resolved runtime path",
154
- "runtime-not-regular-file": "the resolved runtime path is not a regular file",
155
- "runtime-not-executable": "the resolved runtime path is not executable",
143
+ "no-tmux-context": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["no-tmux-context"],
144
+ "anchor-malformed": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-malformed"],
145
+ "anchor-unresolved": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-unresolved"],
146
+ "anchor-mismatch": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-mismatch"],
147
+ "cwd-not-absolute": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-not-absolute"],
148
+ "cwd-format-token": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-format-token"],
149
+ "cwd-missing": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-missing"],
150
+ "cwd-not-directory": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-not-directory"],
151
+ "herdr-resume-unsupported": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["herdr-resume-unsupported"],
152
+ "runtime-unresolved": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-unresolved"],
153
+ "runtime-not-absolute": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-absolute"],
154
+ "runtime-path-whitespace": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-path-whitespace"],
155
+ "runtime-missing": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-missing"],
156
+ "runtime-not-regular-file": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-regular-file"],
157
+ "runtime-not-executable": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-executable"],
156
158
  };
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import * as fs from "node:fs";
27
27
  import * as path from "node:path";
28
+ import { ENTWURF_DELIVERY_RESULT_CLAUSES } from "./agent-guidance.js";
28
29
  import { controlSocketPathIn } from "./control-socket-path.js";
29
30
  import { defaultMetaSessionsDir, metaRecordExistsByGardenId, requireGardenId } from "./meta-session.js";
30
31
  import { inspectControlSocketPath } from "./socket-discovery.js";
@@ -204,13 +205,15 @@ export function classifyRemoteReceipt(r) {
204
205
  }
205
206
  return indeterminate(`exit ${r.exitCode} disagrees with its receipt`);
206
207
  }
208
+ // The three static explanatory clauses are authored in root ENTWURF.json (#154 E1a); the receipt
209
+ // syntax, the route label, the slots and the refused/invalid lines stay here.
207
210
  function render(route, o) {
208
211
  const via = `via ${route.ssh}`;
209
212
  switch (o.kind) {
210
213
  case "remote-accepted":
211
214
  return {
212
215
  isError: false,
213
- text: `entwurf_v2 remote ${via} → accepted by the other host (its fact; read/answer unknown until a reply arrives; paths below are on the other host)\n${o.text}`,
216
+ text: `entwurf_v2 remote ${via} → ${ENTWURF_DELIVERY_RESULT_CLAUSES["remote-accepted"]}\n${o.text}`,
214
217
  };
215
218
  case "remote-reported-error":
216
219
  // The other host's dispatch returned isError. That can follow a write (an enqueue whose
@@ -218,7 +221,7 @@ function render(route, o) {
218
221
  // non-delivery nor safety to resend: the other host's own text below is the fact.
219
222
  return {
220
223
  isError: true,
221
- text: `entwurf_v2 remote ${via} → the other host reported an error while processing; read its receipt below, delivery is not inferred from this line; not retried (paths below are on the other host)\n${o.text}`,
224
+ text: `entwurf_v2 remote ${via} → ${ENTWURF_DELIVERY_RESULT_CLAUSES["remote-reported-error"]}\n${o.text}`,
222
225
  };
223
226
  case "remote-pair-refused":
224
227
  return {
@@ -230,8 +233,8 @@ function render(route, o) {
230
233
  case "remote-indeterminate":
231
234
  return {
232
235
  isError: true,
233
- text: `entwurf_v2 remote ${via} → indeterminate: ${o.why}. The other host may or may not have enqueued; ` +
234
- `not retried.${o.stderrTail ? `\n${o.stderrTail}` : ""}`,
236
+ text: `entwurf_v2 remote ${via} → indeterminate: ${o.why}. ` +
237
+ `${ENTWURF_DELIVERY_RESULT_CLAUSES["remote-indeterminate"]}${o.stderrTail ? `\n${o.stderrTail}` : ""}`,
235
238
  };
236
239
  }
237
240
  }