@junghanacs/entwurf 0.24.0 → 0.25.1

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 (75) hide show
  1. package/CHANGELOG.md +564 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/DELIVERY.md +24 -1
  4. package/README.md +2 -2
  5. package/VERIFY.md +20 -2
  6. package/demo/README.md +5 -2
  7. package/docs/acp-backend-rail.md +49 -2
  8. package/docs/setup-clean-host.md +4 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +5 -4
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +13 -3
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +11 -0
  18. package/mcp/entwurf-bridge/dist/protocol.js +0 -7
  19. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
  20. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
  21. package/mcp/entwurf-bridge/src/index.ts +5 -4
  22. package/package.json +10 -9
  23. package/pi-extensions/entwurf-control.ts +27 -154
  24. package/pi-extensions/lib/compaction-send-guard.ts +5 -3
  25. package/pi-extensions/lib/control-send-receipt.ts +108 -0
  26. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  27. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
  28. package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
  29. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
  30. package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
  31. package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
  32. package/pi-extensions/lib/herdr-fresh-call.ts +13 -3
  33. package/pi-extensions/lib/herdr-placement.ts +11 -0
  34. package/protocol.js +0 -8
  35. package/run.sh +202 -33
  36. package/scripts/check-entwurf-v2-surface.ts +289 -10
  37. package/scripts/check-gate-qualification.ts +2354 -11
  38. package/scripts/check-herdr-fresh-call.ts +10 -2
  39. package/scripts/check-herdr-placement.ts +51 -0
  40. package/scripts/check-herdr-plugin-build.ts +11 -4
  41. package/scripts/check-herdr-plugin.ts +745 -38
  42. package/scripts/check-mux-launcher-fence.ts +37 -0
  43. package/scripts/check-pi-launch.ts +7 -4
  44. package/scripts/check-release-gate-outcomes.ts +165 -0
  45. package/scripts/doctor-pi-provider.ts +44 -4
  46. package/scripts/herdr-rails.mjs +75 -0
  47. package/scripts/lib/qualification-acceptance.ts +146 -0
  48. package/scripts/lib/qualification-artifact.ts +359 -0
  49. package/scripts/lib/qualification-collection.ts +384 -0
  50. package/scripts/lib/qualification-gh.ts +231 -0
  51. package/scripts/lib/qualification-ledger.ts +669 -0
  52. package/scripts/lib/qualification-oracle-core.ts +279 -0
  53. package/scripts/lib/qualification-receipt.ts +561 -0
  54. package/scripts/lib/qualification-release.ts +271 -0
  55. package/scripts/meta-bridge-state.py +18 -1
  56. package/scripts/mutants/bridge-command-boot.json +38 -0
  57. package/scripts/mutants/control-send-receipt.json +235 -0
  58. package/scripts/mutants/herdr-fresh-call.json +4 -4
  59. package/scripts/mutants/herdr-plugin-build.json +7 -11
  60. package/scripts/mutants/herdr-plugin.json +177 -9
  61. package/scripts/mutants/meta-retire.json +22 -0
  62. package/scripts/mutants/pack-install.json +4 -4
  63. package/scripts/mutants/pi-floor.json +17 -0
  64. package/scripts/mutants/qualification-ledger.json +489 -0
  65. package/scripts/mutants/release-gate.json +48 -0
  66. package/scripts/mutants/retired-sent-surface.json +32 -0
  67. package/scripts/mutants/v2-surface.json +7 -7
  68. package/scripts/qualification-oracle.ts +124 -0
  69. package/scripts/smoke-codex-fresh-live.ts +94 -13
  70. package/scripts/smoke-herdr-fresh-call-live.ts +15 -8
  71. package/scripts/smoke-herdr-raw-install-live.sh +66 -4
  72. package/scripts/smoke-meta-install-state.sh +107 -1
  73. package/scripts/smoke-mux-lifecycle-live.ts +9 -2
  74. package/scripts/smoke-pi-provider-state.sh +106 -0
  75. package/scripts/smoke-setup-verdict.sh +18 -2
