taskplane 0.29.2 → 0.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/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- 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 +35 -61
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +426 -206
- 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 +542 -311
- 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 +774 -267
- 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 +186 -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
|
/**
|
|
@@ -829,7 +910,7 @@ export async function resolveTaskMonitorState(
|
|
|
829
910
|
// Snapshot not written yet OR snapshot still points to a prior task.
|
|
830
911
|
// Assume alive initially, but if stale for >30s consult the registry
|
|
831
912
|
// to avoid indefinite false "running" if the lane-runner died.
|
|
832
|
-
const staleMs = snap?.updatedAt ?
|
|
913
|
+
const staleMs = snap?.updatedAt ? now - snap.updatedAt : 0;
|
|
833
914
|
const trackerAgeMs = now - tracker.firstObservedAt;
|
|
834
915
|
if (staleMs > 30_000) {
|
|
835
916
|
// Snapshot hasn't been updated for 30s+ — check registry as fallback.
|
|
@@ -875,7 +956,7 @@ export async function resolveTaskMonitorState(
|
|
|
875
956
|
const trackerAgeMs = now - tracker.firstObservedAt;
|
|
876
957
|
if (
|
|
877
958
|
snap.updatedAt &&
|
|
878
|
-
|
|
959
|
+
now - snap.updatedAt > stallTimeoutMs / 2 &&
|
|
879
960
|
trackerAgeMs >= 60_000 &&
|
|
880
961
|
!isV2AgentAlive(sessionName, runtimeBackend, v2Context?.laneNumber)
|
|
881
962
|
) {
|
|
@@ -918,13 +999,13 @@ export async function resolveTaskMonitorState(
|
|
|
918
999
|
}
|
|
919
1000
|
|
|
920
1001
|
// Find the current step (first in-progress, or first not-started after last complete)
|
|
921
|
-
const inProgress = steps.find(s => s.status === "in-progress");
|
|
1002
|
+
const inProgress = steps.find((s) => s.status === "in-progress");
|
|
922
1003
|
if (inProgress) {
|
|
923
1004
|
currentStepName = inProgress.name;
|
|
924
1005
|
currentStepNumber = inProgress.number;
|
|
925
1006
|
} else {
|
|
926
1007
|
// Find first not-started step
|
|
927
|
-
const notStarted = steps.find(s => s.status === "not-started");
|
|
1008
|
+
const notStarted = steps.find((s) => s.status === "not-started");
|
|
928
1009
|
if (notStarted) {
|
|
929
1010
|
currentStepName = notStarted.name;
|
|
930
1011
|
currentStepNumber = notStarted.number;
|
|
@@ -979,7 +1060,7 @@ export async function resolveTaskMonitorState(
|
|
|
979
1060
|
sessionAlive &&
|
|
980
1061
|
tracker.statusFileSeenOnce &&
|
|
981
1062
|
tracker.stallTimerStart !== null &&
|
|
982
|
-
|
|
1063
|
+
now - tracker.stallTimerStart >= stallTimeoutMs
|
|
983
1064
|
) {
|
|
984
1065
|
const stallMinutes = Math.round((now - tracker.stallTimerStart) / 60_000);
|
|
985
1066
|
const stallReason = `STATUS.md unchanged for ${stallMinutes} minutes (threshold: ${Math.round(stallTimeoutMs / 60_000)} min)`;
|
|
@@ -1052,7 +1133,6 @@ export async function resolveTaskMonitorState(
|
|
|
1052
1133
|
};
|
|
1053
1134
|
}
|
|
1054
1135
|
|
|
1055
|
-
|
|
1056
1136
|
// ── Core Monitor Loop ────────────────────────────────────────────────
|
|
1057
1137
|
|
|
1058
1138
|
/**
|
|
@@ -1136,10 +1216,15 @@ export async function monitorLanes(
|
|
|
1136
1216
|
// Build the total task count
|
|
1137
1217
|
const tasksTotal = lanes.reduce((sum, lane) => sum + lane.tasks.length, 0);
|
|
1138
1218
|
|
|
1139
|
-
execLog(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1219
|
+
execLog(
|
|
1220
|
+
"monitor",
|
|
1221
|
+
"ALL",
|
|
1222
|
+
`starting monitoring for ${lanes.length} lane(s), ${tasksTotal} task(s)`,
|
|
1223
|
+
{
|
|
1224
|
+
pollIntervalMs,
|
|
1225
|
+
stallTimeoutMin: Math.round(stallTimeoutMs / 60_000),
|
|
1226
|
+
},
|
|
1227
|
+
);
|
|
1143
1228
|
|
|
1144
1229
|
while (true) {
|
|
1145
1230
|
const now = Date.now();
|
|
@@ -1239,17 +1324,23 @@ export async function monitorLanes(
|
|
|
1239
1324
|
stallTimeoutMs,
|
|
1240
1325
|
now,
|
|
1241
1326
|
runtimeBackend,
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1327
|
+
runtimeBackend === "v2" && batchId
|
|
1328
|
+
? {
|
|
1329
|
+
stateRoot: stateRootForRegistry ?? repoRoot,
|
|
1330
|
+
batchId,
|
|
1331
|
+
laneNumber: lane.laneNumber,
|
|
1332
|
+
}
|
|
1333
|
+
: undefined,
|
|
1247
1334
|
);
|
|
1248
1335
|
|
|
1249
1336
|
currentTaskSnapshot = snapshot;
|
|
1250
1337
|
|
|
1251
1338
|
// Check if this task just became terminal
|
|
1252
|
-
if (
|
|
1339
|
+
if (
|
|
1340
|
+
snapshot.status === "succeeded" ||
|
|
1341
|
+
snapshot.status === "failed" ||
|
|
1342
|
+
snapshot.status === "stalled"
|
|
1343
|
+
) {
|
|
1253
1344
|
terminalTasks.set(task.taskId, snapshot);
|
|
1254
1345
|
if (snapshot.status === "succeeded") {
|
|
1255
1346
|
completedTasks.push(task.taskId);
|
|
@@ -1322,8 +1413,12 @@ export async function monitorLanes(
|
|
|
1322
1413
|
// Log summary only on state changes (lane completes or fails) — not every poll
|
|
1323
1414
|
const currentStateKey = `${totalDone}/${totalFailed}`;
|
|
1324
1415
|
if (currentStateKey !== lastMonitorStateKey) {
|
|
1325
|
-
const activeLanes = laneSnapshots.filter(l => l.currentTaskId !== null);
|
|
1326
|
-
execLog(
|
|
1416
|
+
const activeLanes = laneSnapshots.filter((l) => l.currentTaskId !== null);
|
|
1417
|
+
execLog(
|
|
1418
|
+
"monitor",
|
|
1419
|
+
"ALL",
|
|
1420
|
+
`poll #${pollCount}: ${totalDone}/${tasksTotal} done, ${totalFailed} failed, ${activeLanes.length} active lane(s)`,
|
|
1421
|
+
);
|
|
1327
1422
|
lastMonitorStateKey = currentStateKey;
|
|
1328
1423
|
}
|
|
1329
1424
|
|
|
@@ -1340,12 +1435,12 @@ export async function monitorLanes(
|
|
|
1340
1435
|
}
|
|
1341
1436
|
|
|
1342
1437
|
// Wait for next poll cycle
|
|
1343
|
-
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
1438
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1344
1439
|
}
|
|
1345
1440
|
|
|
1346
1441
|
// Reached here due to pause signal — return current state
|
|
1347
1442
|
const now = Date.now();
|
|
1348
|
-
const laneSnapshots: LaneMonitorSnapshot[] = lanes.map(lane => ({
|
|
1443
|
+
const laneSnapshots: LaneMonitorSnapshot[] = lanes.map((lane) => ({
|
|
1349
1444
|
laneId: lane.laneId,
|
|
1350
1445
|
laneNumber: lane.laneNumber,
|
|
1351
1446
|
sessionName: laneSessionIdOf(lane),
|
|
@@ -1354,7 +1449,7 @@ export async function monitorLanes(
|
|
|
1354
1449
|
currentTaskSnapshot: null,
|
|
1355
1450
|
completedTasks: [],
|
|
1356
1451
|
failedTasks: [],
|
|
1357
|
-
remainingTasks: lane.tasks.map(t => t.taskId),
|
|
1452
|
+
remainingTasks: lane.tasks.map((t) => t.taskId),
|
|
1358
1453
|
}));
|
|
1359
1454
|
|
|
1360
1455
|
setV2LivenessRegistryCache(null);
|
|
@@ -1370,7 +1465,6 @@ export async function monitorLanes(
|
|
|
1370
1465
|
};
|
|
1371
1466
|
}
|
|
1372
1467
|
|
|
1373
|
-
|
|
1374
1468
|
// ── Transitive Dependent Computation ─────────────────────────────────
|
|
1375
1469
|
|
|
1376
1470
|
/**
|
|
@@ -1414,7 +1508,6 @@ export function computeTransitiveDependents(
|
|
|
1414
1508
|
return blocked;
|
|
1415
1509
|
}
|
|
1416
1510
|
|
|
1417
|
-
|
|
1418
1511
|
// ── Pre-flight: Commit Untracked Task Files ─────────────────────────
|
|
1419
1512
|
|
|
1420
1513
|
/**
|
|
@@ -1499,29 +1592,31 @@ export function ensureTaskFilesCommitted(
|
|
|
1499
1592
|
|
|
1500
1593
|
try {
|
|
1501
1594
|
// Read orch branch tree into temporary index
|
|
1502
|
-
const readTreeRes = runGitWithEnv(
|
|
1503
|
-
["read-tree", orchTip],
|
|
1504
|
-
repoRoot,
|
|
1505
|
-
{ GIT_INDEX_FILE: tmpIdx },
|
|
1506
|
-
);
|
|
1595
|
+
const readTreeRes = runGitWithEnv(["read-tree", orchTip], repoRoot, { GIT_INDEX_FILE: tmpIdx });
|
|
1507
1596
|
if (!readTreeRes.ok) {
|
|
1508
|
-
execLog(
|
|
1509
|
-
|
|
1510
|
-
|
|
1597
|
+
execLog(
|
|
1598
|
+
"wave",
|
|
1599
|
+
`W${waveIndex}`,
|
|
1600
|
+
`orch branch staging: read-tree failed, falling back to HEAD commit`,
|
|
1601
|
+
{
|
|
1602
|
+
error: readTreeRes.stderr,
|
|
1603
|
+
},
|
|
1604
|
+
);
|
|
1511
1605
|
// Fall through to legacy path
|
|
1512
1606
|
} else {
|
|
1513
1607
|
// Add task files to temporary index
|
|
1514
1608
|
let addFailed = false;
|
|
1515
1609
|
for (const folder of foldersToStage) {
|
|
1516
|
-
const addRes = runGitWithEnv(
|
|
1517
|
-
["add", "--", folder],
|
|
1518
|
-
repoRoot,
|
|
1519
|
-
{ GIT_INDEX_FILE: tmpIdx },
|
|
1520
|
-
);
|
|
1610
|
+
const addRes = runGitWithEnv(["add", "--", folder], repoRoot, { GIT_INDEX_FILE: tmpIdx });
|
|
1521
1611
|
if (!addRes.ok) {
|
|
1522
|
-
execLog(
|
|
1523
|
-
|
|
1524
|
-
|
|
1612
|
+
execLog(
|
|
1613
|
+
"wave",
|
|
1614
|
+
`W${waveIndex}`,
|
|
1615
|
+
`orch branch staging: git add failed for ${folder}, falling back`,
|
|
1616
|
+
{
|
|
1617
|
+
error: addRes.stderr,
|
|
1618
|
+
},
|
|
1619
|
+
);
|
|
1525
1620
|
addFailed = true;
|
|
1526
1621
|
break;
|
|
1527
1622
|
}
|
|
@@ -1529,15 +1624,11 @@ export function ensureTaskFilesCommitted(
|
|
|
1529
1624
|
|
|
1530
1625
|
if (!addFailed) {
|
|
1531
1626
|
// Write tree from temporary index
|
|
1532
|
-
const writeTreeRes = runGitWithEnv(
|
|
1533
|
-
["write-tree"],
|
|
1534
|
-
repoRoot,
|
|
1535
|
-
{ GIT_INDEX_FILE: tmpIdx },
|
|
1536
|
-
);
|
|
1627
|
+
const writeTreeRes = runGitWithEnv(["write-tree"], repoRoot, { GIT_INDEX_FILE: tmpIdx });
|
|
1537
1628
|
|
|
1538
1629
|
if (writeTreeRes.ok) {
|
|
1539
1630
|
const tree = writeTreeRes.stdout.trim();
|
|
1540
|
-
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1631
|
+
const taskIds = foldersToStage.map((f) => f.split("/").pop() || f).join(", ");
|
|
1541
1632
|
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1542
1633
|
|
|
1543
1634
|
// Create commit directly on orch branch
|
|
@@ -1554,14 +1645,23 @@ export function ensureTaskFilesCommitted(
|
|
|
1554
1645
|
);
|
|
1555
1646
|
|
|
1556
1647
|
if (refUpdateRes.ok) {
|
|
1557
|
-
execLog(
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1648
|
+
execLog(
|
|
1649
|
+
"wave",
|
|
1650
|
+
`W${waveIndex}`,
|
|
1651
|
+
`committed ${foldersToStage.length} task folder(s) directly on orch branch`,
|
|
1652
|
+
{
|
|
1653
|
+
orchBranch,
|
|
1654
|
+
folders: foldersToStage,
|
|
1655
|
+
from: orchTip.slice(0, 8),
|
|
1656
|
+
to: newCommit.slice(0, 8),
|
|
1657
|
+
},
|
|
1658
|
+
);
|
|
1563
1659
|
// Clean up temp index and return — no need for legacy path
|
|
1564
|
-
try {
|
|
1660
|
+
try {
|
|
1661
|
+
unlinkSync(tmpIdx);
|
|
1662
|
+
} catch {
|
|
1663
|
+
/* best effort */
|
|
1664
|
+
}
|
|
1565
1665
|
return;
|
|
1566
1666
|
}
|
|
1567
1667
|
execLog("wave", `W${waveIndex}`, `orch branch staging: ref update failed, falling back`, {
|
|
@@ -1580,12 +1680,21 @@ export function ensureTaskFilesCommitted(
|
|
|
1580
1680
|
}
|
|
1581
1681
|
}
|
|
1582
1682
|
} catch (err: unknown) {
|
|
1583
|
-
execLog(
|
|
1584
|
-
|
|
1585
|
-
|
|
1683
|
+
execLog(
|
|
1684
|
+
"wave",
|
|
1685
|
+
`W${waveIndex}`,
|
|
1686
|
+
`orch branch staging: unexpected error, falling back to HEAD commit`,
|
|
1687
|
+
{
|
|
1688
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1689
|
+
},
|
|
1690
|
+
);
|
|
1586
1691
|
} finally {
|
|
1587
1692
|
// Always clean up temp index
|
|
1588
|
-
try {
|
|
1693
|
+
try {
|
|
1694
|
+
unlinkSync(tmpIdx);
|
|
1695
|
+
} catch {
|
|
1696
|
+
/* best effort */
|
|
1697
|
+
}
|
|
1589
1698
|
}
|
|
1590
1699
|
}
|
|
1591
1700
|
}
|
|
@@ -1609,7 +1718,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1609
1718
|
}
|
|
1610
1719
|
|
|
1611
1720
|
// Commit
|
|
1612
|
-
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1721
|
+
const taskIds = foldersToStage.map((f) => f.split("/").pop() || f).join(", ");
|
|
1613
1722
|
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1614
1723
|
const commitResult = runGit(["commit", "-m", commitMsg], repoRoot);
|
|
1615
1724
|
if (!commitResult.ok) {
|
|
@@ -1622,10 +1731,15 @@ export function ensureTaskFilesCommitted(
|
|
|
1622
1731
|
);
|
|
1623
1732
|
}
|
|
1624
1733
|
|
|
1625
|
-
execLog(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1734
|
+
execLog(
|
|
1735
|
+
"wave",
|
|
1736
|
+
`W${waveIndex}`,
|
|
1737
|
+
`committed ${foldersToStage.length} task folder(s) to ensure worktree visibility`,
|
|
1738
|
+
{
|
|
1739
|
+
folders: foldersToStage,
|
|
1740
|
+
commit: commitResult.stdout.trim().split("\n")[0],
|
|
1741
|
+
},
|
|
1742
|
+
);
|
|
1629
1743
|
|
|
1630
1744
|
// Fast-forward (or merge) the orch branch to include the staging commit so
|
|
1631
1745
|
// that worktrees—which branch from orchBranch—see the new task files and
|
|
@@ -1639,10 +1753,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1639
1753
|
const newHead = headRes.stdout.trim();
|
|
1640
1754
|
const orchTip = orchTipRes.stdout.trim();
|
|
1641
1755
|
|
|
1642
|
-
const ancestorCheck = runGit(
|
|
1643
|
-
["merge-base", "--is-ancestor", orchTip, newHead],
|
|
1644
|
-
repoRoot,
|
|
1645
|
-
);
|
|
1756
|
+
const ancestorCheck = runGit(["merge-base", "--is-ancestor", orchTip, newHead], repoRoot);
|
|
1646
1757
|
|
|
1647
1758
|
if (ancestorCheck.ok) {
|
|
1648
1759
|
const ffResult = runGit(
|
|
@@ -1662,10 +1773,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1662
1773
|
});
|
|
1663
1774
|
}
|
|
1664
1775
|
} else {
|
|
1665
|
-
const mergeTreeRes = runGit(
|
|
1666
|
-
["merge-tree", "--write-tree", orchTip, newHead],
|
|
1667
|
-
repoRoot,
|
|
1668
|
-
);
|
|
1776
|
+
const mergeTreeRes = runGit(["merge-tree", "--write-tree", orchTip, newHead], repoRoot);
|
|
1669
1777
|
if (mergeTreeRes.ok) {
|
|
1670
1778
|
const mergedTree = mergeTreeRes.stdout.trim().split("\n")[0];
|
|
1671
1779
|
if (/^[0-9a-f]{40}$/i.test(mergedTree)) {
|
|
@@ -1692,10 +1800,15 @@ export function ensureTaskFilesCommitted(
|
|
|
1692
1800
|
}
|
|
1693
1801
|
}
|
|
1694
1802
|
} catch (refErr: unknown) {
|
|
1695
|
-
execLog(
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1803
|
+
execLog(
|
|
1804
|
+
"wave",
|
|
1805
|
+
`W${waveIndex}`,
|
|
1806
|
+
`warning: orch branch ref update threw unexpectedly (non-fatal)`,
|
|
1807
|
+
{
|
|
1808
|
+
orchBranch,
|
|
1809
|
+
error: refErr instanceof Error ? refErr.message : String(refErr),
|
|
1810
|
+
},
|
|
1811
|
+
);
|
|
1699
1812
|
}
|
|
1700
1813
|
}
|
|
1701
1814
|
}
|
|
@@ -1768,8 +1881,18 @@ export async function executeWave(
|
|
|
1768
1881
|
runtimeBackend?: RuntimeBackend,
|
|
1769
1882
|
onSupervisorAlert?: SupervisorAlertCallback,
|
|
1770
1883
|
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1771
|
-
reviewerConfig?: {
|
|
1772
|
-
|
|
1884
|
+
reviewerConfig?: {
|
|
1885
|
+
model?: string;
|
|
1886
|
+
thinking?: string;
|
|
1887
|
+
tools?: string;
|
|
1888
|
+
excludeExtensions?: string[];
|
|
1889
|
+
},
|
|
1890
|
+
workerConfig?: {
|
|
1891
|
+
model?: string;
|
|
1892
|
+
thinking?: string;
|
|
1893
|
+
tools?: string;
|
|
1894
|
+
excludeExtensions?: string[];
|
|
1895
|
+
} | null,
|
|
1773
1896
|
workerExcludeExtensions?: string[],
|
|
1774
1897
|
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback,
|
|
1775
1898
|
onLaneRespawned?: (laneNumber: number, agentId: string, batchId: string) => void,
|
|
@@ -1814,7 +1937,15 @@ export async function executeWave(
|
|
|
1814
1937
|
}
|
|
1815
1938
|
|
|
1816
1939
|
// ── Stage 1: Allocate lanes ──────────────────────────────────
|
|
1817
|
-
const allocResult = allocateLanes(
|
|
1940
|
+
const allocResult = allocateLanes(
|
|
1941
|
+
waveTasks,
|
|
1942
|
+
pending,
|
|
1943
|
+
config,
|
|
1944
|
+
repoRoot,
|
|
1945
|
+
batchId,
|
|
1946
|
+
orchBranch,
|
|
1947
|
+
workspaceConfig,
|
|
1948
|
+
);
|
|
1818
1949
|
|
|
1819
1950
|
if (!allocResult.success) {
|
|
1820
1951
|
const errMsg = allocResult.error?.message || "Unknown allocation failure";
|
|
@@ -1859,7 +1990,11 @@ export async function executeWave(
|
|
|
1859
1990
|
const isWsMode = !!workspaceConfig;
|
|
1860
1991
|
const backend: RuntimeBackend = "v2";
|
|
1861
1992
|
if (runtimeBackend && runtimeBackend !== "v2") {
|
|
1862
|
-
execLog(
|
|
1993
|
+
execLog(
|
|
1994
|
+
"wave",
|
|
1995
|
+
`W${waveIndex}`,
|
|
1996
|
+
`legacy runtime backend '${runtimeBackend}' requested but ignored; using Runtime V2`,
|
|
1997
|
+
);
|
|
1863
1998
|
}
|
|
1864
1999
|
execLog("wave", `W${waveIndex}`, "using Runtime V2 backend (executeLaneV2)");
|
|
1865
2000
|
|
|
@@ -1870,19 +2005,39 @@ export async function executeWave(
|
|
|
1870
2005
|
const snapshotStateRoot = resolveRuntimeStateRoot(repoRoot, wsRoot);
|
|
1871
2006
|
for (const lane of lanes) {
|
|
1872
2007
|
try {
|
|
1873
|
-
const snapPath = join(
|
|
2008
|
+
const snapPath = join(
|
|
2009
|
+
snapshotStateRoot,
|
|
2010
|
+
".pi",
|
|
2011
|
+
"runtime",
|
|
2012
|
+
batchId,
|
|
2013
|
+
"lanes",
|
|
2014
|
+
`lane-${lane.laneNumber}.json`,
|
|
2015
|
+
);
|
|
1874
2016
|
if (existsSync(snapPath)) unlinkSync(snapPath);
|
|
1875
|
-
} catch {
|
|
2017
|
+
} catch {
|
|
2018
|
+
/* best effort */
|
|
2019
|
+
}
|
|
1876
2020
|
}
|
|
1877
2021
|
|
|
1878
|
-
const lanePromises = lanes.map(lane =>
|
|
1879
|
-
executeLaneV2(
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2022
|
+
const lanePromises = lanes.map((lane) =>
|
|
2023
|
+
executeLaneV2(
|
|
2024
|
+
lane,
|
|
2025
|
+
config,
|
|
2026
|
+
repoRoot,
|
|
2027
|
+
wavePauseSignal,
|
|
2028
|
+
wsRoot,
|
|
2029
|
+
isWsMode,
|
|
2030
|
+
{
|
|
2031
|
+
ORCH_BATCH_ID: batchId,
|
|
2032
|
+
TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
|
|
2033
|
+
...buildWorkerEnv(workerConfig),
|
|
2034
|
+
...buildReviewerEnv(reviewerConfig),
|
|
2035
|
+
...buildWorkerExcludeEnv(workerExcludeExtensions),
|
|
2036
|
+
},
|
|
2037
|
+
onSupervisorAlert,
|
|
2038
|
+
onLaneTerminated,
|
|
2039
|
+
onLaneRespawned,
|
|
2040
|
+
),
|
|
1886
2041
|
);
|
|
1887
2042
|
|
|
1888
2043
|
// Start monitoring as a sibling async loop
|
|
@@ -1929,7 +2084,7 @@ export async function executeWave(
|
|
|
1929
2084
|
return {
|
|
1930
2085
|
laneNumber: lanes[idx].laneNumber,
|
|
1931
2086
|
laneId: lanes[idx].laneId,
|
|
1932
|
-
tasks: lanes[idx].tasks.map(t => ({
|
|
2087
|
+
tasks: lanes[idx].tasks.map((t) => ({
|
|
1933
2088
|
taskId: t.taskId,
|
|
1934
2089
|
status: "failed" as LaneTaskStatus,
|
|
1935
2090
|
startTime: null,
|
|
@@ -1947,8 +2102,8 @@ export async function executeWave(
|
|
|
1947
2102
|
|
|
1948
2103
|
// For stop-wave: if any task failed, set pause to prevent next wave
|
|
1949
2104
|
if (policy === "stop-wave") {
|
|
1950
|
-
const hasFailure = laneResults.some(lr =>
|
|
1951
|
-
lr.tasks.some(t => t.status === "failed" || t.status === "stalled"),
|
|
2105
|
+
const hasFailure = laneResults.some((lr) =>
|
|
2106
|
+
lr.tasks.some((t) => t.status === "failed" || t.status === "stalled"),
|
|
1952
2107
|
);
|
|
1953
2108
|
if (hasFailure) {
|
|
1954
2109
|
wavePauseSignal.paused = true;
|
|
@@ -1991,21 +2146,24 @@ export async function executeWave(
|
|
|
1991
2146
|
// Compute blocked tasks for future waves (skip-dependents policy)
|
|
1992
2147
|
let blockedTaskIds: string[] = [];
|
|
1993
2148
|
if (policy === "skip-dependents" && failedTaskIds.length > 0) {
|
|
1994
|
-
const blocked = computeTransitiveDependents(
|
|
1995
|
-
new Set(failedTaskIds),
|
|
1996
|
-
dependencyGraph,
|
|
1997
|
-
);
|
|
2149
|
+
const blocked = computeTransitiveDependents(new Set(failedTaskIds), dependencyGraph);
|
|
1998
2150
|
blockedTaskIds = [...blocked].sort();
|
|
1999
2151
|
if (blockedTaskIds.length > 0) {
|
|
2000
|
-
execLog(
|
|
2001
|
-
|
|
2002
|
-
|
|
2152
|
+
execLog(
|
|
2153
|
+
"wave",
|
|
2154
|
+
`W${waveIndex}`,
|
|
2155
|
+
`skip-dependents: ${blockedTaskIds.length} task(s) blocked for future waves`,
|
|
2156
|
+
{
|
|
2157
|
+
blocked: blockedTaskIds.join(","),
|
|
2158
|
+
},
|
|
2159
|
+
);
|
|
2003
2160
|
}
|
|
2004
2161
|
}
|
|
2005
2162
|
|
|
2006
2163
|
// Determine overall wave status
|
|
2007
|
-
const stoppedEarly =
|
|
2008
|
-
|
|
2164
|
+
const stoppedEarly =
|
|
2165
|
+
(policy === "stop-all" && failedTaskIds.length > 0) ||
|
|
2166
|
+
(policy === "stop-wave" && failedTaskIds.length > 0);
|
|
2009
2167
|
|
|
2010
2168
|
let overallStatus: WaveExecutionResult["overallStatus"];
|
|
2011
2169
|
if (policy === "stop-all" && failedTaskIds.length > 0) {
|
|
@@ -2085,9 +2243,7 @@ export async function executeWithStopAll(
|
|
|
2085
2243
|
|
|
2086
2244
|
// Check if any task failed
|
|
2087
2245
|
if (!abortTriggered) {
|
|
2088
|
-
const hasFailure = result.tasks.some(
|
|
2089
|
-
t => t.status === "failed" || t.status === "stalled",
|
|
2090
|
-
);
|
|
2246
|
+
const hasFailure = result.tasks.some((t) => t.status === "failed" || t.status === "stalled");
|
|
2091
2247
|
if (hasFailure) {
|
|
2092
2248
|
// First failure detected — trigger stop-all
|
|
2093
2249
|
abortTriggered = true;
|
|
@@ -2095,7 +2251,7 @@ export async function executeWithStopAll(
|
|
|
2095
2251
|
|
|
2096
2252
|
// Determine which task failed first for logging
|
|
2097
2253
|
const firstFailed = result.tasks
|
|
2098
|
-
.filter(t => t.status === "failed" || t.status === "stalled")
|
|
2254
|
+
.filter((t) => t.status === "failed" || t.status === "stalled")
|
|
2099
2255
|
.sort((a, b) => {
|
|
2100
2256
|
// Sort by startTime, then by taskId for deterministic tie-break
|
|
2101
2257
|
const timeA = a.startTime || 0;
|
|
@@ -2104,9 +2260,14 @@ export async function executeWithStopAll(
|
|
|
2104
2260
|
return a.taskId.localeCompare(b.taskId);
|
|
2105
2261
|
})[0];
|
|
2106
2262
|
|
|
2107
|
-
execLog(
|
|
2108
|
-
|
|
2109
|
-
|
|
2263
|
+
execLog(
|
|
2264
|
+
"wave",
|
|
2265
|
+
`W${waveIndex}`,
|
|
2266
|
+
`stop-all triggered by ${firstFailed?.taskId || "unknown"} in ${lanes[idx].laneId}`,
|
|
2267
|
+
{
|
|
2268
|
+
session: laneSessionIdOf(lanes[idx]),
|
|
2269
|
+
},
|
|
2270
|
+
);
|
|
2110
2271
|
|
|
2111
2272
|
// Kill ALL lane sessions immediately
|
|
2112
2273
|
for (const lane of lanes) {
|
|
@@ -2122,7 +2283,11 @@ export async function executeWithStopAll(
|
|
|
2122
2283
|
if (!abortTriggered) {
|
|
2123
2284
|
abortTriggered = true;
|
|
2124
2285
|
pauseSignal.paused = true;
|
|
2125
|
-
execLog(
|
|
2286
|
+
execLog(
|
|
2287
|
+
"wave",
|
|
2288
|
+
`W${waveIndex}`,
|
|
2289
|
+
`stop-all triggered by lane error in ${lanes[idx].laneId}: ${errMsg}`,
|
|
2290
|
+
);
|
|
2126
2291
|
for (const lane of lanes) {
|
|
2127
2292
|
killV2LaneAgents(laneSessionIdOf(lane), { laneNumber: lane.laneNumber });
|
|
2128
2293
|
}
|
|
@@ -2132,7 +2297,7 @@ export async function executeWithStopAll(
|
|
|
2132
2297
|
const failedResult: LaneExecutionResult = {
|
|
2133
2298
|
laneNumber: lanes[idx].laneNumber,
|
|
2134
2299
|
laneId: lanes[idx].laneId,
|
|
2135
|
-
tasks: lanes[idx].tasks.map(t => ({
|
|
2300
|
+
tasks: lanes[idx].tasks.map((t) => ({
|
|
2136
2301
|
taskId: t.taskId,
|
|
2137
2302
|
status: "failed" as LaneTaskStatus,
|
|
2138
2303
|
startTime: null,
|
|
@@ -2155,14 +2320,17 @@ export async function executeWithStopAll(
|
|
|
2155
2320
|
await Promise.allSettled(wrappedPromises);
|
|
2156
2321
|
|
|
2157
2322
|
// Fill in any null results (shouldn't happen, but defensive)
|
|
2158
|
-
return results.map(
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2323
|
+
return results.map(
|
|
2324
|
+
(r, idx) =>
|
|
2325
|
+
r || {
|
|
2326
|
+
laneNumber: lanes[idx].laneNumber,
|
|
2327
|
+
laneId: lanes[idx].laneId,
|
|
2328
|
+
tasks: [],
|
|
2329
|
+
overallStatus: "failed" as const,
|
|
2330
|
+
startTime: Date.now(),
|
|
2331
|
+
endTime: Date.now(),
|
|
2332
|
+
},
|
|
2333
|
+
);
|
|
2166
2334
|
}
|
|
2167
2335
|
|
|
2168
2336
|
// ── Runtime V2 Bridge Helpers (TP-102) ─────────────────────────────────────
|
|
@@ -2223,8 +2391,8 @@ export function buildExecutionUnit(
|
|
|
2223
2391
|
throw new ExecutionError(
|
|
2224
2392
|
"EXEC_MISSING_TASK_FOLDER",
|
|
2225
2393
|
`Cannot build execution unit for task ${task.taskId}: taskFolder is ${taskFolder === "" ? "empty" : "undefined"}. ` +
|
|
2226
|
-
|
|
2227
|
-
|
|
2394
|
+
`This typically means the task's persisted record was not enriched with discovery data. ` +
|
|
2395
|
+
`Re-run discovery or check that the task exists in the task area.`,
|
|
2228
2396
|
"execution",
|
|
2229
2397
|
task.taskId,
|
|
2230
2398
|
);
|
|
@@ -2248,18 +2416,17 @@ export function buildExecutionUnit(
|
|
|
2248
2416
|
// the execution repo (cross-repo segment). When they're the same repo,
|
|
2249
2417
|
// resolve packet paths inside the worktree so .DONE, STATUS.md etc. are
|
|
2250
2418
|
// written to the worktree (not the original repo outside the worktree).
|
|
2251
|
-
const useAbsolutePacketPath = task.task.packetTaskPath
|
|
2252
|
-
&& packetHomeRepoId !== executionRepoId;
|
|
2419
|
+
const useAbsolutePacketPath = task.task.packetTaskPath && packetHomeRepoId !== executionRepoId;
|
|
2253
2420
|
|
|
2254
2421
|
const packet = useAbsolutePacketPath
|
|
2255
2422
|
? resolvePacketPaths(task.task.packetTaskPath!)
|
|
2256
2423
|
: {
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2424
|
+
promptPath: resolved.taskFolderResolved + "/PROMPT.md",
|
|
2425
|
+
statusPath: resolved.statusPath,
|
|
2426
|
+
donePath: resolved.donePath,
|
|
2427
|
+
reviewsDir: resolved.taskFolderResolved + "/.reviews",
|
|
2428
|
+
taskFolder: resolved.taskFolderResolved,
|
|
2429
|
+
};
|
|
2263
2430
|
|
|
2264
2431
|
return {
|
|
2265
2432
|
id,
|
|
@@ -2339,7 +2506,9 @@ function parseAgentFile(filePath: string): { fm: Record<string, string>; body: s
|
|
|
2339
2506
|
if (m) fm[m[1]] = m[2].trim();
|
|
2340
2507
|
}
|
|
2341
2508
|
return { fm, body: raw.slice(fmEnd + 3).trim() };
|
|
2342
|
-
} catch {
|
|
2509
|
+
} catch {
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2343
2512
|
}
|
|
2344
2513
|
|
|
2345
2514
|
/**
|
|
@@ -2357,7 +2526,9 @@ function loadBaseAgentPrompt(agentName: string): string {
|
|
|
2357
2526
|
const def = parseAgentFile(resolved);
|
|
2358
2527
|
if (def?.body) return def.body;
|
|
2359
2528
|
}
|
|
2360
|
-
} catch {
|
|
2529
|
+
} catch {
|
|
2530
|
+
/* fall through */
|
|
2531
|
+
}
|
|
2361
2532
|
return "";
|
|
2362
2533
|
}
|
|
2363
2534
|
|
|
@@ -2433,11 +2604,11 @@ function resolveAgentPointerRoot(): string | null {
|
|
|
2433
2604
|
* @returns Composed agent definition, or null if no base and no local file found
|
|
2434
2605
|
* @since TP-161
|
|
2435
2606
|
*/
|
|
2436
|
-
export function loadAgentDef(
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
];
|
|
2607
|
+
export function loadAgentDef(
|
|
2608
|
+
cwd: string,
|
|
2609
|
+
name: string,
|
|
2610
|
+
): { systemPrompt: string; tools: string; model: string } | null {
|
|
2611
|
+
const localPaths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
|
|
2441
2612
|
|
|
2442
2613
|
// In workspace mode, add pointer-resolved agent root as fallback
|
|
2443
2614
|
const agentRoot = resolveAgentPointerRoot();
|
|
@@ -2452,7 +2623,9 @@ export function loadAgentDef(cwd: string, name: string): { systemPrompt: string;
|
|
|
2452
2623
|
if (existsSync(basePath)) {
|
|
2453
2624
|
baseDef = parseAgentFile(basePath);
|
|
2454
2625
|
}
|
|
2455
|
-
} catch {
|
|
2626
|
+
} catch {
|
|
2627
|
+
/* fall through */
|
|
2628
|
+
}
|
|
2456
2629
|
|
|
2457
2630
|
// Load local override (first found wins)
|
|
2458
2631
|
let localDef: { fm: Record<string, string>; body: string } | null = null;
|
|
@@ -2487,10 +2660,7 @@ export function loadAgentDef(cwd: string, name: string): { systemPrompt: string;
|
|
|
2487
2660
|
return { systemPrompt: composedPrompt.trim(), tools, model };
|
|
2488
2661
|
}
|
|
2489
2662
|
|
|
2490
|
-
export function resolveRuntimeStateRoot(
|
|
2491
|
-
repoRoot: string,
|
|
2492
|
-
workspaceRoot?: string,
|
|
2493
|
-
): string {
|
|
2663
|
+
export function resolveRuntimeStateRoot(repoRoot: string, workspaceRoot?: string): string {
|
|
2494
2664
|
return workspaceRoot ?? repoRoot;
|
|
2495
2665
|
}
|
|
2496
2666
|
|
|
@@ -2532,13 +2702,21 @@ function parseJsonArrayEnv(value?: string): string[] {
|
|
|
2532
2702
|
if (!value) return [];
|
|
2533
2703
|
try {
|
|
2534
2704
|
const parsed = JSON.parse(value);
|
|
2535
|
-
if (Array.isArray(parsed))
|
|
2536
|
-
|
|
2705
|
+
if (Array.isArray(parsed))
|
|
2706
|
+
return parsed.filter((v: unknown): v is string => typeof v === "string");
|
|
2707
|
+
} catch {
|
|
2708
|
+
/* ignore malformed */
|
|
2709
|
+
}
|
|
2537
2710
|
return [];
|
|
2538
2711
|
}
|
|
2539
2712
|
|
|
2540
2713
|
export function buildReviewerEnv(
|
|
2541
|
-
reviewerConfig?: {
|
|
2714
|
+
reviewerConfig?: {
|
|
2715
|
+
model?: string;
|
|
2716
|
+
thinking?: string;
|
|
2717
|
+
tools?: string;
|
|
2718
|
+
excludeExtensions?: string[];
|
|
2719
|
+
} | null,
|
|
2542
2720
|
): Record<string, string> {
|
|
2543
2721
|
const env: Record<string, string> = {};
|
|
2544
2722
|
if (reviewerConfig?.model) env.TASKPLANE_REVIEWER_MODEL = reviewerConfig.model;
|
|
@@ -2560,7 +2738,12 @@ export function buildReviewerEnv(
|
|
|
2560
2738
|
* @since TP-181
|
|
2561
2739
|
*/
|
|
2562
2740
|
export function buildWorkerEnv(
|
|
2563
|
-
workerConfig?: {
|
|
2741
|
+
workerConfig?: {
|
|
2742
|
+
model?: string;
|
|
2743
|
+
thinking?: string;
|
|
2744
|
+
tools?: string;
|
|
2745
|
+
excludeExtensions?: string[];
|
|
2746
|
+
} | null,
|
|
2564
2747
|
): Record<string, string> {
|
|
2565
2748
|
const env: Record<string, string> = {};
|
|
2566
2749
|
if (workerConfig?.model) env.TASKPLANE_WORKER_MODEL = workerConfig.model;
|
|
@@ -2620,7 +2803,8 @@ export async function executeLaneV2(
|
|
|
2620
2803
|
// The base template (templates/agents/task-worker.md) contains critical behavioral
|
|
2621
2804
|
// rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
|
|
2622
2805
|
// The local file (.pi/agents/task-worker.md) adds project-specific guidance.
|
|
2623
|
-
let workerSystemPrompt =
|
|
2806
|
+
let workerSystemPrompt =
|
|
2807
|
+
"You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2624
2808
|
let workerSegmentPrompt = "";
|
|
2625
2809
|
try {
|
|
2626
2810
|
const basePrompt = loadBaseAgentPrompt("task-worker");
|
|
@@ -2635,7 +2819,9 @@ export async function executeLaneV2(
|
|
|
2635
2819
|
// Load segment-scoped prompt overlay (appended when isSegmentScoped)
|
|
2636
2820
|
const segPrompt = loadBaseAgentPrompt("task-worker-segment");
|
|
2637
2821
|
if (segPrompt) workerSegmentPrompt = segPrompt;
|
|
2638
|
-
} catch {
|
|
2822
|
+
} catch {
|
|
2823
|
+
/* use default */
|
|
2824
|
+
}
|
|
2639
2825
|
|
|
2640
2826
|
execLog(laneId, "LANE", `starting Runtime V2 execution of ${lane.tasks.length} task(s)`, {
|
|
2641
2827
|
worktree: lane.worktreePath,
|
|
@@ -2647,16 +2833,26 @@ export async function executeLaneV2(
|
|
|
2647
2833
|
// this lane number is lifted before new alerts begin to flow.
|
|
2648
2834
|
if (onLaneRespawned) {
|
|
2649
2835
|
try {
|
|
2650
|
-
onLaneRespawned(
|
|
2836
|
+
onLaneRespawned(
|
|
2837
|
+
lane.laneNumber,
|
|
2838
|
+
buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
|
|
2839
|
+
batchId,
|
|
2840
|
+
);
|
|
2651
2841
|
} catch (err) {
|
|
2652
|
-
execLog(
|
|
2842
|
+
execLog(
|
|
2843
|
+
laneId,
|
|
2844
|
+
"LANE",
|
|
2845
|
+
`lane-respawned callback failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2846
|
+
);
|
|
2653
2847
|
}
|
|
2654
2848
|
}
|
|
2655
2849
|
|
|
2656
2850
|
for (const task of lane.tasks) {
|
|
2657
2851
|
const taskSegmentId = task.task.activeSegmentId ?? null;
|
|
2658
2852
|
if (shouldSkipRemaining || pauseSignal.paused) {
|
|
2659
|
-
const reason = pauseSignal.paused
|
|
2853
|
+
const reason = pauseSignal.paused
|
|
2854
|
+
? "Skipped due to pause signal"
|
|
2855
|
+
: "Skipped due to prior task failure in lane";
|
|
2660
2856
|
outcomes.push({
|
|
2661
2857
|
taskId: task.taskId,
|
|
2662
2858
|
status: "skipped",
|
|
@@ -2674,10 +2870,12 @@ export async function executeLaneV2(
|
|
|
2674
2870
|
// Build execution unit
|
|
2675
2871
|
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
2676
2872
|
|
|
2677
|
-
const rawAutonomy = String(
|
|
2873
|
+
const rawAutonomy = String(
|
|
2874
|
+
extraEnvVars?.TASKPLANE_SUPERVISOR_AUTONOMY ?? "autonomous",
|
|
2875
|
+
).toLowerCase();
|
|
2678
2876
|
const supervisorAutonomy: LaneRunnerConfig["supervisorAutonomy"] =
|
|
2679
|
-
|
|
2680
|
-
? rawAutonomy as LaneRunnerConfig["supervisorAutonomy"]
|
|
2877
|
+
rawAutonomy === "interactive" || rawAutonomy === "supervised" || rawAutonomy === "autonomous"
|
|
2878
|
+
? (rawAutonomy as LaneRunnerConfig["supervisorAutonomy"])
|
|
2681
2879
|
: "autonomous";
|
|
2682
2880
|
|
|
2683
2881
|
const laneRunnerConfig: LaneRunnerConfig = {
|
|
@@ -2701,12 +2899,26 @@ export async function executeLaneV2(
|
|
|
2701
2899
|
reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
|
|
2702
2900
|
// TP-180: Extension exclusion lists from config
|
|
2703
2901
|
workerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS),
|
|
2704
|
-
reviewerExcludeExtensions: parseJsonArrayEnv(
|
|
2902
|
+
reviewerExcludeExtensions: parseJsonArrayEnv(
|
|
2903
|
+
extraEnvVars?.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS,
|
|
2904
|
+
),
|
|
2705
2905
|
supervisorAutonomy,
|
|
2706
|
-
|
|
2906
|
+
// TP-195: replaced `config.project?.name` (no `project` field on
|
|
2907
|
+
// `OrchestratorConfig`; always undefined) with the env-var read
|
|
2908
|
+
// already used elsewhere in the codebase (lane-runner.ts:668 sets
|
|
2909
|
+
// `TASKPLANE_PROJECT_NAME` from the same source). When the env
|
|
2910
|
+
// var is unset, falls through to the same `"project"` literal as
|
|
2911
|
+
// before — behavior-neutral.
|
|
2912
|
+
projectName: extraEnvVars?.TASKPLANE_PROJECT_NAME || "project",
|
|
2707
2913
|
maxIterations: 20,
|
|
2708
2914
|
noProgressLimit: 3,
|
|
2709
|
-
|
|
2915
|
+
// TP-195: read the canonical `max_worker_minutes` field (snake_case
|
|
2916
|
+
// per `OrchestratorConfig.failure` in types.ts). The previous code
|
|
2917
|
+
// read a non-existent `maxWorkerMinutes` camelCase alias — always
|
|
2918
|
+
// undefined — silently ignoring any operator-set value. Honoring
|
|
2919
|
+
// the config is the intended behavior; default of 120 preserved
|
|
2920
|
+
// when the field is unset.
|
|
2921
|
+
maxWorkerMinutes: config.failure?.max_worker_minutes || 120,
|
|
2710
2922
|
warnPercent: 85,
|
|
2711
2923
|
killPercent: 95,
|
|
2712
2924
|
onSupervisorAlert,
|
|
@@ -2811,13 +3023,22 @@ export async function executeLaneV2(
|
|
|
2811
3023
|
progress: null,
|
|
2812
3024
|
updatedAt: Date.now(),
|
|
2813
3025
|
};
|
|
2814
|
-
writeLaneSnapshot(
|
|
3026
|
+
writeLaneSnapshot(
|
|
3027
|
+
stateRoot,
|
|
3028
|
+
batchId,
|
|
3029
|
+
lane.laneNumber,
|
|
3030
|
+
spawnFailureSnapshot as unknown as Record<string, unknown>,
|
|
3031
|
+
);
|
|
2815
3032
|
} catch (snapErr) {
|
|
2816
3033
|
// Best effort — if the snapshot write fails, the monitor's
|
|
2817
3034
|
// 30s-staleness fallback (snap with old updatedAt) eventually
|
|
2818
3035
|
// kicks in via the registry liveness check. Log so this is
|
|
2819
3036
|
// visible in operator diagnostics, but do NOT throw.
|
|
2820
|
-
execLog(
|
|
3037
|
+
execLog(
|
|
3038
|
+
laneId,
|
|
3039
|
+
task.taskId,
|
|
3040
|
+
`spawn-failure snapshot write failed (non-fatal): ${snapErr instanceof Error ? snapErr.message : String(snapErr)}`,
|
|
3041
|
+
);
|
|
2821
3042
|
}
|
|
2822
3043
|
|
|
2823
3044
|
shouldSkipRemaining = true;
|
|
@@ -2825,8 +3046,8 @@ export async function executeLaneV2(
|
|
|
2825
3046
|
}
|
|
2826
3047
|
|
|
2827
3048
|
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");
|
|
3049
|
+
const succeeded = outcomes.every((o) => o.status === "succeeded");
|
|
3050
|
+
const failed = outcomes.some((o) => o.status === "failed" || o.status === "stalled");
|
|
2830
3051
|
|
|
2831
3052
|
return {
|
|
2832
3053
|
laneNumber: lane.laneNumber,
|
|
@@ -2839,4 +3060,3 @@ export async function executeLaneV2(
|
|
|
2839
3060
|
}
|
|
2840
3061
|
|
|
2841
3062
|
// ── /orch Command — Full Execution (Step 5) ─────────────────────────
|
|
2842
|
-
|