taskplane 0.29.2 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/dashboard/public/app.js +124 -15
- package/dashboard/public/style.css +83 -2
- package/extensions/reviewer-extension.ts +17 -11
- package/extensions/taskplane/abort.ts +50 -18
- package/extensions/taskplane/agent-bridge-extension.ts +232 -105
- package/extensions/taskplane/agent-host.ts +224 -97
- package/extensions/taskplane/cleanup.ts +71 -42
- package/extensions/taskplane/config-loader.ts +142 -58
- package/extensions/taskplane/config-schema.ts +6 -13
- package/extensions/taskplane/config.ts +10 -2
- package/extensions/taskplane/diagnostic-reports.ts +59 -47
- package/extensions/taskplane/diagnostics.ts +13 -13
- package/extensions/taskplane/discovery.ts +78 -63
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +469 -207
- package/extensions/taskplane/extension.ts +1073 -598
- package/extensions/taskplane/formatting.ts +136 -124
- package/extensions/taskplane/git.ts +0 -2
- package/extensions/taskplane/lane-runner.ts +652 -319
- package/extensions/taskplane/mailbox.ts +57 -49
- package/extensions/taskplane/merge.ts +662 -383
- package/extensions/taskplane/messages.ts +109 -51
- package/extensions/taskplane/migrations.ts +1 -1
- package/extensions/taskplane/path-resolver.ts +8 -9
- package/extensions/taskplane/persistence.ts +425 -262
- package/extensions/taskplane/process-registry.ts +36 -7
- package/extensions/taskplane/quality-gate.ts +107 -55
- package/extensions/taskplane/resume.ts +832 -280
- package/extensions/taskplane/sessions.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +505 -164
- package/extensions/taskplane/sidecar-telemetry.ts +25 -10
- package/extensions/taskplane/supervisor.ts +477 -270
- package/extensions/taskplane/task-executor-core.ts +178 -53
- package/extensions/taskplane/types.ts +209 -108
- package/extensions/taskplane/verification.ts +27 -22
- package/extensions/taskplane/waves.ts +59 -43
- package/extensions/taskplane/workspace.ts +14 -12
- package/extensions/taskplane/worktree.ts +218 -196
- package/package.json +14 -2
|
@@ -2,16 +2,61 @@
|
|
|
2
2
|
* Lane execution, monitoring, wave execution loop
|
|
3
3
|
* @module orch/execution
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
readFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
statSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
copyFileSync,
|
|
13
|
+
} from "fs";
|
|
6
14
|
import { access as fsAccess, readFile as fsReadFile, stat as fsStat } from "fs/promises";
|
|
7
15
|
import { join, dirname, basename, resolve, relative, delimiter as pathDelimiter } from "path";
|
|
8
16
|
import { userInfo } from "os";
|
|
9
17
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
18
|
+
import {
|
|
19
|
+
DONE_GRACE_MS,
|
|
20
|
+
EXECUTION_POLL_INTERVAL_MS,
|
|
21
|
+
ExecutionError,
|
|
22
|
+
SESSION_SPAWN_RETRY_MAX,
|
|
23
|
+
} from "./types.ts";
|
|
24
|
+
import type {
|
|
25
|
+
AllocatedLane,
|
|
26
|
+
AllocatedTask,
|
|
27
|
+
DependencyGraph,
|
|
28
|
+
LaneExecutionResult,
|
|
29
|
+
LaneMonitorSnapshot,
|
|
30
|
+
LaneTaskOutcome,
|
|
31
|
+
LaneTaskStatus,
|
|
32
|
+
MonitorState,
|
|
33
|
+
MtimeTracker,
|
|
34
|
+
OrchestratorConfig,
|
|
35
|
+
ParsedTask,
|
|
36
|
+
TaskMonitorSnapshot,
|
|
37
|
+
WaveExecutionResult,
|
|
38
|
+
WorkspaceConfig,
|
|
39
|
+
ExecutionUnit,
|
|
40
|
+
PacketPaths,
|
|
41
|
+
RuntimeAgentId,
|
|
42
|
+
RuntimeAgentRole,
|
|
43
|
+
RuntimeLaneSnapshot,
|
|
44
|
+
RuntimeRegistry,
|
|
45
|
+
SupervisorAlertCallback,
|
|
46
|
+
} from "./types.ts";
|
|
12
47
|
import { resolvePacketPaths, buildRuntimeAgentId } from "./types.ts";
|
|
13
48
|
import type { TaskExitDiagnostic } from "./diagnostics.ts";
|
|
14
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
readRegistrySnapshot,
|
|
51
|
+
readLaneSnapshot,
|
|
52
|
+
isTerminalStatus,
|
|
53
|
+
isProcessAlive,
|
|
54
|
+
detectOrphans,
|
|
55
|
+
markOrphansCrashed,
|
|
56
|
+
buildRegistrySnapshot,
|
|
57
|
+
writeRegistrySnapshot,
|
|
58
|
+
writeLaneSnapshot,
|
|
59
|
+
} from "./process-registry.ts";
|
|
15
60
|
import { allocateLanes } from "./waves.ts";
|
|
16
61
|
import { resolveOperatorId } from "./naming.ts";
|
|
17
62
|
import { runGit, runGitWithEnv } from "./git.ts";
|
|
@@ -51,7 +96,15 @@ export function execLog(
|
|
|
51
96
|
laneId: string,
|
|
52
97
|
taskId: string,
|
|
53
98
|
message: string,
|
|
54
|
-
|
|
99
|
+
// TP-195: widened from `Record<string, string|number|boolean>` to
|
|
100
|
+
// `Record<string, unknown>` so callers can pass structured values
|
|
101
|
+
// (string[] arrays, repo objects, etc.) without TS errors. Runtime
|
|
102
|
+
// stringification via `${v}` is unchanged: primitives render as today,
|
|
103
|
+
// arrays render with comma separators (existing behavior), objects
|
|
104
|
+
// render as `[object Object]` (already today's behavior — see
|
|
105
|
+
// historic execLog calls in engine.ts/resume.ts that have always been
|
|
106
|
+
// passing structured payloads). No runtime change.
|
|
107
|
+
extra?: Record<string, unknown>,
|
|
55
108
|
): void {
|
|
56
109
|
const prefix = `[orch] ${laneId}/${taskId}`;
|
|
57
110
|
if (extra) {
|
|
@@ -74,7 +127,11 @@ export function execLog(
|
|
|
74
127
|
* @returns true if agent is alive
|
|
75
128
|
* @since TP-112
|
|
76
129
|
*/
|
|
77
|
-
export function isV2AgentAlive(
|
|
130
|
+
export function isV2AgentAlive(
|
|
131
|
+
agentIdOrSessionName: string,
|
|
132
|
+
_runtimeBackend?: RuntimeBackend,
|
|
133
|
+
laneNumber?: number,
|
|
134
|
+
): boolean {
|
|
78
135
|
// Read the registry from the global state root.
|
|
79
136
|
// Since this is a pure liveness check, we scan for matching agentId
|
|
80
137
|
// patterns: direct match, or lane-session + "-worker" suffix.
|
|
@@ -85,15 +142,24 @@ export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: R
|
|
|
85
142
|
if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) return true;
|
|
86
143
|
// Try worker suffix (monitor uses lane session name, registry uses agentId)
|
|
87
144
|
const workerManifest = agents[`${agentIdOrSessionName}-worker`];
|
|
88
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
workerManifest &&
|
|
147
|
+
!isTerminalStatus(workerManifest.status) &&
|
|
148
|
+
isProcessAlive(workerManifest.pid)
|
|
149
|
+
)
|
|
150
|
+
return true;
|
|
89
151
|
// TP-148: In workspace mode, laneSessionId includes repoId and uses a local
|
|
90
152
|
// lane number (e.g., "orch-henry-api-lane-1") while the V2 registry uses
|
|
91
153
|
// global lane numbers without repoId (e.g., "orch-henry-lane-3-worker").
|
|
92
154
|
// Fall back to scanning the registry by global lane number when provided.
|
|
93
155
|
if (laneNumber != null) {
|
|
94
156
|
for (const agent of Object.values(agents)) {
|
|
95
|
-
if (
|
|
96
|
-
|
|
157
|
+
if (
|
|
158
|
+
agent.laneNumber === laneNumber &&
|
|
159
|
+
agent.role === "worker" &&
|
|
160
|
+
!isTerminalStatus(agent.status) &&
|
|
161
|
+
isProcessAlive(agent.pid)
|
|
162
|
+
) {
|
|
97
163
|
return true;
|
|
98
164
|
}
|
|
99
165
|
}
|
|
@@ -102,14 +168,14 @@ export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: R
|
|
|
102
168
|
}
|
|
103
169
|
|
|
104
170
|
/** Cached registry for V2 liveness checks within a monitor cycle. @since TP-112 */
|
|
105
|
-
let _v2LivenessRegistryCache:
|
|
171
|
+
let _v2LivenessRegistryCache: RuntimeRegistry | null = null;
|
|
106
172
|
|
|
107
173
|
/**
|
|
108
174
|
* Set the V2 liveness registry cache for the current monitor cycle.
|
|
109
175
|
* Called at the start of each monitor poll to avoid re-reading the file per-task.
|
|
110
176
|
* @since TP-112
|
|
111
177
|
*/
|
|
112
|
-
export function setV2LivenessRegistryCache(registry:
|
|
178
|
+
export function setV2LivenessRegistryCache(registry: RuntimeRegistry | null): void {
|
|
113
179
|
_v2LivenessRegistryCache = registry;
|
|
114
180
|
}
|
|
115
181
|
|
|
@@ -125,11 +191,11 @@ export function killV2LaneAgents(
|
|
|
125
191
|
sessionName: string,
|
|
126
192
|
options?: { stateRoot?: string; batchId?: string; logContext?: string; laneNumber?: number },
|
|
127
193
|
): void {
|
|
128
|
-
const registry =
|
|
129
|
-
|
|
194
|
+
const registry =
|
|
195
|
+
_v2LivenessRegistryCache ??
|
|
196
|
+
(options?.stateRoot && options?.batchId
|
|
130
197
|
? readRegistrySnapshot(options.stateRoot, options.batchId)
|
|
131
|
-
: null
|
|
132
|
-
);
|
|
198
|
+
: null);
|
|
133
199
|
if (!registry) return;
|
|
134
200
|
|
|
135
201
|
const agents = registry.agents;
|
|
@@ -138,25 +204,38 @@ export function killV2LaneAgents(
|
|
|
138
204
|
for (const suffix of ["-worker", "-reviewer", ""]) {
|
|
139
205
|
const key = `${sessionName}${suffix}`;
|
|
140
206
|
const manifest = agents[key];
|
|
141
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
manifest &&
|
|
209
|
+
!isTerminalStatus(manifest.status) &&
|
|
210
|
+
isProcessAlive(manifest.pid) &&
|
|
211
|
+
!killedPids.has(manifest.pid)
|
|
212
|
+
) {
|
|
142
213
|
try {
|
|
143
214
|
process.kill(manifest.pid, "SIGTERM");
|
|
144
215
|
killedPids.add(manifest.pid);
|
|
145
216
|
execLog(logContext, key, `killed V2 agent (PID ${manifest.pid})`);
|
|
146
|
-
} catch {
|
|
217
|
+
} catch {
|
|
218
|
+
/* already dead */
|
|
219
|
+
}
|
|
147
220
|
}
|
|
148
221
|
}
|
|
149
222
|
// TP-148: Workspace-mode fallback — match by global lane number when
|
|
150
223
|
// session name lookup misses (repoId/local-vs-global lane mismatch).
|
|
151
224
|
if (options?.laneNumber != null) {
|
|
152
225
|
for (const agent of Object.values(agents)) {
|
|
153
|
-
if (
|
|
154
|
-
|
|
226
|
+
if (
|
|
227
|
+
agent.laneNumber === options.laneNumber &&
|
|
228
|
+
!isTerminalStatus(agent.status) &&
|
|
229
|
+
isProcessAlive(agent.pid) &&
|
|
230
|
+
!killedPids.has(agent.pid)
|
|
231
|
+
) {
|
|
155
232
|
try {
|
|
156
233
|
process.kill(agent.pid, "SIGTERM");
|
|
157
234
|
killedPids.add(agent.pid);
|
|
158
235
|
execLog(logContext, agent.agentId, `killed V2 agent by lane number (PID ${agent.pid})`);
|
|
159
|
-
} catch {
|
|
236
|
+
} catch {
|
|
237
|
+
/* already dead */
|
|
238
|
+
}
|
|
160
239
|
}
|
|
161
240
|
}
|
|
162
241
|
}
|
|
@@ -164,7 +243,6 @@ export function killV2LaneAgents(
|
|
|
164
243
|
|
|
165
244
|
// ── Async File/Status Helpers (TP-070) ───────────────────────────────
|
|
166
245
|
|
|
167
|
-
|
|
168
246
|
/**
|
|
169
247
|
* Async version of readTaskStatusTail — reads STATUS.md tail without
|
|
170
248
|
* blocking the event loop.
|
|
@@ -215,10 +293,7 @@ function laneSessionIdOf(lane: Pick<AllocatedLane, "laneSessionId">): string {
|
|
|
215
293
|
* Logs are written under the lane worktree to keep per-lane execution
|
|
216
294
|
* artifacts colocated with task state and available after failures.
|
|
217
295
|
*/
|
|
218
|
-
export function resolveLaneLogPath(
|
|
219
|
-
lane: AllocatedLane,
|
|
220
|
-
task: AllocatedTask,
|
|
221
|
-
): string {
|
|
296
|
+
export function resolveLaneLogPath(lane: AllocatedLane, task: AllocatedTask): string {
|
|
222
297
|
return join(lane.worktreePath, ".pi", "orch-logs", `${laneSessionIdOf(lane)}-${task.taskId}.log`);
|
|
223
298
|
}
|
|
224
299
|
|
|
@@ -227,10 +302,7 @@ export function resolveLaneLogPath(
|
|
|
227
302
|
*
|
|
228
303
|
* Relative paths avoid Windows drive-letter parsing issues in shell redirection.
|
|
229
304
|
*/
|
|
230
|
-
export function resolveLaneLogRelativePath(
|
|
231
|
-
lane: AllocatedLane,
|
|
232
|
-
task: AllocatedTask,
|
|
233
|
-
): string {
|
|
305
|
+
export function resolveLaneLogRelativePath(lane: AllocatedLane, task: AllocatedTask): string {
|
|
234
306
|
return join(".pi", "orch-logs", `${laneSessionIdOf(lane)}-${task.taskId}.log`).replace(/\\/g, "/");
|
|
235
307
|
}
|
|
236
308
|
|
|
@@ -449,7 +521,6 @@ export function resolveTaskDonePath(
|
|
|
449
521
|
return resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode).donePath;
|
|
450
522
|
}
|
|
451
523
|
|
|
452
|
-
|
|
453
524
|
/*
|
|
454
525
|
* Removed in TP-120 while decommissioning the legacy session backend.
|
|
455
526
|
*
|
|
@@ -466,7 +537,11 @@ export async function pollUntilTaskComplete(
|
|
|
466
537
|
_pauseSignal: { paused: boolean },
|
|
467
538
|
_isWorkspaceMode?: boolean,
|
|
468
539
|
): Promise<{ status: LaneTaskStatus; exitReason: string; doneFileFound: boolean }> {
|
|
469
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
status: "failed",
|
|
542
|
+
exitReason: "Legacy pollUntilTaskComplete removed — use V2 lane-runner",
|
|
543
|
+
doneFileFound: false,
|
|
544
|
+
};
|
|
470
545
|
}
|
|
471
546
|
|
|
472
547
|
// ── Post-Task Commit ─────────────────────────────────────────────────
|
|
@@ -485,11 +560,7 @@ export async function pollUntilTaskComplete(
|
|
|
485
560
|
* @param task - The task that just completed
|
|
486
561
|
* @param laneId - Lane identifier for logging
|
|
487
562
|
*/
|
|
488
|
-
function commitTaskArtifacts(
|
|
489
|
-
lane: AllocatedLane,
|
|
490
|
-
task: AllocatedTask,
|
|
491
|
-
laneId: string,
|
|
492
|
-
): void {
|
|
563
|
+
function commitTaskArtifacts(lane: AllocatedLane, task: AllocatedTask, laneId: string): void {
|
|
493
564
|
const worktreePath = lane.worktreePath;
|
|
494
565
|
|
|
495
566
|
// Check if there are any uncommitted changes in the worktree
|
|
@@ -502,7 +573,11 @@ function commitTaskArtifacts(
|
|
|
502
573
|
// Stage all changes in the worktree
|
|
503
574
|
const addResult = runGit(["add", "-A"], worktreePath);
|
|
504
575
|
if (!addResult.ok) {
|
|
505
|
-
execLog(
|
|
576
|
+
execLog(
|
|
577
|
+
laneId,
|
|
578
|
+
task.taskId,
|
|
579
|
+
`post-task stage failed (non-fatal): ${addResult.stderr.slice(0, 200)}`,
|
|
580
|
+
);
|
|
506
581
|
return;
|
|
507
582
|
}
|
|
508
583
|
|
|
@@ -514,7 +589,11 @@ function commitTaskArtifacts(
|
|
|
514
589
|
if (!commitResult.ok) {
|
|
515
590
|
// "nothing to commit" is not an error — worker may have already committed
|
|
516
591
|
if (!commitResult.stderr.includes("nothing to commit")) {
|
|
517
|
-
execLog(
|
|
592
|
+
execLog(
|
|
593
|
+
laneId,
|
|
594
|
+
task.taskId,
|
|
595
|
+
`post-task commit failed (non-fatal): ${commitResult.stderr.slice(0, 200)}`,
|
|
596
|
+
);
|
|
518
597
|
}
|
|
519
598
|
return;
|
|
520
599
|
}
|
|
@@ -524,9 +603,6 @@ function commitTaskArtifacts(
|
|
|
524
603
|
});
|
|
525
604
|
}
|
|
526
605
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
606
|
// ── STATUS.md Parsing for Worktree ───────────────────────────────────
|
|
531
607
|
|
|
532
608
|
/**
|
|
@@ -583,7 +659,10 @@ export function parseWorktreeStatusMd(
|
|
|
583
659
|
content = readFileSync(statusPath, "utf-8");
|
|
584
660
|
mtime = statSync(statusPath).mtimeMs;
|
|
585
661
|
} catch (err: unknown) {
|
|
586
|
-
return {
|
|
662
|
+
return {
|
|
663
|
+
parsed: null,
|
|
664
|
+
error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}`,
|
|
665
|
+
};
|
|
587
666
|
}
|
|
588
667
|
|
|
589
668
|
// Parse using same regex patterns as task-runner's parseStatusMd
|
|
@@ -607,7 +686,7 @@ export function parseWorktreeStatusMd(
|
|
|
607
686
|
const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
|
|
608
687
|
if (stepMatch) {
|
|
609
688
|
if (currentStep) {
|
|
610
|
-
const totalChecked = currentStep.checkboxes.filter(c => c).length;
|
|
689
|
+
const totalChecked = currentStep.checkboxes.filter((c) => c).length;
|
|
611
690
|
steps.push({
|
|
612
691
|
number: currentStep.number,
|
|
613
692
|
name: currentStep.name,
|
|
@@ -641,7 +720,7 @@ export function parseWorktreeStatusMd(
|
|
|
641
720
|
}
|
|
642
721
|
}
|
|
643
722
|
if (currentStep) {
|
|
644
|
-
const totalChecked = currentStep.checkboxes.filter(c => c).length;
|
|
723
|
+
const totalChecked = currentStep.checkboxes.filter((c) => c).length;
|
|
645
724
|
steps.push({
|
|
646
725
|
number: currentStep.number,
|
|
647
726
|
name: currentStep.name,
|
|
@@ -708,7 +787,10 @@ async function parseStatusMdContent(
|
|
|
708
787
|
content = await fsReadFile(statusPath, "utf-8");
|
|
709
788
|
mtime = (await fsStat(statusPath)).mtimeMs;
|
|
710
789
|
} catch (err: unknown) {
|
|
711
|
-
return {
|
|
790
|
+
return {
|
|
791
|
+
parsed: null,
|
|
792
|
+
error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}`,
|
|
793
|
+
};
|
|
712
794
|
}
|
|
713
795
|
|
|
714
796
|
// Parse logic is identical to the sync version
|
|
@@ -732,7 +814,7 @@ async function parseStatusMdContent(
|
|
|
732
814
|
const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
|
|
733
815
|
if (stepMatch) {
|
|
734
816
|
if (currentStep) {
|
|
735
|
-
const totalChecked = currentStep.checkboxes.filter(c => c).length;
|
|
817
|
+
const totalChecked = currentStep.checkboxes.filter((c) => c).length;
|
|
736
818
|
steps.push({
|
|
737
819
|
number: currentStep.number,
|
|
738
820
|
name: currentStep.name,
|
|
@@ -766,7 +848,7 @@ async function parseStatusMdContent(
|
|
|
766
848
|
}
|
|
767
849
|
}
|
|
768
850
|
if (currentStep) {
|
|
769
|
-
const totalChecked = currentStep.checkboxes.filter(c => c).length;
|
|
851
|
+
const totalChecked = currentStep.checkboxes.filter((c) => c).length;
|
|
770
852
|
steps.push({
|
|
771
853
|
number: currentStep.number,
|
|
772
854
|
name: currentStep.name,
|
|
@@ -782,7 +864,6 @@ async function parseStatusMdContent(
|
|
|
782
864
|
};
|
|
783
865
|
}
|
|
784
866
|
|
|
785
|
-
|
|
786
867
|
// ── State Resolution ─────────────────────────────────────────────────
|
|
787
868
|
|
|
788
869
|
/**
|
|
@@ -804,6 +885,13 @@ async function parseStatusMdContent(
|
|
|
804
885
|
* @param tracker - Mtime tracker for stall detection
|
|
805
886
|
* @param stallTimeoutMs - Stall timeout in milliseconds
|
|
806
887
|
* @param now - Current timestamp (epoch ms) for deterministic testing
|
|
888
|
+
* @param multiSegmentContext - Optional segment-authority context (TP-196 / #462).
|
|
889
|
+
* When provided AND `isFinalSegment === false`,
|
|
890
|
+
* `.DONE` is treated as a non-authoritative signal
|
|
891
|
+
* (Priority 1 is skipped). This guards against a
|
|
892
|
+
* stale or premature `.DONE` from a non-final
|
|
893
|
+
* segment short-circuiting the task to succeeded
|
|
894
|
+
* before the remaining segments have run.
|
|
807
895
|
*/
|
|
808
896
|
export async function resolveTaskMonitorState(
|
|
809
897
|
taskId: string,
|
|
@@ -815,6 +903,7 @@ export async function resolveTaskMonitorState(
|
|
|
815
903
|
now: number,
|
|
816
904
|
runtimeBackend?: RuntimeBackend,
|
|
817
905
|
v2Context?: { stateRoot: string; batchId: string; laneNumber: number },
|
|
906
|
+
multiSegmentContext?: { isFinalSegment: boolean; segmentId: string },
|
|
818
907
|
): Promise<TaskMonitorSnapshot> {
|
|
819
908
|
// TP-115/TP-127: Backend-aware liveness check.
|
|
820
909
|
// V2: read the lane snapshot file written by lane-runner every second.
|
|
@@ -829,7 +918,7 @@ export async function resolveTaskMonitorState(
|
|
|
829
918
|
// Snapshot not written yet OR snapshot still points to a prior task.
|
|
830
919
|
// Assume alive initially, but if stale for >30s consult the registry
|
|
831
920
|
// to avoid indefinite false "running" if the lane-runner died.
|
|
832
|
-
const staleMs = snap?.updatedAt ?
|
|
921
|
+
const staleMs = snap?.updatedAt ? now - snap.updatedAt : 0;
|
|
833
922
|
const trackerAgeMs = now - tracker.firstObservedAt;
|
|
834
923
|
if (staleMs > 30_000) {
|
|
835
924
|
// Snapshot hasn't been updated for 30s+ — check registry as fallback.
|
|
@@ -875,7 +964,7 @@ export async function resolveTaskMonitorState(
|
|
|
875
964
|
const trackerAgeMs = now - tracker.firstObservedAt;
|
|
876
965
|
if (
|
|
877
966
|
snap.updatedAt &&
|
|
878
|
-
|
|
967
|
+
now - snap.updatedAt > stallTimeoutMs / 2 &&
|
|
879
968
|
trackerAgeMs >= 60_000 &&
|
|
880
969
|
!isV2AgentAlive(sessionName, runtimeBackend, v2Context?.laneNumber)
|
|
881
970
|
) {
|
|
@@ -918,13 +1007,13 @@ export async function resolveTaskMonitorState(
|
|
|
918
1007
|
}
|
|
919
1008
|
|
|
920
1009
|
// Find the current step (first in-progress, or first not-started after last complete)
|
|
921
|
-
const inProgress = steps.find(s => s.status === "in-progress");
|
|
1010
|
+
const inProgress = steps.find((s) => s.status === "in-progress");
|
|
922
1011
|
if (inProgress) {
|
|
923
1012
|
currentStepName = inProgress.name;
|
|
924
1013
|
currentStepNumber = inProgress.number;
|
|
925
1014
|
} else {
|
|
926
1015
|
// Find first not-started step
|
|
927
|
-
const notStarted = steps.find(s => s.status === "not-started");
|
|
1016
|
+
const notStarted = steps.find((s) => s.status === "not-started");
|
|
928
1017
|
if (notStarted) {
|
|
929
1018
|
currentStepName = notStarted.name;
|
|
930
1019
|
currentStepNumber = notStarted.number;
|
|
@@ -954,7 +1043,27 @@ export async function resolveTaskMonitorState(
|
|
|
954
1043
|
}
|
|
955
1044
|
|
|
956
1045
|
// ── Priority 1: .DONE file found → succeeded ────────────────
|
|
957
|
-
|
|
1046
|
+
// TP-196 / #462: Monitor guard for multi-segment tasks. When the caller
|
|
1047
|
+
// has provided a segment-authority context AND tells us the active segment
|
|
1048
|
+
// is NOT the final segment in the task plan, `.DONE` MUST NOT be accepted
|
|
1049
|
+
// as authoritative — a non-final segment's worker should never have
|
|
1050
|
+
// produced one. We log a WARN and fall through to the lower priorities
|
|
1051
|
+
// (which keep the task in a non-terminal state so the engine can recover).
|
|
1052
|
+
const doneAcceptedAsAuthority =
|
|
1053
|
+
doneFileFound && !(multiSegmentContext && multiSegmentContext.isFinalSegment === false);
|
|
1054
|
+
if (doneFileFound && !doneAcceptedAsAuthority) {
|
|
1055
|
+
execLog(
|
|
1056
|
+
"monitor",
|
|
1057
|
+
taskId,
|
|
1058
|
+
`WARN: .DONE present for non-final segment '${multiSegmentContext?.segmentId}' — ignoring (#462 guard)`,
|
|
1059
|
+
{
|
|
1060
|
+
session: sessionName,
|
|
1061
|
+
segmentId: multiSegmentContext?.segmentId,
|
|
1062
|
+
donePath,
|
|
1063
|
+
},
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (doneAcceptedAsAuthority) {
|
|
958
1067
|
return {
|
|
959
1068
|
taskId,
|
|
960
1069
|
status: "succeeded",
|
|
@@ -979,7 +1088,7 @@ export async function resolveTaskMonitorState(
|
|
|
979
1088
|
sessionAlive &&
|
|
980
1089
|
tracker.statusFileSeenOnce &&
|
|
981
1090
|
tracker.stallTimerStart !== null &&
|
|
982
|
-
|
|
1091
|
+
now - tracker.stallTimerStart >= stallTimeoutMs
|
|
983
1092
|
) {
|
|
984
1093
|
const stallMinutes = Math.round((now - tracker.stallTimerStart) / 60_000);
|
|
985
1094
|
const stallReason = `STATUS.md unchanged for ${stallMinutes} minutes (threshold: ${Math.round(stallTimeoutMs / 60_000)} min)`;
|
|
@@ -1052,7 +1161,6 @@ export async function resolveTaskMonitorState(
|
|
|
1052
1161
|
};
|
|
1053
1162
|
}
|
|
1054
1163
|
|
|
1055
|
-
|
|
1056
1164
|
// ── Core Monitor Loop ────────────────────────────────────────────────
|
|
1057
1165
|
|
|
1058
1166
|
/**
|
|
@@ -1136,10 +1244,15 @@ export async function monitorLanes(
|
|
|
1136
1244
|
// Build the total task count
|
|
1137
1245
|
const tasksTotal = lanes.reduce((sum, lane) => sum + lane.tasks.length, 0);
|
|
1138
1246
|
|
|
1139
|
-
execLog(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1247
|
+
execLog(
|
|
1248
|
+
"monitor",
|
|
1249
|
+
"ALL",
|
|
1250
|
+
`starting monitoring for ${lanes.length} lane(s), ${tasksTotal} task(s)`,
|
|
1251
|
+
{
|
|
1252
|
+
pollIntervalMs,
|
|
1253
|
+
stallTimeoutMin: Math.round(stallTimeoutMs / 60_000),
|
|
1254
|
+
},
|
|
1255
|
+
);
|
|
1143
1256
|
|
|
1144
1257
|
while (true) {
|
|
1145
1258
|
const now = Date.now();
|
|
@@ -1230,6 +1343,19 @@ export async function monitorLanes(
|
|
|
1230
1343
|
const statusPath = unit.packet.statusPath;
|
|
1231
1344
|
const statusResult = await parseStatusMdAtPath(statusPath);
|
|
1232
1345
|
|
|
1346
|
+
// TP-196 / #462: Build multi-segment authority context so
|
|
1347
|
+
// `.DONE` from a non-final segment is not accepted as terminal.
|
|
1348
|
+
const taskSegmentIds = task.task.segmentIds ?? [];
|
|
1349
|
+
const taskActiveSegmentId = task.task.activeSegmentId ?? null;
|
|
1350
|
+
let multiSegmentContext: { isFinalSegment: boolean; segmentId: string } | undefined;
|
|
1351
|
+
if (taskSegmentIds.length > 1 && taskActiveSegmentId) {
|
|
1352
|
+
const finalSegmentId = taskSegmentIds[taskSegmentIds.length - 1];
|
|
1353
|
+
multiSegmentContext = {
|
|
1354
|
+
isFinalSegment: taskActiveSegmentId === finalSegmentId,
|
|
1355
|
+
segmentId: taskActiveSegmentId,
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1233
1359
|
const snapshot = await resolveTaskMonitorState(
|
|
1234
1360
|
task.taskId,
|
|
1235
1361
|
donePath,
|
|
@@ -1239,17 +1365,24 @@ export async function monitorLanes(
|
|
|
1239
1365
|
stallTimeoutMs,
|
|
1240
1366
|
now,
|
|
1241
1367
|
runtimeBackend,
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1368
|
+
runtimeBackend === "v2" && batchId
|
|
1369
|
+
? {
|
|
1370
|
+
stateRoot: stateRootForRegistry ?? repoRoot,
|
|
1371
|
+
batchId,
|
|
1372
|
+
laneNumber: lane.laneNumber,
|
|
1373
|
+
}
|
|
1374
|
+
: undefined,
|
|
1375
|
+
multiSegmentContext,
|
|
1247
1376
|
);
|
|
1248
1377
|
|
|
1249
1378
|
currentTaskSnapshot = snapshot;
|
|
1250
1379
|
|
|
1251
1380
|
// Check if this task just became terminal
|
|
1252
|
-
if (
|
|
1381
|
+
if (
|
|
1382
|
+
snapshot.status === "succeeded" ||
|
|
1383
|
+
snapshot.status === "failed" ||
|
|
1384
|
+
snapshot.status === "stalled"
|
|
1385
|
+
) {
|
|
1253
1386
|
terminalTasks.set(task.taskId, snapshot);
|
|
1254
1387
|
if (snapshot.status === "succeeded") {
|
|
1255
1388
|
completedTasks.push(task.taskId);
|
|
@@ -1322,8 +1455,12 @@ export async function monitorLanes(
|
|
|
1322
1455
|
// Log summary only on state changes (lane completes or fails) — not every poll
|
|
1323
1456
|
const currentStateKey = `${totalDone}/${totalFailed}`;
|
|
1324
1457
|
if (currentStateKey !== lastMonitorStateKey) {
|
|
1325
|
-
const activeLanes = laneSnapshots.filter(l => l.currentTaskId !== null);
|
|
1326
|
-
execLog(
|
|
1458
|
+
const activeLanes = laneSnapshots.filter((l) => l.currentTaskId !== null);
|
|
1459
|
+
execLog(
|
|
1460
|
+
"monitor",
|
|
1461
|
+
"ALL",
|
|
1462
|
+
`poll #${pollCount}: ${totalDone}/${tasksTotal} done, ${totalFailed} failed, ${activeLanes.length} active lane(s)`,
|
|
1463
|
+
);
|
|
1327
1464
|
lastMonitorStateKey = currentStateKey;
|
|
1328
1465
|
}
|
|
1329
1466
|
|
|
@@ -1340,12 +1477,12 @@ export async function monitorLanes(
|
|
|
1340
1477
|
}
|
|
1341
1478
|
|
|
1342
1479
|
// Wait for next poll cycle
|
|
1343
|
-
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
1480
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1344
1481
|
}
|
|
1345
1482
|
|
|
1346
1483
|
// Reached here due to pause signal — return current state
|
|
1347
1484
|
const now = Date.now();
|
|
1348
|
-
const laneSnapshots: LaneMonitorSnapshot[] = lanes.map(lane => ({
|
|
1485
|
+
const laneSnapshots: LaneMonitorSnapshot[] = lanes.map((lane) => ({
|
|
1349
1486
|
laneId: lane.laneId,
|
|
1350
1487
|
laneNumber: lane.laneNumber,
|
|
1351
1488
|
sessionName: laneSessionIdOf(lane),
|
|
@@ -1354,7 +1491,7 @@ export async function monitorLanes(
|
|
|
1354
1491
|
currentTaskSnapshot: null,
|
|
1355
1492
|
completedTasks: [],
|
|
1356
1493
|
failedTasks: [],
|
|
1357
|
-
remainingTasks: lane.tasks.map(t => t.taskId),
|
|
1494
|
+
remainingTasks: lane.tasks.map((t) => t.taskId),
|
|
1358
1495
|
}));
|
|
1359
1496
|
|
|
1360
1497
|
setV2LivenessRegistryCache(null);
|
|
@@ -1370,7 +1507,6 @@ export async function monitorLanes(
|
|
|
1370
1507
|
};
|
|
1371
1508
|
}
|
|
1372
1509
|
|
|
1373
|
-
|
|
1374
1510
|
// ── Transitive Dependent Computation ─────────────────────────────────
|
|
1375
1511
|
|
|
1376
1512
|
/**
|
|
@@ -1414,7 +1550,6 @@ export function computeTransitiveDependents(
|
|
|
1414
1550
|
return blocked;
|
|
1415
1551
|
}
|
|
1416
1552
|
|
|
1417
|
-
|
|
1418
1553
|
// ── Pre-flight: Commit Untracked Task Files ─────────────────────────
|
|
1419
1554
|
|
|
1420
1555
|
/**
|
|
@@ -1499,29 +1634,31 @@ export function ensureTaskFilesCommitted(
|
|
|
1499
1634
|
|
|
1500
1635
|
try {
|
|
1501
1636
|
// Read orch branch tree into temporary index
|
|
1502
|
-
const readTreeRes = runGitWithEnv(
|
|
1503
|
-
["read-tree", orchTip],
|
|
1504
|
-
repoRoot,
|
|
1505
|
-
{ GIT_INDEX_FILE: tmpIdx },
|
|
1506
|
-
);
|
|
1637
|
+
const readTreeRes = runGitWithEnv(["read-tree", orchTip], repoRoot, { GIT_INDEX_FILE: tmpIdx });
|
|
1507
1638
|
if (!readTreeRes.ok) {
|
|
1508
|
-
execLog(
|
|
1509
|
-
|
|
1510
|
-
|
|
1639
|
+
execLog(
|
|
1640
|
+
"wave",
|
|
1641
|
+
`W${waveIndex}`,
|
|
1642
|
+
`orch branch staging: read-tree failed, falling back to HEAD commit`,
|
|
1643
|
+
{
|
|
1644
|
+
error: readTreeRes.stderr,
|
|
1645
|
+
},
|
|
1646
|
+
);
|
|
1511
1647
|
// Fall through to legacy path
|
|
1512
1648
|
} else {
|
|
1513
1649
|
// Add task files to temporary index
|
|
1514
1650
|
let addFailed = false;
|
|
1515
1651
|
for (const folder of foldersToStage) {
|
|
1516
|
-
const addRes = runGitWithEnv(
|
|
1517
|
-
["add", "--", folder],
|
|
1518
|
-
repoRoot,
|
|
1519
|
-
{ GIT_INDEX_FILE: tmpIdx },
|
|
1520
|
-
);
|
|
1652
|
+
const addRes = runGitWithEnv(["add", "--", folder], repoRoot, { GIT_INDEX_FILE: tmpIdx });
|
|
1521
1653
|
if (!addRes.ok) {
|
|
1522
|
-
execLog(
|
|
1523
|
-
|
|
1524
|
-
|
|
1654
|
+
execLog(
|
|
1655
|
+
"wave",
|
|
1656
|
+
`W${waveIndex}`,
|
|
1657
|
+
`orch branch staging: git add failed for ${folder}, falling back`,
|
|
1658
|
+
{
|
|
1659
|
+
error: addRes.stderr,
|
|
1660
|
+
},
|
|
1661
|
+
);
|
|
1525
1662
|
addFailed = true;
|
|
1526
1663
|
break;
|
|
1527
1664
|
}
|
|
@@ -1529,15 +1666,11 @@ export function ensureTaskFilesCommitted(
|
|
|
1529
1666
|
|
|
1530
1667
|
if (!addFailed) {
|
|
1531
1668
|
// Write tree from temporary index
|
|
1532
|
-
const writeTreeRes = runGitWithEnv(
|
|
1533
|
-
["write-tree"],
|
|
1534
|
-
repoRoot,
|
|
1535
|
-
{ GIT_INDEX_FILE: tmpIdx },
|
|
1536
|
-
);
|
|
1669
|
+
const writeTreeRes = runGitWithEnv(["write-tree"], repoRoot, { GIT_INDEX_FILE: tmpIdx });
|
|
1537
1670
|
|
|
1538
1671
|
if (writeTreeRes.ok) {
|
|
1539
1672
|
const tree = writeTreeRes.stdout.trim();
|
|
1540
|
-
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1673
|
+
const taskIds = foldersToStage.map((f) => f.split("/").pop() || f).join(", ");
|
|
1541
1674
|
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1542
1675
|
|
|
1543
1676
|
// Create commit directly on orch branch
|
|
@@ -1554,14 +1687,23 @@ export function ensureTaskFilesCommitted(
|
|
|
1554
1687
|
);
|
|
1555
1688
|
|
|
1556
1689
|
if (refUpdateRes.ok) {
|
|
1557
|
-
execLog(
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1690
|
+
execLog(
|
|
1691
|
+
"wave",
|
|
1692
|
+
`W${waveIndex}`,
|
|
1693
|
+
`committed ${foldersToStage.length} task folder(s) directly on orch branch`,
|
|
1694
|
+
{
|
|
1695
|
+
orchBranch,
|
|
1696
|
+
folders: foldersToStage,
|
|
1697
|
+
from: orchTip.slice(0, 8),
|
|
1698
|
+
to: newCommit.slice(0, 8),
|
|
1699
|
+
},
|
|
1700
|
+
);
|
|
1563
1701
|
// Clean up temp index and return — no need for legacy path
|
|
1564
|
-
try {
|
|
1702
|
+
try {
|
|
1703
|
+
unlinkSync(tmpIdx);
|
|
1704
|
+
} catch {
|
|
1705
|
+
/* best effort */
|
|
1706
|
+
}
|
|
1565
1707
|
return;
|
|
1566
1708
|
}
|
|
1567
1709
|
execLog("wave", `W${waveIndex}`, `orch branch staging: ref update failed, falling back`, {
|
|
@@ -1580,12 +1722,21 @@ export function ensureTaskFilesCommitted(
|
|
|
1580
1722
|
}
|
|
1581
1723
|
}
|
|
1582
1724
|
} catch (err: unknown) {
|
|
1583
|
-
execLog(
|
|
1584
|
-
|
|
1585
|
-
|
|
1725
|
+
execLog(
|
|
1726
|
+
"wave",
|
|
1727
|
+
`W${waveIndex}`,
|
|
1728
|
+
`orch branch staging: unexpected error, falling back to HEAD commit`,
|
|
1729
|
+
{
|
|
1730
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1731
|
+
},
|
|
1732
|
+
);
|
|
1586
1733
|
} finally {
|
|
1587
1734
|
// Always clean up temp index
|
|
1588
|
-
try {
|
|
1735
|
+
try {
|
|
1736
|
+
unlinkSync(tmpIdx);
|
|
1737
|
+
} catch {
|
|
1738
|
+
/* best effort */
|
|
1739
|
+
}
|
|
1589
1740
|
}
|
|
1590
1741
|
}
|
|
1591
1742
|
}
|
|
@@ -1609,7 +1760,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1609
1760
|
}
|
|
1610
1761
|
|
|
1611
1762
|
// Commit
|
|
1612
|
-
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1763
|
+
const taskIds = foldersToStage.map((f) => f.split("/").pop() || f).join(", ");
|
|
1613
1764
|
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1614
1765
|
const commitResult = runGit(["commit", "-m", commitMsg], repoRoot);
|
|
1615
1766
|
if (!commitResult.ok) {
|
|
@@ -1622,10 +1773,15 @@ export function ensureTaskFilesCommitted(
|
|
|
1622
1773
|
);
|
|
1623
1774
|
}
|
|
1624
1775
|
|
|
1625
|
-
execLog(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1776
|
+
execLog(
|
|
1777
|
+
"wave",
|
|
1778
|
+
`W${waveIndex}`,
|
|
1779
|
+
`committed ${foldersToStage.length} task folder(s) to ensure worktree visibility`,
|
|
1780
|
+
{
|
|
1781
|
+
folders: foldersToStage,
|
|
1782
|
+
commit: commitResult.stdout.trim().split("\n")[0],
|
|
1783
|
+
},
|
|
1784
|
+
);
|
|
1629
1785
|
|
|
1630
1786
|
// Fast-forward (or merge) the orch branch to include the staging commit so
|
|
1631
1787
|
// that worktrees—which branch from orchBranch—see the new task files and
|
|
@@ -1639,10 +1795,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1639
1795
|
const newHead = headRes.stdout.trim();
|
|
1640
1796
|
const orchTip = orchTipRes.stdout.trim();
|
|
1641
1797
|
|
|
1642
|
-
const ancestorCheck = runGit(
|
|
1643
|
-
["merge-base", "--is-ancestor", orchTip, newHead],
|
|
1644
|
-
repoRoot,
|
|
1645
|
-
);
|
|
1798
|
+
const ancestorCheck = runGit(["merge-base", "--is-ancestor", orchTip, newHead], repoRoot);
|
|
1646
1799
|
|
|
1647
1800
|
if (ancestorCheck.ok) {
|
|
1648
1801
|
const ffResult = runGit(
|
|
@@ -1662,10 +1815,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1662
1815
|
});
|
|
1663
1816
|
}
|
|
1664
1817
|
} else {
|
|
1665
|
-
const mergeTreeRes = runGit(
|
|
1666
|
-
["merge-tree", "--write-tree", orchTip, newHead],
|
|
1667
|
-
repoRoot,
|
|
1668
|
-
);
|
|
1818
|
+
const mergeTreeRes = runGit(["merge-tree", "--write-tree", orchTip, newHead], repoRoot);
|
|
1669
1819
|
if (mergeTreeRes.ok) {
|
|
1670
1820
|
const mergedTree = mergeTreeRes.stdout.trim().split("\n")[0];
|
|
1671
1821
|
if (/^[0-9a-f]{40}$/i.test(mergedTree)) {
|
|
@@ -1692,10 +1842,15 @@ export function ensureTaskFilesCommitted(
|
|
|
1692
1842
|
}
|
|
1693
1843
|
}
|
|
1694
1844
|
} catch (refErr: unknown) {
|
|
1695
|
-
execLog(
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1845
|
+
execLog(
|
|
1846
|
+
"wave",
|
|
1847
|
+
`W${waveIndex}`,
|
|
1848
|
+
`warning: orch branch ref update threw unexpectedly (non-fatal)`,
|
|
1849
|
+
{
|
|
1850
|
+
orchBranch,
|
|
1851
|
+
error: refErr instanceof Error ? refErr.message : String(refErr),
|
|
1852
|
+
},
|
|
1853
|
+
);
|
|
1699
1854
|
}
|
|
1700
1855
|
}
|
|
1701
1856
|
}
|
|
@@ -1768,8 +1923,18 @@ export async function executeWave(
|
|
|
1768
1923
|
runtimeBackend?: RuntimeBackend,
|
|
1769
1924
|
onSupervisorAlert?: SupervisorAlertCallback,
|
|
1770
1925
|
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1771
|
-
reviewerConfig?: {
|
|
1772
|
-
|
|
1926
|
+
reviewerConfig?: {
|
|
1927
|
+
model?: string;
|
|
1928
|
+
thinking?: string;
|
|
1929
|
+
tools?: string;
|
|
1930
|
+
excludeExtensions?: string[];
|
|
1931
|
+
},
|
|
1932
|
+
workerConfig?: {
|
|
1933
|
+
model?: string;
|
|
1934
|
+
thinking?: string;
|
|
1935
|
+
tools?: string;
|
|
1936
|
+
excludeExtensions?: string[];
|
|
1937
|
+
} | null,
|
|
1773
1938
|
workerExcludeExtensions?: string[],
|
|
1774
1939
|
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback,
|
|
1775
1940
|
onLaneRespawned?: (laneNumber: number, agentId: string, batchId: string) => void,
|
|
@@ -1814,7 +1979,15 @@ export async function executeWave(
|
|
|
1814
1979
|
}
|
|
1815
1980
|
|
|
1816
1981
|
// ── Stage 1: Allocate lanes ──────────────────────────────────
|
|
1817
|
-
const allocResult = allocateLanes(
|
|
1982
|
+
const allocResult = allocateLanes(
|
|
1983
|
+
waveTasks,
|
|
1984
|
+
pending,
|
|
1985
|
+
config,
|
|
1986
|
+
repoRoot,
|
|
1987
|
+
batchId,
|
|
1988
|
+
orchBranch,
|
|
1989
|
+
workspaceConfig,
|
|
1990
|
+
);
|
|
1818
1991
|
|
|
1819
1992
|
if (!allocResult.success) {
|
|
1820
1993
|
const errMsg = allocResult.error?.message || "Unknown allocation failure";
|
|
@@ -1859,7 +2032,11 @@ export async function executeWave(
|
|
|
1859
2032
|
const isWsMode = !!workspaceConfig;
|
|
1860
2033
|
const backend: RuntimeBackend = "v2";
|
|
1861
2034
|
if (runtimeBackend && runtimeBackend !== "v2") {
|
|
1862
|
-
execLog(
|
|
2035
|
+
execLog(
|
|
2036
|
+
"wave",
|
|
2037
|
+
`W${waveIndex}`,
|
|
2038
|
+
`legacy runtime backend '${runtimeBackend}' requested but ignored; using Runtime V2`,
|
|
2039
|
+
);
|
|
1863
2040
|
}
|
|
1864
2041
|
execLog("wave", `W${waveIndex}`, "using Runtime V2 backend (executeLaneV2)");
|
|
1865
2042
|
|
|
@@ -1870,19 +2047,39 @@ export async function executeWave(
|
|
|
1870
2047
|
const snapshotStateRoot = resolveRuntimeStateRoot(repoRoot, wsRoot);
|
|
1871
2048
|
for (const lane of lanes) {
|
|
1872
2049
|
try {
|
|
1873
|
-
const snapPath = join(
|
|
2050
|
+
const snapPath = join(
|
|
2051
|
+
snapshotStateRoot,
|
|
2052
|
+
".pi",
|
|
2053
|
+
"runtime",
|
|
2054
|
+
batchId,
|
|
2055
|
+
"lanes",
|
|
2056
|
+
`lane-${lane.laneNumber}.json`,
|
|
2057
|
+
);
|
|
1874
2058
|
if (existsSync(snapPath)) unlinkSync(snapPath);
|
|
1875
|
-
} catch {
|
|
2059
|
+
} catch {
|
|
2060
|
+
/* best effort */
|
|
2061
|
+
}
|
|
1876
2062
|
}
|
|
1877
2063
|
|
|
1878
|
-
const lanePromises = lanes.map(lane =>
|
|
1879
|
-
executeLaneV2(
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2064
|
+
const lanePromises = lanes.map((lane) =>
|
|
2065
|
+
executeLaneV2(
|
|
2066
|
+
lane,
|
|
2067
|
+
config,
|
|
2068
|
+
repoRoot,
|
|
2069
|
+
wavePauseSignal,
|
|
2070
|
+
wsRoot,
|
|
2071
|
+
isWsMode,
|
|
2072
|
+
{
|
|
2073
|
+
ORCH_BATCH_ID: batchId,
|
|
2074
|
+
TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
|
|
2075
|
+
...buildWorkerEnv(workerConfig),
|
|
2076
|
+
...buildReviewerEnv(reviewerConfig),
|
|
2077
|
+
...buildWorkerExcludeEnv(workerExcludeExtensions),
|
|
2078
|
+
},
|
|
2079
|
+
onSupervisorAlert,
|
|
2080
|
+
onLaneTerminated,
|
|
2081
|
+
onLaneRespawned,
|
|
2082
|
+
),
|
|
1886
2083
|
);
|
|
1887
2084
|
|
|
1888
2085
|
// Start monitoring as a sibling async loop
|
|
@@ -1929,7 +2126,7 @@ export async function executeWave(
|
|
|
1929
2126
|
return {
|
|
1930
2127
|
laneNumber: lanes[idx].laneNumber,
|
|
1931
2128
|
laneId: lanes[idx].laneId,
|
|
1932
|
-
tasks: lanes[idx].tasks.map(t => ({
|
|
2129
|
+
tasks: lanes[idx].tasks.map((t) => ({
|
|
1933
2130
|
taskId: t.taskId,
|
|
1934
2131
|
status: "failed" as LaneTaskStatus,
|
|
1935
2132
|
startTime: null,
|
|
@@ -1947,8 +2144,8 @@ export async function executeWave(
|
|
|
1947
2144
|
|
|
1948
2145
|
// For stop-wave: if any task failed, set pause to prevent next wave
|
|
1949
2146
|
if (policy === "stop-wave") {
|
|
1950
|
-
const hasFailure = laneResults.some(lr =>
|
|
1951
|
-
lr.tasks.some(t => t.status === "failed" || t.status === "stalled"),
|
|
2147
|
+
const hasFailure = laneResults.some((lr) =>
|
|
2148
|
+
lr.tasks.some((t) => t.status === "failed" || t.status === "stalled"),
|
|
1952
2149
|
);
|
|
1953
2150
|
if (hasFailure) {
|
|
1954
2151
|
wavePauseSignal.paused = true;
|
|
@@ -1991,21 +2188,24 @@ export async function executeWave(
|
|
|
1991
2188
|
// Compute blocked tasks for future waves (skip-dependents policy)
|
|
1992
2189
|
let blockedTaskIds: string[] = [];
|
|
1993
2190
|
if (policy === "skip-dependents" && failedTaskIds.length > 0) {
|
|
1994
|
-
const blocked = computeTransitiveDependents(
|
|
1995
|
-
new Set(failedTaskIds),
|
|
1996
|
-
dependencyGraph,
|
|
1997
|
-
);
|
|
2191
|
+
const blocked = computeTransitiveDependents(new Set(failedTaskIds), dependencyGraph);
|
|
1998
2192
|
blockedTaskIds = [...blocked].sort();
|
|
1999
2193
|
if (blockedTaskIds.length > 0) {
|
|
2000
|
-
execLog(
|
|
2001
|
-
|
|
2002
|
-
|
|
2194
|
+
execLog(
|
|
2195
|
+
"wave",
|
|
2196
|
+
`W${waveIndex}`,
|
|
2197
|
+
`skip-dependents: ${blockedTaskIds.length} task(s) blocked for future waves`,
|
|
2198
|
+
{
|
|
2199
|
+
blocked: blockedTaskIds.join(","),
|
|
2200
|
+
},
|
|
2201
|
+
);
|
|
2003
2202
|
}
|
|
2004
2203
|
}
|
|
2005
2204
|
|
|
2006
2205
|
// Determine overall wave status
|
|
2007
|
-
const stoppedEarly =
|
|
2008
|
-
|
|
2206
|
+
const stoppedEarly =
|
|
2207
|
+
(policy === "stop-all" && failedTaskIds.length > 0) ||
|
|
2208
|
+
(policy === "stop-wave" && failedTaskIds.length > 0);
|
|
2009
2209
|
|
|
2010
2210
|
let overallStatus: WaveExecutionResult["overallStatus"];
|
|
2011
2211
|
if (policy === "stop-all" && failedTaskIds.length > 0) {
|
|
@@ -2085,9 +2285,7 @@ export async function executeWithStopAll(
|
|
|
2085
2285
|
|
|
2086
2286
|
// Check if any task failed
|
|
2087
2287
|
if (!abortTriggered) {
|
|
2088
|
-
const hasFailure = result.tasks.some(
|
|
2089
|
-
t => t.status === "failed" || t.status === "stalled",
|
|
2090
|
-
);
|
|
2288
|
+
const hasFailure = result.tasks.some((t) => t.status === "failed" || t.status === "stalled");
|
|
2091
2289
|
if (hasFailure) {
|
|
2092
2290
|
// First failure detected — trigger stop-all
|
|
2093
2291
|
abortTriggered = true;
|
|
@@ -2095,7 +2293,7 @@ export async function executeWithStopAll(
|
|
|
2095
2293
|
|
|
2096
2294
|
// Determine which task failed first for logging
|
|
2097
2295
|
const firstFailed = result.tasks
|
|
2098
|
-
.filter(t => t.status === "failed" || t.status === "stalled")
|
|
2296
|
+
.filter((t) => t.status === "failed" || t.status === "stalled")
|
|
2099
2297
|
.sort((a, b) => {
|
|
2100
2298
|
// Sort by startTime, then by taskId for deterministic tie-break
|
|
2101
2299
|
const timeA = a.startTime || 0;
|
|
@@ -2104,9 +2302,14 @@ export async function executeWithStopAll(
|
|
|
2104
2302
|
return a.taskId.localeCompare(b.taskId);
|
|
2105
2303
|
})[0];
|
|
2106
2304
|
|
|
2107
|
-
execLog(
|
|
2108
|
-
|
|
2109
|
-
|
|
2305
|
+
execLog(
|
|
2306
|
+
"wave",
|
|
2307
|
+
`W${waveIndex}`,
|
|
2308
|
+
`stop-all triggered by ${firstFailed?.taskId || "unknown"} in ${lanes[idx].laneId}`,
|
|
2309
|
+
{
|
|
2310
|
+
session: laneSessionIdOf(lanes[idx]),
|
|
2311
|
+
},
|
|
2312
|
+
);
|
|
2110
2313
|
|
|
2111
2314
|
// Kill ALL lane sessions immediately
|
|
2112
2315
|
for (const lane of lanes) {
|
|
@@ -2122,7 +2325,11 @@ export async function executeWithStopAll(
|
|
|
2122
2325
|
if (!abortTriggered) {
|
|
2123
2326
|
abortTriggered = true;
|
|
2124
2327
|
pauseSignal.paused = true;
|
|
2125
|
-
execLog(
|
|
2328
|
+
execLog(
|
|
2329
|
+
"wave",
|
|
2330
|
+
`W${waveIndex}`,
|
|
2331
|
+
`stop-all triggered by lane error in ${lanes[idx].laneId}: ${errMsg}`,
|
|
2332
|
+
);
|
|
2126
2333
|
for (const lane of lanes) {
|
|
2127
2334
|
killV2LaneAgents(laneSessionIdOf(lane), { laneNumber: lane.laneNumber });
|
|
2128
2335
|
}
|
|
@@ -2132,7 +2339,7 @@ export async function executeWithStopAll(
|
|
|
2132
2339
|
const failedResult: LaneExecutionResult = {
|
|
2133
2340
|
laneNumber: lanes[idx].laneNumber,
|
|
2134
2341
|
laneId: lanes[idx].laneId,
|
|
2135
|
-
tasks: lanes[idx].tasks.map(t => ({
|
|
2342
|
+
tasks: lanes[idx].tasks.map((t) => ({
|
|
2136
2343
|
taskId: t.taskId,
|
|
2137
2344
|
status: "failed" as LaneTaskStatus,
|
|
2138
2345
|
startTime: null,
|
|
@@ -2155,14 +2362,17 @@ export async function executeWithStopAll(
|
|
|
2155
2362
|
await Promise.allSettled(wrappedPromises);
|
|
2156
2363
|
|
|
2157
2364
|
// Fill in any null results (shouldn't happen, but defensive)
|
|
2158
|
-
return results.map(
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2365
|
+
return results.map(
|
|
2366
|
+
(r, idx) =>
|
|
2367
|
+
r || {
|
|
2368
|
+
laneNumber: lanes[idx].laneNumber,
|
|
2369
|
+
laneId: lanes[idx].laneId,
|
|
2370
|
+
tasks: [],
|
|
2371
|
+
overallStatus: "failed" as const,
|
|
2372
|
+
startTime: Date.now(),
|
|
2373
|
+
endTime: Date.now(),
|
|
2374
|
+
},
|
|
2375
|
+
);
|
|
2166
2376
|
}
|
|
2167
2377
|
|
|
2168
2378
|
// ── Runtime V2 Bridge Helpers (TP-102) ─────────────────────────────────────
|
|
@@ -2223,8 +2433,8 @@ export function buildExecutionUnit(
|
|
|
2223
2433
|
throw new ExecutionError(
|
|
2224
2434
|
"EXEC_MISSING_TASK_FOLDER",
|
|
2225
2435
|
`Cannot build execution unit for task ${task.taskId}: taskFolder is ${taskFolder === "" ? "empty" : "undefined"}. ` +
|
|
2226
|
-
|
|
2227
|
-
|
|
2436
|
+
`This typically means the task's persisted record was not enriched with discovery data. ` +
|
|
2437
|
+
`Re-run discovery or check that the task exists in the task area.`,
|
|
2228
2438
|
"execution",
|
|
2229
2439
|
task.taskId,
|
|
2230
2440
|
);
|
|
@@ -2248,18 +2458,17 @@ export function buildExecutionUnit(
|
|
|
2248
2458
|
// the execution repo (cross-repo segment). When they're the same repo,
|
|
2249
2459
|
// resolve packet paths inside the worktree so .DONE, STATUS.md etc. are
|
|
2250
2460
|
// written to the worktree (not the original repo outside the worktree).
|
|
2251
|
-
const useAbsolutePacketPath = task.task.packetTaskPath
|
|
2252
|
-
&& packetHomeRepoId !== executionRepoId;
|
|
2461
|
+
const useAbsolutePacketPath = task.task.packetTaskPath && packetHomeRepoId !== executionRepoId;
|
|
2253
2462
|
|
|
2254
2463
|
const packet = useAbsolutePacketPath
|
|
2255
2464
|
? resolvePacketPaths(task.task.packetTaskPath!)
|
|
2256
2465
|
: {
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2466
|
+
promptPath: resolved.taskFolderResolved + "/PROMPT.md",
|
|
2467
|
+
statusPath: resolved.statusPath,
|
|
2468
|
+
donePath: resolved.donePath,
|
|
2469
|
+
reviewsDir: resolved.taskFolderResolved + "/.reviews",
|
|
2470
|
+
taskFolder: resolved.taskFolderResolved,
|
|
2471
|
+
};
|
|
2263
2472
|
|
|
2264
2473
|
return {
|
|
2265
2474
|
id,
|
|
@@ -2339,7 +2548,9 @@ function parseAgentFile(filePath: string): { fm: Record<string, string>; body: s
|
|
|
2339
2548
|
if (m) fm[m[1]] = m[2].trim();
|
|
2340
2549
|
}
|
|
2341
2550
|
return { fm, body: raw.slice(fmEnd + 3).trim() };
|
|
2342
|
-
} catch {
|
|
2551
|
+
} catch {
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2343
2554
|
}
|
|
2344
2555
|
|
|
2345
2556
|
/**
|
|
@@ -2357,7 +2568,9 @@ function loadBaseAgentPrompt(agentName: string): string {
|
|
|
2357
2568
|
const def = parseAgentFile(resolved);
|
|
2358
2569
|
if (def?.body) return def.body;
|
|
2359
2570
|
}
|
|
2360
|
-
} catch {
|
|
2571
|
+
} catch {
|
|
2572
|
+
/* fall through */
|
|
2573
|
+
}
|
|
2361
2574
|
return "";
|
|
2362
2575
|
}
|
|
2363
2576
|
|
|
@@ -2433,11 +2646,11 @@ function resolveAgentPointerRoot(): string | null {
|
|
|
2433
2646
|
* @returns Composed agent definition, or null if no base and no local file found
|
|
2434
2647
|
* @since TP-161
|
|
2435
2648
|
*/
|
|
2436
|
-
export function loadAgentDef(
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
];
|
|
2649
|
+
export function loadAgentDef(
|
|
2650
|
+
cwd: string,
|
|
2651
|
+
name: string,
|
|
2652
|
+
): { systemPrompt: string; tools: string; model: string } | null {
|
|
2653
|
+
const localPaths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
|
|
2441
2654
|
|
|
2442
2655
|
// In workspace mode, add pointer-resolved agent root as fallback
|
|
2443
2656
|
const agentRoot = resolveAgentPointerRoot();
|
|
@@ -2452,7 +2665,9 @@ export function loadAgentDef(cwd: string, name: string): { systemPrompt: string;
|
|
|
2452
2665
|
if (existsSync(basePath)) {
|
|
2453
2666
|
baseDef = parseAgentFile(basePath);
|
|
2454
2667
|
}
|
|
2455
|
-
} catch {
|
|
2668
|
+
} catch {
|
|
2669
|
+
/* fall through */
|
|
2670
|
+
}
|
|
2456
2671
|
|
|
2457
2672
|
// Load local override (first found wins)
|
|
2458
2673
|
let localDef: { fm: Record<string, string>; body: string } | null = null;
|
|
@@ -2487,10 +2702,7 @@ export function loadAgentDef(cwd: string, name: string): { systemPrompt: string;
|
|
|
2487
2702
|
return { systemPrompt: composedPrompt.trim(), tools, model };
|
|
2488
2703
|
}
|
|
2489
2704
|
|
|
2490
|
-
export function resolveRuntimeStateRoot(
|
|
2491
|
-
repoRoot: string,
|
|
2492
|
-
workspaceRoot?: string,
|
|
2493
|
-
): string {
|
|
2705
|
+
export function resolveRuntimeStateRoot(repoRoot: string, workspaceRoot?: string): string {
|
|
2494
2706
|
return workspaceRoot ?? repoRoot;
|
|
2495
2707
|
}
|
|
2496
2708
|
|
|
@@ -2532,13 +2744,21 @@ function parseJsonArrayEnv(value?: string): string[] {
|
|
|
2532
2744
|
if (!value) return [];
|
|
2533
2745
|
try {
|
|
2534
2746
|
const parsed = JSON.parse(value);
|
|
2535
|
-
if (Array.isArray(parsed))
|
|
2536
|
-
|
|
2747
|
+
if (Array.isArray(parsed))
|
|
2748
|
+
return parsed.filter((v: unknown): v is string => typeof v === "string");
|
|
2749
|
+
} catch {
|
|
2750
|
+
/* ignore malformed */
|
|
2751
|
+
}
|
|
2537
2752
|
return [];
|
|
2538
2753
|
}
|
|
2539
2754
|
|
|
2540
2755
|
export function buildReviewerEnv(
|
|
2541
|
-
reviewerConfig?: {
|
|
2756
|
+
reviewerConfig?: {
|
|
2757
|
+
model?: string;
|
|
2758
|
+
thinking?: string;
|
|
2759
|
+
tools?: string;
|
|
2760
|
+
excludeExtensions?: string[];
|
|
2761
|
+
} | null,
|
|
2542
2762
|
): Record<string, string> {
|
|
2543
2763
|
const env: Record<string, string> = {};
|
|
2544
2764
|
if (reviewerConfig?.model) env.TASKPLANE_REVIEWER_MODEL = reviewerConfig.model;
|
|
@@ -2560,7 +2780,12 @@ export function buildReviewerEnv(
|
|
|
2560
2780
|
* @since TP-181
|
|
2561
2781
|
*/
|
|
2562
2782
|
export function buildWorkerEnv(
|
|
2563
|
-
workerConfig?: {
|
|
2783
|
+
workerConfig?: {
|
|
2784
|
+
model?: string;
|
|
2785
|
+
thinking?: string;
|
|
2786
|
+
tools?: string;
|
|
2787
|
+
excludeExtensions?: string[];
|
|
2788
|
+
} | null,
|
|
2564
2789
|
): Record<string, string> {
|
|
2565
2790
|
const env: Record<string, string> = {};
|
|
2566
2791
|
if (workerConfig?.model) env.TASKPLANE_WORKER_MODEL = workerConfig.model;
|
|
@@ -2620,7 +2845,8 @@ export async function executeLaneV2(
|
|
|
2620
2845
|
// The base template (templates/agents/task-worker.md) contains critical behavioral
|
|
2621
2846
|
// rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
|
|
2622
2847
|
// The local file (.pi/agents/task-worker.md) adds project-specific guidance.
|
|
2623
|
-
let workerSystemPrompt =
|
|
2848
|
+
let workerSystemPrompt =
|
|
2849
|
+
"You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2624
2850
|
let workerSegmentPrompt = "";
|
|
2625
2851
|
try {
|
|
2626
2852
|
const basePrompt = loadBaseAgentPrompt("task-worker");
|
|
@@ -2635,7 +2861,9 @@ export async function executeLaneV2(
|
|
|
2635
2861
|
// Load segment-scoped prompt overlay (appended when isSegmentScoped)
|
|
2636
2862
|
const segPrompt = loadBaseAgentPrompt("task-worker-segment");
|
|
2637
2863
|
if (segPrompt) workerSegmentPrompt = segPrompt;
|
|
2638
|
-
} catch {
|
|
2864
|
+
} catch {
|
|
2865
|
+
/* use default */
|
|
2866
|
+
}
|
|
2639
2867
|
|
|
2640
2868
|
execLog(laneId, "LANE", `starting Runtime V2 execution of ${lane.tasks.length} task(s)`, {
|
|
2641
2869
|
worktree: lane.worktreePath,
|
|
@@ -2647,16 +2875,26 @@ export async function executeLaneV2(
|
|
|
2647
2875
|
// this lane number is lifted before new alerts begin to flow.
|
|
2648
2876
|
if (onLaneRespawned) {
|
|
2649
2877
|
try {
|
|
2650
|
-
onLaneRespawned(
|
|
2878
|
+
onLaneRespawned(
|
|
2879
|
+
lane.laneNumber,
|
|
2880
|
+
buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
|
|
2881
|
+
batchId,
|
|
2882
|
+
);
|
|
2651
2883
|
} catch (err) {
|
|
2652
|
-
execLog(
|
|
2884
|
+
execLog(
|
|
2885
|
+
laneId,
|
|
2886
|
+
"LANE",
|
|
2887
|
+
`lane-respawned callback failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2888
|
+
);
|
|
2653
2889
|
}
|
|
2654
2890
|
}
|
|
2655
2891
|
|
|
2656
2892
|
for (const task of lane.tasks) {
|
|
2657
2893
|
const taskSegmentId = task.task.activeSegmentId ?? null;
|
|
2658
2894
|
if (shouldSkipRemaining || pauseSignal.paused) {
|
|
2659
|
-
const reason = pauseSignal.paused
|
|
2895
|
+
const reason = pauseSignal.paused
|
|
2896
|
+
? "Skipped due to pause signal"
|
|
2897
|
+
: "Skipped due to prior task failure in lane";
|
|
2660
2898
|
outcomes.push({
|
|
2661
2899
|
taskId: task.taskId,
|
|
2662
2900
|
status: "skipped",
|
|
@@ -2674,10 +2912,12 @@ export async function executeLaneV2(
|
|
|
2674
2912
|
// Build execution unit
|
|
2675
2913
|
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
2676
2914
|
|
|
2677
|
-
const rawAutonomy = String(
|
|
2915
|
+
const rawAutonomy = String(
|
|
2916
|
+
extraEnvVars?.TASKPLANE_SUPERVISOR_AUTONOMY ?? "autonomous",
|
|
2917
|
+
).toLowerCase();
|
|
2678
2918
|
const supervisorAutonomy: LaneRunnerConfig["supervisorAutonomy"] =
|
|
2679
|
-
|
|
2680
|
-
? rawAutonomy as LaneRunnerConfig["supervisorAutonomy"]
|
|
2919
|
+
rawAutonomy === "interactive" || rawAutonomy === "supervised" || rawAutonomy === "autonomous"
|
|
2920
|
+
? (rawAutonomy as LaneRunnerConfig["supervisorAutonomy"])
|
|
2681
2921
|
: "autonomous";
|
|
2682
2922
|
|
|
2683
2923
|
const laneRunnerConfig: LaneRunnerConfig = {
|
|
@@ -2701,12 +2941,26 @@ export async function executeLaneV2(
|
|
|
2701
2941
|
reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
|
|
2702
2942
|
// TP-180: Extension exclusion lists from config
|
|
2703
2943
|
workerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS),
|
|
2704
|
-
reviewerExcludeExtensions: parseJsonArrayEnv(
|
|
2944
|
+
reviewerExcludeExtensions: parseJsonArrayEnv(
|
|
2945
|
+
extraEnvVars?.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS,
|
|
2946
|
+
),
|
|
2705
2947
|
supervisorAutonomy,
|
|
2706
|
-
|
|
2948
|
+
// TP-195: replaced `config.project?.name` (no `project` field on
|
|
2949
|
+
// `OrchestratorConfig`; always undefined) with the env-var read
|
|
2950
|
+
// already used elsewhere in the codebase (lane-runner.ts:668 sets
|
|
2951
|
+
// `TASKPLANE_PROJECT_NAME` from the same source). When the env
|
|
2952
|
+
// var is unset, falls through to the same `"project"` literal as
|
|
2953
|
+
// before — behavior-neutral.
|
|
2954
|
+
projectName: extraEnvVars?.TASKPLANE_PROJECT_NAME || "project",
|
|
2707
2955
|
maxIterations: 20,
|
|
2708
2956
|
noProgressLimit: 3,
|
|
2709
|
-
|
|
2957
|
+
// TP-195: read the canonical `max_worker_minutes` field (snake_case
|
|
2958
|
+
// per `OrchestratorConfig.failure` in types.ts). The previous code
|
|
2959
|
+
// read a non-existent `maxWorkerMinutes` camelCase alias — always
|
|
2960
|
+
// undefined — silently ignoring any operator-set value. Honoring
|
|
2961
|
+
// the config is the intended behavior; default of 120 preserved
|
|
2962
|
+
// when the field is unset.
|
|
2963
|
+
maxWorkerMinutes: config.failure?.max_worker_minutes || 120,
|
|
2710
2964
|
warnPercent: 85,
|
|
2711
2965
|
killPercent: 95,
|
|
2712
2966
|
onSupervisorAlert,
|
|
@@ -2811,13 +3065,22 @@ export async function executeLaneV2(
|
|
|
2811
3065
|
progress: null,
|
|
2812
3066
|
updatedAt: Date.now(),
|
|
2813
3067
|
};
|
|
2814
|
-
writeLaneSnapshot(
|
|
3068
|
+
writeLaneSnapshot(
|
|
3069
|
+
stateRoot,
|
|
3070
|
+
batchId,
|
|
3071
|
+
lane.laneNumber,
|
|
3072
|
+
spawnFailureSnapshot as unknown as Record<string, unknown>,
|
|
3073
|
+
);
|
|
2815
3074
|
} catch (snapErr) {
|
|
2816
3075
|
// Best effort — if the snapshot write fails, the monitor's
|
|
2817
3076
|
// 30s-staleness fallback (snap with old updatedAt) eventually
|
|
2818
3077
|
// kicks in via the registry liveness check. Log so this is
|
|
2819
3078
|
// visible in operator diagnostics, but do NOT throw.
|
|
2820
|
-
execLog(
|
|
3079
|
+
execLog(
|
|
3080
|
+
laneId,
|
|
3081
|
+
task.taskId,
|
|
3082
|
+
`spawn-failure snapshot write failed (non-fatal): ${snapErr instanceof Error ? snapErr.message : String(snapErr)}`,
|
|
3083
|
+
);
|
|
2821
3084
|
}
|
|
2822
3085
|
|
|
2823
3086
|
shouldSkipRemaining = true;
|
|
@@ -2825,8 +3088,8 @@ export async function executeLaneV2(
|
|
|
2825
3088
|
}
|
|
2826
3089
|
|
|
2827
3090
|
const endTime = Date.now();
|
|
2828
|
-
const succeeded = outcomes.every(o => o.status === "succeeded");
|
|
2829
|
-
const failed = outcomes.some(o => o.status === "failed" || o.status === "stalled");
|
|
3091
|
+
const succeeded = outcomes.every((o) => o.status === "succeeded");
|
|
3092
|
+
const failed = outcomes.some((o) => o.status === "failed" || o.status === "stalled");
|
|
2830
3093
|
|
|
2831
3094
|
return {
|
|
2832
3095
|
laneNumber: lane.laneNumber,
|
|
@@ -2839,4 +3102,3 @@ export async function executeLaneV2(
|
|
|
2839
3102
|
}
|
|
2840
3103
|
|
|
2841
3104
|
// ── /orch Command — Full Execution (Step 5) ─────────────────────────
|
|
2842
|
-
|