@@ -29,6 +29,7 @@
29
29
  * claiming "no conflict". The decider's later `inspectSocket` probe is a SEPARATE step.
30
30
  */
31
31
 
32
+ import { describeUnknownBoundary, parseAcceptanceBoundary } from "./control-send-receipt.ts";
32
33
  import {
33
34
  type RpcClientOptions,
34
35
  type RpcCommand,
@@ -318,7 +319,24 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
318
319
  wants_reply: plan.wantsReply,
319
320
  sender: opts.senderProvider(),
320
321
  });
321
- return { success: response.success, error: response.error };
322
+ // #120 P2 THE WIRE SEAM. `response.data` is another process's payload, so it is
323
+ // classified here by one total function and never carried raw any further: a shape we
324
+ // do not recognise becomes `accepted-unknown-boundary`, an ACCEPTANCE we could not
325
+ // name rather than a failure — the RPC already answered success, so the message is out
326
+ // of our hands and inventing a failure here would invite a double delivery. Only a
327
+ // SUCCESSFUL answer gets a boundary; a refusal carries its error instead.
328
+ const boundary = response.success ? parseAcceptanceBoundary(response.data) : undefined;
329
+ if (boundary === "accepted-unknown-boundary") {
330
+ // The operator has to be able to tell version skew from a broken peer, and the
331
+ // receipt itself may not carry another process's payload onto a status line.
332
+ // Hard Rule 15: diagnostics go to stderr, bounded and single-line.
333
+ const shape = describeUnknownBoundary(response.data);
334
+ process.stderr.write(
335
+ `[entwurf_v2] ${plan.targetGardenId} accepted the message but named no boundary this build knows` +
336
+ `${shape === undefined ? "" : ` (${shape})`}\n`,
337
+ );
338
+ }
339
+ return { success: response.success, error: response.error, boundary };
322
340
  },
323
341
  classifyConnect: io.classifyConnect,
324
342
  releaseLock: release,
@@ -28,6 +28,7 @@
28
28
  * lets the hand's `decideReleasePolicy` fail loud on a mis-pairing.
29
29
  */
30
30
 
