@junghanacs/entwurf 0.45.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +169 -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 +124 -188
- package/VERIFY.md +2 -2
- package/docs/external-mcp-host.md +3 -3
- package/docs/setup-clean-host.md +37 -12
- 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-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 +6 -3
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +108 -9
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/overlay/upstream-pin.json +11 -5
- 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 +29 -15
- package/run.sh +411 -657
- package/scripts/check-claude-launch.ts +341 -0
- 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 +18 -13
- 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/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/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +979 -1
- 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
|
@@ -41,30 +41,37 @@
|
|
|
41
41
|
* not allowed to turn one into the other.
|
|
42
42
|
*/
|
|
43
43
|
import { createHash } from "node:crypto";
|
|
44
|
-
import { statSync } from "node:fs";
|
|
44
|
+
import { accessSync, constants as fsConstants, statSync } from "node:fs";
|
|
45
|
+
import { createConnection } from "node:net";
|
|
45
46
|
import path from "node:path";
|
|
46
|
-
import { HERDR_FRESH_CALL_BACKENDS } from "../../scripts/herdr-rails.mjs";
|
|
47
|
+
import { HERDR_AGENT_START_BACKENDS, HERDR_FRESH_CALL_BACKENDS, HERDR_LAYOUT_LAUNCH_BACKENDS, } from "../../scripts/herdr-rails.mjs";
|
|
47
48
|
import { callbackEnvAssignments } from "./callback-env.js";
|
|
48
|
-
import { buildOmpBootstrapPayload, composeBackendArgs, composeFreshCallFraming, FRESH_CALL_PROVIDER_HINT, normalizeFreshCallInputs, TASK_MAX_CHARS, } from "./fresh-call-composition.js";
|
|
49
|
+
import { buildOmpBootstrapPayload, composeBackendArgs, composeFreshCallFraming, composeFreshCallPrompt, FRESH_CALL_PROVIDER_HINT, normalizeFreshCallInputs, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, } from "./fresh-call-composition.js";
|
|
50
|
+
import { PI_DURABLE_PREFLIGHT_HINT, piDurableFreshPreflight, } from "./pi-durable-fresh-preflight.js";
|
|
51
|
+
import { checkPiDurableRuntime, PI_DURABLE_RUNTIME_HINT, piDurablePackageLayout, } from "./pi-durable-runtime.js";
|
|
49
52
|
/**
|
|
50
|
-
*
|
|
51
|
-
* NOT evidence of support
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* Three closed sets, one leaf. `[#116 decision]` herdr's own `--kind` enum is much larger, and that
|
|
54
|
+
* is NOT evidence of support. Since #157 A-L1 the rail has two launch domains — the agent-start set
|
|
55
|
+
* (pi, claude-code: `tab create` + `agent start --kind`) and the layout set (pi-durable: one
|
|
56
|
+
* `layout.apply`) — and admits exactly their union: every backend outside it is a pre-mutation
|
|
57
|
+
* named reject, with no fallback to the tmux rail — a caller inside herdr who asks for codex gets a
|
|
58
|
+
* refusal, not a window somewhere else.
|
|
54
59
|
*
|
|
55
|
-
* The
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* still
|
|
59
|
-
* and
|
|
60
|
-
* passed. A gate pins that.
|
|
60
|
+
* The VALUES live in `scripts/herdr-rails.mjs` (#116 A7). The status pane's RAILS block renders
|
|
61
|
+
* the agent-start and layout sets as two groups (#157 A-L2); the install narration covers only the
|
|
62
|
+
* integrations it activated, which pi-durable never is. The re-export keeps this module's
|
|
63
|
+
* public names. The TYPES still have to be tuples: a `.mjs` leaf infers `readonly string[]` without
|
|
64
|
+
* the JSDoc cast there, and these types would silently widen to `string` while every runtime check
|
|
65
|
+
* still passed. A gate pins that.
|
|
61
66
|
*/
|
|
62
|
-
export { HERDR_FRESH_CALL_BACKENDS };
|
|
67
|
+
export { HERDR_AGENT_START_BACKENDS, HERDR_FRESH_CALL_BACKENDS, HERDR_LAYOUT_LAUNCH_BACKENDS };
|
|
63
68
|
/** Our backend name → the `--kind` token herdr accepts. `[측정 2026-09-14]` both round-tripped with
|
|
64
69
|
* the vendor executable echoed back in `argv`. This is a REQUESTED kind: `[file:line @ c77af189]`
|
|
65
70
|
* `src/app/agents.rs:197-199` turns it into a bare executable name and writes it into the pane's
|
|
66
71
|
* shell, so nothing here has proven the binary exists, is on PATH, or will start. Runtime failure
|
|
67
|
-
* is a post-mutation outcome on this rail, named as such, never a precondition we can claim.
|
|
72
|
+
* is a post-mutation outcome on this rail, named as such, never a precondition we can claim.
|
|
73
|
+
* Keyed by the agent-start domain ONLY: a layout-launched backend has no kind, and inventing one
|
|
74
|
+
* would start herdr's program for that kind under another backend's name. */
|
|
68
75
|
export const HERDR_AGENT_KIND = {
|
|
69
76
|
pi: "pi",
|
|
70
77
|
"claude-code": "claude",
|
|
@@ -503,7 +510,7 @@ export function decideConditionalClose(created, current, getFailed) {
|
|
|
503
510
|
* `callerGardenId` comes from the SURFACE that registered the tool, out of its own record-backed
|
|
504
511
|
* context. This module never derives it, looks it up, or guesses: an empty value is a refusal.
|
|
505
512
|
*/
|
|
506
|
-
export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL_CLOCK) {
|
|
513
|
+
export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL_CLOCK, layoutDeps = {}) {
|
|
507
514
|
const context = resolveHerdrContext(env);
|
|
508
515
|
if (!context.ok)
|
|
509
516
|
return { ok: false, reason: context.reason };
|
|
@@ -515,20 +522,36 @@ export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL
|
|
|
515
522
|
// worse than a refusal they can read.
|
|
516
523
|
return { ok: false, reason: "herdr-backend-unsupported" };
|
|
517
524
|
}
|
|
525
|
+
if (HERDR_LAYOUT_LAUNCH_BACKENDS.includes(params.backend)) {
|
|
526
|
+
// #157 A-L1: a backend herdr has no kind for. It never reaches `tab create`/`agent start`
|
|
527
|
+
// below and never runs the herdr CLI; its own branch owns its whole order.
|
|
528
|
+
return herdrLayoutFreshCall(params, context.context, env, nonce, {
|
|
529
|
+
...HERDR_LAYOUT_PRODUCTION_DEPS,
|
|
530
|
+
...layoutDeps,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
518
533
|
const backend = params.backend;
|
|
519
534
|
// The caller's inputs are judged by the SHARED contract — same order, same trimming, same
|
|
520
535
|
// five words — so a caller cannot learn a different vocabulary by being inside herdr. The
|
|
521
|
-
// cwd omission rule comes with it: `undefined` and `""` both mean "no cwd", which this
|
|
522
|
-
//
|
|
536
|
+
// cwd omission rule comes with it: `undefined` and `""` both mean "no cwd requested", which this
|
|
537
|
+
// rail once mistook for an invalid path. An omitted cwd means the CALLER's directory, chosen by
|
|
538
|
+
// the same helper the layout branch uses (#157 A-L1x), and is always sent explicitly: herdr's own
|
|
539
|
+
// default for an omitted `--cwd` is herdr's, not a fact about the caller.
|
|
523
540
|
const normalized = normalizeFreshCallInputs(params);
|
|
524
541
|
if (!normalized.ok)
|
|
525
542
|
return { ok: false, reason: normalized.reason };
|
|
526
|
-
const { callerGardenId, model, task, cwd } = normalized.inputs;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
543
|
+
const { callerGardenId, model, task, cwd: requestedCwd } = normalized.inputs;
|
|
544
|
+
const chosenCwd = chooseHerdrLayoutCwd({
|
|
545
|
+
...(requestedCwd === undefined ? {} : { requestedCwd }),
|
|
546
|
+
callerIsCodex: params.callerIsCodex === true,
|
|
547
|
+
...(params.callerCwd === undefined ? {} : { callerCwd: params.callerCwd }),
|
|
548
|
+
callerProcessCwd: layoutDeps.callerProcessCwd ?? HERDR_LAYOUT_PRODUCTION_DEPS.callerProcessCwd,
|
|
549
|
+
});
|
|
550
|
+
if (!chosenCwd.ok)
|
|
551
|
+
return { ok: false, reason: chosenCwd.reason };
|
|
552
|
+
const badCwd = classifyHerdrCwd(chosenCwd.cwd);
|
|
553
|
+
if (badCwd !== null)
|
|
554
|
+
return { ok: false, reason: badCwd };
|
|
532
555
|
const framing = composeFreshCallFraming({
|
|
533
556
|
backend,
|
|
534
557
|
callerGardenId,
|
|
@@ -576,7 +599,7 @@ export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL
|
|
|
576
599
|
// ── everything above this line leaves nothing behind ──────────────────────────────────
|
|
577
600
|
const tabRun = await run(buildHerdrTabCreateArgs({
|
|
578
601
|
workspaceId: callerPane.workspaceId,
|
|
579
|
-
|
|
602
|
+
cwd: chosenCwd.cwd,
|
|
580
603
|
callback: { target: callerGardenId, nonce },
|
|
581
604
|
}));
|
|
582
605
|
if (tabRun.status !== 0) {
|
|
@@ -665,7 +688,8 @@ export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL
|
|
|
665
688
|
backend,
|
|
666
689
|
requestedKind: HERDR_AGENT_KIND[backend],
|
|
667
690
|
model,
|
|
668
|
-
|
|
691
|
+
cwd: chosenCwd.cwd,
|
|
692
|
+
cwdSource: chosenCwd.source,
|
|
669
693
|
herdrAgentName: agentName,
|
|
670
694
|
// The tab coordinates come from the reply that CREATED them, not from the start's
|
|
671
695
|
// echo: the create receipt is what the reclaim above is bound to, so the receipt an
|
|
@@ -771,8 +795,13 @@ export async function settleAgentWitness(params) {
|
|
|
771
795
|
}
|
|
772
796
|
}
|
|
773
797
|
}
|
|
774
|
-
/** Reclaim the pane we opened — conditionally, or not at all.
|
|
775
|
-
|
|
798
|
+
/** Reclaim the pane we opened — conditionally, or not at all.
|
|
799
|
+
*
|
|
800
|
+
* Exported for the ONE other herdr launch that creates a tab and can fail after it: the same-id pi
|
|
801
|
+
* resume (`herdr-resume-call.ts`, #157 G1a). It is the existing within-generation witness
|
|
802
|
+
* (`decideConditionalClose`) plus the one pane close, reused rather than copied, so the two herdr
|
|
803
|
+
* launches cannot drift into different ideas of what may be closed. */
|
|
804
|
+
export async function reclaim(pane, run) {
|
|
776
805
|
const getRun = await run(buildHerdrPaneGetArgs(pane.paneId));
|
|
777
806
|
const decision = decideConditionalClose(pane, getRun.status === 0 ? parseHerdrPaneGetResponse(getRun.stdout) : null, getRun.status !== 0);
|
|
778
807
|
if (!decision.close)
|
|
@@ -782,12 +811,477 @@ async function reclaim(pane, run) {
|
|
|
782
811
|
return { outcome: "orphan-unreclaimed", paneId: pane.paneId, reason: "close-failed" };
|
|
783
812
|
return { outcome: "closed", paneId: pane.paneId, terminalId: pane.terminalId };
|
|
784
813
|
}
|
|
814
|
+
// ── #157 A-L1: pi-durable through ONE layout.apply on the caller's own herdr socket ──────────────
|
|
815
|
+
//
|
|
816
|
+
// WHY A SOCKET AND NOT THE CLI. The documented herdr CLI exposes no layout mutation, and
|
|
817
|
+
// `layout.apply` is the only herdr surface that execs an argv VECTOR in a new tab — no `--kind`, no
|
|
818
|
+
// shell, no PTY keystrokes (`[blob 2ecf19a9]` app-api-layouts.rs:117-140, pane.rs:2456-2500). So this
|
|
819
|
+
// branch sends one fixed JSON line per connection and reads one reply line: the framing herdr's own
|
|
820
|
+
// pi integration uses, and the one measured on the pinned 0.9.5 server (G2/G3).
|
|
821
|
+
//
|
|
822
|
+
// ONE ENDPOINT FOR THE READ AND THE WRITE. The caller-pane read that names the workspace and the
|
|
823
|
+
// mutation that uses it go to the SAME explicit endpoint, `HERDR_SOCKET_PATH` — never the CLI for one
|
|
824
|
+
// and the socket for the other, which could each reach a different server where `w1` names a
|
|
825
|
+
// different workspace. Two connections do NOT establish server-generation invariance across a
|
|
826
|
+
// restart or rebind: a valid inherited env pair (`HERDR_PANE_ID`, `HERDR_SOCKET_PATH`) and endpoint
|
|
827
|
+
// continuity during this call are ASSUMPTIONS, named here rather than proven, and no generation or
|
|
828
|
+
// liveness manager is added for them. The read is also a physical socket write; "exactly one" below
|
|
829
|
+
// always means exactly one LAYOUT MUTATION write.
|
|
830
|
+
//
|
|
831
|
+
// WHAT THE PARSERS ARE. An authority-bearing REDUCTION of herdr's replies, not a vendor-schema
|
|
832
|
+
// validator: they check the envelope this launch relies on and the id/echo fields it needs, against
|
|
833
|
+
// the binary-bundled schema (protocol 22) and recorded replies, and nothing else. `agent_status` is
|
|
834
|
+
// checked for presence only and is never interpreted. `result` XOR `error` is Entwurf's own strict
|
|
835
|
+
// acceptance policy — the schema does not forbid extra properties.
|
|
836
|
+
/** Each reported diagnostic string is cut to this many characters for DISPLAY, and rendered quoted.
|
|
837
|
+
* Every decision is taken on the complete original value before it is cut. */
|
|
838
|
+
export const HERDR_DIAGNOSTIC_MAX_CHARS = 256;
|
|
839
|
+
/**
|
|
840
|
+
* The error codes that prove NOTHING was created, for exactly this request shape (a `workspace_id`,
|
|
841
|
+
* no `tab_id`, one pane). `[blob 2ecf19a9]` the pinned `handle_layout_apply` returns these only
|
|
842
|
+
* before its first creation call: `workspace_not_found` at app-api-layouts.rs:56/66/114,
|
|
843
|
+
* `invalid_layout` at :69/:97/:109, and `invalid_env` at :105 (app-api-env.rs:3-32 yields no other
|
|
844
|
+
* code). `tab_not_found`/`invalid_target` need a `tab_id`, which this branch never sends, so they are
|
|
845
|
+
* NOT here — and `layout_apply_failed` is returned at :143/:158/:196 and after the creation events,
|
|
846
|
+
* so it never proves anything about creation.
|
|
847
|
+
*/
|
|
848
|
+
export const HERDR_LAYOUT_NONE_CODES = Object.freeze([
|
|
849
|
+
"workspace_not_found",
|
|
850
|
+
"invalid_layout",
|
|
851
|
+
"invalid_env",
|
|
852
|
+
]);
|
|
853
|
+
/**
|
|
854
|
+
* The production socket client. The reply is collected as BYTES and decoded once, after its newline,
|
|
855
|
+
* so a multi-byte character split across chunks is never decoded in halves. A bound that expires
|
|
856
|
+
* before the connection exists is a connect failure (nothing written); after it, a timeout (the
|
|
857
|
+
* request may have been written). Exactly one settle: connect, data, error, close and the timer race.
|
|
858
|
+
*/
|
|
859
|
+
export function createHerdrSocketCall(socketPath, bounds = { timeoutMs: HERDR_CLI_TIMEOUT_MS, maxReplyBytes: HERDR_MAX_OUTPUT_BYTES }) {
|
|
860
|
+
return (requestLine) => new Promise((resolve) => {
|
|
861
|
+
let settled = false;
|
|
862
|
+
let connected = false;
|
|
863
|
+
let lineBytes = 0;
|
|
864
|
+
const chunks = [];
|
|
865
|
+
const socket = createConnection({ path: socketPath });
|
|
866
|
+
const settle = (outcome) => {
|
|
867
|
+
if (settled)
|
|
868
|
+
return;
|
|
869
|
+
settled = true;
|
|
870
|
+
clearTimeout(timer);
|
|
871
|
+
socket.destroy();
|
|
872
|
+
resolve(outcome);
|
|
873
|
+
};
|
|
874
|
+
const timer = setTimeout(() => settle(connected ? { kind: "timeout" } : { kind: "connect-failed", code: "timeout-before-connect" }), bounds.timeoutMs);
|
|
875
|
+
timer.unref?.();
|
|
876
|
+
socket.on("connect", () => {
|
|
877
|
+
connected = true;
|
|
878
|
+
socket.write(requestLine);
|
|
879
|
+
});
|
|
880
|
+
socket.on("data", (chunk) => {
|
|
881
|
+
const newline = chunk.indexOf(0x0a);
|
|
882
|
+
const part = newline === -1 ? chunk : chunk.subarray(0, newline);
|
|
883
|
+
lineBytes += part.length;
|
|
884
|
+
if (lineBytes > bounds.maxReplyBytes) {
|
|
885
|
+
settle({ kind: "oversize" });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
chunks.push(part);
|
|
889
|
+
if (newline !== -1)
|
|
890
|
+
settle({ kind: "reply", line: decodeHerdrReplyBytes(chunks) });
|
|
891
|
+
});
|
|
892
|
+
socket.on("error", (error) => {
|
|
893
|
+
const code = error.code ?? error.message;
|
|
894
|
+
settle(connected ? { kind: "io-failed", code } : { kind: "connect-failed", code });
|
|
895
|
+
});
|
|
896
|
+
socket.on("close", () => settle({ kind: "eof-before-newline" }));
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
/** The reply line's ONE decode: every collected chunk concatenated, then UTF-8 — never chunk by
|
|
900
|
+
* chunk, so a multi-byte character split between reads is decoded whole. */
|
|
901
|
+
export function decodeHerdrReplyBytes(chunks) {
|
|
902
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
903
|
+
}
|
|
904
|
+
/** The fixed caller-pane read. Product function, not an RPC surface: method and shape are literal. */
|
|
905
|
+
export function buildHerdrPaneGetPacket(id, paneId) {
|
|
906
|
+
return `${JSON.stringify({ id, method: "pane.get", params: { pane_id: paneId } })}\n`;
|
|
907
|
+
}
|
|
908
|
+
/** The fixed layout mutation: one unfocused pane tab in a named workspace, an explicit cwd, an argv
|
|
909
|
+
* vector and an env map. No `tab_id`, `tab_label`, split, label or ratio can be expressed through it. */
|
|
910
|
+
export function buildHerdrLayoutApplyPacket(id, params) {
|
|
911
|
+
return `${JSON.stringify({
|
|
912
|
+
id,
|
|
913
|
+
method: "layout.apply",
|
|
914
|
+
params: {
|
|
915
|
+
workspace_id: params.workspaceId,
|
|
916
|
+
focus: false,
|
|
917
|
+
root: { type: "pane", cwd: params.cwd, command: [...params.command], env: { ...params.env } },
|
|
918
|
+
},
|
|
919
|
+
})}\n`;
|
|
920
|
+
}
|
|
921
|
+
/** The launch env: the identity scrub the other launches use, plus the callback pair from its one
|
|
922
|
+
* validating formatter (which throws rather than inject a pair the callback verb would refuse). */
|
|
923
|
+
export function buildHerdrLayoutLaunchEnv(callback) {
|
|
924
|
+
const env = { PI_SESSION_ID: "", PI_AGENT_ID: "" };
|
|
925
|
+
for (const assignment of callbackEnvAssignments(callback)) {
|
|
926
|
+
const at = assignment.indexOf("=");
|
|
927
|
+
env[assignment.slice(0, at)] = assignment.slice(at + 1);
|
|
928
|
+
}
|
|
929
|
+
return env;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* The argv vector: the verified package's own launcher, then the SHARED pi-durable composition, with
|
|
933
|
+
* the bootstrap payload made Cc-free at ITS OWN layer. `JSON.stringify` escapes C0, `"` and `\` but
|
|
934
|
+
* leaves DEL and C1 literal; escaping only the outer request would be undone by herdr's own decode,
|
|
935
|
+
* so the reversible `\uXXXX` escape is applied to the payload element itself. The bootstrap decoder
|
|
936
|
+
* is `JSON.parse` (meta-bridge-omp.ts `decodeOmpBootstrapPayload`), so the task comes back byte-exact
|
|
937
|
+
* — asserted here, and a failure is OUR bug, so it throws rather than launch a task we cannot
|
|
938
|
+
* reproduce. No blanket control-character refusal: this exec path has no Cc rule of its own.
|
|
939
|
+
*/
|
|
940
|
+
export function composeHerdrPiDurableCommand(params) {
|
|
941
|
+
const args = composeBackendArgs("pi-durable", {
|
|
942
|
+
prompt: params.prompt,
|
|
943
|
+
bootstrapPayload: buildOmpBootstrapPayload({
|
|
944
|
+
callerGardenId: params.callerGardenId,
|
|
945
|
+
nonce: params.nonce,
|
|
946
|
+
task: params.task,
|
|
947
|
+
}),
|
|
948
|
+
}, params.model, () => {
|
|
949
|
+
throw new Error("herdr-fresh-call: codex is not a herdr backend");
|
|
950
|
+
}, () => {
|
|
951
|
+
throw new Error("herdr-fresh-call: codex is not a herdr backend");
|
|
952
|
+
});
|
|
953
|
+
const raw = args.at(-1);
|
|
954
|
+
if (args.at(-2) !== "--entwurf-bootstrap" || raw === undefined) {
|
|
955
|
+
throw new Error("herdr-fresh-call: the pi-durable composition no longer ends with its bootstrap payload");
|
|
956
|
+
}
|
|
957
|
+
const payload = escapeRemainingControlChars(raw);
|
|
958
|
+
const decoded = JSON.parse(payload);
|
|
959
|
+
const intact = typeof decoded === "object" &&
|
|
960
|
+
decoded !== null &&
|
|
961
|
+
decoded.v === OMP_BOOTSTRAP_VERSION &&
|
|
962
|
+
decoded.task === params.prompt &&
|
|
963
|
+
!containsControlChar(payload);
|
|
964
|
+
if (!intact) {
|
|
965
|
+
throw new Error("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");
|
|
966
|
+
}
|
|
967
|
+
return [params.runtime, ...args.slice(0, -1), payload];
|
|
968
|
+
}
|
|
969
|
+
/** `HERDR_SOCKET_PATH`, read only here and only as given: non-empty and absolute, or `null`. */
|
|
970
|
+
export function resolveHerdrSocketPath(env) {
|
|
971
|
+
const value = env.HERDR_SOCKET_PATH;
|
|
972
|
+
return typeof value === "string" && value.length > 0 && path.isAbsolute(value) ? value : null;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* The explicit start directory. herdr's own default for an omitted `root.cwd` is the FOCUSED pane's
|
|
976
|
+
* cwd in the target workspace (`[blob 2ecf19a9]` app-api-layouts.rs:343-365) — unrelated to the
|
|
977
|
+
* caller — so this branch always sends one. Requested first; then a known Codex caller's RECORD
|
|
978
|
+
* cwd, with NO fallback when it has none (its process directory is the app-server's, #95 lane C);
|
|
979
|
+
* then, for every other caller, this process's own directory — the inherited fact the tmux rail's
|
|
980
|
+
* omitted cwd rests on. Never herdr's own `cwd`/`foreground_cwd` for the caller's pane: that is a view.
|
|
981
|
+
* The agent-start branch reuses this same choice for its `tab create --cwd` (#157 A-L1x), so both
|
|
982
|
+
* herdr branches start an omitted cwd where the caller is; the name stays layout-first only by history.
|
|
983
|
+
*/
|
|
984
|
+
export function chooseHerdrLayoutCwd(input) {
|
|
985
|
+
if (input.requestedCwd !== undefined)
|
|
986
|
+
return { ok: true, cwd: input.requestedCwd, source: "requested" };
|
|
987
|
+
if (input.callerIsCodex) {
|
|
988
|
+
const record = input.callerCwd === undefined || input.callerCwd === "" ? undefined : input.callerCwd;
|
|
989
|
+
return record === undefined
|
|
990
|
+
? { ok: false, reason: "codex-caller-cwd-unavailable" }
|
|
991
|
+
: { ok: true, cwd: record, source: "codex-caller-record" };
|
|
992
|
+
}
|
|
993
|
+
return { ok: true, cwd: input.callerProcessCwd(), source: "caller-process" };
|
|
994
|
+
}
|
|
995
|
+
function isPlainObject(value) {
|
|
996
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
997
|
+
}
|
|
998
|
+
function isNonEmptyString(value) {
|
|
999
|
+
return typeof value === "string" && value.length > 0;
|
|
1000
|
+
}
|
|
1001
|
+
/** Cut for display, never for comparison. A lone high surrogate is not left at the cut. */
|
|
1002
|
+
export function boundHerdrDiagnostic(value) {
|
|
1003
|
+
if (value.length <= HERDR_DIAGNOSTIC_MAX_CHARS)
|
|
1004
|
+
return value;
|
|
1005
|
+
let cut = HERDR_DIAGNOSTIC_MAX_CHARS - 1;
|
|
1006
|
+
const last = value.charCodeAt(cut - 1);
|
|
1007
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
1008
|
+
cut -= 1;
|
|
1009
|
+
return `${value.slice(0, cut)}…`;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* The envelope, before any certainty is decided: a JSON object (not an array, not null), the exact
|
|
1013
|
+
* id we sent, and EXACTLY ONE of `result`/`error`, whose value is itself a non-null, non-array
|
|
1014
|
+
* object. An error arm also needs a non-empty string `code` and a string `message` (both required
|
|
1015
|
+
* by the bundled schema). Anything else is `null`: unreadable, never a partially believed answer.
|
|
1016
|
+
*/
|
|
1017
|
+
export function readHerdrEnvelope(line, id) {
|
|
1018
|
+
let parsed;
|
|
1019
|
+
try {
|
|
1020
|
+
parsed = JSON.parse(line);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
if (!isPlainObject(parsed))
|
|
1026
|
+
return null;
|
|
1027
|
+
if (typeof parsed.id !== "string" || parsed.id !== id)
|
|
1028
|
+
return null;
|
|
1029
|
+
const hasResult = Object.hasOwn(parsed, "result");
|
|
1030
|
+
if (hasResult === Object.hasOwn(parsed, "error"))
|
|
1031
|
+
return null;
|
|
1032
|
+
if (hasResult)
|
|
1033
|
+
return isPlainObject(parsed.result) ? { arm: "result", result: parsed.result } : null;
|
|
1034
|
+
const error = parsed.error;
|
|
1035
|
+
if (!isPlainObject(error))
|
|
1036
|
+
return null;
|
|
1037
|
+
if (!isNonEmptyString(error.code) || typeof error.message !== "string")
|
|
1038
|
+
return null;
|
|
1039
|
+
return { arm: "error", code: error.code, message: error.message };
|
|
1040
|
+
}
|
|
1041
|
+
/** The transport cause of a read that got no reply line, cut for display. */
|
|
1042
|
+
function readTransportCause(outcome) {
|
|
1043
|
+
return boundHerdrDiagnostic("code" in outcome ? `${outcome.kind}: ${outcome.code}` : outcome.kind);
|
|
1044
|
+
}
|
|
1045
|
+
/** The caller-pane read, reduced. Every failure is a REFUSAL: nothing has been mutated yet. A
|
|
1046
|
+
* transport cause or a well-formed herdr error travels with it as bounded, display-only report. */
|
|
1047
|
+
export function classifyHerdrLayoutPaneRead(outcome, id, callerPaneId) {
|
|
1048
|
+
if (outcome.kind !== "reply") {
|
|
1049
|
+
return { ok: false, reason: "herdr-caller-pane-get-failed", reported: { transport: readTransportCause(outcome) } };
|
|
1050
|
+
}
|
|
1051
|
+
const envelope = readHerdrEnvelope(outcome.line, id);
|
|
1052
|
+
if (envelope === null)
|
|
1053
|
+
return { ok: false, reason: "herdr-caller-pane-unparsable", reported: {} };
|
|
1054
|
+
if (envelope.arm === "error") {
|
|
1055
|
+
return {
|
|
1056
|
+
ok: false,
|
|
1057
|
+
reason: "herdr-caller-pane-get-failed",
|
|
1058
|
+
reported: {
|
|
1059
|
+
errorCode: boundHerdrDiagnostic(envelope.code),
|
|
1060
|
+
errorMessage: boundHerdrDiagnostic(envelope.message),
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
if (envelope.result.type !== "pane_info")
|
|
1065
|
+
return { ok: false, reason: "herdr-caller-pane-unparsable", reported: {} };
|
|
1066
|
+
const pane = envelope.result.pane;
|
|
1067
|
+
if (!isPlainObject(pane) ||
|
|
1068
|
+
!isNonEmptyString(pane.pane_id) ||
|
|
1069
|
+
!isNonEmptyString(pane.terminal_id) ||
|
|
1070
|
+
!isNonEmptyString(pane.tab_id) ||
|
|
1071
|
+
typeof pane.focused !== "boolean" ||
|
|
1072
|
+
typeof pane.revision !== "number" ||
|
|
1073
|
+
!Number.isSafeInteger(pane.revision) ||
|
|
1074
|
+
pane.revision < 0 ||
|
|
1075
|
+
!Object.hasOwn(pane, "agent_status")) {
|
|
1076
|
+
return { ok: false, reason: "herdr-caller-pane-unparsable", reported: {} };
|
|
1077
|
+
}
|
|
1078
|
+
if (pane.pane_id !== callerPaneId)
|
|
1079
|
+
return { ok: false, reason: "herdr-caller-pane-drift", reported: {} };
|
|
1080
|
+
if (!isNonEmptyString(pane.workspace_id)) {
|
|
1081
|
+
return { ok: false, reason: "herdr-caller-workspace-missing", reported: {} };
|
|
1082
|
+
}
|
|
1083
|
+
return { ok: true, workspaceId: pane.workspace_id };
|
|
1084
|
+
}
|
|
1085
|
+
function layoutFailure(reason, outcome, reported, herdrErrorCode) {
|
|
1086
|
+
return {
|
|
1087
|
+
ok: false,
|
|
1088
|
+
reason,
|
|
1089
|
+
...(herdrErrorCode === undefined ? {} : { herdrErrorCode }),
|
|
1090
|
+
recovery: { outcome },
|
|
1091
|
+
reported,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
/** The coordinates a drifted success reply named — strings only, cut for display. Never a receipt. */
|
|
1095
|
+
function reportedCoordinates(layout, root) {
|
|
1096
|
+
return {
|
|
1097
|
+
...(typeof layout?.workspace_id === "string" ? { workspaceId: boundHerdrDiagnostic(layout.workspace_id) } : {}),
|
|
1098
|
+
...(typeof layout?.tab_id === "string" ? { tabId: boundHerdrDiagnostic(layout.tab_id) } : {}),
|
|
1099
|
+
...(typeof root?.pane_id === "string" ? { paneId: boundHerdrDiagnostic(root.pane_id) } : {}),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function sameArgv(echoed, sent) {
|
|
1103
|
+
return (Array.isArray(echoed) &&
|
|
1104
|
+
echoed.length === sent.length &&
|
|
1105
|
+
echoed.every((token, index) => typeof token === "string" && token === sent[index]));
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* The ONE layout mutation write's outcome. Envelope first (`readHerdrEnvelope`), certainty second:
|
|
1109
|
+
* `none` only when the request was never written or herdr answered a well-formed matching error
|
|
1110
|
+
* with a pre-create code; EVERY other non-success is `unknown` — a tab may exist, nothing is closed
|
|
1111
|
+
* and nothing is resent. A success must prove THIS launch: the arm, the workspace we asked for, a
|
|
1112
|
+
* tab, a single-pane root whose pane is the tab's focus, and the cwd and argv we sent echoed back
|
|
1113
|
+
* (an echo, not an execution witness; schema-nullable `pane_id`/`cwd`/`command` that come back null
|
|
1114
|
+
* cannot prove it, so they are `unknown` too).
|
|
1115
|
+
*/
|
|
1116
|
+
export function classifyHerdrLayoutApplyReply(outcome, expect) {
|
|
1117
|
+
switch (outcome.kind) {
|
|
1118
|
+
case "connect-failed":
|
|
1119
|
+
return layoutFailure("herdr-layout-transport-failed", "none", {
|
|
1120
|
+
transport: boundHerdrDiagnostic(`connect-failed: ${outcome.code}`),
|
|
1121
|
+
});
|
|
1122
|
+
case "io-failed":
|
|
1123
|
+
return layoutFailure("herdr-layout-transport-failed", "unknown", {
|
|
1124
|
+
transport: boundHerdrDiagnostic(`io-failed: ${outcome.code}`),
|
|
1125
|
+
});
|
|
1126
|
+
case "timeout":
|
|
1127
|
+
case "oversize":
|
|
1128
|
+
case "eof-before-newline":
|
|
1129
|
+
return layoutFailure("herdr-layout-transport-failed", "unknown", { transport: outcome.kind });
|
|
1130
|
+
default:
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
const envelope = readHerdrEnvelope(outcome.line, expect.id);
|
|
1134
|
+
if (envelope === null)
|
|
1135
|
+
return layoutFailure("herdr-layout-reply-unparsable", "unknown", {});
|
|
1136
|
+
if (envelope.arm === "error") {
|
|
1137
|
+
const reported = {
|
|
1138
|
+
errorCode: boundHerdrDiagnostic(envelope.code),
|
|
1139
|
+
errorMessage: boundHerdrDiagnostic(envelope.message),
|
|
1140
|
+
};
|
|
1141
|
+
return HERDR_LAYOUT_NONE_CODES.includes(envelope.code)
|
|
1142
|
+
? layoutFailure("herdr-layout-apply-refused", "none", reported, envelope.code)
|
|
1143
|
+
: layoutFailure("herdr-layout-apply-failed", "unknown", reported, envelope.code);
|
|
1144
|
+
}
|
|
1145
|
+
const layout = isPlainObject(envelope.result.layout) ? envelope.result.layout : undefined;
|
|
1146
|
+
const root = isPlainObject(layout?.root) ? layout.root : undefined;
|
|
1147
|
+
if (envelope.result.type !== "layout_apply" ||
|
|
1148
|
+
layout === undefined ||
|
|
1149
|
+
root === undefined ||
|
|
1150
|
+
layout.workspace_id !== expect.workspaceId ||
|
|
1151
|
+
!isNonEmptyString(layout.tab_id) ||
|
|
1152
|
+
typeof layout.zoomed !== "boolean" ||
|
|
1153
|
+
!isNonEmptyString(layout.focused_pane_id) ||
|
|
1154
|
+
root.type !== "pane" ||
|
|
1155
|
+
!isNonEmptyString(root.pane_id) ||
|
|
1156
|
+
layout.focused_pane_id !== root.pane_id ||
|
|
1157
|
+
root.cwd !== expect.cwd ||
|
|
1158
|
+
!sameArgv(root.command, expect.command)) {
|
|
1159
|
+
return layoutFailure("herdr-layout-reply-drift", "unknown", reportedCoordinates(layout, root));
|
|
1160
|
+
}
|
|
1161
|
+
return { ok: true, workspaceId: expect.workspaceId, tabId: layout.tab_id, paneId: root.pane_id };
|
|
1162
|
+
}
|
|
1163
|
+
export const HERDR_LAYOUT_PRODUCTION_DEPS = {
|
|
1164
|
+
socket: (socketPath) => createHerdrSocketCall(socketPath),
|
|
1165
|
+
callerProcessCwd: () => process.cwd(),
|
|
1166
|
+
packageLayout: () => piDurablePackageLayout(),
|
|
1167
|
+
runtimeVerdict: (layout) => {
|
|
1168
|
+
const verdict = checkPiDurableRuntime(layout);
|
|
1169
|
+
return verdict.ok ? null : verdict.reason;
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
/** A bounded environment probe: is the verified package's own launcher an executable regular file? */
|
|
1173
|
+
function isExecutableRegularFile(file) {
|
|
1174
|
+
try {
|
|
1175
|
+
if (!statSync(file).isFile())
|
|
1176
|
+
return false;
|
|
1177
|
+
accessSync(file, fsConstants.X_OK);
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* #157 B3: the verified package's own launcher for a layout command, or `null` when it is missing or
|
|
1186
|
+
* not executable — the same path and probe the fresh layout branch puts first in its argv vector. The
|
|
1187
|
+
* pi-durable same-id resume (`herdr-resume-call.ts`) asks it through this export.
|
|
1188
|
+
*/
|
|
1189
|
+
export function resolvePiDurableLauncher(layout) {
|
|
1190
|
+
const runtime = path.join(layout.root, "run.sh");
|
|
1191
|
+
return isExecutableRegularFile(runtime) ? runtime : null;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Open a pi-durable sibling in a new herdr tab. ORDER IS THE ARGUMENT: everything decidable without
|
|
1195
|
+
* herdr — inputs, the explicit cwd, the endpoint, the pi-durable preflight (provider split, first
|
|
1196
|
+
* input cap, package, carrier and SDK set, units), the launcher and the argv — is decided before
|
|
1197
|
+
* the first socket request. The caller-pane read comes next and mutates nothing, so any failure of
|
|
1198
|
+
* it refuses with zero layout writes. Then the ONE layout mutation write, never resent, and no
|
|
1199
|
+
* reclaim: herdr's layout reply names no terminal id, so this branch holds no create-receipt
|
|
1200
|
+
* terminal and therefore no authority to close what it opened.
|
|
1201
|
+
*/
|
|
1202
|
+
async function herdrLayoutFreshCall(params, context, env, nonce, deps) {
|
|
1203
|
+
const normalized = normalizeFreshCallInputs(params);
|
|
1204
|
+
if (!normalized.ok)
|
|
1205
|
+
return { ok: false, reason: normalized.reason };
|
|
1206
|
+
const { callerGardenId, model, task, cwd: requestedCwd } = normalized.inputs;
|
|
1207
|
+
const chosen = chooseHerdrLayoutCwd({
|
|
1208
|
+
...(requestedCwd === undefined ? {} : { requestedCwd }),
|
|
1209
|
+
callerIsCodex: params.callerIsCodex === true,
|
|
1210
|
+
...(params.callerCwd === undefined ? {} : { callerCwd: params.callerCwd }),
|
|
1211
|
+
callerProcessCwd: deps.callerProcessCwd,
|
|
1212
|
+
});
|
|
1213
|
+
if (!chosen.ok)
|
|
1214
|
+
return { ok: false, reason: chosen.reason };
|
|
1215
|
+
const badCwd = classifyHerdrCwd(chosen.cwd);
|
|
1216
|
+
if (badCwd !== null)
|
|
1217
|
+
return { ok: false, reason: badCwd };
|
|
1218
|
+
const socketPath = resolveHerdrSocketPath(env);
|
|
1219
|
+
if (socketPath === null)
|
|
1220
|
+
return { ok: false, reason: "herdr-socket-path-missing" };
|
|
1221
|
+
const prompt = composeFreshCallPrompt({
|
|
1222
|
+
backend: "pi-durable",
|
|
1223
|
+
task,
|
|
1224
|
+
callerGardenId,
|
|
1225
|
+
nonce,
|
|
1226
|
+
openingLine: HERDR_FRESH_CALL_OPENING_LINE,
|
|
1227
|
+
});
|
|
1228
|
+
const packageLayout = deps.packageLayout();
|
|
1229
|
+
const missing = piDurableFreshPreflight({
|
|
1230
|
+
model,
|
|
1231
|
+
firstInput: prompt,
|
|
1232
|
+
layout: packageLayout,
|
|
1233
|
+
runtime: packageLayout === null ? null : deps.runtimeVerdict(packageLayout),
|
|
1234
|
+
});
|
|
1235
|
+
if (missing !== null)
|
|
1236
|
+
return { ok: false, reason: missing };
|
|
1237
|
+
if (packageLayout === null)
|
|
1238
|
+
throw new Error("herdr-fresh-call: the pi-durable preflight passed without a package");
|
|
1239
|
+
const runtime = path.join(packageLayout.root, "run.sh");
|
|
1240
|
+
if (!isExecutableRegularFile(runtime))
|
|
1241
|
+
return { ok: false, reason: "pi-durable-launcher-missing" };
|
|
1242
|
+
const command = composeHerdrPiDurableCommand({ runtime, prompt, model, callerGardenId, nonce, task });
|
|
1243
|
+
const launchEnv = buildHerdrLayoutLaunchEnv({ target: callerGardenId, nonce });
|
|
1244
|
+
const call = deps.socket(socketPath);
|
|
1245
|
+
const base = herdrAgentNameFromNonce(nonce);
|
|
1246
|
+
const readId = `${base}:pane.get`;
|
|
1247
|
+
const read = classifyHerdrLayoutPaneRead(await call(buildHerdrPaneGetPacket(readId, context.callerPaneId)), readId, context.callerPaneId);
|
|
1248
|
+
if (!read.ok)
|
|
1249
|
+
return { ok: false, reason: read.reason, reported: read.reported };
|
|
1250
|
+
// ── everything above this line leaves nothing behind; the ONE layout mutation write follows ──
|
|
1251
|
+
const writeId = `${base}:layout.apply`;
|
|
1252
|
+
const applied = classifyHerdrLayoutApplyReply(await call(buildHerdrLayoutApplyPacket(writeId, {
|
|
1253
|
+
workspaceId: read.workspaceId,
|
|
1254
|
+
cwd: chosen.cwd,
|
|
1255
|
+
command,
|
|
1256
|
+
env: launchEnv,
|
|
1257
|
+
})), { id: writeId, workspaceId: read.workspaceId, cwd: chosen.cwd, command });
|
|
1258
|
+
if (!applied.ok)
|
|
1259
|
+
return { ...applied, ...(applied.recovery.outcome === "unknown" ? { nonce } : {}) };
|
|
1260
|
+
return {
|
|
1261
|
+
ok: true,
|
|
1262
|
+
receipt: {
|
|
1263
|
+
backend: "pi-durable",
|
|
1264
|
+
launch: "layout-apply",
|
|
1265
|
+
model,
|
|
1266
|
+
cwd: chosen.cwd,
|
|
1267
|
+
cwdSource: chosen.source,
|
|
1268
|
+
runtime,
|
|
1269
|
+
herdrWorkspaceId: applied.workspaceId,
|
|
1270
|
+
herdrTabId: applied.tabId,
|
|
1271
|
+
herdrPaneId: applied.paneId,
|
|
1272
|
+
nonce,
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
785
1276
|
/** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
|
|
786
1277
|
* they will guess about. The five shared input reasons keep the wording the public verb has always
|
|
787
1278
|
* used; the rest are this rail's own. */
|
|
788
|
-
const HERDR_REJECT_HINT = {
|
|
1279
|
+
export const HERDR_REJECT_HINT = {
|
|
789
1280
|
// #76: decided at the dispatch root, worded once in the shared composition leaf.
|
|
790
1281
|
...FRESH_CALL_PROVIDER_HINT,
|
|
1282
|
+
// #157 A-L1: the pi-durable preflight's repair text stays beside its predicates, as on tmux.
|
|
1283
|
+
...PI_DURABLE_PREFLIGHT_HINT,
|
|
1284
|
+
...PI_DURABLE_RUNTIME_HINT,
|
|
791
1285
|
"caller-identity-unavailable": "this surface has no record-backed identity, so a sibling would have nowhere to call home",
|
|
792
1286
|
"model-empty": "pass the model you want the sibling opened with — it is never inherited from this process",
|
|
793
1287
|
"model-invalid": "the model is not in the accepted grammar (no whitespace, no leading dash)",
|
|
@@ -798,11 +1292,19 @@ const HERDR_REJECT_HINT = {
|
|
|
798
1292
|
"task-too-long": `the task is over the ${TASK_MAX_CHARS}-character interface bound this verb shares with entwurf_v2`,
|
|
799
1293
|
"herdr-context-missing": "HERDR_ENV/HERDR_BIN_PATH are absent, so this process is not inside herdr and this rail does not exist here",
|
|
800
1294
|
"herdr-caller-pane-missing": "herdr did not give this process a HERDR_PANE_ID, so there is no way to ask herdr which workspace this caller is in",
|
|
801
|
-
"herdr-caller-pane-get-failed": "herdr would not report this process's own pane — run `herdr pane get $HERDR_PANE_ID` to see what it says",
|
|
1295
|
+
"herdr-caller-pane-get-failed": "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",
|
|
802
1296
|
"herdr-caller-pane-unparsable": "herdr's reply about this process's own pane could not be read",
|
|
803
1297
|
"herdr-caller-pane-drift": "herdr answered about a different pane than HERDR_PANE_ID names, so its workspace says nothing about where this caller is",
|
|
804
1298
|
"herdr-caller-workspace-missing": "herdr reported this process's pane with no workspace, and guessing one would open the sibling wherever the operator is looking",
|
|
805
|
-
"herdr-backend-unsupported": "this rail opens pi and claude-code
|
|
1299
|
+
"herdr-backend-unsupported": "this rail opens pi and claude-code through herdr agent start, and pi-durable through one layout.apply tab; nothing is opened elsewhere instead",
|
|
1300
|
+
"herdr-socket-path-missing": "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",
|
|
1301
|
+
"pi-durable-launcher-missing": "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",
|
|
1302
|
+
"codex-caller-cwd-unavailable": "this Codex caller's record carries no directory, and its process directory is the app-server's, not its own — pass cwd explicitly",
|
|
1303
|
+
"herdr-layout-transport-failed": "the one exchange with herdr's socket did not complete — read the recovery line for whether the request can have reached herdr",
|
|
1304
|
+
"herdr-layout-apply-refused": "herdr declined the layout request with a code its handler returns before creating anything",
|
|
1305
|
+
"herdr-layout-apply-failed": "herdr answered the layout request with an error that does not prove nothing was created",
|
|
1306
|
+
"herdr-layout-reply-unparsable": "herdr's reply was not one well-formed answer to this exact request",
|
|
1307
|
+
"herdr-layout-reply-drift": "herdr's success reply does not describe the tab this request asked for, so its coordinates were not accepted",
|
|
806
1308
|
"herdr-placement-tmux-rejected": "`placement` names a tmux session, which does not exist inside herdr — drop it rather than have the sibling silently placed somewhere else",
|
|
807
1309
|
"herdr-argv-control-character": "an argument still holds a control character after encoding; herdr refuses those and no pane was created",
|
|
808
1310
|
"cwd-not-absolute": "pass an absolute path, or omit cwd to use this agent's own directory",
|
|
@@ -863,6 +1365,76 @@ function renderFailureHeader(recovery) {
|
|
|
863
1365
|
return "entwurf_fresh_call failed after the tab was created";
|
|
864
1366
|
}
|
|
865
1367
|
}
|
|
1368
|
+
const HERDR_LAYOUT_FAILURE_REASONS = new Set([
|
|
1369
|
+
"herdr-layout-transport-failed",
|
|
1370
|
+
"herdr-layout-apply-refused",
|
|
1371
|
+
"herdr-layout-apply-failed",
|
|
1372
|
+
"herdr-layout-reply-unparsable",
|
|
1373
|
+
"herdr-layout-reply-drift",
|
|
1374
|
+
]);
|
|
1375
|
+
/** The reported diagnostics, each already cut, rendered QUOTED so a value cannot pose as prose. */
|
|
1376
|
+
function renderReported(reported) {
|
|
1377
|
+
if (reported === undefined)
|
|
1378
|
+
return "";
|
|
1379
|
+
const all = [
|
|
1380
|
+
["transport", reported.transport],
|
|
1381
|
+
["code", reported.errorCode],
|
|
1382
|
+
["message", reported.errorMessage],
|
|
1383
|
+
["workspace", reported.workspaceId],
|
|
1384
|
+
["tab", reported.tabId],
|
|
1385
|
+
["pane", reported.paneId],
|
|
1386
|
+
];
|
|
1387
|
+
const fields = all.filter((field) => field[1] !== undefined);
|
|
1388
|
+
if (fields.length === 0)
|
|
1389
|
+
return "";
|
|
1390
|
+
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`;
|
|
1391
|
+
}
|
|
1392
|
+
/** A layout-branch failure. The certainty line says only what the outcome licenses: `none` never
|
|
1393
|
+
* says a sibling may exist, and only a POST-WRITE `unknown` mentions a possible callback. */
|
|
1394
|
+
function renderLayoutFailure(result, hint) {
|
|
1395
|
+
const code = result.herdrErrorCode === undefined
|
|
1396
|
+
? ""
|
|
1397
|
+
: ` [herdr: ${JSON.stringify(boundHerdrDiagnostic(result.herdrErrorCode))}]`;
|
|
1398
|
+
if (result.recovery.outcome === "none") {
|
|
1399
|
+
const why = result.reason === "herdr-layout-transport-failed"
|
|
1400
|
+
? "the request was never written to herdr"
|
|
1401
|
+
: "herdr declined by name before creating anything";
|
|
1402
|
+
return (`entwurf_fresh_call failed at the herdr layout.apply step: ${result.reason}${code} — ${hint}.\n` +
|
|
1403
|
+
` recovery: none — ${why}, so nothing was created\n` +
|
|
1404
|
+
renderReported(result.reported));
|
|
1405
|
+
}
|
|
1406
|
+
const nonce = result.nonce === undefined ? "this call's launch nonce" : `nonce ${result.nonce}`;
|
|
1407
|
+
return (`entwurf_fresh_call failed at the herdr layout.apply step with an UNKNOWN outcome: ${result.reason}${code} — ${hint}.\n` +
|
|
1408
|
+
` 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` +
|
|
1409
|
+
renderReported(result.reported));
|
|
1410
|
+
}
|
|
1411
|
+
const HERDR_LAYOUT_CWD_SOURCE_TEXT = {
|
|
1412
|
+
requested: "the requested start directory",
|
|
1413
|
+
"codex-caller-record": "this Codex caller's own record directory",
|
|
1414
|
+
"caller-process": "this agent's own process directory",
|
|
1415
|
+
};
|
|
1416
|
+
function renderLayoutReceipt(r) {
|
|
1417
|
+
return (`[entwurf fresh call → herdr]\n` +
|
|
1418
|
+
` backend: ${r.backend} (ONE layout.apply request — herdr has no agent kind for it, and none was requested)\n` +
|
|
1419
|
+
` model: ${r.model} (requested on the runtime CLI)\n` +
|
|
1420
|
+
` cwd: ${r.cwd} (${HERDR_LAYOUT_CWD_SOURCE_TEXT[r.cwdSource]}, sent explicitly — not an observation)\n` +
|
|
1421
|
+
` runtime: ${r.runtime} (this package's verified launcher, the first element of the requested argv)\n` +
|
|
1422
|
+
` 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` +
|
|
1423
|
+
` pane: ${r.herdrPaneId} — the tab's root pane; herdr VIEW coordinates, not an address (herdr's layout reply names no terminal)\n` +
|
|
1424
|
+
` nonce: ${r.nonce}\n` +
|
|
1425
|
+
` 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` +
|
|
1426
|
+
`\n` +
|
|
1427
|
+
`This is a LAUNCH receipt: herdr accepted one layout request and named the tab above. It does NOT ` +
|
|
1428
|
+
`mean the sibling is running, that its first turn ran, or that the task was delivered. The tab and pane ` +
|
|
1429
|
+
`coordinates are a view and can change under the sibling — they are not an address and nothing may be ` +
|
|
1430
|
+
`dispatched to them.\n` +
|
|
1431
|
+
`The sibling's garden id arrives separately — before its task it calls the zero-argument callback verb ` +
|
|
1432
|
+
`(a read-only look at the caller may come first), and ` +
|
|
1433
|
+
`the sender envelope of THAT message is the address. The nonce above is the correlation tag it carries; ` +
|
|
1434
|
+
`the sibling reads it from its own launch env, never from this receipt. Nothing is polling for it; if it ` +
|
|
1435
|
+
`never comes — not started, stuck, or held back after its check; this receipt cannot tell which — the ` +
|
|
1436
|
+
`pane is visible and can be read directly.`);
|
|
1437
|
+
}
|
|
866
1438
|
/**
|
|
867
1439
|
* Three outcomes, and an operator must be able to tell them apart at a glance: a refusal that
|
|
868
1440
|
* created nothing, a failure that may have left a tab behind, and a launch.
|
|
@@ -872,10 +1444,16 @@ export function renderHerdrFreshCall(result) {
|
|
|
872
1444
|
const hint = HERDR_REJECT_HINT[result.reason];
|
|
873
1445
|
if (!("recovery" in result)) {
|
|
874
1446
|
return {
|
|
875
|
-
text: `entwurf_fresh_call rejected: ${result.reason} — ${hint}. No tab and no pane were created
|
|
1447
|
+
text: `entwurf_fresh_call rejected: ${result.reason} — ${hint}. No tab and no pane were created.` +
|
|
1448
|
+
("reported" in result && renderReported(result.reported) !== ""
|
|
1449
|
+
? `\n${renderReported(result.reported).trimEnd()}`
|
|
1450
|
+
: ""),
|
|
876
1451
|
isError: true,
|
|
877
1452
|
};
|
|
878
1453
|
}
|
|
1454
|
+
if (HERDR_LAYOUT_FAILURE_REASONS.has(result.reason)) {
|
|
1455
|
+
return { text: renderLayoutFailure(result, hint), isError: true };
|
|
1456
|
+
}
|
|
879
1457
|
const code = result.herdrErrorCode === undefined ? "" : ` [herdr: ${result.herdrErrorCode}]`;
|
|
880
1458
|
return {
|
|
881
1459
|
text: `${renderFailureHeader(result.recovery)}: ${result.reason}${code} — ${hint}.\n` +
|
|
@@ -883,12 +1461,14 @@ export function renderHerdrFreshCall(result) {
|
|
|
883
1461
|
isError: true,
|
|
884
1462
|
};
|
|
885
1463
|
}
|
|
1464
|
+
if (result.receipt.backend === "pi-durable")
|
|
1465
|
+
return { text: renderLayoutReceipt(result.receipt), isError: false };
|
|
886
1466
|
const r = result.receipt;
|
|
887
1467
|
return {
|
|
888
1468
|
text: `[entwurf fresh call → herdr]\n` +
|
|
889
1469
|
` backend: ${r.backend} (requested kind ${r.requestedKind} — herdr resolves the executable, we did not)\n` +
|
|
890
1470
|
` model: ${r.model} (requested on the runtime CLI)\n` +
|
|
891
|
-
|
|
1471
|
+
` cwd: ${r.cwd} (${HERDR_LAYOUT_CWD_SOURCE_TEXT[r.cwdSource]}, sent explicitly — not an observation)\n` +
|
|
892
1472
|
` agent: ${r.herdrAgentName} (herdr's name for it, derived from the nonce)\n` +
|
|
893
1473
|
` tab: ${r.herdrTabId} in workspace ${r.herdrWorkspaceId} — a NEW tab beside the caller's, opened without taking focus\n` +
|
|
894
1474
|
` pane: ${r.herdrPaneId} terminal ${r.herdrTerminalId} — the tab's initial pane; herdr VIEW coordinates, not an address\n` +
|