tickmarkr 1.30.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/README.md +347 -0
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.js +191 -0
- package/dist/adapters/codex.d.ts +8 -0
- package/dist/adapters/codex.js +257 -0
- package/dist/adapters/cursor-agent.d.ts +4 -0
- package/dist/adapters/cursor-agent.js +62 -0
- package/dist/adapters/fake.d.ts +45 -0
- package/dist/adapters/fake.js +115 -0
- package/dist/adapters/grok.d.ts +4 -0
- package/dist/adapters/grok.js +122 -0
- package/dist/adapters/model-lints.d.ts +6 -0
- package/dist/adapters/model-lints.js +130 -0
- package/dist/adapters/opencode.d.ts +3 -0
- package/dist/adapters/opencode.js +108 -0
- package/dist/adapters/pi.d.ts +3 -0
- package/dist/adapters/pi.js +191 -0
- package/dist/adapters/prompt.d.ts +6 -0
- package/dist/adapters/prompt.js +84 -0
- package/dist/adapters/registry.d.ts +31 -0
- package/dist/adapters/registry.js +165 -0
- package/dist/adapters/types.d.ts +98 -0
- package/dist/adapters/types.js +56 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +72 -0
- package/dist/cli/commands/compile.d.ts +1 -0
- package/dist/cli/commands/compile.js +23 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +103 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +32 -0
- package/dist/cli/commands/plan.d.ts +2 -0
- package/dist/cli/commands/plan.js +101 -0
- package/dist/cli/commands/profile.d.ts +1 -0
- package/dist/cli/commands/profile.js +56 -0
- package/dist/cli/commands/report.d.ts +4 -0
- package/dist/cli/commands/report.js +306 -0
- package/dist/cli/commands/resume.d.ts +1 -0
- package/dist/cli/commands/resume.js +11 -0
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.js +35 -0
- package/dist/cli/commands/scope.d.ts +3 -0
- package/dist/cli/commands/scope.js +22 -0
- package/dist/cli/commands/status.d.ts +9 -0
- package/dist/cli/commands/status.js +162 -0
- package/dist/cli/commands/unlock.d.ts +1 -0
- package/dist/cli/commands/unlock.js +11 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +65 -0
- package/dist/compile/collateral.d.ts +6 -0
- package/dist/compile/collateral.js +128 -0
- package/dist/compile/common.d.ts +12 -0
- package/dist/compile/common.js +56 -0
- package/dist/compile/gsd.d.ts +3 -0
- package/dist/compile/gsd.js +222 -0
- package/dist/compile/index.d.ts +4 -0
- package/dist/compile/index.js +33 -0
- package/dist/compile/native.d.ts +4 -0
- package/dist/compile/native.js +196 -0
- package/dist/compile/prd.d.ts +2 -0
- package/dist/compile/prd.js +64 -0
- package/dist/compile/speckit.d.ts +2 -0
- package/dist/compile/speckit.js +66 -0
- package/dist/config/config.d.ts +184 -0
- package/dist/config/config.js +329 -0
- package/dist/drivers/herdr.d.ts +44 -0
- package/dist/drivers/herdr.js +394 -0
- package/dist/drivers/index.d.ts +3 -0
- package/dist/drivers/index.js +13 -0
- package/dist/drivers/subprocess.d.ts +25 -0
- package/dist/drivers/subprocess.js +106 -0
- package/dist/drivers/types.d.ts +61 -0
- package/dist/drivers/types.js +77 -0
- package/dist/gates/acceptance.d.ts +19 -0
- package/dist/gates/acceptance.js +92 -0
- package/dist/gates/baseline.d.ts +12 -0
- package/dist/gates/baseline.js +97 -0
- package/dist/gates/evidence.d.ts +4 -0
- package/dist/gates/evidence.js +12 -0
- package/dist/gates/llm.d.ts +25 -0
- package/dist/gates/llm.js +101 -0
- package/dist/gates/review.d.ts +12 -0
- package/dist/gates/review.js +80 -0
- package/dist/gates/run-gates.d.ts +34 -0
- package/dist/gates/run-gates.js +105 -0
- package/dist/gates/scope.d.ts +8 -0
- package/dist/gates/scope.js +32 -0
- package/dist/gates/types.d.ts +6 -0
- package/dist/gates/types.js +1 -0
- package/dist/graph/graph.d.ts +20 -0
- package/dist/graph/graph.js +148 -0
- package/dist/graph/schema.d.ts +185 -0
- package/dist/graph/schema.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plan/prompt.d.ts +4 -0
- package/dist/plan/prompt.js +31 -0
- package/dist/plan/scope.d.ts +17 -0
- package/dist/plan/scope.js +141 -0
- package/dist/report/cost.d.ts +24 -0
- package/dist/report/cost.js +89 -0
- package/dist/route/preference.d.ts +27 -0
- package/dist/route/preference.js +49 -0
- package/dist/route/profile.d.ts +65 -0
- package/dist/route/profile.js +197 -0
- package/dist/route/router.d.ts +26 -0
- package/dist/route/router.js +200 -0
- package/dist/run/consult.d.ts +23 -0
- package/dist/run/consult.js +80 -0
- package/dist/run/daemon.d.ts +23 -0
- package/dist/run/daemon.js +728 -0
- package/dist/run/git.d.ts +12 -0
- package/dist/run/git.js +56 -0
- package/dist/run/journal.d.ts +91 -0
- package/dist/run/journal.js +287 -0
- package/dist/run/lock.d.ts +29 -0
- package/dist/run/lock.js +182 -0
- package/dist/run/merge.d.ts +12 -0
- package/dist/run/merge.js +44 -0
- package/dist/run/reconcile.d.ts +4 -0
- package/dist/run/reconcile.js +95 -0
- package/package.json +39 -0
- package/schema/rungraph.schema.json +306 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { shq } from "../adapters/types.js";
|
|
2
|
+
import { createWorktree, sh } from "../run/git.js";
|
|
3
|
+
import { herdrSealShellPrefix } from "./subprocess.js";
|
|
4
|
+
import { canonicalizeLegacyName, formatOwnedName, panesToClose } from "./types.js";
|
|
5
|
+
// VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
|
|
6
|
+
export const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
7
|
+
export const TRAILER_WIDTH_MARGIN = 2; // cols below (floor + margin) refuse a rightward first split
|
|
8
|
+
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
9
|
+
export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_COLS, margin = TRAILER_WIDTH_MARGIN) {
|
|
10
|
+
if (paneCols == null || paneCols <= 0)
|
|
11
|
+
return "down";
|
|
12
|
+
return paneCols / 2 >= safeFloor + margin ? "right" : "down";
|
|
13
|
+
}
|
|
14
|
+
export class HerdrDriver {
|
|
15
|
+
bin;
|
|
16
|
+
workersPerTab;
|
|
17
|
+
id = "herdr";
|
|
18
|
+
interactive = true;
|
|
19
|
+
groups = new Map();
|
|
20
|
+
// grouped slot()/close() mutate shared group state across awaits — serialize them so two
|
|
21
|
+
// concurrent first members can never both create the group tab (mergeSerial idiom, daemon.ts)
|
|
22
|
+
groupSerial = Promise.resolve();
|
|
23
|
+
// VIS-10: the run's workspace id, captured once at construction (the daemon inherits it from the
|
|
24
|
+
// operator's env before the driver is built). Required at slot() time, never in the constructor —
|
|
25
|
+
// pickDriver and its unit test construct HerdrDriver without env, so slot() is the trust gate.
|
|
26
|
+
ws = process.env.HERDR_WORKSPACE_ID;
|
|
27
|
+
constructor(bin = "herdr", workersPerTab = 3) {
|
|
28
|
+
this.bin = bin;
|
|
29
|
+
this.workersPerTab = workersPerTab;
|
|
30
|
+
}
|
|
31
|
+
serial(fn) {
|
|
32
|
+
const p = this.groupSerial.then(fn, fn);
|
|
33
|
+
this.groupSerial = p.catch(() => undefined);
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
static available() {
|
|
37
|
+
return process.env.HERDR_ENV === "1";
|
|
38
|
+
}
|
|
39
|
+
herdr(args, cwd = process.cwd(), timeoutMs) {
|
|
40
|
+
return sh(`${shq(this.bin)} ${args}`, cwd, timeoutMs);
|
|
41
|
+
}
|
|
42
|
+
// pane ids compact when panes close — resolve fresh via the durable agent name (spec §5)
|
|
43
|
+
async paneId(slot) {
|
|
44
|
+
const r = await this.herdr(`agent get ${shq(slot.name)}`);
|
|
45
|
+
try {
|
|
46
|
+
const id = JSON.parse(r.stdout).result?.agent?.pane_id;
|
|
47
|
+
if (typeof id === "string" && id)
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* fall back to cached id */
|
|
52
|
+
}
|
|
53
|
+
return slot.id;
|
|
54
|
+
}
|
|
55
|
+
// VIS-09 P43-03: runtime width for the layout gate (43-MEASUREMENT.md licensing condition 2).
|
|
56
|
+
async paneWidth(paneId) {
|
|
57
|
+
const r = await this.herdr(`pane layout --pane ${shq(paneId)}`);
|
|
58
|
+
if (r.code !== 0)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
const layout = JSON.parse(r.stdout).result?.layout;
|
|
62
|
+
const pane = layout?.panes?.find((p) => p.pane_id === paneId);
|
|
63
|
+
const w = pane?.rect?.width ?? layout?.area?.width;
|
|
64
|
+
return typeof w === "number" && w > 0 ? w : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async slot(cwd, name, opts) {
|
|
71
|
+
// T1 ownership contract: `opts.owned` (T2 call sites) names the pane canonically —
|
|
72
|
+
// drovr:<role>:<taskId>:<attempt>:<runId>. Without it, `name` passes through byte-identical
|
|
73
|
+
// (today's legacy daemon/gates/consult shapes) — canonicalizeLegacyName (types.ts) is what lets
|
|
74
|
+
// reconcile.ts and this driver's own renameGroupTab/glyphFor decode role/taskId/attempt from
|
|
75
|
+
// those shapes without a call-site migration; T2 retires this branch by always passing `owned`.
|
|
76
|
+
const resolved = opts?.owned ? formatOwnedName(opts.owned) : name;
|
|
77
|
+
// group wins if both are set (a group tab is already stage-labeled; passing both is a caller bug).
|
|
78
|
+
// label (without group) → dedicated labeled tab via tabSlot's third param: no groups-map entry, no
|
|
79
|
+
// refcount, no groupSerial, no degrade latch — dedicated tabs have no shared state to guard (SUP-01).
|
|
80
|
+
if (opts?.group)
|
|
81
|
+
return this.serial(() => this.groupSlot(cwd, resolved, opts.group));
|
|
82
|
+
return this.tabSlot(cwd, resolved, opts?.label); // label undefined → defaults to name (today's behavior)
|
|
83
|
+
}
|
|
84
|
+
// today's per-slot tab path, plus the VIS-04 orphan reap
|
|
85
|
+
// label defaults to the slot name; group tabs pass the STAGE name instead — a first-member label
|
|
86
|
+
// outlives its member once keepPanes reaps it (run-20260709-104447: the codex pane sat in a tab
|
|
87
|
+
// named after a dead cursor worker and the operator read it as a mislabeled agent)
|
|
88
|
+
async tabSlot(cwd, name, label = name) {
|
|
89
|
+
// tab-per-slot: concurrent agents in one tab split it into sliver columns — TUIs exit or
|
|
90
|
+
// hard-wrap at COLUMNS≈2, shredding even the DROVR_RESULT marker (v1.4 phase-1 incident).
|
|
91
|
+
// A dedicated named tab gives every agent a full-width pane; tab close() reaps it.
|
|
92
|
+
// VIS-10 (operator ruling 2026-07-11): "pane placed by focus heuristic" is a DEFECT CLASS.
|
|
93
|
+
// Fail closed at every step — env unset, tab-create non-zero, unparseable stdout, or a parsed
|
|
94
|
+
// payload with no tab_id all REJECT. There is no path from here to an untargeted pane.
|
|
95
|
+
if (!this.ws)
|
|
96
|
+
throw new Error("herdr placement requires HERDR_WORKSPACE_ID — refusing untargeted pane (VIS-10: fail closed, never place by focus)");
|
|
97
|
+
// pin the tab to the RUN's workspace, UNCONDITIONALLY (inherited via HERDR_WORKSPACE_ID), never the
|
|
98
|
+
// operator's focused one (Intl-Dossier run-20260709-104447 incident: worker tabs opened in the drovr repo workspace)
|
|
99
|
+
const t = await this.herdr(`tab create --label ${shq(label)} --no-focus --workspace ${shq(this.ws)}`);
|
|
100
|
+
if (t.code !== 0)
|
|
101
|
+
throw new Error(`herdr tab create failed (exit ${t.code}, refusing untargeted placement): ${t.stderr || t.stdout}`);
|
|
102
|
+
let res;
|
|
103
|
+
try {
|
|
104
|
+
res = JSON.parse(t.stdout).result;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new Error(`herdr tab create returned unparseable JSON (refusing untargeted placement): ${t.stdout}`);
|
|
108
|
+
}
|
|
109
|
+
const tabId = res?.tab?.tab_id;
|
|
110
|
+
const rootPane = res?.root_pane?.pane_id; // auto-created shell pane (SKILL:343)
|
|
111
|
+
if (typeof tabId !== "string" || !tabId)
|
|
112
|
+
throw new Error(`herdr tab create returned no tab_id (refusing untargeted placement): ${t.stdout}`);
|
|
113
|
+
let r = await this.herdr(`agent start ${shq(name)} --cwd ${shq(cwd)} --tab ${shq(tabId)} --no-focus -- bash`);
|
|
114
|
+
if (r.code !== 0 && /agent_name_taken/.test(r.stderr + r.stdout)) {
|
|
115
|
+
// DEFECT-01: a prior (killed) process's kept pane still holds this durable name — reclaim it.
|
|
116
|
+
// The old attempt is void by construction: its worktree was rm -rf'd on re-dispatch (git.ts:34),
|
|
117
|
+
// so adoption is meaningless; close the stale PANE only (never its tab — sibling prior-run panes
|
|
118
|
+
// may still be under the operator's eye) and restart. Match BOTH streams (herdr's error-stream
|
|
119
|
+
// convention varies — Pitfall 6); an unrelated nonzero exit must never reach pane close (T-10-04).
|
|
120
|
+
const g = await this.herdr(`agent get ${shq(name)}`);
|
|
121
|
+
try {
|
|
122
|
+
const stale = JSON.parse(g.stdout).result?.agent?.pane_id;
|
|
123
|
+
if (typeof stale === "string" && stale)
|
|
124
|
+
await this.herdr(`pane close ${shq(stale)}`);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* holder vanished between calls (agent_not_found) — retry the start regardless */
|
|
128
|
+
}
|
|
129
|
+
r = await this.herdr(`agent start ${shq(name)} --cwd ${shq(cwd)} --tab ${shq(tabId)} --no-focus -- bash`); // once
|
|
130
|
+
}
|
|
131
|
+
if (r.code !== 0)
|
|
132
|
+
throw new Error(`herdr agent start failed: ${r.stderr || r.stdout}`); // fail closed, no loop
|
|
133
|
+
const id = JSON.parse(r.stdout).result?.agent?.pane_id;
|
|
134
|
+
if (typeof id !== "string" || !id)
|
|
135
|
+
throw new Error(`herdr agent start returned no pane id: ${r.stdout}`);
|
|
136
|
+
// tab create auto-spawns a root shell pane and agent start --tab adds the agent as a SECOND
|
|
137
|
+
// pane — reap the idle shell so no tab shows a dead "drovr git:" prompt beside its agent
|
|
138
|
+
// (VIS-04 orphan fix). Best-effort: a failed reap costs cosmetics only.
|
|
139
|
+
if (typeof rootPane === "string" && rootPane && rootPane !== id) {
|
|
140
|
+
await this.herdr(`pane close ${shq(rootPane)}`);
|
|
141
|
+
}
|
|
142
|
+
// VIS-10 hole 3: seed the run's workspace id into the agent pane's shell so a worker's own ad-hoc
|
|
143
|
+
// `herdr agent start` is workspace-targeted BY CONSTRUCTION — correct placement is the default a
|
|
144
|
+
// worker GETS, not a rule it must remember (P40-02 probe leak). Fail closed: a failed seed rejects.
|
|
145
|
+
// v1.22 T3 / OBS-17: also strip HERDR_ENV + socket path so the worker cannot open/mutate panes in
|
|
146
|
+
// the operator's session. Daemon-side this.herdr() calls keep process.env (unsealed).
|
|
147
|
+
const seed = await this.herdr(`pane run ${shq(id)} ${shq(`export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`);
|
|
148
|
+
if (seed.code !== 0)
|
|
149
|
+
throw new Error(`herdr workspace-id seed failed (refusing untargeted pane): ${seed.stderr || seed.stdout}`);
|
|
150
|
+
return { id, name, cwd, tabId };
|
|
151
|
+
}
|
|
152
|
+
// VIS-04 role-tab (extended VIS-09 item 2): first member bootstraps generation 1 (WORKERS);
|
|
153
|
+
// later members join the latest generation with live-member room via joinGroup, else a NEW
|
|
154
|
+
// overflow generation tab (cleanup, cleanup, …) opens — a second `tab create`, NOT a further
|
|
155
|
+
// split of tab 1. The cap is the constructor's workersPerTab (plumbed from config via pickDriver).
|
|
156
|
+
// splitUnsupported stays PER GROUP: a herdr that can't split can't split in any tab, so once it
|
|
157
|
+
// latches every later member degrades to a per-slot tab (no overflow tab can be populated by join).
|
|
158
|
+
async groupSlot(cwd, name, group) {
|
|
159
|
+
const state = this.groups.get(group);
|
|
160
|
+
if (state?.splitUnsupported)
|
|
161
|
+
return this.tabSlot(cwd, name); // D-09: degrade, NOT a shared-tab member
|
|
162
|
+
if (state) {
|
|
163
|
+
const latest = state.generations[state.generations.length - 1];
|
|
164
|
+
if (latest && latest.members.length < this.workersPerTab) {
|
|
165
|
+
const joined = await this.joinGroup(cwd, name, group, latest);
|
|
166
|
+
if (joined)
|
|
167
|
+
return joined;
|
|
168
|
+
state.splitUnsupported = true; // D-09 fail-safe: this and future members degrade to per-slot tabs
|
|
169
|
+
return this.tabSlot(cwd, name);
|
|
170
|
+
}
|
|
171
|
+
return this.newGeneration(cwd, name, group, state); // cap full → overflow to a new generation tab
|
|
172
|
+
}
|
|
173
|
+
const fresh = { generations: [], created: 0 };
|
|
174
|
+
this.groups.set(group, fresh);
|
|
175
|
+
return this.newGeneration(cwd, name, group, fresh); // first member ever → bootstrap generation 1
|
|
176
|
+
}
|
|
177
|
+
// bootstrap a fresh generation tab: gen 1 keeps the WORKERS stage label (today's primary tab);
|
|
178
|
+
// every overflow generation is a "cleanup" tab (VIS-13 amendment) — never a WORKERS-N numeric suffix.
|
|
179
|
+
async newGeneration(cwd, name, group, state) {
|
|
180
|
+
state.created++;
|
|
181
|
+
const label = state.created === 1 ? group.toUpperCase() : "cleanup";
|
|
182
|
+
const s = await this.tabSlot(cwd, name, label);
|
|
183
|
+
const entry = { tabId: s.tabId, label, members: [{ name, paneId: s.id }] };
|
|
184
|
+
state.generations.push(entry);
|
|
185
|
+
await this.renameGroupTab(entry);
|
|
186
|
+
return { ...s, group };
|
|
187
|
+
}
|
|
188
|
+
// Only GroupEntry instances originate in newGeneration(), after tabSlot created the tab. This never
|
|
189
|
+
// adopts or renames an operator tab; the newest live worker contributes at most one task-id token.
|
|
190
|
+
// VIS-13: that token carries ONE state glyph — ↻ for a retry attempt (attempt > 0 parsed from the
|
|
191
|
+
// member name), ✋ when the driver observes the member blocked (queried live); bare on normal running.
|
|
192
|
+
// T1: token/attempt now come from canonicalizeLegacyName (types.ts) instead of ad hoc regex — same
|
|
193
|
+
// extraction for today's legacy names ("T2-worker-...-a0-...") and, once T2 passes `owned`, for
|
|
194
|
+
// canonical names too — one parser, not two.
|
|
195
|
+
async renameGroupTab(entry) {
|
|
196
|
+
const newest = [...entry.members].reverse().find((m) => canonicalizeLegacyName(m.name, "").role === "worker");
|
|
197
|
+
const token = newest ? canonicalizeLegacyName(newest.name, "").taskId : undefined;
|
|
198
|
+
const glyph = newest ? await this.glyphFor(newest) : "";
|
|
199
|
+
const label = token ? `${entry.label} · ${token}${glyph}` : entry.label;
|
|
200
|
+
try {
|
|
201
|
+
await this.herdr(`tab rename ${shq(entry.tabId)} ${shq(label)}`);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
/* cosmetic only — a relabel failure never blocks membership or teardown (v1.18 invariant) */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// VIS-13: at most one glyph on the hot token. ✋ wins — the driver observes the member blocked live
|
|
208
|
+
// (the actionable state the operator must clear); else ↻ for a retry attempt (attempt > 0 in the
|
|
209
|
+
// member's owned name); else bare. Status observation failure → "unknown" (never ✋), fail-safe.
|
|
210
|
+
async glyphFor(m) {
|
|
211
|
+
if ((await this.statusByName(m.name)) === "blocked")
|
|
212
|
+
return "✋";
|
|
213
|
+
return canonicalizeLegacyName(m.name, "").attempt > 0 ? "↻" : "";
|
|
214
|
+
}
|
|
215
|
+
// stack a subsequent member into the group tab; null → caller degrades (D-09)
|
|
216
|
+
async joinGroup(cwd, name, group, entry) {
|
|
217
|
+
// pane ids compact when panes close — resolve the split source fresh from the newest LIVE
|
|
218
|
+
// member's durable name (never a cached id: closing the newest member must not poison this)
|
|
219
|
+
const src = entry.members[entry.members.length - 1];
|
|
220
|
+
const srcPane = await this.paneId({ id: src.paneId, name: src.name, cwd });
|
|
221
|
+
// VIS-09 P43-03 (43-MEASUREMENT.md): first join in a generation may split right when
|
|
222
|
+
// paneWidth/2 ≥ TRAILER_SAFE_FLOOR_COLS + margin (measured floor 108, margin 2); later joins
|
|
223
|
+
// stack down. Rightward splits below the measured floor shred the marker (e8aa003 at COLUMNS≈2;
|
|
224
|
+
// unrecoverable at 25 cols per measurement). Introspection failure → down (fail closed).
|
|
225
|
+
const direction = entry.members.length === 1 ? workerSplitDirection(await this.paneWidth(srcPane)) : "down";
|
|
226
|
+
const sp = await this.herdr(`pane split ${shq(srcPane)} --direction ${direction} --no-focus`);
|
|
227
|
+
if (sp.code !== 0)
|
|
228
|
+
return null;
|
|
229
|
+
let pane;
|
|
230
|
+
try {
|
|
231
|
+
pane = JSON.parse(sp.stdout).result?.pane?.pane_id ?? undefined;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
/* fall through to degrade */
|
|
235
|
+
}
|
|
236
|
+
if (typeof pane !== "string" || !pane)
|
|
237
|
+
return null;
|
|
238
|
+
// the split pane is a bare shell: give it a durable agent name (SKILL:197) and VERIFY the name
|
|
239
|
+
// resolves back to this pane (research A1 is checked live per join, never assumed) — then cd the
|
|
240
|
+
// shell into this member's own worktree (a split inherits its parent's cwd, not ours).
|
|
241
|
+
const rn = await this.herdr(`agent rename ${shq(pane)} ${shq(name)}`);
|
|
242
|
+
const verified = rn.code === 0 && (await this.paneId({ id: "", name, cwd })) === pane;
|
|
243
|
+
const cd = verified ? await this.herdr(`pane run ${shq(pane)} ${shq(`cd ${shq(cwd)}`)}`) : null;
|
|
244
|
+
if (!cd || cd.code !== 0) {
|
|
245
|
+
await this.herdr(`pane close ${shq(pane)}`); // reap the failed join, best-effort
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
// VIS-10 hole 3: seed the run's workspace id into the split pane's shell. A split pane is a bare
|
|
249
|
+
// shell with FRESH env from herdr (not the parent pane's exports), so without this a worker's
|
|
250
|
+
// ad-hoc herdr call from this pane would be untargeted. Fail closed: a failed seed reaps + degrades
|
|
251
|
+
// like a failed cd (return null → caller falls back to a per-slot tab). this.ws is guaranteed set —
|
|
252
|
+
// joinGroup runs only after the first member's tabSlot succeeded, which requires it (VIS-10).
|
|
253
|
+
// v1.22 T3: same env seal as tabSlot — strip control-plane vars after the workspace seed.
|
|
254
|
+
const seed = await this.herdr(`pane run ${shq(pane)} ${shq(`export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`);
|
|
255
|
+
if (seed.code !== 0) {
|
|
256
|
+
await this.herdr(`pane close ${shq(pane)}`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
entry.members = [...entry.members, { name, paneId: pane }];
|
|
260
|
+
await this.renameGroupTab(entry);
|
|
261
|
+
return { id: pane, name, cwd, tabId: entry.tabId, group };
|
|
262
|
+
}
|
|
263
|
+
async run(slot, cmd) {
|
|
264
|
+
const pane = await this.paneId(slot);
|
|
265
|
+
const r = await this.herdr(`pane run ${shq(pane)} ${shq(cmd)}`, slot.cwd);
|
|
266
|
+
if (r.code !== 0)
|
|
267
|
+
throw new Error(`herdr pane run failed: ${r.stderr || r.stdout}`);
|
|
268
|
+
}
|
|
269
|
+
async waitOutput(slot, pattern, timeoutMs, opts) {
|
|
270
|
+
const pane = await this.paneId(slot);
|
|
271
|
+
const r = await this.herdr(`wait output ${shq(pane)} --match ${shq(pattern)}${opts?.regex ? " --regex" : ""} --timeout ${Math.floor(timeoutMs)}`, slot.cwd, timeoutMs + 15_000);
|
|
272
|
+
return r.code === 0 && !r.stdout.includes('"error"'); // dead pane: exit 0 + error json (herdr bite)
|
|
273
|
+
}
|
|
274
|
+
async waitAgentStatus(slot, status, timeoutMs) {
|
|
275
|
+
const pane = await this.paneId(slot);
|
|
276
|
+
const r = await this.herdr(`wait agent-status ${shq(pane)} --status ${shq(status)} --timeout ${Math.floor(timeoutMs)}`, slot.cwd, timeoutMs + 15_000);
|
|
277
|
+
return r.code === 0 && !r.stdout.includes('"error"');
|
|
278
|
+
}
|
|
279
|
+
async status(slot) {
|
|
280
|
+
return this.statusByName(slot.name);
|
|
281
|
+
}
|
|
282
|
+
// shared by status() and the VIS-13 blocked glyph (renameGroupTab): resolve the live agent_status
|
|
283
|
+
// by durable name; "unknown" on any failure (dead pane, unparseable json) — never throws.
|
|
284
|
+
async statusByName(name) {
|
|
285
|
+
const r = await this.herdr(`agent get ${shq(name)}`);
|
|
286
|
+
try {
|
|
287
|
+
const s = JSON.parse(r.stdout).result?.agent?.agent_status;
|
|
288
|
+
return typeof s === "string" ? s : "unknown";
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return "unknown";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async read(slot, lines) {
|
|
295
|
+
const pane = await this.paneId(slot);
|
|
296
|
+
const r = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${lines}`, slot.cwd);
|
|
297
|
+
return r.stdout;
|
|
298
|
+
}
|
|
299
|
+
// v1.22 T5 / OBS-19: raw keystroke into the pane TUI (trust-dialog auto-answer). Resolves the pane
|
|
300
|
+
// id fresh like every other pane-addressed call (ids compact). Fail closed on nonzero herdr exit.
|
|
301
|
+
async sendKey(slot, key) {
|
|
302
|
+
const pane = await this.paneId(slot);
|
|
303
|
+
const r = await this.herdr(`pane send-keys ${shq(pane)} ${shq(key)}`, slot.cwd);
|
|
304
|
+
if (r.code !== 0)
|
|
305
|
+
throw new Error(`herdr pane send-keys failed: ${r.stderr || r.stdout}`);
|
|
306
|
+
}
|
|
307
|
+
async notify(msg, opts) {
|
|
308
|
+
if (opts?.tier === "routine")
|
|
309
|
+
return;
|
|
310
|
+
await this.herdr(`notification show ${shq(msg)} --sound ${opts?.tier === "attention" ? "request" : opts?.sound ?? "request"}`);
|
|
311
|
+
}
|
|
312
|
+
async close(slot) {
|
|
313
|
+
if (slot.group && this.groups.has(slot.group)) {
|
|
314
|
+
return this.serial(() => this.closeGrouped(slot));
|
|
315
|
+
}
|
|
316
|
+
if (slot.tabId) {
|
|
317
|
+
await this.herdr(`tab close ${shq(slot.tabId)}`); // reaps the slot's whole tab, best-effort
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const pane = await this.paneId(slot);
|
|
321
|
+
await this.herdr(`pane close ${shq(pane)}`); // best-effort
|
|
322
|
+
}
|
|
323
|
+
// D-08 ref-counted teardown, PER GENERATION (VIS-09 item 2): pane close per member; a generation's
|
|
324
|
+
// tab closes only when ITS OWN last member leaves; the group entry dies when all generations are gone.
|
|
325
|
+
// C2's "resolve the newest LIVE member" rule applies per generation — members[] holds only live members.
|
|
326
|
+
async closeGrouped(slot) {
|
|
327
|
+
const state = this.groups.get(slot.group);
|
|
328
|
+
if (!state)
|
|
329
|
+
return; // group already torn down — its tabs are gone
|
|
330
|
+
// find THIS member's generation by tab id: each overflow tab closes when its own last member leaves
|
|
331
|
+
const gen = state.generations.find((g) => g.tabId === slot.tabId);
|
|
332
|
+
if (!gen)
|
|
333
|
+
return; // generation already torn down — its tab is gone
|
|
334
|
+
const pane = await this.paneId(slot);
|
|
335
|
+
await this.herdr(`pane close ${shq(pane)}`); // best-effort
|
|
336
|
+
gen.members = gen.members.filter((m) => m.name !== slot.name);
|
|
337
|
+
await this.renameGroupTab(gen);
|
|
338
|
+
if (gen.members.length === 0) {
|
|
339
|
+
await this.herdr(`tab close ${shq(gen.tabId)}`); // refcount 0 → reap THIS generation's tab only
|
|
340
|
+
state.generations = state.generations.filter((g) => g.tabId !== gen.tabId);
|
|
341
|
+
if (state.generations.length === 0)
|
|
342
|
+
this.groups.delete(slot.group); // group dies when all generations gone
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// T6 narrator: the run's single live surface — a drovr-created "watch" tab running the daemon's
|
|
346
|
+
// status command. Reuses the dedicated-tab slot path (label → tabSlot); close() reaps the whole tab
|
|
347
|
+
// at run end. One per run (the daemon calls this once). Failures propagate — the daemon swallows.
|
|
348
|
+
// T2: with a runId the pane is named canonically (drovr:watch:run:0:<runId>) so reconcile owns it;
|
|
349
|
+
// the legacy `narrator-watch-<pid>` shape survives for runId-less callers (canonicalizeLegacyName).
|
|
350
|
+
async narrator(cwd, command, runId) {
|
|
351
|
+
const name = runId ? formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }) : `narrator-watch-${process.pid}`;
|
|
352
|
+
const s = await this.slot(cwd, name, { label: "watch" });
|
|
353
|
+
await this.run(s, command);
|
|
354
|
+
return s;
|
|
355
|
+
}
|
|
356
|
+
// OBS-17 T2 / v1.22b T1: close every drovr-owned pane that should not exist (superseded attempts,
|
|
357
|
+
// killed-daemon orphans, leftovers from OLDER runs) — in this run's workspace OR misplaced in any
|
|
358
|
+
// other one — then reap the tabs those closes emptied. Ownership is decided ONLY by parseOwnedName
|
|
359
|
+
// (drivers/types.ts panesToClose) — foreign names never become candidates, in any workspace; a pane
|
|
360
|
+
// this same run legitimately holds elsewhere is left alone (only run age marks a misplaced pane
|
|
361
|
+
// garbage). spareLiveLlm: same-run judge/review/consult panes have no journal row while live (their
|
|
362
|
+
// events land after the verdict is read), so mid-run sweeps spare them; boundary sweeps (start/
|
|
363
|
+
// resume/end) run with nothing in flight and take them too. Cosmetic by contract: every failure —
|
|
364
|
+
// herdr gone, pane vanished mid-sweep, unparseable listing — is swallowed; this method never throws.
|
|
365
|
+
async reconcile(desired, runId, opts) {
|
|
366
|
+
try {
|
|
367
|
+
if (!this.ws)
|
|
368
|
+
return;
|
|
369
|
+
const list = await this.herdr("agent list");
|
|
370
|
+
const agents = JSON.parse(list.stdout).result?.agents ?? [];
|
|
371
|
+
const toClose = panesToClose(agents.map((a) => ({ name: a.name, paneId: a.pane_id, tabId: a.tab_id, workspaceId: a.workspace_id })), desired, this.ws, runId, opts);
|
|
372
|
+
const touched = new Set();
|
|
373
|
+
for (const c of toClose) {
|
|
374
|
+
if (typeof c.tabId === "string")
|
|
375
|
+
touched.add(c.tabId);
|
|
376
|
+
await this.herdr(`pane close ${shq(c.paneId)}`); // best-effort — a vanished pane is already reconciled
|
|
377
|
+
}
|
|
378
|
+
if (touched.size === 0)
|
|
379
|
+
return;
|
|
380
|
+
// a tab our closes emptied was ours by construction (a tab with operator panes still has panes)
|
|
381
|
+
const pl = await this.herdr("pane list");
|
|
382
|
+
const alive = new Set((JSON.parse(pl.stdout).result?.panes ?? []).map((p) => p.tab_id));
|
|
383
|
+
for (const tab of touched)
|
|
384
|
+
if (!alive.has(tab))
|
|
385
|
+
await this.herdr(`tab close ${shq(tab)}`);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
/* cosmetic — visibility hygiene never fails the run */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
worktree(repo, branch, baseRef) {
|
|
392
|
+
return createWorktree(repo, branch, baseRef);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HerdrDriver } from "./herdr.js";
|
|
2
|
+
import { SubprocessDriver } from "./subprocess.js";
|
|
3
|
+
export function pickDriver(cfg, override) {
|
|
4
|
+
const want = override ?? cfg.driver;
|
|
5
|
+
// VIS-09 item 2: plumb the per-tab cap into the HerdrDriver — the driver takes it as a constructor
|
|
6
|
+
// param and never imports config (cfg is the only seam). Guaranteed present: DEFAULT_CONFIG seeds
|
|
7
|
+
// workersPerTab:3 and deepMerge overlays on top, so a missing overlay key still resolves.
|
|
8
|
+
if (want === "herdr")
|
|
9
|
+
return new HerdrDriver("herdr", cfg.visibility.workersPerTab);
|
|
10
|
+
if (want === "subprocess")
|
|
11
|
+
return new SubprocessDriver();
|
|
12
|
+
return HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab) : new SubprocessDriver();
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExecutorDriver, NotifyOpts, Slot } from "./types.js";
|
|
2
|
+
export declare const MAX_BUF: number;
|
|
3
|
+
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH"];
|
|
4
|
+
/** Copy of env with herdr control-plane vars stripped. Daemon process.env is left alone. */
|
|
5
|
+
export declare function sealHerdrEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
6
|
+
/** Shell prefix that unsets herdr control vars in a pane/login shell (herdr seed + daemon setup). */
|
|
7
|
+
export declare function herdrSealShellPrefix(): string;
|
|
8
|
+
export declare class SubprocessDriver implements ExecutorDriver {
|
|
9
|
+
id: string;
|
|
10
|
+
interactive: boolean;
|
|
11
|
+
private slots;
|
|
12
|
+
private n;
|
|
13
|
+
slot(cwd: string, name: string): Promise<Slot>;
|
|
14
|
+
private state;
|
|
15
|
+
run(slot: Slot, cmd: string): Promise<void>;
|
|
16
|
+
waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
|
|
17
|
+
regex?: boolean;
|
|
18
|
+
}): Promise<boolean>;
|
|
19
|
+
waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
|
|
20
|
+
status(_slot: Slot): Promise<string>;
|
|
21
|
+
read(slot: Slot, lines: number): Promise<string>;
|
|
22
|
+
notify(msg: string, _opts?: NotifyOpts): Promise<void>;
|
|
23
|
+
close(slot: Slot): Promise<void>;
|
|
24
|
+
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createWorktree } from "../run/git.js";
|
|
3
|
+
// HARD-03: cap retained worker output so a chatty worker can't grow drovr's heap unbounded.
|
|
4
|
+
// 2MB is safe against BOTH consumers: the largest read() call site asks for 1000 lines
|
|
5
|
+
// (daemon.ts:217/247), and every marker (DROVR_RESULT, DROVR_EXIT_<nonce>) is emitted at the
|
|
6
|
+
// END of output — a trailer would only be evicted if >2MB arrived AFTER it inside one 200ms
|
|
7
|
+
// waitOutput poll (~10MB/s sustained, far beyond agent-CLI rates). Tail-truncate, never head:
|
|
8
|
+
// consumers only ever tail-read.
|
|
9
|
+
export const MAX_BUF = 2 * 1024 * 1024;
|
|
10
|
+
// OBS-17 / v1.22 T3: control-plane vars that let a process talk to the operator's herdr.
|
|
11
|
+
// Workers, judges, reviewers, and consults must never inherit them — only the daemon process
|
|
12
|
+
// (and its own herdr driver CLI calls) keep the live session. Socket path is the wire; HERDR_ENV
|
|
13
|
+
// is the "I am inside herdr" gate every agent skill checks before mutating panes.
|
|
14
|
+
export const HERDR_CONTROL_VARS = ["HERDR_ENV", "HERDR_SOCKET_PATH"];
|
|
15
|
+
/** Copy of env with herdr control-plane vars stripped. Daemon process.env is left alone. */
|
|
16
|
+
export function sealHerdrEnv(env = process.env) {
|
|
17
|
+
const out = { ...env };
|
|
18
|
+
for (const k of HERDR_CONTROL_VARS)
|
|
19
|
+
delete out[k];
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** Shell prefix that unsets herdr control vars in a pane/login shell (herdr seed + daemon setup). */
|
|
23
|
+
export function herdrSealShellPrefix() {
|
|
24
|
+
return HERDR_CONTROL_VARS.map((k) => `unset ${k}`).join("; ") + "; ";
|
|
25
|
+
}
|
|
26
|
+
export class SubprocessDriver {
|
|
27
|
+
id = "subprocess";
|
|
28
|
+
interactive = false; // no pane, no operator — interactive workers force print here
|
|
29
|
+
// OBS-17 T2: no `reconcile` — there are no panes/tabs to sweep; the daemon's optional-chain
|
|
30
|
+
// call is a no-op here by construction, so the oracle suite runs byte-identical.
|
|
31
|
+
slots = new Map();
|
|
32
|
+
n = 0;
|
|
33
|
+
async slot(cwd, name) {
|
|
34
|
+
const id = `sp-${++this.n}`;
|
|
35
|
+
this.slots.set(id, { buf: "", exited: false });
|
|
36
|
+
return { id, name, cwd };
|
|
37
|
+
}
|
|
38
|
+
state(slot) {
|
|
39
|
+
const s = this.slots.get(slot.id);
|
|
40
|
+
if (!s)
|
|
41
|
+
throw new Error(`unknown slot ${slot.id}`);
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
async run(slot, cmd) {
|
|
45
|
+
const s = this.state(slot);
|
|
46
|
+
// HARD-05: interactive=false — no operator, so an open stdin pipe is a promise drovr can never
|
|
47
|
+
// keep; codex exec appends a piped stdin as a <stdin> block (`codex exec --help`) and blocks on a
|
|
48
|
+
// read that never EOFs. One spawn site covers every adapter (D-06).
|
|
49
|
+
// v1.22 T3: seal herdr control vars so worker/judge/review/consult children cannot reach the
|
|
50
|
+
// operator's herdr (OBS-17 watch-tab leak class). process.env of the daemon is untouched.
|
|
51
|
+
const p = spawn("bash", ["-lc", cmd], {
|
|
52
|
+
cwd: slot.cwd,
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
env: sealHerdrEnv(process.env),
|
|
55
|
+
});
|
|
56
|
+
s.proc = p;
|
|
57
|
+
p.stdout.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
|
|
58
|
+
p.stderr.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
|
|
59
|
+
p.on("close", () => (s.exited = true));
|
|
60
|
+
p.on("error", (e) => { s.buf = (s.buf + `\n[drovr subprocess error] ${e}\n`).slice(-MAX_BUF); s.exited = true; });
|
|
61
|
+
}
|
|
62
|
+
async waitOutput(slot, pattern, timeoutMs, opts) {
|
|
63
|
+
const s = this.state(slot);
|
|
64
|
+
const re = opts?.regex ? new RegExp(pattern) : null; // compile once, not per 200ms poll
|
|
65
|
+
const hit = re ? (b) => re.test(b) : (b) => b.includes(pattern);
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
if (hit(s.buf))
|
|
69
|
+
return true;
|
|
70
|
+
await new Promise((r) => setTimeout(r, 200)); // ponytail: 200ms poll; herdr driver has real event waits
|
|
71
|
+
}
|
|
72
|
+
return hit(s.buf);
|
|
73
|
+
}
|
|
74
|
+
async waitAgentStatus(slot, status, timeoutMs) {
|
|
75
|
+
if (status !== "done")
|
|
76
|
+
return false; // subprocess knows only process-exit
|
|
77
|
+
const s = this.state(slot);
|
|
78
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
if (s.exited)
|
|
81
|
+
return true;
|
|
82
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
83
|
+
}
|
|
84
|
+
return s.exited;
|
|
85
|
+
}
|
|
86
|
+
async status(_slot) {
|
|
87
|
+
return "unknown"; // no screen to scrape; never reports blocked
|
|
88
|
+
}
|
|
89
|
+
async read(slot, lines) {
|
|
90
|
+
return this.state(slot).buf.split("\n").slice(-lines).join("\n");
|
|
91
|
+
}
|
|
92
|
+
async notify(msg, _opts) {
|
|
93
|
+
if (_opts?.tier === "routine")
|
|
94
|
+
return;
|
|
95
|
+
console.log(`[drovr] ${msg}`);
|
|
96
|
+
}
|
|
97
|
+
async close(slot) {
|
|
98
|
+
const s = this.slots.get(slot.id);
|
|
99
|
+
if (s?.proc && !s.exited)
|
|
100
|
+
s.proc.kill("SIGKILL");
|
|
101
|
+
this.slots.delete(slot.id);
|
|
102
|
+
}
|
|
103
|
+
worktree(repo, branch, baseRef) {
|
|
104
|
+
return createWorktree(repo, branch, baseRef);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface Slot {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
tabId?: string;
|
|
6
|
+
group?: string;
|
|
7
|
+
}
|
|
8
|
+
export type NotifyTier = "routine" | "attention";
|
|
9
|
+
export interface NotifyOpts {
|
|
10
|
+
tier?: NotifyTier;
|
|
11
|
+
sound?: "none" | "done" | "request";
|
|
12
|
+
}
|
|
13
|
+
export interface SlotOpts {
|
|
14
|
+
group?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
owned?: OwnedName;
|
|
17
|
+
}
|
|
18
|
+
export declare const OWNED_ROLES: readonly ["worker", "judge", "review", "consult", "watch", "other"];
|
|
19
|
+
export type OwnedRole = (typeof OWNED_ROLES)[number];
|
|
20
|
+
export interface OwnedName {
|
|
21
|
+
role: OwnedRole;
|
|
22
|
+
taskId: string;
|
|
23
|
+
attempt: number;
|
|
24
|
+
runId: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function formatOwnedName(o: OwnedName): string;
|
|
27
|
+
export declare function parseOwnedName(name: string): OwnedName | null;
|
|
28
|
+
export declare function isForeignName(name: string): boolean;
|
|
29
|
+
export interface FleetAgent {
|
|
30
|
+
name?: string;
|
|
31
|
+
paneId?: string;
|
|
32
|
+
tabId?: string;
|
|
33
|
+
workspaceId?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function panesToClose(agents: FleetAgent[], desired: Set<string>, ws: string, runId: string, opts?: {
|
|
36
|
+
spareLiveLlm?: boolean;
|
|
37
|
+
}): {
|
|
38
|
+
paneId: string;
|
|
39
|
+
tabId?: string;
|
|
40
|
+
}[];
|
|
41
|
+
export declare function canonicalizeLegacyName(name: string, runId: string): OwnedName;
|
|
42
|
+
export interface ExecutorDriver {
|
|
43
|
+
id: string;
|
|
44
|
+
interactive: boolean;
|
|
45
|
+
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
46
|
+
run(slot: Slot, cmd: string): Promise<void>;
|
|
47
|
+
waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
|
|
48
|
+
regex?: boolean;
|
|
49
|
+
}): Promise<boolean>;
|
|
50
|
+
waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
|
|
51
|
+
status(slot: Slot): Promise<string>;
|
|
52
|
+
read(slot: Slot, lines: number): Promise<string>;
|
|
53
|
+
sendKey?(slot: Slot, key: string): Promise<void>;
|
|
54
|
+
notify(msg: string, opts?: NotifyOpts): Promise<void>;
|
|
55
|
+
close(slot: Slot): Promise<void>;
|
|
56
|
+
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
57
|
+
narrator?: (cwd: string, command: string, runId?: string) => Promise<Slot>;
|
|
58
|
+
reconcile?: (desired: Set<string>, runId: string, opts?: {
|
|
59
|
+
spareLiveLlm?: boolean;
|
|
60
|
+
}) => Promise<void>;
|
|
61
|
+
}
|