31
+ import type { AcceptanceBoundary } from "./control-send-receipt.ts";
31
32
  import type {
32
33
  DispatchDecision,
33
34
  DispatchInput,
@@ -71,7 +72,15 @@ export interface DispatchExecutorDeps {
71
72
  * and it is the ONLY enqueue-side datum carried (never a read timestamp; see
72
73
  * `RpcSendResult`). */
73
74
  export type ExecutedOutcome =
74
- | { transport: "control-socket"; outcome: SendFinalOutcome; rejectReason?: string; messagePath?: string }
75
+ | {
76
+ transport: "control-socket";
77
+ outcome: SendFinalOutcome;
78
+ rejectReason?: string;
79
+ messagePath?: string;
80
+ /** #120 P2: the receiver's acceptance boundary. The SURFACE renders this, not
81
+ * `outcome` — that one names the route/release leg, which an operator cannot act on. */
82
+ boundary?: AcceptanceBoundary;
83
+ }
75
84
  | { transport: "meta-mailbox"; success: true; messagePath?: string }
76
85
  // native-push carries `retried` so the surface can note the 1-shot re-probe retry fired.
77
86
  | { transport: "native-push"; success: true; retried: boolean };
@@ -89,6 +98,15 @@ export type EntwurfV2RunResult =
89
98
  receipt: SuccessReceipt;
90
99
  transport: ExecutionPlan["transport"];
91
100
  error: string;
101
+ /** #120 P2, N1: the ACCEPTANCE the send reached before the release failed. Without it
102
+ * the dirty-lock line is the one surface left that answers "what happened to my
103
+ * message" with an internal route word. Absent on a `rejected` finalization (nothing
104
+ * was accepted) and on the mailbox fallback leg (which carries a file instead). */
105
+ finalizedBoundary?: AcceptanceBoundary;
106
+ /** #98 R, same case: the `.msg` a mailbox fallback wrote before the release failed. */
107
+ finalizedMessagePath?: string;
108
+ /** The receiver's own refusal text, when the finalization was a `rejected` one. */
109
+ finalizedRejectReason?: string;
92
110
  /** Present ONLY for the N1 case: the delivery/refusal reached a terminal outcome
93
111
  * but `releaseLock` then threw (lock dirty). A re-send would double-deliver. */
94
112
  finalizedOutcome?: Exclude<SendFinalOutcome, "failed">;
@@ -129,6 +147,7 @@ export async function executeDispatch(
129
147
  outcome: r.outcome,
130
148
  rejectReason: r.rejectReason,
131
149
  messagePath: r.messagePath,
150
+ boundary: r.boundary,
132
151
  },
133
152
  };
134
153
  } catch (err) {
@@ -140,6 +159,9 @@ export async function executeDispatch(
140
159
  transport,
141
160
  error: errorMessage(err),
142
161
  finalizedOutcome: err.finalizedOutcome,
162
+ finalizedBoundary: err.finalizedBoundary,
163
+ finalizedMessagePath: err.finalizedMessagePath,
164
+ finalizedRejectReason: err.finalizedRejectReason,
143
165
  releaseFailed: true,
144
166
  retrySafe: false,
145
167
  };
@@ -32,6 +32,7 @@
32
32
  * `releaseLock` throw never MASKS a send failure — the original error wins (5b).
33
33
  */
34
34
 
35
+ import type { AcceptanceBoundary } from "./control-send-receipt.ts";
35
36
  import type { ExecutionPlan } from "./entwurf-v2-decider.ts";
36
37
  import type { LockClaim } from "./entwurf-v2-lock.ts";
37
38
  import {
@@ -50,6 +51,12 @@ export type MetaMailboxPlan = Extract<ExecutionPlan, { transport: "meta-mailbox"
50
51
  * `send-final` outcomes. `fallback-sent` is a REAL final outcome (legacy parity: a
51
52
  * re-resolved delivery is not a hidden retry). */
52
53
  export type SendFinalOutcome = "sent" | "fallback-sent" | "rejected" | "failed";
54
+ // NOTE (#120 P2): the type above is the ROUTE/RELEASE algebra and is NOT the sender-visible
55
+ // delivery boundary. It answers WHICH LEG carried the bytes — first socket, re-resolved socket,
56
+ // mailbox — which is exactly what the release reducer needs and exactly what an operator cannot
57
+ // act on: `sent` here says a receiver accepted, never what the receiver then did with it. The
58
+ // boundary is a SEPARATE axis (`AcceptanceBoundary`, control-send-receipt.ts) riding beside this
59
+ // one, so neither can overwrite the other.
53
60
 
54
61
  /** What a single RPC / mailbox enqueue reports. `success:false` is an in-band reject
55
62
  * (the receiver answered and refused) — distinct from a thrown connect error.
@@ -66,6 +73,12 @@ export interface RpcSendResult {
66
73
  success: boolean;
67
74
  error?: string;
68
75
  messagePath?: string;
76
+ /** #120 P2: the receiver's OWN acceptance boundary, already classified by
77
+ * `control-send-receipt.ts` at the wire seam. Present on every SUCCESSFUL control-socket
78
+ * answer — an answer we could not classify is `accepted-unknown-boundary`, not an absence —
79
+ * and absent on the mailbox rail, which has no turn-injection boundary to report and answers
80
+ * with a durable file receipt instead. */
81
+ boundary?: AcceptanceBoundary;
69
82
  }
70
83
 
71
84
  /** The same-lock one-shot re-resolve result (5c-2b implements the resolver; 5c-2a
@@ -116,6 +129,12 @@ export interface ControlSocketSendResult {
116
129
  * mailbox delivery whose sender gets no per-message identifier — the same letter the
117
130
  * primary mailbox rail names. */
118
131
  messagePath?: string;
132
+ /** #120 P2: the acceptance boundary the receiver reported, on EVERY successful
133
+ * control-socket leg including the re-resolved one — a `fallback-sent` that dropped it would
134
+ * say "delivered" about a message sitting in a volatile queue behind whatever else is in it.
135
+ * Absent on the mailbox fallback leg (no turn-injection boundary exists there) and on
136
+ * rejected/failed. */
137
+ boundary?: AcceptanceBoundary;
119
138
  }
120
139
 
121
140
  // A drive step's verdict: the terminal outcome, plus the original error to RETHROW on
@@ -127,6 +146,24 @@ interface SendDrive {
127
146
  rejectReason?: string;
128
147
  /** #98 R receipt from the mailbox fallback leg (see `ControlSocketSendResult`). */
129
148
  messagePath?: string;
149
+ /** #120 P2 acceptance boundary from a socket leg (see `ControlSocketSendResult`). */
150
+ boundary?: AcceptanceBoundary;
151
+ }
152
+
153
+ /**
154
+ * A successful SOCKET answer always carries a boundary: the wire seam classifies every answer with
155
+ * one total function, and an answer it cannot place is `accepted-unknown-boundary` rather than
156
+ * nothing. So `undefined` here is not a host state, it is a bypassed seam — a dep wired straight
157
+ * past the classifier. Fail loud. A silent default would make the one shape that must never be
158
+ * mistaken for a direct trigger — delivered, boundary unknown to us — look exactly like one.
159
+ */
160
+ function requireBoundary(result: RpcSendResult): AcceptanceBoundary {
161
+ if (result.boundary === undefined) {
162
+ throw new Error(
163
+ "entwurf-v2-send: a successful control-socket RPC reported no acceptance boundary — the wire seam that classifies one was bypassed (contract violation).",
164
+ );
165
+ }
166
+ return result.boundary;
130
167
  }
131
168
 
132
169
  /**
@@ -159,7 +196,12 @@ export async function executeControlSocketSend(
159
196
  drive = { outcome: "failed", error: err };
160
197
  }
161
198
  finalizeRelease(policy, deps, held, drive);
162
- return { outcome: drive.outcome, rejectReason: drive.rejectReason, messagePath: drive.messagePath };
199
+ return {
200
+ outcome: drive.outcome,
201
+ rejectReason: drive.rejectReason,
202
+ messagePath: drive.messagePath,
203
+ boundary: drive.boundary,
204
+ };
163
205
  }
164
206
 
165
207
  /** Drive the 1차 send and route a connect failure through the F3 split. */
@@ -179,7 +221,7 @@ async function driveSend(plan: ControlSocketPlan, lock: LockClaim, deps: Control
179
221
  }
180
222
  // A completed RPC: ack ⇒ sent; in-band refusal ⇒ rejected, NO fallback. Preserve
181
223
  // its receiver error verbatim when supplied; do not make a reason up when absent.
182
- return result.success ? { outcome: "sent" } : inBandRejected(result);
224
+ return result.success ? { outcome: "sent", boundary: requireBoundary(result) } : inBandRejected(result);
183
225
  }
