@junghanacs/entwurf 0.45.0 → 0.51.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.
- package/AGENTS.md +5 -3
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +249 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +63 -15
- package/FAQ.md +4 -4
- package/README.md +132 -188
- package/VERIFY.md +18 -11
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +4 -4
- package/docs/setup-clean-host.md +94 -22
- package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +28 -8
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +16 -20
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +327 -57
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/carrier/subagent.js +5 -2
- package/pi/pi-durable/carrier/tui.js +60 -22
- package/pi/pi-durable/overlay/upstream-pin.json +26 -19
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +73 -69
- package/pi-extensions/lib/entwurf-v2-contract.ts +4 -4
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-resume-call.ts +77 -4
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +30 -16
- package/run.sh +552 -699
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-v2-contract.ts +4 -4
- package/scripts/check-entwurf-v2-surface.ts +2 -2
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +19 -14
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +28 -0
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +4 -4
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-resume-call.json +63 -2
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +1167 -3
- package/scripts/mutants/pi-floor.json +22 -5
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +255 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- package/team.json.example +39 -0
|
@@ -42,44 +42,72 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { createHash } from "node:crypto";
|
|
45
|
-
import { statSync } from "node:fs";
|
|
45
|
+
import { accessSync, constants as fsConstants, statSync } from "node:fs";
|
|
46
|
+
import { createConnection } from "node:net";
|
|
46
47
|
import path from "node:path";
|
|
47
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
HERDR_AGENT_START_BACKENDS,
|
|
50
|
+
HERDR_FRESH_CALL_BACKENDS,
|
|
51
|
+
HERDR_LAYOUT_LAUNCH_BACKENDS,
|
|
52
|
+
} from "../../scripts/herdr-rails.mjs";
|
|
48
53
|
import { callbackEnvAssignments } from "./callback-env.ts";
|
|
49
54
|
import {
|
|
50
55
|
buildOmpBootstrapPayload,
|
|
51
56
|
composeBackendArgs,
|
|
52
57
|
composeFreshCallFraming,
|
|
58
|
+
composeFreshCallPrompt,
|
|
53
59
|
FRESH_CALL_PROVIDER_HINT,
|
|
54
60
|
type FreshCallComposition,
|
|
55
61
|
type FreshCallInputRejectReason,
|
|
56
62
|
type FreshCallProviderRejectReason,
|
|
57
63
|
normalizeFreshCallInputs,
|
|
64
|
+
OMP_BOOTSTRAP_VERSION,
|
|
58
65
|
TASK_MAX_CHARS,
|
|
59
66
|
} from "./fresh-call-composition.ts";
|
|
67
|
+
import {
|
|
68
|
+
PI_DURABLE_PREFLIGHT_HINT,
|
|
69
|
+
type PiDurablePreflightRejectReason,
|
|
70
|
+
piDurableFreshPreflight,
|
|
71
|
+
} from "./pi-durable-fresh-preflight.ts";
|
|
72
|
+
import {
|
|
73
|
+
checkPiDurableRuntime,
|
|
74
|
+
PI_DURABLE_RUNTIME_HINT,
|
|
75
|
+
type PiDurablePackageLayout,
|
|
76
|
+
type PiDurableRuntimeRejectReason,
|
|
77
|
+
piDurablePackageLayout,
|
|
78
|
+
} from "./pi-durable-runtime.ts";
|
|
60
79
|
|
|
61
80
|
/**
|
|
62
|
-
*
|
|
63
|
-
* NOT evidence of support
|
|
64
|
-
*
|
|
65
|
-
*
|
|
81
|
+
* Three closed sets, one leaf. `[#116 decision]` herdr's own `--kind` enum is much larger, and that
|
|
82
|
+
* is NOT evidence of support. Since #157 A-L1 the rail has two launch domains — the agent-start set
|
|
83
|
+
* (pi, claude-code: `tab create` + `agent start --kind`) and the layout set (pi-durable: one
|
|
84
|
+
* `layout.apply`) — and admits exactly their union: every backend outside it is a pre-mutation
|
|
85
|
+
* named reject, with no fallback to the tmux rail — a caller inside herdr who asks for codex gets a
|
|
86
|
+
* refusal, not a window somewhere else.
|
|
66
87
|
*
|
|
67
|
-
* The
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* still
|
|
71
|
-
* and
|
|
72
|
-
* passed. A gate pins that.
|
|
88
|
+
* The VALUES live in `scripts/herdr-rails.mjs` (#116 A7). The status pane's RAILS block renders
|
|
89
|
+
* the agent-start and layout sets as two groups (#157 A-L2); the install narration covers only the
|
|
90
|
+
* integrations it activated, which pi-durable never is. The re-export keeps this module's
|
|
91
|
+
* public names. The TYPES still have to be tuples: a `.mjs` leaf infers `readonly string[]` without
|
|
92
|
+
* the JSDoc cast there, and these types would silently widen to `string` while every runtime check
|
|
93
|
+
* still passed. A gate pins that.
|
|
73
94
|
*/
|
|
74
|
-
export { HERDR_FRESH_CALL_BACKENDS };
|
|
95
|
+
export { HERDR_AGENT_START_BACKENDS, HERDR_FRESH_CALL_BACKENDS, HERDR_LAYOUT_LAUNCH_BACKENDS };
|
|
96
|
+
/** Everything this rail admits (#157 A-L1: the union of the two launch domains below). */
|
|
75
97
|
export type HerdrFreshCallBackend = (typeof HERDR_FRESH_CALL_BACKENDS)[number];
|
|
98
|
+
/** The `tab create` + `agent start --kind` domain. The kind maps are keyed by THIS type only. */
|
|
99
|
+
export type HerdrAgentStartBackend = (typeof HERDR_AGENT_START_BACKENDS)[number];
|
|
100
|
+
/** The one-`layout.apply` domain: no herdr kind exists for these, so none is ever requested. */
|
|
101
|
+
export type HerdrLayoutLaunchBackend = (typeof HERDR_LAYOUT_LAUNCH_BACKENDS)[number];
|
|
76
102
|
|
|
77
103
|
/** Our backend name → the `--kind` token herdr accepts. `[측정 2026-09-14]` both round-tripped with
|
|
78
104
|
* the vendor executable echoed back in `argv`. This is a REQUESTED kind: `[file:line @ c77af189]`
|
|
79
105
|
* `src/app/agents.rs:197-199` turns it into a bare executable name and writes it into the pane's
|
|
80
106
|
* shell, so nothing here has proven the binary exists, is on PATH, or will start. Runtime failure
|
|
81
|
-
* is a post-mutation outcome on this rail, named as such, never a precondition we can claim.
|
|
82
|
-
|
|
107
|
+
* is a post-mutation outcome on this rail, named as such, never a precondition we can claim.
|
|
108
|
+
* Keyed by the agent-start domain ONLY: a layout-launched backend has no kind, and inventing one
|
|
109
|
+
* would start herdr's program for that kind under another backend's name. */
|
|
110
|
+
export const HERDR_AGENT_KIND: Record<HerdrAgentStartBackend, string> = {
|
|
83
111
|
pi: "pi",
|
|
84
112
|
"claude-code": "claude",
|
|
85
113
|
};
|
|
@@ -141,7 +169,17 @@ export type HerdrFreshCallRejectReason =
|
|
|
141
169
|
| "herdr-argv-control-character"
|
|
142
170
|
| "cwd-not-absolute"
|
|
143
171
|
| "cwd-missing"
|
|
144
|
-
| "cwd-not-directory"
|
|
172
|
+
| "cwd-not-directory"
|
|
173
|
+
// #157 A-L1 — the layout branch's own pre-mutation refusals.
|
|
174
|
+
/** `HERDR_SOCKET_PATH` is absent, empty or relative. Read ONLY on the layout branch: the
|
|
175
|
+
* agent-start backends never need it and are never refused for its absence. */
|
|
176
|
+
| "herdr-socket-path-missing"
|
|
177
|
+
/** The verified package's own `run.sh` is not an executable regular file. */
|
|
178
|
+
| "pi-durable-launcher-missing"
|
|
179
|
+
/** The caller is a record-backed Codex citizen, asked for no cwd, and its record carries none.
|
|
180
|
+
* Its process directory is the app-server's, so it is NOT a fallback (#95 lane C). */
|
|
181
|
+
| "codex-caller-cwd-unavailable"
|
|
182
|
+
| PiDurablePreflightRejectReason;
|
|
145
183
|
|
|
146
184
|
/** Why a launch that had ALREADY created a tab failed. These are separate from the refusals above
|
|
147
185
|
* because the operator's next question is different: something exists and may need reclaiming. */
|
|
@@ -164,6 +202,25 @@ export type HerdrLaunchFailureReason =
|
|
|
164
202
|
/** The echoed argv is not the canonical executable plus exactly what we passed after `--`. */
|
|
165
203
|
| "herdr-agent-start-argv-drift";
|
|
166
204
|
|
|
205
|
+
/**
|
|
206
|
+
* #157 A-L1 — why the ONE layout mutation write did not yield a launch receipt. A separate type
|
|
207
|
+
* from the agent-start failures above because these do NOT all mean a tab exists: each carries a
|
|
208
|
+
* `none` or `unknown` recovery, and only the connect-before-write case or a well-formed pre-create
|
|
209
|
+
* herdr error is `none`. The same-id resume reuses the agent-start set and never sees these.
|
|
210
|
+
*/
|
|
211
|
+
export type HerdrLayoutFailureReason =
|
|
212
|
+
/** The socket exchange itself failed: before the write (`none`) or after it (`unknown`). */
|
|
213
|
+
| "herdr-layout-transport-failed"
|
|
214
|
+
/** A well-formed herdr error whose code the pinned handler returns before any creation. */
|
|
215
|
+
| "herdr-layout-apply-refused"
|
|
216
|
+
/** A well-formed herdr error with any other code, `layout_apply_failed` included — it is also
|
|
217
|
+
* returned after a tab was created or a rollback attempted, so it proves nothing about creation. */
|
|
218
|
+
| "herdr-layout-apply-failed"
|
|
219
|
+
/** The reply line is not one well-formed, id-correlated envelope with exactly one arm. */
|
|
220
|
+
| "herdr-layout-reply-unparsable"
|
|
221
|
+
/** A success envelope that does not prove THIS launch: wrong arm, workspace, shape or echo. */
|
|
222
|
+
| "herdr-layout-reply-drift";
|
|
223
|
+
|
|
167
224
|
/** Why an orphan pane could NOT be reclaimed. `[측정 2026-09-14, isolated sandbox server]` a herdr
|
|
168
225
|
* server restart brought the SAME pane ids back bound to entirely different terminals
|
|
169
226
|
* (`w1:p1/p2/p3`, `term_65b6d90e…` → `term_65b6d916…`). So a bare pane id carries no authority to
|
|
@@ -222,11 +279,13 @@ export type HerdrRecovery =
|
|
|
222
279
|
* whether herdr had been told a session id by the time we stopped asking, and carries no id.
|
|
223
280
|
*/
|
|
224
281
|
export interface HerdrFreshCallReceipt {
|
|
225
|
-
readonly backend:
|
|
282
|
+
readonly backend: HerdrAgentStartBackend;
|
|
226
283
|
/** The kind herdr was ASKED for. Not proof that the runtime exists — see `HERDR_AGENT_KIND`. */
|
|
227
284
|
readonly requestedKind: string;
|
|
228
285
|
readonly model: string;
|
|
229
|
-
|
|
286
|
+
/** The explicit start directory sent as `--cwd` — always present since #157 A-L1x. */
|
|
287
|
+
readonly cwd: string;
|
|
288
|
+
readonly cwdSource: HerdrLayoutCwdSource;
|
|
230
289
|
readonly herdrAgentName: string;
|
|
231
290
|
readonly herdrWorkspaceId: string;
|
|
232
291
|
readonly herdrTabId: string;
|
|
@@ -268,15 +327,77 @@ export interface HerdrWitness {
|
|
|
268
327
|
readonly settleMs: number;
|
|
269
328
|
}
|
|
270
329
|
|
|
330
|
+
/**
|
|
331
|
+
* The pi-durable LAYOUT launch receipt (#157 A-L1): what was requested and the coordinates herdr's
|
|
332
|
+
* own `layout_apply` reply named. Read it as "herdr accepted one layout request and named this tab".
|
|
333
|
+
*
|
|
334
|
+
* ABSENT BY CONTRACT, typed `never` so no reader can mistake one for present: no herdr kind (none
|
|
335
|
+
* exists for pi-durable and none was requested), no agent name, no terminal id (the layout reply
|
|
336
|
+
* carries none, so this branch has no create-receipt terminal and therefore no close authority),
|
|
337
|
+
* and no witness (no detection was requested). Like the agent-start receipt it carries no garden id
|
|
338
|
+
* or native session id: the address arrives only in the sibling's callback envelope.
|
|
339
|
+
*
|
|
340
|
+
* `focus:false` is what we REQUESTED. herdr's `focused_pane_id` is focus INSIDE the new tab, so this
|
|
341
|
+
* receipt does not certify that the tab is unfocused; the actual view is a separate observation.
|
|
342
|
+
*/
|
|
343
|
+
export interface HerdrLayoutFreshCallReceipt {
|
|
344
|
+
readonly backend: HerdrLayoutLaunchBackend;
|
|
345
|
+
readonly launch: "layout-apply";
|
|
346
|
+
readonly model: string;
|
|
347
|
+
/** The explicit start directory sent as `root.cwd` — always present on this branch. */
|
|
348
|
+
readonly cwd: string;
|
|
349
|
+
readonly cwdSource: HerdrLayoutCwdSource;
|
|
350
|
+
/** The verified package's own launcher, the first element of the requested argv vector. */
|
|
351
|
+
readonly runtime: string;
|
|
352
|
+
readonly herdrWorkspaceId: string;
|
|
353
|
+
readonly herdrTabId: string;
|
|
354
|
+
readonly herdrPaneId: string;
|
|
355
|
+
readonly nonce: string;
|
|
356
|
+
readonly requestedKind?: never;
|
|
357
|
+
readonly herdrAgentName?: never;
|
|
358
|
+
readonly herdrTerminalId?: never;
|
|
359
|
+
readonly witness?: never;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Where the layout branch's explicit cwd came from. `caller-process` is THIS process's directory —
|
|
363
|
+
* the same inherited fact the tmux rail's omitted cwd rests on — and is never used for a Codex
|
|
364
|
+
* caller, whose process directory is the app-server's. */
|
|
365
|
+
export type HerdrLayoutCwdSource = "requested" | "codex-caller-record" | "caller-process";
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Display-only diagnostics a layout failure carries: what herdr or the transport REPORTED. Every
|
|
369
|
+
* value is cut to `HERDR_DIAGNOSTIC_MAX_CHARS` and is rendered quoted. All decisions were made on the
|
|
370
|
+
* complete original values before this object was built, and nothing here is a receipt, a
|
|
371
|
+
* placement or authority to close anything.
|
|
372
|
+
*/
|
|
373
|
+
export interface HerdrReportedDiagnostics {
|
|
374
|
+
readonly transport?: string;
|
|
375
|
+
readonly errorCode?: string;
|
|
376
|
+
readonly errorMessage?: string;
|
|
377
|
+
readonly workspaceId?: string;
|
|
378
|
+
readonly tabId?: string;
|
|
379
|
+
readonly paneId?: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
271
382
|
export type HerdrFreshCallResult =
|
|
272
|
-
| { readonly ok: true; readonly receipt: HerdrFreshCallReceipt }
|
|
273
|
-
| { readonly ok: false; readonly reason: HerdrFreshCallRejectReason }
|
|
383
|
+
| { readonly ok: true; readonly receipt: HerdrFreshCallReceipt | HerdrLayoutFreshCallReceipt }
|
|
274
384
|
| {
|
|
275
385
|
readonly ok: false;
|
|
276
|
-
readonly reason:
|
|
386
|
+
readonly reason: HerdrFreshCallRejectReason;
|
|
387
|
+
/** Layout branch caller-pane read only: herdr's bounded, display-only cause (A-L1 D4). */
|
|
388
|
+
readonly reported?: HerdrReportedDiagnostics;
|
|
389
|
+
}
|
|
390
|
+
| {
|
|
391
|
+
readonly ok: false;
|
|
392
|
+
readonly reason: HerdrLaunchFailureReason | HerdrLayoutFailureReason;
|
|
277
393
|
/** herdr's own error code when it gave one (`{"error":{"code":…}}`) — quoted, never retyped. */
|
|
278
394
|
readonly herdrErrorCode?: string;
|
|
279
395
|
readonly recovery: HerdrRecovery;
|
|
396
|
+
/** Layout branch only: bounded, display-only reported values (see the type). */
|
|
397
|
+
readonly reported?: HerdrReportedDiagnostics;
|
|
398
|
+
/** Layout branch, POST-WRITE `unknown` only: the launch nonce a sibling that did start
|
|
399
|
+
* would still carry in its callback, so the caller can correlate one if it arrives. */
|
|
400
|
+
readonly nonce?: string;
|
|
280
401
|
};
|
|
281
402
|
|
|
282
403
|
/** One invocation of the herdr CLI, injected. Production supplies `execFileSync`; tests supply a
|
|
@@ -652,7 +773,7 @@ export function parseHerdrAgentStartResponse(stdout: string): HerdrStartFacts |
|
|
|
652
773
|
/** The canonical executable herdr writes for a requested kind, measured twice today (`["claude"]`
|
|
653
774
|
* and `["pi","--entwurf-control"]`) and pinned upstream at `src/detect/mod.rs:155-156 @ c77af189`.
|
|
654
775
|
* We only assert it for the two pilot kinds we have actually seen. */
|
|
655
|
-
export const HERDR_CANONICAL_EXECUTABLE: Record<
|
|
776
|
+
export const HERDR_CANONICAL_EXECUTABLE: Record<HerdrAgentStartBackend, string> = {
|
|
656
777
|
pi: "pi",
|
|
657
778
|
"claude-code": "claude",
|
|
658
779
|
};
|
|
@@ -662,7 +783,7 @@ export const HERDR_CANONICAL_EXECUTABLE: Record<HerdrFreshCallBackend, string> =
|
|
|
662
783
|
* reveal. */
|
|
663
784
|
export function argvMatchesRequest(
|
|
664
785
|
echoed: readonly string[] | null,
|
|
665
|
-
backend:
|
|
786
|
+
backend: HerdrAgentStartBackend,
|
|
666
787
|
backendArgs: readonly string[],
|
|
667
788
|
): boolean {
|
|
668
789
|
if (echoed === null) return false;
|
|
@@ -786,11 +907,18 @@ export async function herdrFreshCall(
|
|
|
786
907
|
readonly cwd?: string;
|
|
787
908
|
readonly placement?: { readonly tmuxSession?: string };
|
|
788
909
|
readonly callerGardenId: string | null;
|
|
910
|
+
/** Both herdr branches (layout #157 A-L1, agent start A-L1x): the record-backed Codex caller's
|
|
911
|
+
* own record cwd, consulted only when no cwd was requested. */
|
|
912
|
+
readonly callerCwd?: string;
|
|
913
|
+
/** Both herdr branches: whether the caller is a record-backed Codex citizen. A boolean on
|
|
914
|
+
* purpose — no thread id, pane title or seat reaches this rail. */
|
|
915
|
+
readonly callerIsCodex?: boolean;
|
|
789
916
|
},
|
|
790
917
|
run: HerdrRun,
|
|
791
918
|
env: NodeJS.ProcessEnv,
|
|
792
919
|
nonce: string,
|
|
793
920
|
clock: HerdrClock = HERDR_REAL_CLOCK,
|
|
921
|
+
layoutDeps: Partial<HerdrLayoutDeps> = {},
|
|
794
922
|
): Promise<HerdrFreshCallResult> {
|
|
795
923
|
const context = resolveHerdrContext(env);
|
|
796
924
|
if (!context.ok) return { ok: false, reason: context.reason };
|
|
@@ -803,19 +931,34 @@ export async function herdrFreshCall(
|
|
|
803
931
|
// worse than a refusal they can read.
|
|
804
932
|
return { ok: false, reason: "herdr-backend-unsupported" };
|
|
805
933
|
}
|
|
806
|
-
|
|
934
|
+
if ((HERDR_LAYOUT_LAUNCH_BACKENDS as readonly string[]).includes(params.backend)) {
|
|
935
|
+
// #157 A-L1: a backend herdr has no kind for. It never reaches `tab create`/`agent start`
|
|
936
|
+
// below and never runs the herdr CLI; its own branch owns its whole order.
|
|
937
|
+
return herdrLayoutFreshCall(params, context.context, env, nonce, {
|
|
938
|
+
...HERDR_LAYOUT_PRODUCTION_DEPS,
|
|
939
|
+
...layoutDeps,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const backend = params.backend as HerdrAgentStartBackend;
|
|
807
943
|
|
|
808
944
|
// The caller's inputs are judged by the SHARED contract — same order, same trimming, same
|
|
809
945
|
// five words — so a caller cannot learn a different vocabulary by being inside herdr. The
|
|
810
|
-
// cwd omission rule comes with it: `undefined` and `""` both mean "no cwd", which this
|
|
811
|
-
//
|
|
946
|
+
// cwd omission rule comes with it: `undefined` and `""` both mean "no cwd requested", which this
|
|
947
|
+
// rail once mistook for an invalid path. An omitted cwd means the CALLER's directory, chosen by
|
|
948
|
+
// the same helper the layout branch uses (#157 A-L1x), and is always sent explicitly: herdr's own
|
|
949
|
+
// default for an omitted `--cwd` is herdr's, not a fact about the caller.
|
|
812
950
|
const normalized = normalizeFreshCallInputs(params);
|
|
813
951
|
if (!normalized.ok) return { ok: false, reason: normalized.reason };
|
|
814
|
-
const { callerGardenId, model, task, cwd } = normalized.inputs;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
952
|
+
const { callerGardenId, model, task, cwd: requestedCwd } = normalized.inputs;
|
|
953
|
+
const chosenCwd = chooseHerdrLayoutCwd({
|
|
954
|
+
...(requestedCwd === undefined ? {} : { requestedCwd }),
|
|
955
|
+
callerIsCodex: params.callerIsCodex === true,
|
|
956
|
+
...(params.callerCwd === undefined ? {} : { callerCwd: params.callerCwd }),
|
|
957
|
+
callerProcessCwd: layoutDeps.callerProcessCwd ?? HERDR_LAYOUT_PRODUCTION_DEPS.callerProcessCwd,
|
|
958
|
+
});
|
|
959
|
+
if (!chosenCwd.ok) return { ok: false, reason: chosenCwd.reason };
|
|
960
|
+
const badCwd = classifyHerdrCwd(chosenCwd.cwd);
|
|
961
|
+
if (badCwd !== null) return { ok: false, reason: badCwd };
|
|
819
962
|
|
|
820
963
|
const framing = composeFreshCallFraming({
|
|
821
964
|
backend,
|
|
@@ -868,7 +1011,7 @@ export async function herdrFreshCall(
|
|
|
868
1011
|
const tabRun = await run(
|
|
869
1012
|
buildHerdrTabCreateArgs({
|
|
870
1013
|
workspaceId: callerPane.workspaceId,
|
|
871
|
-
|
|
1014
|
+
cwd: chosenCwd.cwd,
|
|
872
1015
|
callback: { target: callerGardenId, nonce },
|
|
873
1016
|
}),
|
|
874
1017
|
);
|
|
@@ -964,7 +1107,8 @@ export async function herdrFreshCall(
|
|
|
964
1107
|
backend,
|
|
965
1108
|
requestedKind: HERDR_AGENT_KIND[backend],
|
|
966
1109
|
model,
|
|
967
|
-
|
|
1110
|
+
cwd: chosenCwd.cwd,
|
|
1111
|
+
cwdSource: chosenCwd.source,
|
|
968
1112
|
herdrAgentName: agentName,
|
|
969
1113
|
// The tab coordinates come from the reply that CREATED them, not from the start's
|
|
970
1114
|
// echo: the create receipt is what the reclaim above is bound to, so the receipt an
|
|
@@ -1084,8 +1228,13 @@ export async function settleAgentWitness(params: {
|
|
|
1084
1228
|
}
|
|
1085
1229
|
}
|
|
1086
1230
|
|
|
1087
|
-
/** Reclaim the pane we opened — conditionally, or not at all.
|
|
1088
|
-
|
|
1231
|
+
/** Reclaim the pane we opened — conditionally, or not at all.
|
|
1232
|
+
*
|
|
1233
|
+
* Exported for the ONE other herdr launch that creates a tab and can fail after it: the same-id pi
|
|
1234
|
+
* resume (`herdr-resume-call.ts`, #157 G1a). It is the existing within-generation witness
|
|
1235
|
+
* (`decideConditionalClose`) plus the one pane close, reused rather than copied, so the two herdr
|
|
1236
|
+
* launches cannot drift into different ideas of what may be closed. */
|
|
1237
|
+
export async function reclaim(pane: HerdrPaneFacts, run: HerdrRun): Promise<HerdrRecovery> {
|
|
1089
1238
|
const getRun = await run(buildHerdrPaneGetArgs(pane.paneId));
|
|
1090
1239
|
const decision = decideConditionalClose(
|
|
1091
1240
|
pane,
|
|
@@ -1098,12 +1247,628 @@ async function reclaim(pane: HerdrPaneFacts, run: HerdrRun): Promise<HerdrRecove
|
|
|
1098
1247
|
return { outcome: "closed", paneId: pane.paneId, terminalId: pane.terminalId };
|
|
1099
1248
|
}
|
|
1100
1249
|
|
|
1250
|
+
// ── #157 A-L1: pi-durable through ONE layout.apply on the caller's own herdr socket ──────────────
|
|
1251
|
+
//
|
|
1252
|
+
// WHY A SOCKET AND NOT THE CLI. The documented herdr CLI exposes no layout mutation, and
|
|
1253
|
+
// `layout.apply` is the only herdr surface that execs an argv VECTOR in a new tab — no `--kind`, no
|
|
1254
|
+
// shell, no PTY keystrokes (`[blob 2ecf19a9]` app-api-layouts.rs:117-140, pane.rs:2456-2500). So this
|
|
1255
|
+
// branch sends one fixed JSON line per connection and reads one reply line: the framing herdr's own
|
|
1256
|
+
// pi integration uses, and the one measured on the pinned 0.9.5 server (G2/G3).
|
|
1257
|
+
//
|
|
1258
|
+
// ONE ENDPOINT FOR THE READ AND THE WRITE. The caller-pane read that names the workspace and the
|
|
1259
|
+
// mutation that uses it go to the SAME explicit endpoint, `HERDR_SOCKET_PATH` — never the CLI for one
|
|
1260
|
+
// and the socket for the other, which could each reach a different server where `w1` names a
|
|
1261
|
+
// different workspace. Two connections do NOT establish server-generation invariance across a
|
|
1262
|
+
// restart or rebind: a valid inherited env pair (`HERDR_PANE_ID`, `HERDR_SOCKET_PATH`) and endpoint
|
|
1263
|
+
// continuity during this call are ASSUMPTIONS, named here rather than proven, and no generation or
|
|
1264
|
+
// liveness manager is added for them. The read is also a physical socket write; "exactly one" below
|
|
1265
|
+
// always means exactly one LAYOUT MUTATION write.
|
|
1266
|
+
//
|
|
1267
|
+
// WHAT THE PARSERS ARE. An authority-bearing REDUCTION of herdr's replies, not a vendor-schema
|
|
1268
|
+
// validator: they check the envelope this launch relies on and the id/echo fields it needs, against
|
|
1269
|
+
// the binary-bundled schema (protocol 22) and recorded replies, and nothing else. `agent_status` is
|
|
1270
|
+
// checked for presence only and is never interpreted. `result` XOR `error` is Entwurf's own strict
|
|
1271
|
+
// acceptance policy — the schema does not forbid extra properties.
|
|
1272
|
+
|
|
1273
|
+
/** Each reported diagnostic string is cut to this many characters for DISPLAY, and rendered quoted.
|
|
1274
|
+
* Every decision is taken on the complete original value before it is cut. */
|
|
1275
|
+
export const HERDR_DIAGNOSTIC_MAX_CHARS = 256;
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* The error codes that prove NOTHING was created, for exactly this request shape (a `workspace_id`,
|
|
1279
|
+
* no `tab_id`, one pane). `[blob 2ecf19a9]` the pinned `handle_layout_apply` returns these only
|
|
1280
|
+
* before its first creation call: `workspace_not_found` at app-api-layouts.rs:56/66/114,
|
|
1281
|
+
* `invalid_layout` at :69/:97/:109, and `invalid_env` at :105 (app-api-env.rs:3-32 yields no other
|
|
1282
|
+
* code). `tab_not_found`/`invalid_target` need a `tab_id`, which this branch never sends, so they are
|
|
1283
|
+
* NOT here — and `layout_apply_failed` is returned at :143/:158/:196 and after the creation events,
|
|
1284
|
+
* so it never proves anything about creation.
|
|
1285
|
+
*/
|
|
1286
|
+
export const HERDR_LAYOUT_NONE_CODES: readonly string[] = Object.freeze([
|
|
1287
|
+
"workspace_not_found",
|
|
1288
|
+
"invalid_layout",
|
|
1289
|
+
"invalid_env",
|
|
1290
|
+
]);
|
|
1291
|
+
|
|
1292
|
+
/** What one request on the socket came back as. `connect-failed` is the ONLY outcome in which the
|
|
1293
|
+
* request line was certainly never written. */
|
|
1294
|
+
export type HerdrSocketOutcome =
|
|
1295
|
+
| { readonly kind: "connect-failed"; readonly code: string }
|
|
1296
|
+
| { readonly kind: "reply"; readonly line: string }
|
|
1297
|
+
| { readonly kind: "io-failed"; readonly code: string }
|
|
1298
|
+
| { readonly kind: "timeout" }
|
|
1299
|
+
| { readonly kind: "oversize" }
|
|
1300
|
+
| { readonly kind: "eof-before-newline" };
|
|
1301
|
+
|
|
1302
|
+
/** One request, one connection, one reply line. No retry, no reconnect, no resend. */
|
|
1303
|
+
export type HerdrSocketCall = (requestLine: string) => Promise<HerdrSocketOutcome>;
|
|
1304
|
+
/** Bound to ONE endpoint: both of this branch's requests go through the call it returns. */
|
|
1305
|
+
export type HerdrSocketCallFactory = (socketPath: string) => HerdrSocketCall;
|
|
1306
|
+
|
|
1307
|
+
export interface HerdrSocketBounds {
|
|
1308
|
+
readonly timeoutMs: number;
|
|
1309
|
+
/** The whole reply LINE (bytes before its newline). Larger is `oversize`, never truncated. */
|
|
1310
|
+
readonly maxReplyBytes: number;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* The production socket client. The reply is collected as BYTES and decoded once, after its newline,
|
|
1315
|
+
* so a multi-byte character split across chunks is never decoded in halves. A bound that expires
|
|
1316
|
+
* before the connection exists is a connect failure (nothing written); after it, a timeout (the
|
|
1317
|
+
* request may have been written). Exactly one settle: connect, data, error, close and the timer race.
|
|
1318
|
+
*/
|
|
1319
|
+
export function createHerdrSocketCall(
|
|
1320
|
+
socketPath: string,
|
|
1321
|
+
bounds: HerdrSocketBounds = { timeoutMs: HERDR_CLI_TIMEOUT_MS, maxReplyBytes: HERDR_MAX_OUTPUT_BYTES },
|
|
1322
|
+
): HerdrSocketCall {
|
|
1323
|
+
return (requestLine) =>
|
|
1324
|
+
new Promise((resolve) => {
|
|
1325
|
+
let settled = false;
|
|
1326
|
+
let connected = false;
|
|
1327
|
+
let lineBytes = 0;
|
|
1328
|
+
const chunks: Buffer[] = [];
|
|
1329
|
+
const socket = createConnection({ path: socketPath });
|
|
1330
|
+
const settle = (outcome: HerdrSocketOutcome) => {
|
|
1331
|
+
if (settled) return;
|
|
1332
|
+
settled = true;
|
|
1333
|
+
clearTimeout(timer);
|
|
1334
|
+
socket.destroy();
|
|
1335
|
+
resolve(outcome);
|
|
1336
|
+
};
|
|
1337
|
+
const timer = setTimeout(
|
|
1338
|
+
() => settle(connected ? { kind: "timeout" } : { kind: "connect-failed", code: "timeout-before-connect" }),
|
|
1339
|
+
bounds.timeoutMs,
|
|
1340
|
+
);
|
|
1341
|
+
timer.unref?.();
|
|
1342
|
+
socket.on("connect", () => {
|
|
1343
|
+
connected = true;
|
|
1344
|
+
socket.write(requestLine);
|
|
1345
|
+
});
|
|
1346
|
+
socket.on("data", (chunk: Buffer) => {
|
|
1347
|
+
const newline = chunk.indexOf(0x0a);
|
|
1348
|
+
const part = newline === -1 ? chunk : chunk.subarray(0, newline);
|
|
1349
|
+
lineBytes += part.length;
|
|
1350
|
+
if (lineBytes > bounds.maxReplyBytes) {
|
|
1351
|
+
settle({ kind: "oversize" });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
chunks.push(part);
|
|
1355
|
+
if (newline !== -1) settle({ kind: "reply", line: decodeHerdrReplyBytes(chunks) });
|
|
1356
|
+
});
|
|
1357
|
+
socket.on("error", (error: NodeJS.ErrnoException) => {
|
|
1358
|
+
const code = error.code ?? error.message;
|
|
1359
|
+
settle(connected ? { kind: "io-failed", code } : { kind: "connect-failed", code });
|
|
1360
|
+
});
|
|
1361
|
+
socket.on("close", () => settle({ kind: "eof-before-newline" }));
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/** The reply line's ONE decode: every collected chunk concatenated, then UTF-8 — never chunk by
|
|
1366
|
+
* chunk, so a multi-byte character split between reads is decoded whole. */
|
|
1367
|
+
export function decodeHerdrReplyBytes(chunks: readonly Buffer[]): string {
|
|
1368
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/** The fixed caller-pane read. Product function, not an RPC surface: method and shape are literal. */
|
|
1372
|
+
export function buildHerdrPaneGetPacket(id: string, paneId: string): string {
|
|
1373
|
+
return `${JSON.stringify({ id, method: "pane.get", params: { pane_id: paneId } })}\n`;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** The fixed layout mutation: one unfocused pane tab in a named workspace, an explicit cwd, an argv
|
|
1377
|
+
* vector and an env map. No `tab_id`, `tab_label`, split, label or ratio can be expressed through it. */
|
|
1378
|
+
export function buildHerdrLayoutApplyPacket(
|
|
1379
|
+
id: string,
|
|
1380
|
+
params: {
|
|
1381
|
+
readonly workspaceId: string;
|
|
1382
|
+
readonly cwd: string;
|
|
1383
|
+
readonly command: readonly string[];
|
|
1384
|
+
readonly env: Readonly<Record<string, string>>;
|
|
1385
|
+
},
|
|
1386
|
+
): string {
|
|
1387
|
+
return `${JSON.stringify({
|
|
1388
|
+
id,
|
|
1389
|
+
method: "layout.apply",
|
|
1390
|
+
params: {
|
|
1391
|
+
workspace_id: params.workspaceId,
|
|
1392
|
+
focus: false,
|
|
1393
|
+
root: { type: "pane", cwd: params.cwd, command: [...params.command], env: { ...params.env } },
|
|
1394
|
+
},
|
|
1395
|
+
})}\n`;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** The launch env: the identity scrub the other launches use, plus the callback pair from its one
|
|
1399
|
+
* validating formatter (which throws rather than inject a pair the callback verb would refuse). */
|
|
1400
|
+
export function buildHerdrLayoutLaunchEnv(callback: { target: string; nonce: string }): Record<string, string> {
|
|
1401
|
+
const env: Record<string, string> = { PI_SESSION_ID: "", PI_AGENT_ID: "" };
|
|
1402
|
+
for (const assignment of callbackEnvAssignments(callback)) {
|
|
1403
|
+
const at = assignment.indexOf("=");
|
|
1404
|
+
env[assignment.slice(0, at)] = assignment.slice(at + 1);
|
|
1405
|
+
}
|
|
1406
|
+
return env;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* The argv vector: the verified package's own launcher, then the SHARED pi-durable composition, with
|
|
1411
|
+
* the bootstrap payload made Cc-free at ITS OWN layer. `JSON.stringify` escapes C0, `"` and `\` but
|
|
1412
|
+
* leaves DEL and C1 literal; escaping only the outer request would be undone by herdr's own decode,
|
|
1413
|
+
* so the reversible `\uXXXX` escape is applied to the payload element itself. The bootstrap decoder
|
|
1414
|
+
* is `JSON.parse` (meta-bridge-omp.ts `decodeOmpBootstrapPayload`), so the task comes back byte-exact
|
|
1415
|
+
* — asserted here, and a failure is OUR bug, so it throws rather than launch a task we cannot
|
|
1416
|
+
* reproduce. No blanket control-character refusal: this exec path has no Cc rule of its own.
|
|
1417
|
+
*/
|
|
1418
|
+
export function composeHerdrPiDurableCommand(params: {
|
|
1419
|
+
readonly runtime: string;
|
|
1420
|
+
readonly prompt: string;
|
|
1421
|
+
readonly model: string;
|
|
1422
|
+
readonly callerGardenId: string;
|
|
1423
|
+
readonly nonce: string;
|
|
1424
|
+
readonly task: string;
|
|
1425
|
+
}): string[] {
|
|
1426
|
+
const args = composeBackendArgs(
|
|
1427
|
+
"pi-durable",
|
|
1428
|
+
{
|
|
1429
|
+
prompt: params.prompt,
|
|
1430
|
+
bootstrapPayload: buildOmpBootstrapPayload({
|
|
1431
|
+
callerGardenId: params.callerGardenId,
|
|
1432
|
+
nonce: params.nonce,
|
|
1433
|
+
task: params.task,
|
|
1434
|
+
}),
|
|
1435
|
+
},
|
|
1436
|
+
params.model,
|
|
1437
|
+
() => {
|
|
1438
|
+
throw new Error("herdr-fresh-call: codex is not a herdr backend");
|
|
1439
|
+
},
|
|
1440
|
+
() => {
|
|
1441
|
+
throw new Error("herdr-fresh-call: codex is not a herdr backend");
|
|
1442
|
+
},
|
|
1443
|
+
);
|
|
1444
|
+
const raw = args.at(-1);
|
|
1445
|
+
if (args.at(-2) !== "--entwurf-bootstrap" || raw === undefined) {
|
|
1446
|
+
throw new Error("herdr-fresh-call: the pi-durable composition no longer ends with its bootstrap payload");
|
|
1447
|
+
}
|
|
1448
|
+
const payload = escapeRemainingControlChars(raw);
|
|
1449
|
+
const decoded: unknown = JSON.parse(payload);
|
|
1450
|
+
const intact =
|
|
1451
|
+
typeof decoded === "object" &&
|
|
1452
|
+
decoded !== null &&
|
|
1453
|
+
(decoded as Record<string, unknown>).v === OMP_BOOTSTRAP_VERSION &&
|
|
1454
|
+
(decoded as Record<string, unknown>).task === params.prompt &&
|
|
1455
|
+
!containsControlChar(payload);
|
|
1456
|
+
if (!intact) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
"herdr-fresh-call: the pi-durable bootstrap payload did not decode back to the framed prompt — refusing to launch a sibling with a task we cannot reproduce",
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
return [params.runtime, ...args.slice(0, -1), payload];
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** `HERDR_SOCKET_PATH`, read only here and only as given: non-empty and absolute, or `null`. */
|
|
1465
|
+
export function resolveHerdrSocketPath(env: NodeJS.ProcessEnv): string | null {
|
|
1466
|
+
const value = env.HERDR_SOCKET_PATH;
|
|
1467
|
+
return typeof value === "string" && value.length > 0 && path.isAbsolute(value) ? value : null;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* The explicit start directory. herdr's own default for an omitted `root.cwd` is the FOCUSED pane's
|
|
1472
|
+
* cwd in the target workspace (`[blob 2ecf19a9]` app-api-layouts.rs:343-365) — unrelated to the
|
|
1473
|
+
* caller — so this branch always sends one. Requested first; then a known Codex caller's RECORD
|
|
1474
|
+
* cwd, with NO fallback when it has none (its process directory is the app-server's, #95 lane C);
|
|
1475
|
+
* then, for every other caller, this process's own directory — the inherited fact the tmux rail's
|
|
1476
|
+
* omitted cwd rests on. Never herdr's own `cwd`/`foreground_cwd` for the caller's pane: that is a view.
|
|
1477
|
+
* The agent-start branch reuses this same choice for its `tab create --cwd` (#157 A-L1x), so both
|
|
1478
|
+
* herdr branches start an omitted cwd where the caller is; the name stays layout-first only by history.
|
|
1479
|
+
*/
|
|
1480
|
+
export function chooseHerdrLayoutCwd(input: {
|
|
1481
|
+
readonly requestedCwd?: string;
|
|
1482
|
+
readonly callerIsCodex: boolean;
|
|
1483
|
+
readonly callerCwd?: string;
|
|
1484
|
+
readonly callerProcessCwd: () => string;
|
|
1485
|
+
}):
|
|
1486
|
+
| { readonly ok: true; readonly cwd: string; readonly source: HerdrLayoutCwdSource }
|
|
1487
|
+
| { readonly ok: false; readonly reason: "codex-caller-cwd-unavailable" } {
|
|
1488
|
+
if (input.requestedCwd !== undefined) return { ok: true, cwd: input.requestedCwd, source: "requested" };
|
|
1489
|
+
if (input.callerIsCodex) {
|
|
1490
|
+
const record = input.callerCwd === undefined || input.callerCwd === "" ? undefined : input.callerCwd;
|
|
1491
|
+
return record === undefined
|
|
1492
|
+
? { ok: false, reason: "codex-caller-cwd-unavailable" }
|
|
1493
|
+
: { ok: true, cwd: record, source: "codex-caller-record" };
|
|
1494
|
+
}
|
|
1495
|
+
return { ok: true, cwd: input.callerProcessCwd(), source: "caller-process" };
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
1499
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
1503
|
+
return typeof value === "string" && value.length > 0;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/** Cut for display, never for comparison. A lone high surrogate is not left at the cut. */
|
|
1507
|
+
export function boundHerdrDiagnostic(value: string): string {
|
|
1508
|
+
if (value.length <= HERDR_DIAGNOSTIC_MAX_CHARS) return value;
|
|
1509
|
+
let cut = HERDR_DIAGNOSTIC_MAX_CHARS - 1;
|
|
1510
|
+
const last = value.charCodeAt(cut - 1);
|
|
1511
|
+
if (last >= 0xd800 && last <= 0xdbff) cut -= 1;
|
|
1512
|
+
return `${value.slice(0, cut)}…`;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* The envelope, before any certainty is decided: a JSON object (not an array, not null), the exact
|
|
1517
|
+
* id we sent, and EXACTLY ONE of `result`/`error`, whose value is itself a non-null, non-array
|
|
1518
|
+
* object. An error arm also needs a non-empty string `code` and a string `message` (both required
|
|
1519
|
+
* by the bundled schema). Anything else is `null`: unreadable, never a partially believed answer.
|
|
1520
|
+
*/
|
|
1521
|
+
export function readHerdrEnvelope(
|
|
1522
|
+
line: string,
|
|
1523
|
+
id: string,
|
|
1524
|
+
):
|
|
1525
|
+
| { readonly arm: "result"; readonly result: Record<string, unknown> }
|
|
1526
|
+
| { readonly arm: "error"; readonly code: string; readonly message: string }
|
|
1527
|
+
| null {
|
|
1528
|
+
let parsed: unknown;
|
|
1529
|
+
try {
|
|
1530
|
+
parsed = JSON.parse(line);
|
|
1531
|
+
} catch {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
if (!isPlainObject(parsed)) return null;
|
|
1535
|
+
if (typeof parsed.id !== "string" || parsed.id !== id) return null;
|
|
1536
|
+
const hasResult = Object.hasOwn(parsed, "result");
|
|
1537
|
+
if (hasResult === Object.hasOwn(parsed, "error")) return null;
|
|
1538
|
+
if (hasResult) return isPlainObject(parsed.result) ? { arm: "result", result: parsed.result } : null;
|
|
1539
|
+
const error = parsed.error;
|
|
1540
|
+
if (!isPlainObject(error)) return null;
|
|
1541
|
+
if (!isNonEmptyString(error.code) || typeof error.message !== "string") return null;
|
|
1542
|
+
return { arm: "error", code: error.code, message: error.message };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/** The transport cause of a read that got no reply line, cut for display. */
|
|
1546
|
+
function readTransportCause(outcome: Exclude<HerdrSocketOutcome, { readonly kind: "reply" }>): string {
|
|
1547
|
+
return boundHerdrDiagnostic("code" in outcome ? `${outcome.kind}: ${outcome.code}` : outcome.kind);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** The caller-pane read, reduced. Every failure is a REFUSAL: nothing has been mutated yet. A
|
|
1551
|
+
* transport cause or a well-formed herdr error travels with it as bounded, display-only report. */
|
|
1552
|
+
export function classifyHerdrLayoutPaneRead(
|
|
1553
|
+
outcome: HerdrSocketOutcome,
|
|
1554
|
+
id: string,
|
|
1555
|
+
callerPaneId: string,
|
|
1556
|
+
):
|
|
1557
|
+
| { readonly ok: true; readonly workspaceId: string }
|
|
1558
|
+
| {
|
|
1559
|
+
readonly ok: false;
|
|
1560
|
+
readonly reason:
|
|
1561
|
+
| "herdr-caller-pane-get-failed"
|
|
1562
|
+
| "herdr-caller-pane-unparsable"
|
|
1563
|
+
| "herdr-caller-pane-drift"
|
|
1564
|
+
| "herdr-caller-workspace-missing";
|
|
1565
|
+
readonly reported: HerdrReportedDiagnostics;
|
|
1566
|
+
} {
|
|
1567
|
+
if (outcome.kind !== "reply") {
|
|
1568
|
+
return { ok: false, reason: "herdr-caller-pane-get-failed", reported: { transport: readTransportCause(outcome) } };
|
|
1569
|
+
}
|
|
1570
|
+
const envelope = readHerdrEnvelope(outcome.line, id);
|
|
1571
|
+
if (envelope === null) return { ok: false, reason: "herdr-caller-pane-unparsable", reported: {} };
|
|
1572
|
+
if (envelope.arm === "error") {
|
|
1573
|
+
return {
|
|
1574
|
+
ok: false,
|
|
1575
|
+
reason: "herdr-caller-pane-get-failed",
|
|
1576
|
+
reported: {
|
|
1577
|
+
errorCode: boundHerdrDiagnostic(envelope.code),
|
|
1578
|
+
errorMessage: boundHerdrDiagnostic(envelope.message),
|
|
1579
|
+
},
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
if (envelope.result.type !== "pane_info") return { ok: false, reason: "herdr-caller-pane-unparsable", reported: {} };
|
|
1583
|
+
const pane = envelope.result.pane;
|
|
1584
|
+
if (
|
|
1585
|
+
!isPlainObject(pane) ||
|
|
1586
|
+
!isNonEmptyString(pane.pane_id) ||
|
|
1587
|
+
!isNonEmptyString(pane.terminal_id) ||
|
|
1588
|
+
!isNonEmptyString(pane.tab_id) ||
|
|
1589
|
+
typeof pane.focused !== "boolean" ||
|
|
1590
|
+
typeof pane.revision !== "number" ||
|
|
1591
|
+
!Number.isSafeInteger(pane.revision) ||
|
|
1592
|
+
pane.revision < 0 ||
|
|
1593
|
+
!Object.hasOwn(pane, "agent_status")
|
|
1594
|
+
) {
|
|
1595
|
+
return { ok: false, reason: "herdr-caller-pane-unparsable", reported: {} };
|
|
1596
|
+
}
|
|
1597
|
+
if (pane.pane_id !== callerPaneId) return { ok: false, reason: "herdr-caller-pane-drift", reported: {} };
|
|
1598
|
+
if (!isNonEmptyString(pane.workspace_id)) {
|
|
1599
|
+
return { ok: false, reason: "herdr-caller-workspace-missing", reported: {} };
|
|
1600
|
+
}
|
|
1601
|
+
return { ok: true, workspaceId: pane.workspace_id };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/** What a layout-branch failure carries: reason, certainty, and bounded display-only reports. */
|
|
1605
|
+
export interface HerdrLayoutFailure {
|
|
1606
|
+
readonly ok: false;
|
|
1607
|
+
readonly reason: HerdrLayoutFailureReason;
|
|
1608
|
+
readonly herdrErrorCode?: string;
|
|
1609
|
+
readonly recovery: { readonly outcome: "none" } | { readonly outcome: "unknown" };
|
|
1610
|
+
readonly reported: HerdrReportedDiagnostics;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function layoutFailure(
|
|
1614
|
+
reason: HerdrLayoutFailureReason,
|
|
1615
|
+
outcome: "none" | "unknown",
|
|
1616
|
+
reported: HerdrReportedDiagnostics,
|
|
1617
|
+
herdrErrorCode?: string,
|
|
1618
|
+
): HerdrLayoutFailure {
|
|
1619
|
+
return {
|
|
1620
|
+
ok: false,
|
|
1621
|
+
reason,
|
|
1622
|
+
...(herdrErrorCode === undefined ? {} : { herdrErrorCode }),
|
|
1623
|
+
recovery: { outcome },
|
|
1624
|
+
reported,
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** The coordinates a drifted success reply named — strings only, cut for display. Never a receipt. */
|
|
1629
|
+
function reportedCoordinates(
|
|
1630
|
+
layout: Record<string, unknown> | undefined,
|
|
1631
|
+
root: Record<string, unknown> | undefined,
|
|
1632
|
+
): HerdrReportedDiagnostics {
|
|
1633
|
+
return {
|
|
1634
|
+
...(typeof layout?.workspace_id === "string" ? { workspaceId: boundHerdrDiagnostic(layout.workspace_id) } : {}),
|
|
1635
|
+
...(typeof layout?.tab_id === "string" ? { tabId: boundHerdrDiagnostic(layout.tab_id) } : {}),
|
|
1636
|
+
...(typeof root?.pane_id === "string" ? { paneId: boundHerdrDiagnostic(root.pane_id) } : {}),
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function sameArgv(echoed: unknown, sent: readonly string[]): boolean {
|
|
1641
|
+
return (
|
|
1642
|
+
Array.isArray(echoed) &&
|
|
1643
|
+
echoed.length === sent.length &&
|
|
1644
|
+
echoed.every((token, index) => typeof token === "string" && token === sent[index])
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* The ONE layout mutation write's outcome. Envelope first (`readHerdrEnvelope`), certainty second:
|
|
1650
|
+
* `none` only when the request was never written or herdr answered a well-formed matching error
|
|
1651
|
+
* with a pre-create code; EVERY other non-success is `unknown` — a tab may exist, nothing is closed
|
|
1652
|
+
* and nothing is resent. A success must prove THIS launch: the arm, the workspace we asked for, a
|
|
1653
|
+
* tab, a single-pane root whose pane is the tab's focus, and the cwd and argv we sent echoed back
|
|
1654
|
+
* (an echo, not an execution witness; schema-nullable `pane_id`/`cwd`/`command` that come back null
|
|
1655
|
+
* cannot prove it, so they are `unknown` too).
|
|
1656
|
+
*/
|
|
1657
|
+
export function classifyHerdrLayoutApplyReply(
|
|
1658
|
+
outcome: HerdrSocketOutcome,
|
|
1659
|
+
expect: {
|
|
1660
|
+
readonly id: string;
|
|
1661
|
+
readonly workspaceId: string;
|
|
1662
|
+
readonly cwd: string;
|
|
1663
|
+
readonly command: readonly string[];
|
|
1664
|
+
},
|
|
1665
|
+
):
|
|
1666
|
+
| { readonly ok: true; readonly workspaceId: string; readonly tabId: string; readonly paneId: string }
|
|
1667
|
+
| HerdrLayoutFailure {
|
|
1668
|
+
switch (outcome.kind) {
|
|
1669
|
+
case "connect-failed":
|
|
1670
|
+
return layoutFailure("herdr-layout-transport-failed", "none", {
|
|
1671
|
+
transport: boundHerdrDiagnostic(`connect-failed: ${outcome.code}`),
|
|
1672
|
+
});
|
|
1673
|
+
case "io-failed":
|
|
1674
|
+
return layoutFailure("herdr-layout-transport-failed", "unknown", {
|
|
1675
|
+
transport: boundHerdrDiagnostic(`io-failed: ${outcome.code}`),
|
|
1676
|
+
});
|
|
1677
|
+
case "timeout":
|
|
1678
|
+
case "oversize":
|
|
1679
|
+
case "eof-before-newline":
|
|
1680
|
+
return layoutFailure("herdr-layout-transport-failed", "unknown", { transport: outcome.kind });
|
|
1681
|
+
default:
|
|
1682
|
+
break;
|
|
1683
|
+
}
|
|
1684
|
+
const envelope = readHerdrEnvelope(outcome.line, expect.id);
|
|
1685
|
+
if (envelope === null) return layoutFailure("herdr-layout-reply-unparsable", "unknown", {});
|
|
1686
|
+
if (envelope.arm === "error") {
|
|
1687
|
+
const reported = {
|
|
1688
|
+
errorCode: boundHerdrDiagnostic(envelope.code),
|
|
1689
|
+
errorMessage: boundHerdrDiagnostic(envelope.message),
|
|
1690
|
+
};
|
|
1691
|
+
return HERDR_LAYOUT_NONE_CODES.includes(envelope.code)
|
|
1692
|
+
? layoutFailure("herdr-layout-apply-refused", "none", reported, envelope.code)
|
|
1693
|
+
: layoutFailure("herdr-layout-apply-failed", "unknown", reported, envelope.code);
|
|
1694
|
+
}
|
|
1695
|
+
const layout = isPlainObject(envelope.result.layout) ? envelope.result.layout : undefined;
|
|
1696
|
+
const root = isPlainObject(layout?.root) ? layout.root : undefined;
|
|
1697
|
+
if (
|
|
1698
|
+
envelope.result.type !== "layout_apply" ||
|
|
1699
|
+
layout === undefined ||
|
|
1700
|
+
root === undefined ||
|
|
1701
|
+
layout.workspace_id !== expect.workspaceId ||
|
|
1702
|
+
!isNonEmptyString(layout.tab_id) ||
|
|
1703
|
+
typeof layout.zoomed !== "boolean" ||
|
|
1704
|
+
!isNonEmptyString(layout.focused_pane_id) ||
|
|
1705
|
+
root.type !== "pane" ||
|
|
1706
|
+
!isNonEmptyString(root.pane_id) ||
|
|
1707
|
+
layout.focused_pane_id !== root.pane_id ||
|
|
1708
|
+
root.cwd !== expect.cwd ||
|
|
1709
|
+
!sameArgv(root.command, expect.command)
|
|
1710
|
+
) {
|
|
1711
|
+
return layoutFailure("herdr-layout-reply-drift", "unknown", reportedCoordinates(layout, root));
|
|
1712
|
+
}
|
|
1713
|
+
return { ok: true, workspaceId: expect.workspaceId, tabId: layout.tab_id, paneId: root.pane_id };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** What the layout branch needs from its surroundings, injectable so a gate can drive the whole
|
|
1717
|
+
* branch without a herdr server, a package closure or this process's directory. */
|
|
1718
|
+
export interface HerdrLayoutDeps {
|
|
1719
|
+
readonly socket: HerdrSocketCallFactory;
|
|
1720
|
+
readonly callerProcessCwd: () => string;
|
|
1721
|
+
readonly packageLayout: () => PiDurablePackageLayout | null;
|
|
1722
|
+
readonly runtimeVerdict: (layout: PiDurablePackageLayout) => PiDurableRuntimeRejectReason | null;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
export const HERDR_LAYOUT_PRODUCTION_DEPS: HerdrLayoutDeps = {
|
|
1726
|
+
socket: (socketPath) => createHerdrSocketCall(socketPath),
|
|
1727
|
+
callerProcessCwd: () => process.cwd(),
|
|
1728
|
+
packageLayout: () => piDurablePackageLayout(),
|
|
1729
|
+
runtimeVerdict: (layout) => {
|
|
1730
|
+
const verdict = checkPiDurableRuntime(layout);
|
|
1731
|
+
return verdict.ok ? null : verdict.reason;
|
|
1732
|
+
},
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
/** A bounded environment probe: is the verified package's own launcher an executable regular file? */
|
|
1736
|
+
function isExecutableRegularFile(file: string): boolean {
|
|
1737
|
+
try {
|
|
1738
|
+
if (!statSync(file).isFile()) return false;
|
|
1739
|
+
accessSync(file, fsConstants.X_OK);
|
|
1740
|
+
return true;
|
|
1741
|
+
} catch {
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* #157 B3: the verified package's own launcher for a layout command, or `null` when it is missing or
|
|
1748
|
+
* not executable — the same path and probe the fresh layout branch puts first in its argv vector. The
|
|
1749
|
+
* pi-durable same-id resume (`herdr-resume-call.ts`) asks it through this export.
|
|
1750
|
+
*/
|
|
1751
|
+
export function resolvePiDurableLauncher(layout: PiDurablePackageLayout): string | null {
|
|
1752
|
+
const runtime = path.join(layout.root, "run.sh");
|
|
1753
|
+
return isExecutableRegularFile(runtime) ? runtime : null;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/**
|
|
1757
|
+
* Open a pi-durable sibling in a new herdr tab. ORDER IS THE ARGUMENT: everything decidable without
|
|
1758
|
+
* herdr — inputs, the explicit cwd, the endpoint, the pi-durable preflight (provider split, first
|
|
1759
|
+
* input cap, package, carrier and SDK set, units), the launcher and the argv — is decided before
|
|
1760
|
+
* the first socket request. The caller-pane read comes next and mutates nothing, so any failure of
|
|
1761
|
+
* it refuses with zero layout writes. Then the ONE layout mutation write, never resent, and no
|
|
1762
|
+
* reclaim: herdr's layout reply names no terminal id, so this branch holds no create-receipt
|
|
1763
|
+
* terminal and therefore no authority to close what it opened.
|
|
1764
|
+
*/
|
|
1765
|
+
async function herdrLayoutFreshCall(
|
|
1766
|
+
params: {
|
|
1767
|
+
readonly backend: string;
|
|
1768
|
+
readonly model: string;
|
|
1769
|
+
readonly task: string;
|
|
1770
|
+
readonly cwd?: string;
|
|
1771
|
+
readonly callerGardenId: string | null;
|
|
1772
|
+
readonly callerCwd?: string;
|
|
1773
|
+
readonly callerIsCodex?: boolean;
|
|
1774
|
+
},
|
|
1775
|
+
context: HerdrContext,
|
|
1776
|
+
env: NodeJS.ProcessEnv,
|
|
1777
|
+
nonce: string,
|
|
1778
|
+
deps: HerdrLayoutDeps,
|
|
1779
|
+
): Promise<HerdrFreshCallResult> {
|
|
1780
|
+
const normalized = normalizeFreshCallInputs(params);
|
|
1781
|
+
if (!normalized.ok) return { ok: false, reason: normalized.reason };
|
|
1782
|
+
const { callerGardenId, model, task, cwd: requestedCwd } = normalized.inputs;
|
|
1783
|
+
|
|
1784
|
+
const chosen = chooseHerdrLayoutCwd({
|
|
1785
|
+
...(requestedCwd === undefined ? {} : { requestedCwd }),
|
|
1786
|
+
callerIsCodex: params.callerIsCodex === true,
|
|
1787
|
+
...(params.callerCwd === undefined ? {} : { callerCwd: params.callerCwd }),
|
|
1788
|
+
callerProcessCwd: deps.callerProcessCwd,
|
|
1789
|
+
});
|
|
1790
|
+
if (!chosen.ok) return { ok: false, reason: chosen.reason };
|
|
1791
|
+
const badCwd = classifyHerdrCwd(chosen.cwd);
|
|
1792
|
+
if (badCwd !== null) return { ok: false, reason: badCwd };
|
|
1793
|
+
|
|
1794
|
+
const socketPath = resolveHerdrSocketPath(env);
|
|
1795
|
+
if (socketPath === null) return { ok: false, reason: "herdr-socket-path-missing" };
|
|
1796
|
+
|
|
1797
|
+
const prompt = composeFreshCallPrompt({
|
|
1798
|
+
backend: "pi-durable",
|
|
1799
|
+
task,
|
|
1800
|
+
callerGardenId,
|
|
1801
|
+
nonce,
|
|
1802
|
+
openingLine: HERDR_FRESH_CALL_OPENING_LINE,
|
|
1803
|
+
});
|
|
1804
|
+
const packageLayout = deps.packageLayout();
|
|
1805
|
+
const missing = piDurableFreshPreflight({
|
|
1806
|
+
model,
|
|
1807
|
+
firstInput: prompt,
|
|
1808
|
+
layout: packageLayout,
|
|
1809
|
+
runtime: packageLayout === null ? null : deps.runtimeVerdict(packageLayout),
|
|
1810
|
+
});
|
|
1811
|
+
if (missing !== null) return { ok: false, reason: missing };
|
|
1812
|
+
if (packageLayout === null) throw new Error("herdr-fresh-call: the pi-durable preflight passed without a package");
|
|
1813
|
+
|
|
1814
|
+
const runtime = path.join(packageLayout.root, "run.sh");
|
|
1815
|
+
if (!isExecutableRegularFile(runtime)) return { ok: false, reason: "pi-durable-launcher-missing" };
|
|
1816
|
+
const command = composeHerdrPiDurableCommand({ runtime, prompt, model, callerGardenId, nonce, task });
|
|
1817
|
+
const launchEnv = buildHerdrLayoutLaunchEnv({ target: callerGardenId, nonce });
|
|
1818
|
+
|
|
1819
|
+
const call = deps.socket(socketPath);
|
|
1820
|
+
const base = herdrAgentNameFromNonce(nonce);
|
|
1821
|
+
const readId = `${base}:pane.get`;
|
|
1822
|
+
const read = classifyHerdrLayoutPaneRead(
|
|
1823
|
+
await call(buildHerdrPaneGetPacket(readId, context.callerPaneId)),
|
|
1824
|
+
readId,
|
|
1825
|
+
context.callerPaneId,
|
|
1826
|
+
);
|
|
1827
|
+
if (!read.ok) return { ok: false, reason: read.reason, reported: read.reported };
|
|
1828
|
+
|
|
1829
|
+
// ── everything above this line leaves nothing behind; the ONE layout mutation write follows ──
|
|
1830
|
+
const writeId = `${base}:layout.apply`;
|
|
1831
|
+
const applied = classifyHerdrLayoutApplyReply(
|
|
1832
|
+
await call(
|
|
1833
|
+
buildHerdrLayoutApplyPacket(writeId, {
|
|
1834
|
+
workspaceId: read.workspaceId,
|
|
1835
|
+
cwd: chosen.cwd,
|
|
1836
|
+
command,
|
|
1837
|
+
env: launchEnv,
|
|
1838
|
+
}),
|
|
1839
|
+
),
|
|
1840
|
+
{ id: writeId, workspaceId: read.workspaceId, cwd: chosen.cwd, command },
|
|
1841
|
+
);
|
|
1842
|
+
if (!applied.ok) return { ...applied, ...(applied.recovery.outcome === "unknown" ? { nonce } : {}) };
|
|
1843
|
+
return {
|
|
1844
|
+
ok: true,
|
|
1845
|
+
receipt: {
|
|
1846
|
+
backend: "pi-durable",
|
|
1847
|
+
launch: "layout-apply",
|
|
1848
|
+
model,
|
|
1849
|
+
cwd: chosen.cwd,
|
|
1850
|
+
cwdSource: chosen.source,
|
|
1851
|
+
runtime,
|
|
1852
|
+
herdrWorkspaceId: applied.workspaceId,
|
|
1853
|
+
herdrTabId: applied.tabId,
|
|
1854
|
+
herdrPaneId: applied.paneId,
|
|
1855
|
+
nonce,
|
|
1856
|
+
},
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1101
1860
|
/** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
|
|
1102
1861
|
* they will guess about. The five shared input reasons keep the wording the public verb has always
|
|
1103
1862
|
* used; the rest are this rail's own. */
|
|
1104
|
-
const HERDR_REJECT_HINT: Record<
|
|
1863
|
+
export const HERDR_REJECT_HINT: Record<
|
|
1864
|
+
HerdrFreshCallRejectReason | HerdrLaunchFailureReason | HerdrLayoutFailureReason,
|
|
1865
|
+
string
|
|
1866
|
+
> = {
|
|
1105
1867
|
// #76: decided at the dispatch root, worded once in the shared composition leaf.
|
|
1106
1868
|
...FRESH_CALL_PROVIDER_HINT,
|
|
1869
|
+
// #157 A-L1: the pi-durable preflight's repair text stays beside its predicates, as on tmux.
|
|
1870
|
+
...PI_DURABLE_PREFLIGHT_HINT,
|
|
1871
|
+
...PI_DURABLE_RUNTIME_HINT,
|
|
1107
1872
|
"caller-identity-unavailable":
|
|
1108
1873
|
"this surface has no record-backed identity, so a sibling would have nowhere to call home",
|
|
1109
1874
|
"model-empty": "pass the model you want the sibling opened with — it is never inherited from this process",
|
|
@@ -1119,13 +1884,29 @@ const HERDR_REJECT_HINT: Record<HerdrFreshCallRejectReason | HerdrLaunchFailureR
|
|
|
1119
1884
|
"herdr-caller-pane-missing":
|
|
1120
1885
|
"herdr did not give this process a HERDR_PANE_ID, so there is no way to ask herdr which workspace this caller is in",
|
|
1121
1886
|
"herdr-caller-pane-get-failed":
|
|
1122
|
-
"herdr would not report this process's own pane — run `herdr pane get $HERDR_PANE_ID` to see what it says",
|
|
1887
|
+
"herdr would not report this process's own pane — for pi and claude-code run `herdr pane get $HERDR_PANE_ID` to see what it says; pi-durable asked through one `pane.get` on HERDR_SOCKET_PATH instead, and any cause herdr reported is quoted below",
|
|
1123
1888
|
"herdr-caller-pane-unparsable": "herdr's reply about this process's own pane could not be read",
|
|
1124
1889
|
"herdr-caller-pane-drift":
|
|
1125
1890
|
"herdr answered about a different pane than HERDR_PANE_ID names, so its workspace says nothing about where this caller is",
|
|
1126
1891
|
"herdr-caller-workspace-missing":
|
|
1127
1892
|
"herdr reported this process's pane with no workspace, and guessing one would open the sibling wherever the operator is looking",
|
|
1128
|
-
"herdr-backend-unsupported":
|
|
1893
|
+
"herdr-backend-unsupported":
|
|
1894
|
+
"this rail opens pi and claude-code through herdr agent start, and pi-durable through one layout.apply tab; nothing is opened elsewhere instead",
|
|
1895
|
+
"herdr-socket-path-missing":
|
|
1896
|
+
"HERDR_SOCKET_PATH is absent, empty or not absolute, so there is no herdr endpoint for pi-durable's one layout request — this rail does not guess or scan for one",
|
|
1897
|
+
"pi-durable-launcher-missing":
|
|
1898
|
+
"this Entwurf package's own run.sh is missing or not executable, so the verified package has no launcher to put in the layout command — reinstall Entwurf",
|
|
1899
|
+
"codex-caller-cwd-unavailable":
|
|
1900
|
+
"this Codex caller's record carries no directory, and its process directory is the app-server's, not its own — pass cwd explicitly",
|
|
1901
|
+
"herdr-layout-transport-failed":
|
|
1902
|
+
"the one exchange with herdr's socket did not complete — read the recovery line for whether the request can have reached herdr",
|
|
1903
|
+
"herdr-layout-apply-refused":
|
|
1904
|
+
"herdr declined the layout request with a code its handler returns before creating anything",
|
|
1905
|
+
"herdr-layout-apply-failed":
|
|
1906
|
+
"herdr answered the layout request with an error that does not prove nothing was created",
|
|
1907
|
+
"herdr-layout-reply-unparsable": "herdr's reply was not one well-formed answer to this exact request",
|
|
1908
|
+
"herdr-layout-reply-drift":
|
|
1909
|
+
"herdr's success reply does not describe the tab this request asked for, so its coordinates were not accepted",
|
|
1129
1910
|
"herdr-placement-tmux-rejected":
|
|
1130
1911
|
"`placement` names a tmux session, which does not exist inside herdr — drop it rather than have the sibling silently placed somewhere else",
|
|
1131
1912
|
"herdr-argv-control-character":
|
|
@@ -1194,6 +1975,96 @@ function renderFailureHeader(recovery: HerdrRecovery): string {
|
|
|
1194
1975
|
}
|
|
1195
1976
|
}
|
|
1196
1977
|
|
|
1978
|
+
const HERDR_LAYOUT_FAILURE_REASONS: ReadonlySet<string> = new Set<HerdrLayoutFailureReason>([
|
|
1979
|
+
"herdr-layout-transport-failed",
|
|
1980
|
+
"herdr-layout-apply-refused",
|
|
1981
|
+
"herdr-layout-apply-failed",
|
|
1982
|
+
"herdr-layout-reply-unparsable",
|
|
1983
|
+
"herdr-layout-reply-drift",
|
|
1984
|
+
]);
|
|
1985
|
+
|
|
1986
|
+
/** The reported diagnostics, each already cut, rendered QUOTED so a value cannot pose as prose. */
|
|
1987
|
+
function renderReported(reported: HerdrReportedDiagnostics | undefined): string {
|
|
1988
|
+
if (reported === undefined) return "";
|
|
1989
|
+
const all: ReadonlyArray<readonly [string, string | undefined]> = [
|
|
1990
|
+
["transport", reported.transport],
|
|
1991
|
+
["code", reported.errorCode],
|
|
1992
|
+
["message", reported.errorMessage],
|
|
1993
|
+
["workspace", reported.workspaceId],
|
|
1994
|
+
["tab", reported.tabId],
|
|
1995
|
+
["pane", reported.paneId],
|
|
1996
|
+
];
|
|
1997
|
+
const fields = all.filter((field): field is readonly [string, string] => field[1] !== undefined);
|
|
1998
|
+
if (fields.length === 0) return "";
|
|
1999
|
+
return ` reported (display only, unverified, each value cut at ${HERDR_DIAGNOSTIC_MAX_CHARS} chars — not a receipt, a placement or anything to close): ${fields.map(([name, value]) => `${name} ${JSON.stringify(value)}`).join(", ")}\n`;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/** A layout-branch failure. The certainty line says only what the outcome licenses: `none` never
|
|
2003
|
+
* says a sibling may exist, and only a POST-WRITE `unknown` mentions a possible callback. */
|
|
2004
|
+
function renderLayoutFailure(
|
|
2005
|
+
result: {
|
|
2006
|
+
readonly reason: HerdrLaunchFailureReason | HerdrLayoutFailureReason;
|
|
2007
|
+
readonly herdrErrorCode?: string;
|
|
2008
|
+
readonly recovery: HerdrRecovery;
|
|
2009
|
+
readonly reported?: HerdrReportedDiagnostics;
|
|
2010
|
+
readonly nonce?: string;
|
|
2011
|
+
},
|
|
2012
|
+
hint: string,
|
|
2013
|
+
): string {
|
|
2014
|
+
const code =
|
|
2015
|
+
result.herdrErrorCode === undefined
|
|
2016
|
+
? ""
|
|
2017
|
+
: ` [herdr: ${JSON.stringify(boundHerdrDiagnostic(result.herdrErrorCode))}]`;
|
|
2018
|
+
if (result.recovery.outcome === "none") {
|
|
2019
|
+
const why =
|
|
2020
|
+
result.reason === "herdr-layout-transport-failed"
|
|
2021
|
+
? "the request was never written to herdr"
|
|
2022
|
+
: "herdr declined by name before creating anything";
|
|
2023
|
+
return (
|
|
2024
|
+
`entwurf_fresh_call failed at the herdr layout.apply step: ${result.reason}${code} — ${hint}.\n` +
|
|
2025
|
+
` recovery: none — ${why}, so nothing was created\n` +
|
|
2026
|
+
renderReported(result.reported)
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
const nonce = result.nonce === undefined ? "this call's launch nonce" : `nonce ${result.nonce}`;
|
|
2030
|
+
return (
|
|
2031
|
+
`entwurf_fresh_call failed at the herdr layout.apply step with an UNKNOWN outcome: ${result.reason}${code} — ${hint}.\n` +
|
|
2032
|
+
` recovery: UNKNOWN — the layout write was attempted and may have reached herdr, so a pi-durable tab MAY exist and its sibling may still call back under ${nonce}; that callback's sender envelope is the only address. Nothing was closed and nothing will be retried — check herdr\n` +
|
|
2033
|
+
renderReported(result.reported)
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
const HERDR_LAYOUT_CWD_SOURCE_TEXT: Record<HerdrLayoutCwdSource, string> = {
|
|
2038
|
+
requested: "the requested start directory",
|
|
2039
|
+
"codex-caller-record": "this Codex caller's own record directory",
|
|
2040
|
+
"caller-process": "this agent's own process directory",
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
function renderLayoutReceipt(r: HerdrLayoutFreshCallReceipt): string {
|
|
2044
|
+
return (
|
|
2045
|
+
`[entwurf fresh call → herdr]\n` +
|
|
2046
|
+
` backend: ${r.backend} (ONE layout.apply request — herdr has no agent kind for it, and none was requested)\n` +
|
|
2047
|
+
` model: ${r.model} (requested on the runtime CLI)\n` +
|
|
2048
|
+
` cwd: ${r.cwd} (${HERDR_LAYOUT_CWD_SOURCE_TEXT[r.cwdSource]}, sent explicitly — not an observation)\n` +
|
|
2049
|
+
` runtime: ${r.runtime} (this package's verified launcher, the first element of the requested argv)\n` +
|
|
2050
|
+
` tab: ${r.herdrTabId} in workspace ${r.herdrWorkspaceId} — a NEW tab beside the caller's, REQUESTED unfocused; whether it is actually unfocused is not observed here\n` +
|
|
2051
|
+
` pane: ${r.herdrPaneId} — the tab's root pane; herdr VIEW coordinates, not an address (herdr's layout reply names no terminal)\n` +
|
|
2052
|
+
` nonce: ${r.nonce}\n` +
|
|
2053
|
+
` detection: none requested — pi-durable has no herdr agent integration, so herdr is not told its session and its peers placement column reads unobserved\n` +
|
|
2054
|
+
`\n` +
|
|
2055
|
+
`This is a LAUNCH receipt: herdr accepted one layout request and named the tab above. It does NOT ` +
|
|
2056
|
+
`mean the sibling is running, that its first turn ran, or that the task was delivered. The tab and pane ` +
|
|
2057
|
+
`coordinates are a view and can change under the sibling — they are not an address and nothing may be ` +
|
|
2058
|
+
`dispatched to them.\n` +
|
|
2059
|
+
`The sibling's garden id arrives separately — before its task it calls the zero-argument callback verb ` +
|
|
2060
|
+
`(a read-only look at the caller may come first), and ` +
|
|
2061
|
+
`the sender envelope of THAT message is the address. The nonce above is the correlation tag it carries; ` +
|
|
2062
|
+
`the sibling reads it from its own launch env, never from this receipt. Nothing is polling for it; if it ` +
|
|
2063
|
+
`never comes — not started, stuck, or held back after its check; this receipt cannot tell which — the ` +
|
|
2064
|
+
`pane is visible and can be read directly.`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1197
2068
|
/**
|
|
1198
2069
|
* Three outcomes, and an operator must be able to tell them apart at a glance: a refusal that
|
|
1199
2070
|
* created nothing, a failure that may have left a tab behind, and a launch.
|
|
@@ -1203,10 +2074,17 @@ export function renderHerdrFreshCall(result: HerdrFreshCallResult): { text: stri
|
|
|
1203
2074
|
const hint = HERDR_REJECT_HINT[result.reason];
|
|
1204
2075
|
if (!("recovery" in result)) {
|
|
1205
2076
|
return {
|
|
1206
|
-
text:
|
|
2077
|
+
text:
|
|
2078
|
+
`entwurf_fresh_call rejected: ${result.reason} — ${hint}. No tab and no pane were created.` +
|
|
2079
|
+
("reported" in result && renderReported(result.reported) !== ""
|
|
2080
|
+
? `\n${renderReported(result.reported).trimEnd()}`
|
|
2081
|
+
: ""),
|
|
1207
2082
|
isError: true,
|
|
1208
2083
|
};
|
|
1209
2084
|
}
|
|
2085
|
+
if (HERDR_LAYOUT_FAILURE_REASONS.has(result.reason)) {
|
|
2086
|
+
return { text: renderLayoutFailure(result, hint), isError: true };
|
|
2087
|
+
}
|
|
1210
2088
|
const code = result.herdrErrorCode === undefined ? "" : ` [herdr: ${result.herdrErrorCode}]`;
|
|
1211
2089
|
return {
|
|
1212
2090
|
text:
|
|
@@ -1215,13 +2093,14 @@ export function renderHerdrFreshCall(result: HerdrFreshCallResult): { text: stri
|
|
|
1215
2093
|
isError: true,
|
|
1216
2094
|
};
|
|
1217
2095
|
}
|
|
2096
|
+
if (result.receipt.backend === "pi-durable") return { text: renderLayoutReceipt(result.receipt), isError: false };
|
|
1218
2097
|
const r = result.receipt;
|
|
1219
2098
|
return {
|
|
1220
2099
|
text:
|
|
1221
2100
|
`[entwurf fresh call → herdr]\n` +
|
|
1222
2101
|
` backend: ${r.backend} (requested kind ${r.requestedKind} — herdr resolves the executable, we did not)\n` +
|
|
1223
2102
|
` model: ${r.model} (requested on the runtime CLI)\n` +
|
|
1224
|
-
|
|
2103
|
+
` cwd: ${r.cwd} (${HERDR_LAYOUT_CWD_SOURCE_TEXT[r.cwdSource]}, sent explicitly — not an observation)\n` +
|
|
1225
2104
|
` agent: ${r.herdrAgentName} (herdr's name for it, derived from the nonce)\n` +
|
|
1226
2105
|
` tab: ${r.herdrTabId} in workspace ${r.herdrWorkspaceId} — a NEW tab beside the caller's, opened without taking focus\n` +
|
|
1227
2106
|
` pane: ${r.herdrPaneId} terminal ${r.herdrTerminalId} — the tab's initial pane; herdr VIEW coordinates, not an address\n` +
|