taskplane 0.28.7 → 0.29.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/bin/get-version.mjs +50 -0
- package/bin/taskplane.mjs +5 -8
- package/dashboard/public/app.js +7 -3
- package/dashboard/public/style.css +16 -13
- package/extensions/taskplane/agent-bridge-extension.ts +58 -3
- package/extensions/taskplane/agent-host.ts +11 -17
- package/extensions/taskplane/config-schema.ts +17 -12
- package/extensions/taskplane/engine-worker.ts +27 -0
- package/extensions/taskplane/engine.ts +78 -1
- package/extensions/taskplane/execution.ts +23 -1
- package/extensions/taskplane/extension.ts +303 -0
- package/extensions/taskplane/lane-runner.ts +74 -3
- package/extensions/taskplane/mailbox.ts +83 -0
- package/extensions/taskplane/messages.ts +19 -0
- package/extensions/taskplane/path-resolver.ts +63 -24
- package/extensions/taskplane/persistence.ts +378 -3
- package/extensions/taskplane/resume.ts +56 -6
- package/extensions/taskplane/tool-allowlist-constants.ts +37 -0
- package/extensions/taskplane/types.ts +34 -5
- package/extensions/taskplane/worktree.ts +101 -2
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +37 -0
- package/templates/agents/supervisor.md +62 -1
- package/templates/agents/task-reviewer.md +68 -0
- package/templates/agents/task-worker.md +58 -6
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `getVersion` — capture a CLI's version string with stdout-precedence,
|
|
3
|
+
* stderr-fallback, and null-on-failure semantics.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the inline `getVersion()` in `bin/taskplane.mjs` so it
|
|
6
|
+
* can be unit-tested without subprocessing the whole CLI.
|
|
7
|
+
*
|
|
8
|
+
* Behavior:
|
|
9
|
+
* - Spawns `${cmd} ${flag}` with shell:true and stdio:['ignore','pipe','pipe']
|
|
10
|
+
* - Returns null if `spawnSync` itself throws (e.g., command not found)
|
|
11
|
+
* - Returns null if the subprocess errored OR exited with non-zero status
|
|
12
|
+
* (matches the prior `execSync`-throws-on-failure contract)
|
|
13
|
+
* - On success, returns stdout if non-empty, else stderr (some CLIs
|
|
14
|
+
* notably `pi` print version output to stderr)
|
|
15
|
+
* - Returns null if both streams are empty
|
|
16
|
+
*
|
|
17
|
+
* @since TP-189-C (extracted) / TP-185 follow-up (original fix scope)
|
|
18
|
+
*
|
|
19
|
+
* @param {string} cmd — command name (or already-formed token sequence)
|
|
20
|
+
* @param {string} [flag="--version"] — flag appended to cmd
|
|
21
|
+
* @returns {string | null} trimmed version string, or null on any failure
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
export function getVersion(cmd, flag = "--version") {
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
// shell:true matches the prior execSync behavior — accepts a
|
|
30
|
+
// space-joined command string and resolves via PATH lookup.
|
|
31
|
+
result = spawnSync(`${cmd} ${flag}`, [], {
|
|
32
|
+
shell: true,
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// Match prior contract: any non-success path → null.
|
|
40
|
+
// `execSync` previously threw on either spawn failure or non-zero exit,
|
|
41
|
+
// and the caller's catch returned null. Replicate that here so a CLI
|
|
42
|
+
// that exits 1 with shell error text in stderr (e.g., "command not
|
|
43
|
+
// found") does NOT surface as a fake version string.
|
|
44
|
+
if (!result || result.error || result.status !== 0) return null;
|
|
45
|
+
const stdout = (result.stdout ?? "").toString().trim();
|
|
46
|
+
const stderr = (result.stderr ?? "").toString().trim();
|
|
47
|
+
if (stdout) return stdout;
|
|
48
|
+
if (stderr) return stderr;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
package/bin/taskplane.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
ALL_GITIGNORE_PATTERNS,
|
|
37
37
|
patternToRegex,
|
|
38
38
|
} from "./gitignore-patterns.mjs";
|
|
39
|
+
import { getVersion } from "./get-version.mjs";
|
|
39
40
|
|
|
40
41
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
41
42
|
|
|
@@ -127,14 +128,10 @@ function commandExists(cmd) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
} catch {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
131
|
+
// `getVersion` lives in `./get-version.mjs` so it can be unit-tested
|
|
132
|
+
// without subprocessing the full CLI. Imported above. (TP-189-C / TP-185
|
|
133
|
+
// follow-up: capture both stdout and stderr because `pi --version`
|
|
134
|
+
// prints to stderr; null on failure preserves the original contract.)
|
|
138
135
|
|
|
139
136
|
/**
|
|
140
137
|
* Parse the tabular output from `pi --list-models` into structured model rows.
|
package/dashboard/public/app.js
CHANGED
|
@@ -942,8 +942,11 @@ function renderLanesTasks(batch, sessions) {
|
|
|
942
942
|
? `<button class="viewer-eye-btn${isViewingStatus ? ' active' : ''}" onclick="viewStatusMd('${escapeHtml(task.taskId)}')" title="View STATUS.md">👁</button>`
|
|
943
943
|
: '';
|
|
944
944
|
|
|
945
|
-
// #485: Show task title
|
|
946
|
-
//
|
|
945
|
+
// #485 (revised): Show task title on a second grid row spanning task-id
|
|
946
|
+
// through progress (cols 3–6), giving ~5× more horizontal space than the
|
|
947
|
+
// original task-id-only width. Stops before col 7 (task-step + telemetry)
|
|
948
|
+
// so the step info and worker stats stay visible alongside the title.
|
|
949
|
+
// Falls back to no subtitle row when taskTitle is missing.
|
|
947
950
|
const titleHtml = task.taskTitle
|
|
948
951
|
? `<div class="task-title-subtitle">${escapeHtml(task.taskTitle)}</div>`
|
|
949
952
|
: "";
|
|
@@ -951,11 +954,12 @@ function renderLanesTasks(batch, sessions) {
|
|
|
951
954
|
<div class="task-row">
|
|
952
955
|
<span class="task-icon"><span class="status-dot ${task.status}"></span></span>
|
|
953
956
|
<span class="task-actions">${eyeHtml}</span>
|
|
954
|
-
<span class="task-id status-${task.status}"
|
|
957
|
+
<span class="task-id status-${task.status}">${escapeHtml(task.taskId)}${showRepos ? repoBadgeHtml(tRepo, "repo-badge-task") : ""}</span>
|
|
955
958
|
<span><span class="status-badge status-${task.status}"><span class="status-dot ${task.status}"></span> ${task.status}</span></span>
|
|
956
959
|
<span class="task-duration">${dur}</span>
|
|
957
960
|
<span>${progressHtml}</span>
|
|
958
961
|
<span class="task-step">${stepHtml}${workerHtml}</span>
|
|
962
|
+
${titleHtml}
|
|
959
963
|
</div>`;
|
|
960
964
|
html += reviewerRowHtml;
|
|
961
965
|
}
|
|
@@ -607,8 +607,11 @@ body {
|
|
|
607
607
|
.task-row {
|
|
608
608
|
display: grid;
|
|
609
609
|
grid-template-columns: 36px 24px 100px 90px 80px 200px 1fr;
|
|
610
|
+
/* #485 (revised): row 1 holds the primary cells; row 2 (auto, collapses to
|
|
611
|
+
* 0 when empty) holds the optional task-title-subtitle spanning cols 3–6. */
|
|
612
|
+
grid-template-rows: auto auto;
|
|
610
613
|
align-items: center;
|
|
611
|
-
gap: 8px;
|
|
614
|
+
gap: 8px 8px;
|
|
612
615
|
padding: 8px 14px;
|
|
613
616
|
border-bottom: 1px solid var(--border-subtle);
|
|
614
617
|
transition: background 0.15s;
|
|
@@ -625,20 +628,19 @@ body {
|
|
|
625
628
|
font-family: var(--font-mono);
|
|
626
629
|
font-weight: 600;
|
|
627
630
|
font-size: 0.85rem;
|
|
628
|
-
/* #485: task-id
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
min-width: 0; /* allow inner ellipsis to work */
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
.task-id-line {
|
|
636
|
-
/* The original TP-XXX line; preserves the previous single-line look */
|
|
637
|
-
white-space: nowrap;
|
|
631
|
+
/* #485 (revised): task-id is back to a single inline cell. The optional
|
|
632
|
+
* title-subtitle is now a separate grid item placed in row 2 — it spans
|
|
633
|
+
* cols 3–6 (task-id through progress) for ~5× more horizontal space than
|
|
634
|
+
* the previous nested-subtitle approach. */
|
|
638
635
|
}
|
|
639
636
|
|
|
640
637
|
.task-title-subtitle {
|
|
641
|
-
/* #485: human-readable task title
|
|
638
|
+
/* #485 (revised): human-readable task title placed on row 2 of the task-row
|
|
639
|
+
* grid, spanning cols 3–6 (task-id, status, duration, progress) and
|
|
640
|
+
* stopping before col 7 (task-step + telemetry). Auto row 2 collapses to
|
|
641
|
+
* 0 height when this element is absent (no title for the task). */
|
|
642
|
+
grid-column: 3 / 7;
|
|
643
|
+
grid-row: 2;
|
|
642
644
|
font-family: var(--font-sans, inherit);
|
|
643
645
|
font-weight: 400;
|
|
644
646
|
font-size: 0.72rem;
|
|
@@ -646,7 +648,8 @@ body {
|
|
|
646
648
|
white-space: nowrap;
|
|
647
649
|
overflow: hidden;
|
|
648
650
|
text-overflow: ellipsis;
|
|
649
|
-
|
|
651
|
+
/* Pull subtitle visually closer to the task-id row above. */
|
|
652
|
+
margin-top: -2px;
|
|
650
653
|
}
|
|
651
654
|
|
|
652
655
|
.task-duration {
|
|
@@ -146,6 +146,12 @@ function writeSegmentExpansionRequest(request: SegmentExpansionRequest): string
|
|
|
146
146
|
* section. All-checkboxes-checked is also NOT a trigger — it is the normal
|
|
147
147
|
* pre-code-review state.
|
|
148
148
|
*
|
|
149
|
+
* Fenced code blocks (delimited by ``` or ~~~) inside a step's body are
|
|
150
|
+
* skipped during the scan (TP-189-A3). This avoids a false-positive
|
|
151
|
+
* refusal when a step documents the literal `**Status:** ✅ Complete`
|
|
152
|
+
* pattern as part of its own instructions — a legitimate authoring case
|
|
153
|
+
* that doesn't represent an actual completion claim.
|
|
154
|
+
*
|
|
149
155
|
* Designed to fail-open: any I/O error or a missing step heading returns
|
|
150
156
|
* `false` (the review proceeds). The prompt-side Recovery Recipe is the
|
|
151
157
|
* primary defense; this guard is a hard backstop, not a gatekeeper.
|
|
@@ -165,15 +171,64 @@ export function isStepMarkedComplete(statusPath: string, stepNum: number): boole
|
|
|
165
171
|
const lines = content.split(/\r?\n/);
|
|
166
172
|
const stepHeadingRe = new RegExp(`^###\\s+Step\\s+${stepNum}\\b`);
|
|
167
173
|
const nextStepHeadingRe = /^###\s+Step\s+\d+\b/;
|
|
168
|
-
|
|
174
|
+
// TP-189-A3: track fenced-code-block state per CommonMark semantics.
|
|
175
|
+
// A fence opens with 3+ backticks OR 3+ tildes optionally followed by
|
|
176
|
+
// an info string (e.g., ```javascript). A fence CLOSES only when a
|
|
177
|
+
// matching delimiter (same char, length >= opener length) is seen on
|
|
178
|
+
// a line by itself — the closer line MUST NOT contain trailing
|
|
179
|
+
// non-whitespace text. This distinction matters: ```javascript
|
|
180
|
+
// inside an open fence is content, not a closer; mistreating it as a
|
|
181
|
+
// closer would let `**Status:** ✅ Complete` later in the same code
|
|
182
|
+
// block trip the guard. Tracking opener char + length also avoids
|
|
183
|
+
// premature close on `~~~` inside a backtick fence (or vice versa).
|
|
184
|
+
const openerRe = /^\s*(`{3,}|~{3,})(.*)$/;
|
|
169
185
|
let inSection = false;
|
|
186
|
+
let fenceOpener: { char: string; length: number } | null = null;
|
|
170
187
|
for (const line of lines) {
|
|
171
188
|
if (!inSection) {
|
|
172
189
|
if (stepHeadingRe.test(line)) inSection = true;
|
|
173
190
|
continue;
|
|
174
191
|
}
|
|
175
|
-
//
|
|
176
|
-
|
|
192
|
+
// Step boundaries are recognized only OUTSIDE a fenced block.
|
|
193
|
+
// (A `### Step N:` line inside a code-fence sample is content,
|
|
194
|
+
// not a real heading.)
|
|
195
|
+
if (fenceOpener === null && nextStepHeadingRe.test(line)) break;
|
|
196
|
+
// Detect fence delimiter lines.
|
|
197
|
+
const fenceMatch = line.match(openerRe);
|
|
198
|
+
if (fenceMatch) {
|
|
199
|
+
const delim = fenceMatch[1];
|
|
200
|
+
const trailing = fenceMatch[2] ?? "";
|
|
201
|
+
const char = delim[0]; // "`" or "~"
|
|
202
|
+
const length = delim.length;
|
|
203
|
+
if (fenceOpener === null) {
|
|
204
|
+
// Opening: any trailing text is the info string — allowed.
|
|
205
|
+
// CommonMark forbids backticks in a backtick info string,
|
|
206
|
+
// but rejecting that case here only risks false negatives
|
|
207
|
+
// (i.e., not opening a fence we should have); the worst-
|
|
208
|
+
// case impact is a real Status line being inspected as if
|
|
209
|
+
// outside a fence — which is the safe default.
|
|
210
|
+
fenceOpener = { char, length };
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Already inside a fence — a line counts as a closer ONLY if:
|
|
214
|
+
// 1. delimiter char matches the opener,
|
|
215
|
+
// 2. delimiter length >= opener length,
|
|
216
|
+
// 3. nothing follows the delimiter except whitespace.
|
|
217
|
+
const trailingIsWhitespace = /^\s*$/.test(trailing);
|
|
218
|
+
if (
|
|
219
|
+
char === fenceOpener.char &&
|
|
220
|
+
length >= fenceOpener.length &&
|
|
221
|
+
trailingIsWhitespace
|
|
222
|
+
) {
|
|
223
|
+
fenceOpener = null;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Else: this line is content INSIDE the open fence (e.g.,
|
|
227
|
+
// ```javascript inside a 4-backtick fence, or a non-matching
|
|
228
|
+
// tilde delimiter). Fall through to the inFence skip below.
|
|
229
|
+
}
|
|
230
|
+
// Skip lines inside an open fenced code block.
|
|
231
|
+
if (fenceOpener !== null) continue;
|
|
177
232
|
// Match a literal status line within this step's section.
|
|
178
233
|
// Examples that should match:
|
|
179
234
|
// **Status:** ✅ Complete
|
|
@@ -87,23 +87,17 @@ export const ENGINE_BRIDGE_TOOLS = [
|
|
|
87
87
|
"request_segment_expansion",
|
|
88
88
|
] as const;
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* process, fs) into pure schema/type files. If you change the default
|
|
102
|
-
* here, update those copies too.
|
|
103
|
-
*
|
|
104
|
-
* @since TP-184
|
|
105
|
-
*/
|
|
106
|
-
export const DEFAULT_WORKER_USER_TOOLS = "read,write,edit,bash,grep,find,ls";
|
|
90
|
+
// TP-189 (Cluster B): `DEFAULT_WORKER_USER_TOOLS` now lives in the
|
|
91
|
+
// import-free `./tool-allowlist-constants.ts` module so that pure-data
|
|
92
|
+
// layers (`config-schema.ts`, `types.ts`) can import it without pulling
|
|
93
|
+
// agent-host's heavy `child_process`/`fs` imports into the schema/type
|
|
94
|
+
// graph. We re-export here so existing internal imports (e.g.,
|
|
95
|
+
// `execution.ts`, `worker-tools-allowlist.test.ts`) continue to work
|
|
96
|
+
// without churn.
|
|
97
|
+
//
|
|
98
|
+
// @since TP-184 (constant introduced) / TP-189 (moved to constants module)
|
|
99
|
+
export { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
|
|
100
|
+
import { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
|
|
107
101
|
|
|
108
102
|
/**
|
|
109
103
|
* Build the final worker `--tools` allowlist string by combining the
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
* @module config/schema
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
// TP-189 (Cluster B): single source of truth for the worker user-tools
|
|
41
|
+
// default literal. This is a deliberately import-free module so we can
|
|
42
|
+
// import it here without pulling `agent-host.ts`'s `child_process`/`fs`
|
|
43
|
+
// imports into the schema layer.
|
|
44
|
+
import { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
|
|
45
|
+
|
|
40
46
|
// ── Config Version ───────────────────────────────────────────────────
|
|
41
47
|
|
|
42
48
|
/**
|
|
@@ -594,14 +600,11 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
|
|
|
594
600
|
testing: { commands: {} },
|
|
595
601
|
standards: { docs: [], rules: [] },
|
|
596
602
|
standardsOverrides: {},
|
|
597
|
-
//
|
|
598
|
-
// `
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
// default, update both copies. Engine bridge tools are appended at the
|
|
603
|
-
// lane-runner spawn site by `buildWorkerToolsAllowlist()`, not here.
|
|
604
|
-
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "", excludeExtensions: [] },
|
|
603
|
+
// TP-189 (Cluster B): user-tools default sourced from
|
|
604
|
+
// `tool-allowlist-constants.ts` (single source of truth). Engine
|
|
605
|
+
// bridge tools are appended at the lane-runner spawn site by
|
|
606
|
+
// `buildWorkerToolsAllowlist()`, not here.
|
|
607
|
+
worker: { model: "", tools: DEFAULT_WORKER_USER_TOOLS, thinking: "", excludeExtensions: [] },
|
|
605
608
|
reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on", excludeExtensions: [] },
|
|
606
609
|
context: {
|
|
607
610
|
workerContextWindow: 0,
|
|
@@ -653,10 +656,12 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
|
|
|
653
656
|
},
|
|
654
657
|
merge: {
|
|
655
658
|
model: "",
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
|
|
659
|
+
// TP-189 (Cluster B): merge default mirrors the worker user-tools
|
|
660
|
+
// constant. The merge agent does NOT run through
|
|
661
|
+
// `buildWorkerToolsAllowlist()` (no bridge-tool needs), so this
|
|
662
|
+
// reference is purely for default-value parity — not a hard
|
|
663
|
+
// coupling to the worker allowlist plumbing.
|
|
664
|
+
tools: DEFAULT_WORKER_USER_TOOLS,
|
|
660
665
|
thinking: "off",
|
|
661
666
|
verify: [],
|
|
662
667
|
order: "fewest-files-first",
|
|
@@ -40,6 +40,17 @@ export type WorkerToMainMessage =
|
|
|
40
40
|
| { type: "monitor-update"; state: MonitorState }
|
|
41
41
|
| { type: "engine-event"; event: EngineEvent }
|
|
42
42
|
| { type: "supervisor-alert"; alert: SupervisorAlert }
|
|
43
|
+
/**
|
|
44
|
+
* TP-187 (#538): Lane has reached a terminal state. The supervisor process
|
|
45
|
+
* uses this to mark the lane terminated and filter any subsequently-arriving
|
|
46
|
+
* (zombie) alerts whose `context.laneNumber`/`context.agentId` matches.
|
|
47
|
+
*/
|
|
48
|
+
| { type: "lane-terminated"; info: import("./types.ts").LaneTerminatedInfo }
|
|
49
|
+
/**
|
|
50
|
+
* TP-187 (#538): Lane number has been re-allocated to a fresh task. The
|
|
51
|
+
* supervisor lifts the suppression so subsequent alerts pass through.
|
|
52
|
+
*/
|
|
53
|
+
| { type: "lane-respawned"; laneNumber: number; agentId: string; batchId: string }
|
|
43
54
|
| { type: "state-sync"; state: SerializedBatchState }
|
|
44
55
|
| { type: "complete"; state: SerializedBatchState }
|
|
45
56
|
| { type: "error"; message: string; stack?: string; source?: WorkerErrorSource };
|
|
@@ -325,6 +336,18 @@ if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "functi
|
|
|
325
336
|
send({ type: "supervisor-alert", alert });
|
|
326
337
|
};
|
|
327
338
|
|
|
339
|
+
// TP-187 (#538): Lane termination callback — forwards lane-terminated to
|
|
340
|
+
// the supervisor process so it can suppress in-flight zombie alerts.
|
|
341
|
+
const onLaneTerminated = (info: import("./types.ts").LaneTerminatedInfo) => {
|
|
342
|
+
send({ type: "lane-terminated", info });
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// TP-187 (#538): Lane respawn callback — forwards lane-respawned to
|
|
346
|
+
// the supervisor process so it can lift suppression for re-allocated lanes.
|
|
347
|
+
const onLaneRespawned = (laneNumber: number, agentId: string, batchId: string) => {
|
|
348
|
+
send({ type: "lane-respawned", laneNumber, agentId, batchId });
|
|
349
|
+
};
|
|
350
|
+
|
|
328
351
|
// ── Execute engine ───────────────────────────────────────────
|
|
329
352
|
const enginePromise = data.mode === "resume"
|
|
330
353
|
? resumeOrchBatch(
|
|
@@ -340,6 +363,8 @@ if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "functi
|
|
|
340
363
|
data.force ?? false,
|
|
341
364
|
onSupervisorAlert,
|
|
342
365
|
data.supervisorAutonomy ?? "autonomous",
|
|
366
|
+
onLaneTerminated,
|
|
367
|
+
onLaneRespawned,
|
|
343
368
|
)
|
|
344
369
|
: executeOrchBatch(
|
|
345
370
|
data.args ?? "",
|
|
@@ -355,6 +380,8 @@ if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "functi
|
|
|
355
380
|
onEngineEvent,
|
|
356
381
|
onSupervisorAlert,
|
|
357
382
|
data.supervisorAutonomy ?? "autonomous",
|
|
383
|
+
onLaneTerminated,
|
|
384
|
+
onLaneRespawned,
|
|
358
385
|
);
|
|
359
386
|
|
|
360
387
|
enginePromise
|
|
@@ -17,8 +17,9 @@ import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolic
|
|
|
17
17
|
import type { CleanupGateRepoFailure } from "./messages.ts";
|
|
18
18
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
19
19
|
import { resolveOperatorId } from "./naming.ts";
|
|
20
|
-
import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, loadBatchState, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
20
|
+
import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, loadBatchState, persistRuntimeState, saveBatchHistory, saveBatchMetaRuntimeArtifact, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
21
21
|
import { readRegistrySnapshot, isTerminalStatus, isProcessAlive as registryIsProcessAlive } from "./process-registry.ts";
|
|
22
|
+
import { drainAgentOutbox } from "./mailbox.ts";
|
|
22
23
|
import { buildBatchProgressSnapshot, buildEngineEventBase, buildSegmentId, buildSupervisorSegmentFrontierSnapshot, defaultResilienceState, FATAL_DISCOVERY_CODES, generateBatchId, TIER0_RETRYABLE_CLASSIFICATIONS, TIER0_RETRY_BUDGETS, tier0ScopeKey, tier0WaveScopeKey } from "./types.ts";
|
|
23
24
|
import type { AllocatedLane, AllocatedTask, BatchHistorySummary, BatchTaskSummary, BatchWaveSummary, DiscoveryResult, EngineEventCallback, EscalationContext, LaneExecutionResult, LaneTaskOutcome, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, ParsedTask, PersistedSegmentRecord, SegmentExpansionRequest, SupervisorAlert, SupervisorAlertCallback, TaskRunnerConfig, TaskSegmentPlan, TaskSegmentPlanMap, TaskSegmentNode, Tier0EscalationPattern, Tier0RecoveryPattern, TokenCounts, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
24
25
|
import { buildDependencyGraph, computeWaveAssignments, resolveBaseBranch, resolveRepoRoot, validateGraph } from "./waves.ts";
|
|
@@ -1781,6 +1782,8 @@ async function attemptStaleWorktreeRecovery(
|
|
|
1781
1782
|
onSupervisorAlert?: SupervisorAlertCallback,
|
|
1782
1783
|
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1783
1784
|
runnerConfig?: TaskRunnerConfig,
|
|
1785
|
+
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback,
|
|
1786
|
+
onLaneRespawned?: (laneNumber: number, agentId: string, batchId: string) => void,
|
|
1784
1787
|
): Promise<WaveExecutionResult | null> {
|
|
1785
1788
|
// Only attempt recovery for ALLOC_WORKTREE_FAILED
|
|
1786
1789
|
if (!waveResult.allocationError || waveResult.allocationError.code !== "ALLOC_WORKTREE_FAILED") {
|
|
@@ -1896,6 +1899,8 @@ async function attemptStaleWorktreeRecovery(
|
|
|
1896
1899
|
excludeExtensions: runnerConfig.worker.excludeExtensions ?? [],
|
|
1897
1900
|
} : undefined,
|
|
1898
1901
|
runnerConfig?.workerExcludeExtensions ?? [],
|
|
1902
|
+
onLaneTerminated,
|
|
1903
|
+
onLaneRespawned,
|
|
1899
1904
|
);
|
|
1900
1905
|
|
|
1901
1906
|
return retryResult;
|
|
@@ -1973,6 +1978,18 @@ export async function executeOrchBatch(
|
|
|
1973
1978
|
onEngineEvent?: EngineEventCallback | null,
|
|
1974
1979
|
onSupervisorAlert?: SupervisorAlertCallback | null,
|
|
1975
1980
|
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1981
|
+
/**
|
|
1982
|
+
* TP-187 (#538): Optional callback fired when a lane reaches a terminal
|
|
1983
|
+
* state. The supervisor process forwards this over IPC and uses it to
|
|
1984
|
+
* suppress zombie alerts queued for the now-dead lane.
|
|
1985
|
+
*/
|
|
1986
|
+
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback | null,
|
|
1987
|
+
/**
|
|
1988
|
+
* TP-187 (#538): Optional callback fired when a lane is freshly
|
|
1989
|
+
* (re-)allocated to a task. The supervisor process uses it to lift any
|
|
1990
|
+
* zombie-alert suppression carried over from a prior wave.
|
|
1991
|
+
*/
|
|
1992
|
+
onLaneRespawned?: ((laneNumber: number, agentId: string, batchId: string) => void) | null,
|
|
1976
1993
|
): Promise<void> {
|
|
1977
1994
|
const repoRoot = cwd;
|
|
1978
1995
|
// State files (.pi/batch-state.json, lane-state, etc.) belong in the workspace root,
|
|
@@ -1999,6 +2016,23 @@ export async function executeOrchBatch(
|
|
|
1999
2016
|
}
|
|
2000
2017
|
};
|
|
2001
2018
|
|
|
2019
|
+
// ── TP-187 (#538): Lane termination forwarding helper ──────
|
|
2020
|
+
// Forwards lane-terminated events through the same callback chain so the
|
|
2021
|
+
// supervisor process can suppress zombie alerts queued for a dead lane.
|
|
2022
|
+
const emitLaneTerminated = (info: import("./types.ts").LaneTerminatedInfo): void => {
|
|
2023
|
+
if (onLaneTerminated) {
|
|
2024
|
+
try {
|
|
2025
|
+
onLaneTerminated(info);
|
|
2026
|
+
} catch (err: unknown) {
|
|
2027
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2028
|
+
execLog("batch", batchState.batchId, `lane-terminated callback failed: ${msg}`, {
|
|
2029
|
+
laneNumber: info.laneNumber,
|
|
2030
|
+
reason: info.reason,
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2002
2036
|
// ── TP-040 R002: Terminal event emission helper ──────────────
|
|
2003
2037
|
// Routes all early-return and terminal paths through consistent event
|
|
2004
2038
|
// emission so external consumers always receive a deterministic terminal
|
|
@@ -2312,6 +2346,22 @@ export async function executeOrchBatch(
|
|
|
2312
2346
|
// ── TS-009: Persist state on batch start (after wave computation) ──
|
|
2313
2347
|
persistRuntimeState("batch-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot);
|
|
2314
2348
|
|
|
2349
|
+
// ── TP-187 (#539): Persist batch-meta runtime artifact ──────────────
|
|
2350
|
+
// Captures the wave plan and core scalars to a runtime-side file that
|
|
2351
|
+
// survives `orch_abort()` (which deletes `.pi/batch-state.json`). Used by
|
|
2352
|
+
// `orch_resume(force=true)` to deterministically reconstruct state when
|
|
2353
|
+
// the main batch-state file is gone. Best-effort write: failures log only.
|
|
2354
|
+
saveBatchMetaRuntimeArtifact(stateRoot, {
|
|
2355
|
+
schemaVersion: 1,
|
|
2356
|
+
batchId: batchState.batchId,
|
|
2357
|
+
wavePlan: wavePlan.map(wave => [...wave]),
|
|
2358
|
+
baseBranch: batchState.baseBranch,
|
|
2359
|
+
orchBranch: batchState.orchBranch,
|
|
2360
|
+
mode: workspaceConfig ? "workspace" : "repo",
|
|
2361
|
+
startedAt: batchState.startedAt,
|
|
2362
|
+
totalWaves: wavePlan.length,
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2315
2365
|
// ── TP-105: Runtime V2 backend selection ────────────────────
|
|
2316
2366
|
// Use Runtime V2 (no-TMUX lane-runner) when ALL conditions are met:
|
|
2317
2367
|
// 1. Exactly one task in the batch
|
|
@@ -2507,6 +2557,8 @@ export async function executeOrchBatch(
|
|
|
2507
2557
|
excludeExtensions: runnerConfig.worker.excludeExtensions ?? [],
|
|
2508
2558
|
} : undefined,
|
|
2509
2559
|
runnerConfig?.workerExcludeExtensions ?? [],
|
|
2560
|
+
emitLaneTerminated,
|
|
2561
|
+
onLaneRespawned ?? undefined,
|
|
2510
2562
|
);
|
|
2511
2563
|
|
|
2512
2564
|
// ── TP-039: Tier 0 — Stale worktree recovery ────────────
|
|
@@ -2530,6 +2582,8 @@ export async function executeOrchBatch(
|
|
|
2530
2582
|
emitAlert,
|
|
2531
2583
|
supervisorAutonomy,
|
|
2532
2584
|
runnerConfig,
|
|
2585
|
+
emitLaneTerminated,
|
|
2586
|
+
onLaneRespawned ?? undefined,
|
|
2533
2587
|
);
|
|
2534
2588
|
if (retryResult) {
|
|
2535
2589
|
const staleRecovered = !retryResult.allocationError;
|
|
@@ -3058,6 +3112,29 @@ export async function executeOrchBatch(
|
|
|
3058
3112
|
batchProgress: buildBatchProgressSnapshot(batchState),
|
|
3059
3113
|
},
|
|
3060
3114
|
});
|
|
3115
|
+
|
|
3116
|
+
// TP-187 (#538): Hard-fail termination — synchronously drain the
|
|
3117
|
+
// agent's outbox so stale escalations/replies don't get re-discovered
|
|
3118
|
+
// later, then emit lane-terminated so the supervisor process
|
|
3119
|
+
// suppresses any in-transit zombie alerts targeting this lane/agent.
|
|
3120
|
+
if (laneForTask) {
|
|
3121
|
+
const hardFailAgentId = outcome?.sessionName && outcome.sessionName.length > 0
|
|
3122
|
+
? outcome.sessionName
|
|
3123
|
+
: `${laneForTask.laneSessionId}-worker`;
|
|
3124
|
+
try {
|
|
3125
|
+
const drained = drainAgentOutbox(stateRoot, batchState.batchId, hardFailAgentId);
|
|
3126
|
+
if (drained > 0) {
|
|
3127
|
+
execLog("batch", batchState.batchId, `hard-fail outbox drain: ${drained} entr${drained === 1 ? "y" : "ies"} for ${hardFailAgentId}`);
|
|
3128
|
+
}
|
|
3129
|
+
} catch { /* best effort — do not block termination */ }
|
|
3130
|
+
emitLaneTerminated({
|
|
3131
|
+
laneNumber: laneForTask.laneNumber,
|
|
3132
|
+
agentId: hardFailAgentId,
|
|
3133
|
+
batchId: batchState.batchId,
|
|
3134
|
+
terminatedAt: Date.now(),
|
|
3135
|
+
reason: "hard-fail",
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3061
3138
|
}
|
|
3062
3139
|
|
|
3063
3140
|
// ── TS-009: Persist state after wave execution ──
|
|
@@ -1757,6 +1757,8 @@ export async function executeWave(
|
|
|
1757
1757
|
reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] },
|
|
1758
1758
|
workerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] } | null,
|
|
1759
1759
|
workerExcludeExtensions?: string[],
|
|
1760
|
+
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback,
|
|
1761
|
+
onLaneRespawned?: (laneNumber: number, agentId: string, batchId: string) => void,
|
|
1760
1762
|
): Promise<WaveExecutionResult> {
|
|
1761
1763
|
const startedAt = Date.now();
|
|
1762
1764
|
const policy = config.failure.on_task_failure;
|
|
@@ -1866,7 +1868,7 @@ export async function executeWave(
|
|
|
1866
1868
|
...buildWorkerEnv(workerConfig),
|
|
1867
1869
|
...buildReviewerEnv(reviewerConfig),
|
|
1868
1870
|
...buildWorkerExcludeEnv(workerExcludeExtensions),
|
|
1869
|
-
}, onSupervisorAlert),
|
|
1871
|
+
}, onSupervisorAlert, onLaneTerminated, onLaneRespawned),
|
|
1870
1872
|
);
|
|
1871
1873
|
|
|
1872
1874
|
// Start monitoring as a sibling async loop
|
|
@@ -2577,6 +2579,14 @@ export async function executeLaneV2(
|
|
|
2577
2579
|
isWorkspaceMode?: boolean,
|
|
2578
2580
|
extraEnvVars?: Record<string, string>,
|
|
2579
2581
|
onSupervisorAlert?: SupervisorAlertCallback,
|
|
2582
|
+
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback,
|
|
2583
|
+
/**
|
|
2584
|
+
* TP-187 (#538): Optional callback fired BEFORE the first task of this
|
|
2585
|
+
* lane begins. The supervisor process uses it to lift any zombie-alert
|
|
2586
|
+
* suppression that was applied when this lane number was previously
|
|
2587
|
+
* terminated (e.g., in a prior wave).
|
|
2588
|
+
*/
|
|
2589
|
+
onLaneRespawned?: (laneNumber: number, agentId: string, batchId: string) => void,
|
|
2580
2590
|
): Promise<LaneExecutionResult> {
|
|
2581
2591
|
const laneId = lane.laneId;
|
|
2582
2592
|
const laneStartTime = Date.now();
|
|
@@ -2618,6 +2628,17 @@ export async function executeLaneV2(
|
|
|
2618
2628
|
agentPrefix: agentIdPrefix,
|
|
2619
2629
|
});
|
|
2620
2630
|
|
|
2631
|
+
// TP-187 (#538): Lane is freshly starting — emit lane-respawned so any
|
|
2632
|
+
// zombie-alert suppression carried over from a prior wave's termination of
|
|
2633
|
+
// this lane number is lifted before new alerts begin to flow.
|
|
2634
|
+
if (onLaneRespawned) {
|
|
2635
|
+
try {
|
|
2636
|
+
onLaneRespawned(lane.laneNumber, buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"), batchId);
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
execLog(laneId, "LANE", `lane-respawned callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2621
2642
|
for (const task of lane.tasks) {
|
|
2622
2643
|
const taskSegmentId = task.task.activeSegmentId ?? null;
|
|
2623
2644
|
if (shouldSkipRemaining || pauseSignal.paused) {
|
|
@@ -2675,6 +2696,7 @@ export async function executeLaneV2(
|
|
|
2675
2696
|
warnPercent: 85,
|
|
2676
2697
|
killPercent: 95,
|
|
2677
2698
|
onSupervisorAlert,
|
|
2699
|
+
onLaneTerminated,
|
|
2678
2700
|
};
|
|
2679
2701
|
|
|
2680
2702
|
try {
|