184
226
 
185
227
  /**
@@ -224,7 +266,7 @@ async function driveDeadFallback(
224
266
  // connect failure finalizes as failed (no further fallback).
225
267
  try {
226
268
  const r = await deps.sendOverSocket(rePlan);
227
- return r.success ? { outcome: "fallback-sent" } : inBandRejected(r);
269
+ return r.success ? { outcome: "fallback-sent", boundary: requireBoundary(r) } : inBandRejected(r);
228
270
  } catch (err) {
229
271
  return { outcome: "failed", error: err };
230
272
  }
@@ -261,14 +303,42 @@ async function driveDeadFallback(
261
303
  */
262
304
  export class SendDeliveredReleaseFailedError extends Error {
263
305
  readonly finalizedOutcome: Exclude<SendFinalOutcome, "failed">;
306
+ /** #120 P2: the ACCEPTANCE the send actually reached, carried through the failure so the
307
+ * dirty-lock line can say what happened instead of a bare route word. Absent on a `rejected`
308
+ * finalization (nothing was accepted) and on the mailbox fallback leg (which carries a file
309
+ * instead) — those two are exactly why this is optional rather than defaulted. */
310
+ readonly finalizedBoundary?: AcceptanceBoundary;
311
+ /** #98 R receipt, same reason: a mailbox fallback that then failed to release still wrote a
312
+ * `.msg`, and the operator has to be told WHICH one before touching the lock. */
313
+ readonly finalizedMessagePath?: string;
314
+ /** The receiver's own refusal text on a `rejected` finalization. Without it the dirty-lock line
315
+ * is the ONE place a refusal loses its reason — and it is the place an operator reads while
316
+ * deciding by hand whether the message still needs sending. */
317
+ readonly finalizedRejectReason?: string;
264
318
  readonly releaseError: unknown;
265
- constructor(finalizedOutcome: Exclude<SendFinalOutcome, "failed">, releaseError: unknown) {
319
+ constructor(
320
+ finalizedOutcome: Exclude<SendFinalOutcome, "failed">,
321
+ releaseError: unknown,
322
+ accepted: { boundary?: AcceptanceBoundary; messagePath?: string; rejectReason?: string } = {},
323
+ ) {
266
324
  const detail = releaseError instanceof Error ? releaseError.message : String(releaseError);
325
+ // NOT "delivered". The word was wrong on two of the three shapes that reach here — a
326
+ // `rejected` finalization delivered nothing at all, and a `queued-*` one is sitting in
327
+ // volatile memory — and DELIVERY.md refuses a bare `delivered` for exactly that reason.
328
+ // What is true of all three is that the send REACHED A TERMINAL RESULT and must not be
329
+ // repeated.
330
+ const what =
331
+ finalizedOutcome === "rejected"
332
+ ? `rejected${accepted.rejectReason ? ` (${accepted.rejectReason})` : ""}`
333
+ : (accepted.boundary ?? "mailbox-enqueued");
267
334
  super(
268
- `entwurf-v2-send: ${finalizedOutcome} delivered but releaseLock failed (lock dirty, do NOT re-send): ${detail}`,
335
+ `entwurf-v2-send: the send finalized as \`${what}\` and releaseLock then failed (lock dirty, do NOT re-send): ${detail}`,
269
336
  );
270
337
  this.name = "SendDeliveredReleaseFailedError";
271
338
  this.finalizedOutcome = finalizedOutcome;
339
+ this.finalizedBoundary = accepted.boundary;
340
+ this.finalizedMessagePath = accepted.messagePath;
341
+ this.finalizedRejectReason = accepted.rejectReason;
272
342
  this.releaseError = releaseError;
273
343
  }
274
344
  }
@@ -294,7 +364,13 @@ function finalizeRelease(policy: ReleasePolicy, deps: ControlSocketSendDeps, loc
294
364
  if (drive.outcome === "failed") throw original;
295
365
  // N1: the delivery/refusal already happened — surface it as a structured,
296
366
  // retry-unsafe error rather than a bare release throw.
297
- throw new SendDeliveredReleaseFailedError(drive.outcome, releaseErr);
367
+ // The acceptance truth rides along: without it the dirty-lock line is the ONE place
368
+ // left that still answers "what happened to my message" with a route word.
369
+ throw new SendDeliveredReleaseFailedError(drive.outcome, releaseErr, {
370
+ boundary: drive.boundary,
371
+ messagePath: drive.messagePath,
372
+ rejectReason: drive.rejectReason,
373
+ });
298
374
  }
299
375
  }
300
376
  if (drive.outcome === "failed") throw original;
@@ -159,9 +159,35 @@ export function renderEntwurfV2Result(result: EntwurfV2RunResult): EntwurfV2Surf
159
159
  // socket-to-socket retry (no file) and on every non-mailbox outcome, so the
160
160
  // line degrades to the bare outcome rather than printing "undefined".
161
161
  const enqueued = o.messagePath ? ` (enqueued ${basename(o.messagePath)})` : "";
162
+ if (delivered) {
163
+ // #120 P2 — THE BOUNDARY IS THE RECEIPT, and the route is an annotation.
164
+ // `sent` / `fallback-sent` name which LEG carried the bytes. An operator
165
+ // cannot act on that: for 78 minutes this line said `sent` about a message
166
+ // lying in a queue. So the visible word is what the RECEIVER observed —
167
+ // `sent` only for an idle direct trigger, `queued-steer` /
168
+ // `queued-follow-up` for the two volatile in-process queues, and
169
+ // `accepted-unknown-boundary` when a peer answered success in a shape this
170
+ // build cannot classify. That last one is never spelled with the word
171
+ // "sent": it was accepted, and saying "sent (unknown)" would put back the
172
+ // exact false claim this axis exists to remove.
173
+ //
174
+ // A re-resolved MAILBOX leg has no turn-injection boundary at all — it
175
+ // wrote a durable file — so it says `mailbox-enqueued` rather than a bare
176
+ // route word. The route survives as a separate annotation either way,
177
+ // because "your first socket was dead" is a real thing to have been told.
178
+ const route = o.outcome === "fallback-sent" ? " (via a re-resolved route)" : "";
179
+ // A re-resolved leg with no boundary IS the mailbox leg — that rail reports a
180
+ // durable file instead. A PRIMARY leg with no boundary is structurally
181
+ // impossible (the send hand throws on it), so if one ever arrives here it is
182
+ // named as the acceptance we could not classify rather than dressed as a
183
+ // mailbox file that was never written.
184
+ const boundary =
185
+ o.boundary ?? (o.outcome === "fallback-sent" ? "mailbox-enqueued" : "accepted-unknown-boundary");
186
+ return { text: `entwurf_v2 control-socket → ${boundary}${route}${enqueued}`, isError: false };
187
+ }
162
188
  return {
163
189
  text: `entwurf_v2 control-socket → ${o.outcome}${reason}${enqueued}`,
164
- isError: !delivered,
190
+ isError: true,
165
191
  };
166
192
  }
167
193
  if (o.transport === "native-push") {
@@ -185,10 +211,23 @@ export function renderEntwurfV2Result(result: EntwurfV2RunResult): EntwurfV2Surf
185
211
  }
186
212
  case "execution-failed": {
187
213
  if (result.releaseFailed && result.finalizedOutcome) {
188
- // N1: the delivery/refusal reached a terminal outcome but releaseLock then threw.
214
+ // N1: the send reached a terminal result and `releaseLock` then threw.
215
+ //
216
+ // #120 P2 — THIS LINE USED TO SAY `DELIVERED (<route>)` ABOUT ALL THREE SHAPES THAT
217
+ // REACH IT, and it was wrong about two of them: a `rejected` finalization delivered
218
+ // nothing at all, and a `queued-*` one is sitting in volatile receiver memory. So
219
+ // the same acceptance vocabulary the ordinary receipt uses is named here too — a
220
+ // dirty lock is a reason to be MORE precise about what happened, not less, because
221
+ // the operator is about to decide by hand whether anything needs re-sending.
222
+ const what =
223
+ result.finalizedOutcome === "rejected"
224
+ ? `rejected${result.finalizedRejectReason ? ` (${result.finalizedRejectReason})` : ""} — the receiver refused it; nothing was accepted`
225
+ : `${result.finalizedBoundary ?? "mailbox-enqueued"}${
226
+ result.finalizedMessagePath ? ` (enqueued ${basename(result.finalizedMessagePath)})` : ""
227
+ }`;
189
228
  return {
190
229
  text:
191
- `entwurf_v2 ${result.transport} DELIVERED (${result.finalizedOutcome}) but the lock is DIRTY ` +
230
+ `entwurf_v2 ${result.transport} finalized as ${what}, and the lock is DIRTY ` +
192
231
  `(release failed) — do NOT retry, a re-send would double-deliver. Clear the lock by hand.` +
193
232
  `\n error: ${result.error}`,
194
233
  isError: true,
@@ -44,6 +44,7 @@
44
44
  import { createHash } from "node:crypto";
45
45
  import { statSync } from "node:fs";
46
46
  import path from "node:path";
47
+ import { HERDR_FRESH_CALL_BACKENDS } from "../../scripts/herdr-rails.mjs";
47
48
  import { callbackEnvAssignments } from "./callback-env.ts";
48
49
  import {
49
50
  buildOmpBootstrapPayload,
@@ -55,11 +56,20 @@ import {
55
56
  TASK_MAX_CHARS,
56
57
  } from "./fresh-call-composition.ts";
57
58
 
58
- /** The pilot set, closed. `[#116 decision]` herdr's own `--kind` enum is much larger, and that is
59
+ /**
60
+ * The pilot set, closed. `[#116 decision]` herdr's own `--kind` enum is much larger, and that is
59
61
  * NOT evidence of support: every other backend is a pre-mutation named reject here, and there is
60
62
  * no fallback to the tmux rail — a caller inside herdr who asks for codex gets a refusal, not a
61
- * window somewhere else. */
62
- export const HERDR_FRESH_CALL_BACKENDS = ["pi", "claude-code"] as const;
63
+ * window somewhere else.
64
+ *
65
+ * The VALUE now lives in `scripts/herdr-rails.mjs`, the one leaf the status pane and the install
66
+ * narration also read (#116 A7), so a fourth backend cannot appear in prose on one surface while
67
+ * this rail still refuses it. The re-export keeps this module's public name unchanged. The TYPE
68
+ * still has to be a tuple: a `.mjs` leaf infers `readonly string[]` without the JSDoc cast there,
69
+ * and `HerdrFreshCallBackend` would silently widen to `string` while every runtime check still
70
+ * passed. A gate pins that.
71
+ */
72
+ export { HERDR_FRESH_CALL_BACKENDS };
63
73
  export type HerdrFreshCallBackend = (typeof HERDR_FRESH_CALL_BACKENDS)[number];
64
74
 
65
75
  /** Our backend name → the `--kind` token herdr accepts. `[측정 2026-09-14]` both round-tripped with
@@ -126,6 +126,17 @@ const OFFICIAL_REPORTS: Readonly<
126
126
  * only diff in that file's `.jsonl` lines is a lambda parameter rename on the READER
127
127
  * side. A source read alone would not have settled it, so the real file is the receipt.
128
128
  *
129
+ * `[측정 2026-09-22, pi 0.87.0]` re-measured, and the shape of the evidence CHANGED for the better.
130
+ * Source: the three lines that BUILD a session name are byte-unchanged from 0.86.0
131
+ * (`packages/coding-agent/src/core/session-manager.ts` `${fileTimestamp}_${sessionId}.jsonl` at
132
+ * :1080/:1666/:1842, moved from :991/:1527/:1703 by the +148/−9 `SessionManager`-canonical work
133
+ * 0.87.0 declares as breaking). File: `scripts/check-herdr-placement.ts` now makes the installed
134
+ * `SessionManager` write a real session file into a temp dir on EVERY run — `create()` plus one
135
+ * `appendMessage`, zero model, zero network, zero child process — and asserts this parser reads
136
+ * that basename back to the id the file's own header declares. So the vendor floor below is no
137
+ * longer a receipt somebody took once; it is re-taken by the gate. The 0.86.0 hand-measured file
138
+ * stays a 0.86.0 fact in the bump ledger rather than being re-dated.
139
+ *
129
140
  * WHY THIS IS PINNED HERE AND NOT INFERRED. The uuid is the join key and it lives in
130
141
  * a VENDOR filename, so this rule depends on pi's naming and would break silently if
131
142
  * pi changed it. **Vendor floor: pi 0.86.0, measured 2026-09-20** — whether that layout
package/protocol.js CHANGED
@@ -21,11 +21,3 @@
21
21
  * AGENTS.md section from its own pi-context augment.
22
22
  */
23
23
  export const ENTWURF_PROJECT_CONTEXT_OPEN_TAG = "<project-context";
24
-
25
- /**
26
- * Sender-side UI echo custom message type for successful entwurf_v2 send calls.
27
- * The bridge emits it, the control renderer draws it, and the provider-level
28
- * context filter removes it before LLM conversion. Keep as one SSOT: a typo
29
- * here otherwise becomes a silent UI/context split.
30
- */
31
- export const ENTWURF_SENT_MESSAGE_TYPE = "entwurf-sent";