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,27 +2,69 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
|
|
|
2
2
|
import { Type } from "@mariozechner/pi-ai";
|
|
3
3
|
|
|
4
4
|
import { execSync, execFileSync } from "child_process";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
writeFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
statSync,
|
|
13
|
+
createWriteStream,
|
|
14
|
+
renameSync,
|
|
15
|
+
} from "fs";
|
|
6
16
|
import { join, dirname } from "path";
|
|
7
17
|
import { fileURLToPath } from "url";
|
|
8
18
|
import { fork, type ChildProcess } from "child_process";
|
|
9
19
|
|
|
10
20
|
// Direct imports — avoid barrel (index.ts) to prevent loading the entire module graph.
|
|
11
21
|
// Each import targets the specific module where the symbol is defined.
|
|
12
|
-
import {
|
|
13
|
-
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_ORCHESTRATOR_CONFIG,
|
|
24
|
+
DEFAULT_TASK_RUNNER_CONFIG,
|
|
25
|
+
FATAL_DISCOVERY_CODES,
|
|
26
|
+
StateFileError,
|
|
27
|
+
WorkspaceConfigError,
|
|
28
|
+
freshOrchBatchState,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
import type {
|
|
31
|
+
AbortMode,
|
|
32
|
+
ExecutionContext,
|
|
33
|
+
MonitorState,
|
|
34
|
+
OrchestratorConfig,
|
|
35
|
+
PersistedBatchState,
|
|
36
|
+
TaskRunnerConfig,
|
|
37
|
+
} from "./types.ts";
|
|
14
38
|
import { ORCH_MESSAGES, computeIntegrateCleanupResult } from "./messages.ts";
|
|
15
39
|
import type { IntegrateCleanupRepoFindings } from "./messages.ts";
|
|
16
40
|
import { computeWaveAssignments } from "./waves.ts";
|
|
17
41
|
import { createOrchWidget, formatDependencyGraph, formatWavePlan } from "./formatting.ts";
|
|
18
|
-
import {
|
|
19
|
-
|
|
42
|
+
import {
|
|
43
|
+
deleteBatchState,
|
|
44
|
+
loadBatchState,
|
|
45
|
+
saveBatchState,
|
|
46
|
+
detectOrphanSessions,
|
|
47
|
+
updateBatchHistoryIntegration,
|
|
48
|
+
} from "./persistence.ts";
|
|
49
|
+
import {
|
|
50
|
+
deleteStaleBranches,
|
|
51
|
+
listWorktrees,
|
|
52
|
+
resolveWorktreeBasePath,
|
|
53
|
+
formatPreflightResults,
|
|
54
|
+
runPreflight,
|
|
55
|
+
} from "./worktree.ts";
|
|
20
56
|
import { computeTransitiveDependents, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
21
57
|
import { executeOrchBatch } from "./engine.ts";
|
|
22
58
|
import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
|
|
23
59
|
import { formatOrchSessions, listOrchSessions } from "./sessions.ts";
|
|
24
60
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
25
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
hasConfigFiles,
|
|
63
|
+
resolveConfigRoot,
|
|
64
|
+
loadOrchestratorConfig,
|
|
65
|
+
loadSupervisorConfig,
|
|
66
|
+
loadTaskRunnerConfig,
|
|
67
|
+
} from "./config.ts";
|
|
26
68
|
import { resolveOperatorId } from "./naming.ts";
|
|
27
69
|
import { reconstructAllocatedLanes, resumeOrchBatch } from "./resume.ts";
|
|
28
70
|
import { buildExecutionContext } from "./workspace.ts";
|
|
@@ -30,9 +72,20 @@ import { openSettingsTui } from "./settings-tui.ts";
|
|
|
30
72
|
import { loadProjectConfig } from "./config-loader.ts";
|
|
31
73
|
import { runMigrations } from "./migrations.ts";
|
|
32
74
|
import { executeAbort } from "./abort.ts";
|
|
33
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
serializeWorkspaceConfig,
|
|
77
|
+
applySerializedState,
|
|
78
|
+
deserializeWorkspaceConfig,
|
|
79
|
+
} from "./engine-worker.ts";
|
|
34
80
|
import type { EngineWorkerData, WorkerToMainMessage } from "./engine-worker.ts";
|
|
35
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
cleanupPostIntegrate,
|
|
83
|
+
formatPostIntegrateCleanup,
|
|
84
|
+
sweepStaleArtifacts,
|
|
85
|
+
formatPreflightSweep,
|
|
86
|
+
rotateSupervisorLogs,
|
|
87
|
+
formatLogRotation,
|
|
88
|
+
} from "./cleanup.ts";
|
|
36
89
|
import {
|
|
37
90
|
writeMailboxMessage,
|
|
38
91
|
readOutbox,
|
|
@@ -65,7 +118,13 @@ import {
|
|
|
65
118
|
presentBatchSummary,
|
|
66
119
|
resolveModelFromString,
|
|
67
120
|
} from "./supervisor.ts";
|
|
68
|
-
import type {
|
|
121
|
+
import type {
|
|
122
|
+
SupervisorConfig,
|
|
123
|
+
SupervisorRoutingContext,
|
|
124
|
+
IntegrationExecutor,
|
|
125
|
+
CiDeps,
|
|
126
|
+
SummaryDeps,
|
|
127
|
+
} from "./supervisor.ts";
|
|
69
128
|
|
|
70
129
|
// ── Integrate Args Parsing ────────────────────────────────────────────
|
|
71
130
|
|
|
@@ -118,7 +177,9 @@ export function parseIntegrateArgs(raw: string | undefined): IntegrateArgs | { e
|
|
|
118
177
|
if (hasPr) mode = "pr";
|
|
119
178
|
|
|
120
179
|
if (positionals.length > 1) {
|
|
121
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
error: `Expected at most one branch argument, got ${positionals.length}: ${positionals.join(", ")}`,
|
|
182
|
+
};
|
|
122
183
|
}
|
|
123
184
|
|
|
124
185
|
return {
|
|
@@ -153,7 +214,10 @@ export function parseResumeArgs(raw: string | undefined): ResumeArgs | { error:
|
|
|
153
214
|
if (token === "--force") {
|
|
154
215
|
force = true;
|
|
155
216
|
} else if (token === "--help") {
|
|
156
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
error:
|
|
219
|
+
"Usage: /orch-resume [--force]\n\n --force Resume from stopped or failed state (runs pre-resume diagnostics first)",
|
|
220
|
+
};
|
|
157
221
|
} else if (token.startsWith("--")) {
|
|
158
222
|
return { error: `Unknown flag: ${token}\n\nUsage: /orch-resume [--force]` };
|
|
159
223
|
} else {
|
|
@@ -250,13 +314,14 @@ export function resolveIntegrationContext(
|
|
|
250
314
|
}
|
|
251
315
|
} catch (err: unknown) {
|
|
252
316
|
// Capture the error but don't return yet — user may have provided a branch arg
|
|
253
|
-
const msg =
|
|
254
|
-
|
|
255
|
-
?
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
317
|
+
const msg =
|
|
318
|
+
err instanceof StateFileError
|
|
319
|
+
? err.code === "STATE_FILE_IO_ERROR"
|
|
320
|
+
? `Could not read batch state file: ${err.message}`
|
|
321
|
+
: err.code === "STATE_FILE_PARSE_ERROR"
|
|
322
|
+
? `Batch state file contains invalid JSON: ${err.message}`
|
|
323
|
+
: `Batch state file has invalid schema: ${err.message}`
|
|
324
|
+
: `Unexpected error loading batch state: ${(err as Error).message}`;
|
|
260
325
|
if (!parsed.orchBranchArg) {
|
|
261
326
|
return {
|
|
262
327
|
error: `⚠️ ${msg}\nYou can specify the orch branch directly: /orch-integrate <orch-branch>`,
|
|
@@ -289,7 +354,7 @@ export function resolveIntegrationContext(
|
|
|
289
354
|
return {
|
|
290
355
|
error:
|
|
291
356
|
`❌ No batch state found and multiple orch branches exist:\n` +
|
|
292
|
-
candidates.map(b => ` • ${b}`).join("\n") +
|
|
357
|
+
candidates.map((b) => ` • ${b}`).join("\n") +
|
|
293
358
|
`\n\nSpecify which branch to integrate: /orch-integrate <orch-branch>`,
|
|
294
359
|
severity: "error",
|
|
295
360
|
};
|
|
@@ -396,15 +461,18 @@ export function withPreservedBatchHistory<T>(stateRoot: string, operation: () =>
|
|
|
396
461
|
try {
|
|
397
462
|
return operation();
|
|
398
463
|
} finally {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
464
|
+
// Conditional cleanup (no `return` in finally — Biome lint/correctness/noUnsafeFinally).
|
|
465
|
+
// Restore the snapshot only when one was captured pre-operation.
|
|
466
|
+
if (snapshot) {
|
|
467
|
+
try {
|
|
468
|
+
const dir = dirname(snapshot.filePath);
|
|
469
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
470
|
+
const tmpPath = snapshot.filePath + ".tmp";
|
|
471
|
+
writeFileSync(tmpPath, snapshot.raw);
|
|
472
|
+
renameSync(tmpPath, snapshot.filePath);
|
|
473
|
+
} catch {
|
|
474
|
+
// Best effort only — never block integration completion.
|
|
475
|
+
}
|
|
408
476
|
}
|
|
409
477
|
}
|
|
410
478
|
}
|
|
@@ -455,7 +523,13 @@ export function executeIntegration(
|
|
|
455
523
|
let stashed = false;
|
|
456
524
|
const statusCheck = deps.runGit(["status", "--porcelain"]);
|
|
457
525
|
if (statusCheck.ok && statusCheck.stdout.trim()) {
|
|
458
|
-
deps.runGit([
|
|
526
|
+
deps.runGit([
|
|
527
|
+
"stash",
|
|
528
|
+
"push",
|
|
529
|
+
"--include-untracked",
|
|
530
|
+
"-m",
|
|
531
|
+
`orch-integrate-autostash-${batchId}`,
|
|
532
|
+
]);
|
|
459
533
|
stashed = true;
|
|
460
534
|
}
|
|
461
535
|
|
|
@@ -468,9 +542,10 @@ export function executeIntegration(
|
|
|
468
542
|
|
|
469
543
|
if (!result.ok) {
|
|
470
544
|
// TP-052: Include branch protection hint when merge fails
|
|
471
|
-
const protectionHint =
|
|
472
|
-
|
|
473
|
-
|
|
545
|
+
const protectionHint =
|
|
546
|
+
result.stderr.includes("protected") || result.stderr.includes("permission")
|
|
547
|
+
? `\n\n 💡 If the branch is protected, use --pr to create a pull request.`
|
|
548
|
+
: "";
|
|
474
549
|
return {
|
|
475
550
|
success: false,
|
|
476
551
|
integratedLocally: false,
|
|
@@ -505,7 +580,13 @@ export function executeIntegration(
|
|
|
505
580
|
let mergeStashed = false;
|
|
506
581
|
const mergeStatusCheck = deps.runGit(["status", "--porcelain"]);
|
|
507
582
|
if (mergeStatusCheck.ok && mergeStatusCheck.stdout.trim()) {
|
|
508
|
-
deps.runGit([
|
|
583
|
+
deps.runGit([
|
|
584
|
+
"stash",
|
|
585
|
+
"push",
|
|
586
|
+
"--include-untracked",
|
|
587
|
+
"-m",
|
|
588
|
+
`orch-integrate-autostash-${batchId}`,
|
|
589
|
+
]);
|
|
509
590
|
mergeStashed = true;
|
|
510
591
|
}
|
|
511
592
|
|
|
@@ -517,9 +598,10 @@ export function executeIntegration(
|
|
|
517
598
|
|
|
518
599
|
if (!result.ok) {
|
|
519
600
|
// TP-052: Include branch protection hint when merge fails
|
|
520
|
-
const mergeProtectionHint =
|
|
521
|
-
|
|
522
|
-
|
|
601
|
+
const mergeProtectionHint =
|
|
602
|
+
result.stderr.includes("protected") || result.stderr.includes("permission")
|
|
603
|
+
? `\n\n 💡 If the branch is protected, use --pr to create a pull request.`
|
|
604
|
+
: "";
|
|
523
605
|
return {
|
|
524
606
|
success: false,
|
|
525
607
|
integratedLocally: false,
|
|
@@ -558,14 +640,16 @@ export function executeIntegration(
|
|
|
558
640
|
}
|
|
559
641
|
|
|
560
642
|
// Step 2: Create pull request via gh CLI
|
|
561
|
-
const prTitle = batchId
|
|
562
|
-
? `Integrate orch batch ${batchId}`
|
|
563
|
-
: `Integrate ${orchBranch}`;
|
|
643
|
+
const prTitle = batchId ? `Integrate orch batch ${batchId}` : `Integrate ${orchBranch}`;
|
|
564
644
|
const ghResult = deps.runCommand("gh", [
|
|
565
|
-
"pr",
|
|
566
|
-
"
|
|
567
|
-
"--
|
|
568
|
-
|
|
645
|
+
"pr",
|
|
646
|
+
"create",
|
|
647
|
+
"--base",
|
|
648
|
+
currentBranch,
|
|
649
|
+
"--head",
|
|
650
|
+
orchBranch,
|
|
651
|
+
"--title",
|
|
652
|
+
prTitle,
|
|
569
653
|
"--fill",
|
|
570
654
|
]);
|
|
571
655
|
if (!ghResult.ok) {
|
|
@@ -711,8 +795,10 @@ export function collectRepoCleanupFindings(
|
|
|
711
795
|
// 1. Stale lane worktrees — check for any worktrees belonging to this operator+batch
|
|
712
796
|
try {
|
|
713
797
|
const wts = listWorktrees(worktreePrefix, repoRoot, opId, batchId);
|
|
714
|
-
findings.staleWorktrees = wts.map(wt => wt.path);
|
|
715
|
-
} catch {
|
|
798
|
+
findings.staleWorktrees = wts.map((wt) => wt.path);
|
|
799
|
+
} catch {
|
|
800
|
+
/* best effort — git worktree list may fail in unusual states */
|
|
801
|
+
}
|
|
716
802
|
|
|
717
803
|
// 2. Lane branches — task/{opId}-lane-* and saved/task/{opId}-lane-*
|
|
718
804
|
try {
|
|
@@ -720,7 +806,7 @@ export function collectRepoCleanupFindings(
|
|
|
720
806
|
if (branchResult.ok && branchResult.stdout.trim()) {
|
|
721
807
|
findings.staleLaneBranches = branchResult.stdout
|
|
722
808
|
.split("\n")
|
|
723
|
-
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
809
|
+
.map((b) => b.replace(/^\*?\s+/, "").trim())
|
|
724
810
|
.filter(Boolean);
|
|
725
811
|
}
|
|
726
812
|
// Also detect saved lane branches (preserved refs from worktree removal)
|
|
@@ -728,11 +814,13 @@ export function collectRepoCleanupFindings(
|
|
|
728
814
|
if (savedBranchResult.ok && savedBranchResult.stdout.trim()) {
|
|
729
815
|
const savedBranches = savedBranchResult.stdout
|
|
730
816
|
.split("\n")
|
|
731
|
-
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
817
|
+
.map((b) => b.replace(/^\*?\s+/, "").trim())
|
|
732
818
|
.filter(Boolean);
|
|
733
819
|
findings.staleLaneBranches.push(...savedBranches);
|
|
734
820
|
}
|
|
735
|
-
} catch {
|
|
821
|
+
} catch {
|
|
822
|
+
/* best effort */
|
|
823
|
+
}
|
|
736
824
|
|
|
737
825
|
// 3. Orch branch — check if the specific orch branch still exists
|
|
738
826
|
// Skip in PR mode where the orch branch is intentionally preserved for the PR.
|
|
@@ -742,7 +830,9 @@ export function collectRepoCleanupFindings(
|
|
|
742
830
|
if (orchCheck.ok) {
|
|
743
831
|
findings.staleOrchBranches = [orchBranch];
|
|
744
832
|
}
|
|
745
|
-
} catch {
|
|
833
|
+
} catch {
|
|
834
|
+
/* best effort */
|
|
835
|
+
}
|
|
746
836
|
}
|
|
747
837
|
|
|
748
838
|
// 4. Autostash entries — same patterns as dropBatchAutostash
|
|
@@ -763,7 +853,9 @@ export function collectRepoCleanupFindings(
|
|
|
763
853
|
}
|
|
764
854
|
}
|
|
765
855
|
}
|
|
766
|
-
} catch {
|
|
856
|
+
} catch {
|
|
857
|
+
/* best effort */
|
|
858
|
+
}
|
|
767
859
|
}
|
|
768
860
|
|
|
769
861
|
// 5. Non-empty .worktrees/ containers (subdirectory mode only)
|
|
@@ -776,7 +868,9 @@ export function collectRepoCleanupFindings(
|
|
|
776
868
|
findings.nonEmptyWorktreeContainers = [basePath];
|
|
777
869
|
}
|
|
778
870
|
}
|
|
779
|
-
} catch {
|
|
871
|
+
} catch {
|
|
872
|
+
/* best effort */
|
|
873
|
+
}
|
|
780
874
|
}
|
|
781
875
|
|
|
782
876
|
return findings;
|
|
@@ -850,8 +944,14 @@ export function validateModelAvailability(
|
|
|
850
944
|
agentModels?: { workerModel?: string; reviewerModel?: string },
|
|
851
945
|
): ModelCheckResult[] {
|
|
852
946
|
const entries: ModelCheckEntry[] = [
|
|
853
|
-
{
|
|
854
|
-
|
|
947
|
+
{
|
|
948
|
+
role: "Worker",
|
|
949
|
+
modelStr: agentModels?.workerModel ?? (runnerConfig as any).worker?.model ?? "",
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
role: "Reviewer",
|
|
953
|
+
modelStr: agentModels?.reviewerModel ?? (runnerConfig as any).reviewer?.model ?? "",
|
|
954
|
+
},
|
|
855
955
|
{ role: "Merger", modelStr: orchConfig.merge?.model ?? "" },
|
|
856
956
|
{ role: "Supervisor", modelStr: supervisorConfig.model ?? "" },
|
|
857
957
|
];
|
|
@@ -951,7 +1051,7 @@ export function startBatchAsync(
|
|
|
951
1051
|
}
|
|
952
1052
|
ctx.ui.notify(
|
|
953
1053
|
`❌ Engine crashed with unhandled error: ${errMsg}\n` +
|
|
954
|
-
|
|
1054
|
+
` Batch ${batchState.batchId} marked as failed.`,
|
|
955
1055
|
"error",
|
|
956
1056
|
);
|
|
957
1057
|
updateWidget();
|
|
@@ -1047,40 +1147,53 @@ export function startBatchInWorker(
|
|
|
1047
1147
|
const wsConfig = wkData.workspaceConfig
|
|
1048
1148
|
? deserializeWorkspaceConfig(wkData.workspaceConfig)
|
|
1049
1149
|
: undefined;
|
|
1050
|
-
const fallbackFn =
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1150
|
+
const fallbackFn =
|
|
1151
|
+
wkData.mode === "resume"
|
|
1152
|
+
? () =>
|
|
1153
|
+
resumeOrchBatch(
|
|
1154
|
+
wkData.orchConfig,
|
|
1155
|
+
wkData.runnerConfig,
|
|
1156
|
+
wkData.cwd,
|
|
1157
|
+
batchState,
|
|
1158
|
+
(msg: string, lvl: "info" | "warning" | "error") => {
|
|
1159
|
+
ctx.ui.notify(msg, lvl);
|
|
1160
|
+
updateWidget();
|
|
1161
|
+
},
|
|
1162
|
+
(monState: import("./types.ts").MonitorState) => {
|
|
1163
|
+
onMonitorUpdate?.(monState);
|
|
1164
|
+
},
|
|
1165
|
+
wsConfig,
|
|
1166
|
+
wkData.workspaceRoot,
|
|
1167
|
+
wkData.agentRoot,
|
|
1168
|
+
wkData.force ?? false,
|
|
1169
|
+
onSupervisorAlert ?? null,
|
|
1170
|
+
wkData.supervisorAutonomy ?? "autonomous",
|
|
1171
|
+
null, // onLaneTerminated — main-thread fallback path; alerts are local-only
|
|
1172
|
+
null, // onLaneRespawned — main-thread fallback path; suppression maps stay clear
|
|
1173
|
+
)
|
|
1174
|
+
: () =>
|
|
1175
|
+
executeOrchBatch(
|
|
1176
|
+
wkData.args ?? "",
|
|
1177
|
+
wkData.orchConfig,
|
|
1178
|
+
wkData.runnerConfig,
|
|
1179
|
+
wkData.cwd,
|
|
1180
|
+
batchState,
|
|
1181
|
+
(msg: string, lvl: "info" | "warning" | "error") => {
|
|
1182
|
+
ctx.ui.notify(msg, lvl);
|
|
1183
|
+
updateWidget();
|
|
1184
|
+
},
|
|
1185
|
+
(monState: import("./types.ts").MonitorState) => {
|
|
1186
|
+
onMonitorUpdate?.(monState);
|
|
1187
|
+
},
|
|
1188
|
+
wsConfig,
|
|
1189
|
+
wkData.workspaceRoot,
|
|
1190
|
+
wkData.agentRoot,
|
|
1191
|
+
null, // onEngineEvent
|
|
1192
|
+
onSupervisorAlert ?? null,
|
|
1193
|
+
wkData.supervisorAutonomy ?? "autonomous",
|
|
1194
|
+
null, // onLaneTerminated — main-thread fallback path
|
|
1195
|
+
null, // onLaneRespawned — main-thread fallback path
|
|
1196
|
+
);
|
|
1084
1197
|
startBatchAsync(fallbackFn, batchState, ctx, updateWidget, onTerminal);
|
|
1085
1198
|
return null;
|
|
1086
1199
|
}
|
|
@@ -1092,7 +1205,9 @@ export function startBatchInWorker(
|
|
|
1092
1205
|
let stderrBatchId = toSafeBatchId(batchState.batchId || pendingBatchId);
|
|
1093
1206
|
let stderrLogPath = join(telemetryDir, `${stderrBatchId}-engine-worker-stderr.log`);
|
|
1094
1207
|
let stderrLogStream = createWriteStream(stderrLogPath, { flags: "a" });
|
|
1095
|
-
stderrLogStream.on("error", () => {
|
|
1208
|
+
stderrLogStream.on("error", () => {
|
|
1209
|
+
/* non-fatal: telemetry stream */
|
|
1210
|
+
});
|
|
1096
1211
|
let stderrTailBuffer = "";
|
|
1097
1212
|
|
|
1098
1213
|
const appendStderr = (chunk: Buffer | string) => {
|
|
@@ -1125,7 +1240,9 @@ export function startBatchInWorker(
|
|
|
1125
1240
|
stderrBatchId = resolvedBatchId;
|
|
1126
1241
|
stderrLogPath = nextPath;
|
|
1127
1242
|
stderrLogStream = createWriteStream(stderrLogPath, { flags: "a" });
|
|
1128
|
-
stderrLogStream.on("error", () => {
|
|
1243
|
+
stderrLogStream.on("error", () => {
|
|
1244
|
+
/* non-fatal: telemetry stream */
|
|
1245
|
+
});
|
|
1129
1246
|
};
|
|
1130
1247
|
|
|
1131
1248
|
const readStderrTail = (lineCount = 25): string => {
|
|
@@ -1135,7 +1252,9 @@ export function startBatchInWorker(
|
|
|
1135
1252
|
if (!content) {
|
|
1136
1253
|
try {
|
|
1137
1254
|
if (existsSync(stderrLogPath)) content = readFileSync(stderrLogPath, "utf-8");
|
|
1138
|
-
} catch {
|
|
1255
|
+
} catch {
|
|
1256
|
+
/* fallback: empty */
|
|
1257
|
+
}
|
|
1139
1258
|
}
|
|
1140
1259
|
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
1141
1260
|
if (lines.length === 0) return "(no stderr output captured)";
|
|
@@ -1218,8 +1337,8 @@ export function startBatchInWorker(
|
|
|
1218
1337
|
}
|
|
1219
1338
|
ctx.ui.notify(
|
|
1220
1339
|
`❌ Engine crashed with unhandled error${sourceLabel}: ${msg.message}\n` +
|
|
1221
|
-
|
|
1222
|
-
|
|
1340
|
+
(stackLine ? ` ${stackLine}\n` : "") +
|
|
1341
|
+
` Batch ${batchState.batchId} marked as failed.`,
|
|
1223
1342
|
"error",
|
|
1224
1343
|
);
|
|
1225
1344
|
// Alert supervisor — this is the PRIMARY notification path for engine
|
|
@@ -1238,20 +1357,27 @@ export function startBatchInWorker(
|
|
|
1238
1357
|
` - orch_status() to inspect state\n` +
|
|
1239
1358
|
` - orch_resume(force=true) to retry from last checkpoint`,
|
|
1240
1359
|
context: {
|
|
1241
|
-
batchProgress:
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1360
|
+
batchProgress:
|
|
1361
|
+
batchState.totalTasks > 0
|
|
1362
|
+
? {
|
|
1363
|
+
succeededTasks: batchState.succeededTasks,
|
|
1364
|
+
failedTasks: batchState.failedTasks,
|
|
1365
|
+
skippedTasks: batchState.skippedTasks,
|
|
1366
|
+
blockedTasks: batchState.blockedTasks,
|
|
1367
|
+
totalTasks: batchState.totalTasks,
|
|
1368
|
+
currentWave: batchState.currentWaveIndex + 1,
|
|
1369
|
+
totalWaves: batchState.taskLevelWaveCount ?? batchState.totalWaves,
|
|
1370
|
+
}
|
|
1371
|
+
: undefined,
|
|
1250
1372
|
},
|
|
1251
1373
|
});
|
|
1252
1374
|
// Persist failed state to disk so dashboard/resume see it.
|
|
1253
1375
|
// The engine-worker is dead and can't persist — we must do it here.
|
|
1254
|
-
try {
|
|
1376
|
+
try {
|
|
1377
|
+
saveBatchState(JSON.stringify(batchState, null, 2), wkData.cwd);
|
|
1378
|
+
} catch {
|
|
1379
|
+
/* best effort */
|
|
1380
|
+
}
|
|
1255
1381
|
updateWidget();
|
|
1256
1382
|
break;
|
|
1257
1383
|
}
|
|
@@ -1267,8 +1393,7 @@ export function startBatchInWorker(
|
|
|
1267
1393
|
batchState.errors.push(`Engine process error: ${err.message}`);
|
|
1268
1394
|
}
|
|
1269
1395
|
ctx.ui.notify(
|
|
1270
|
-
`❌ Engine process error: ${err.message}\n` +
|
|
1271
|
-
` Batch ${batchState.batchId} marked as failed.`,
|
|
1396
|
+
`❌ Engine process error: ${err.message}\n` + ` Batch ${batchState.batchId} marked as failed.`,
|
|
1272
1397
|
"error",
|
|
1273
1398
|
);
|
|
1274
1399
|
updateWidget();
|
|
@@ -1284,15 +1409,18 @@ export function startBatchInWorker(
|
|
|
1284
1409
|
` - orch_status() to inspect state\n` +
|
|
1285
1410
|
` - orch_resume(force=true) to retry from last checkpoint`,
|
|
1286
1411
|
context: {
|
|
1287
|
-
batchProgress:
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1412
|
+
batchProgress:
|
|
1413
|
+
batchState.totalTasks > 0
|
|
1414
|
+
? {
|
|
1415
|
+
succeededTasks: batchState.succeededTasks,
|
|
1416
|
+
failedTasks: batchState.failedTasks,
|
|
1417
|
+
skippedTasks: batchState.skippedTasks,
|
|
1418
|
+
blockedTasks: batchState.blockedTasks,
|
|
1419
|
+
totalTasks: batchState.totalTasks,
|
|
1420
|
+
currentWave: batchState.currentWaveIndex + 1,
|
|
1421
|
+
totalWaves: batchState.taskLevelWaveCount ?? batchState.totalWaves,
|
|
1422
|
+
}
|
|
1423
|
+
: undefined,
|
|
1296
1424
|
},
|
|
1297
1425
|
});
|
|
1298
1426
|
settle();
|
|
@@ -1313,10 +1441,7 @@ export function startBatchInWorker(
|
|
|
1313
1441
|
batchState.endedAt = Date.now();
|
|
1314
1442
|
batchState.errors.push(`Engine process exited with code ${code}`);
|
|
1315
1443
|
}
|
|
1316
|
-
ctx.ui.notify(
|
|
1317
|
-
`❌ Engine process exited unexpectedly (code ${code}).`,
|
|
1318
|
-
"error",
|
|
1319
|
-
);
|
|
1444
|
+
ctx.ui.notify(`❌ Engine process exited unexpectedly (code ${code}).`, "error");
|
|
1320
1445
|
updateWidget();
|
|
1321
1446
|
// ── TP-076: Alert supervisor about unexpected engine exit ──
|
|
1322
1447
|
onSupervisorAlert?.({
|
|
@@ -1330,19 +1455,26 @@ export function startBatchInWorker(
|
|
|
1330
1455
|
` - orch_status() to inspect state\n` +
|
|
1331
1456
|
` - orch_resume(force=true) to retry from last checkpoint`,
|
|
1332
1457
|
context: {
|
|
1333
|
-
batchProgress:
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1458
|
+
batchProgress:
|
|
1459
|
+
batchState.totalTasks > 0
|
|
1460
|
+
? {
|
|
1461
|
+
succeededTasks: batchState.succeededTasks,
|
|
1462
|
+
failedTasks: batchState.failedTasks,
|
|
1463
|
+
skippedTasks: batchState.skippedTasks,
|
|
1464
|
+
blockedTasks: batchState.blockedTasks,
|
|
1465
|
+
totalTasks: batchState.totalTasks,
|
|
1466
|
+
currentWave: batchState.currentWaveIndex + 1,
|
|
1467
|
+
totalWaves: batchState.taskLevelWaveCount ?? batchState.totalWaves,
|
|
1468
|
+
}
|
|
1469
|
+
: undefined,
|
|
1342
1470
|
},
|
|
1343
1471
|
});
|
|
1344
1472
|
// Persist failed state to disk (engine is dead, can't persist itself)
|
|
1345
|
-
try {
|
|
1473
|
+
try {
|
|
1474
|
+
saveBatchState(JSON.stringify(batchState, null, 2), wkData.cwd);
|
|
1475
|
+
} catch {
|
|
1476
|
+
/* best effort */
|
|
1477
|
+
}
|
|
1346
1478
|
}
|
|
1347
1479
|
settle();
|
|
1348
1480
|
});
|
|
@@ -1367,7 +1499,11 @@ export function startBatchInWorker(
|
|
|
1367
1499
|
*
|
|
1368
1500
|
* @since TP-043 R002
|
|
1369
1501
|
*/
|
|
1370
|
-
export function buildIntegrationExecutor(
|
|
1502
|
+
export function buildIntegrationExecutor(
|
|
1503
|
+
repoRoot: string,
|
|
1504
|
+
opId?: string,
|
|
1505
|
+
stateRoot?: string,
|
|
1506
|
+
): IntegrationExecutor {
|
|
1371
1507
|
return (mode, context) => {
|
|
1372
1508
|
// Ensure we're on the base branch before integrating
|
|
1373
1509
|
const currentBranch = getCurrentBranch(repoRoot);
|
|
@@ -1406,16 +1542,24 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
|
|
|
1406
1542
|
}
|
|
1407
1543
|
},
|
|
1408
1544
|
deleteBatchState: () => {
|
|
1409
|
-
try {
|
|
1545
|
+
try {
|
|
1546
|
+
deleteBatchState(stateRoot ?? repoRoot);
|
|
1547
|
+
} catch {
|
|
1548
|
+
/* best effort */
|
|
1549
|
+
}
|
|
1410
1550
|
},
|
|
1411
1551
|
};
|
|
1412
1552
|
|
|
1413
1553
|
const effectiveStateRoot = stateRoot ?? repoRoot;
|
|
1414
1554
|
const result = withPreservedBatchHistory(effectiveStateRoot, () =>
|
|
1415
|
-
executeIntegration(
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1555
|
+
executeIntegration(
|
|
1556
|
+
mode as IntegrateMode,
|
|
1557
|
+
{
|
|
1558
|
+
...context,
|
|
1559
|
+
currentBranch: context.baseBranch,
|
|
1560
|
+
},
|
|
1561
|
+
deps,
|
|
1562
|
+
),
|
|
1419
1563
|
);
|
|
1420
1564
|
|
|
1421
1565
|
// TP-051: Clean up stale task/* and saved/* branches after successful integration.
|
|
@@ -1425,18 +1569,24 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
|
|
|
1425
1569
|
try {
|
|
1426
1570
|
deleteStaleBranches(repoRoot, opId, context.batchId);
|
|
1427
1571
|
dropBatchAutostash(repoRoot, context.batchId);
|
|
1428
|
-
} catch {
|
|
1572
|
+
} catch {
|
|
1573
|
+
/* best effort — don't fail integration for cleanup errors */
|
|
1574
|
+
}
|
|
1429
1575
|
|
|
1430
1576
|
// TP-065: Post-integrate artifact cleanup (Layer 1).
|
|
1431
1577
|
// Also runs on the supervisor auto-integration path.
|
|
1432
1578
|
try {
|
|
1433
1579
|
cleanupPostIntegrate(stateRoot ?? repoRoot, context.batchId);
|
|
1434
|
-
} catch {
|
|
1580
|
+
} catch {
|
|
1581
|
+
/* best effort — don't fail integration for cleanup errors */
|
|
1582
|
+
}
|
|
1435
1583
|
|
|
1436
1584
|
// TP-179: Write integratedAt to batch history before state is gone
|
|
1437
1585
|
try {
|
|
1438
1586
|
updateBatchHistoryIntegration(stateRoot ?? repoRoot, context.batchId, Date.now());
|
|
1439
|
-
} catch {
|
|
1587
|
+
} catch {
|
|
1588
|
+
/* best effort */
|
|
1589
|
+
}
|
|
1440
1590
|
}
|
|
1441
1591
|
|
|
1442
1592
|
return result;
|
|
@@ -1476,7 +1626,11 @@ export function buildCiDeps(repoRoot: string, stateRoot?: string): CiDeps {
|
|
|
1476
1626
|
},
|
|
1477
1627
|
runGit: (gitArgs: string[]) => runGit(gitArgs, repoRoot),
|
|
1478
1628
|
deleteBatchState: () => {
|
|
1479
|
-
try {
|
|
1629
|
+
try {
|
|
1630
|
+
deleteBatchState(stateRoot ?? repoRoot);
|
|
1631
|
+
} catch {
|
|
1632
|
+
/* best effort */
|
|
1633
|
+
}
|
|
1480
1634
|
},
|
|
1481
1635
|
};
|
|
1482
1636
|
}
|
|
@@ -1614,16 +1768,16 @@ export function detectOrchState(deps: OrchStateDetectionDeps): OrchStateDetectio
|
|
|
1614
1768
|
// Covers the case where batch-state.json was deleted but an orch branch remains.
|
|
1615
1769
|
const orchBranches = deps.listOrchBranches();
|
|
1616
1770
|
if (orchBranches.length > 0) {
|
|
1617
|
-
const branchList = orchBranches.map(b => `\`${b}\``).join(", ");
|
|
1771
|
+
const branchList = orchBranches.map((b) => `\`${b}\``).join(", ");
|
|
1618
1772
|
return {
|
|
1619
1773
|
state: "completed-batch",
|
|
1620
1774
|
orchBranch: orchBranches[0],
|
|
1621
1775
|
contextMessage:
|
|
1622
1776
|
orchBranches.length === 1
|
|
1623
1777
|
? `I found an orch branch (${branchList}) that hasn't been integrated yet. ` +
|
|
1624
|
-
|
|
1778
|
+
`Want me to integrate it, or would you like to start fresh?`
|
|
1625
1779
|
: `I found ${orchBranches.length} orch branches (${branchList}) that haven't been integrated. ` +
|
|
1626
|
-
|
|
1780
|
+
`Would you like to integrate one, or start fresh?`,
|
|
1627
1781
|
};
|
|
1628
1782
|
}
|
|
1629
1783
|
|
|
@@ -1699,7 +1853,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1699
1853
|
if (terminatedLanes.size === 0 && terminatedAgents.size === 0) return;
|
|
1700
1854
|
process.stderr.write(
|
|
1701
1855
|
`[taskplane:zombie-filter] cleared termination filter (reason: ${reason}, ` +
|
|
1702
|
-
|
|
1856
|
+
`lanes=${terminatedLanes.size}, agents=${terminatedAgents.size})\n`,
|
|
1703
1857
|
);
|
|
1704
1858
|
terminatedLanes.clear();
|
|
1705
1859
|
terminatedAgents.clear();
|
|
@@ -1752,7 +1906,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1752
1906
|
const ctx = alert.context;
|
|
1753
1907
|
if (!ctx) return false;
|
|
1754
1908
|
if (typeof ctx.laneNumber === "number" && terminatedLanes.has(ctx.laneNumber)) return true;
|
|
1755
|
-
if (typeof ctx.agentId === "string" && ctx.agentId && terminatedAgents.has(ctx.agentId))
|
|
1909
|
+
if (typeof ctx.agentId === "string" && ctx.agentId && terminatedAgents.has(ctx.agentId))
|
|
1910
|
+
return true;
|
|
1756
1911
|
return false;
|
|
1757
1912
|
};
|
|
1758
1913
|
|
|
@@ -1789,8 +1944,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1789
1944
|
// ── Command Guard ────────────────────────────────────────────────
|
|
1790
1945
|
|
|
1791
1946
|
function getExecCtxInitErrorMessage(): string {
|
|
1792
|
-
return
|
|
1793
|
-
|
|
1947
|
+
return (
|
|
1948
|
+
execCtxInitError ??
|
|
1949
|
+
"❌ Orchestrator not initialized. Startup failed before execution context was created.\nRestart the session after fixing configuration/setup issues."
|
|
1950
|
+
);
|
|
1794
1951
|
}
|
|
1795
1952
|
|
|
1796
1953
|
/**
|
|
@@ -1830,13 +1987,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1830
1987
|
const detection = detectOrchState({
|
|
1831
1988
|
hasConfig: () => hasConfigFiles(resolvedConfigRoot),
|
|
1832
1989
|
loadBatchState: () => {
|
|
1833
|
-
try {
|
|
1834
|
-
|
|
1990
|
+
try {
|
|
1991
|
+
return loadBatchState(stateRoot);
|
|
1992
|
+
} catch {
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1835
1995
|
},
|
|
1836
1996
|
listOrchBranches: () => {
|
|
1837
1997
|
const result = runGit(["branch", "--list", "orch/*"], repoRoot);
|
|
1838
1998
|
return result.ok
|
|
1839
|
-
? result.stdout
|
|
1999
|
+
? result.stdout
|
|
2000
|
+
.split("\n")
|
|
2001
|
+
.map((b) => b.replace(/^\*?\s+/, "").trim())
|
|
2002
|
+
.filter(Boolean)
|
|
1840
2003
|
: [];
|
|
1841
2004
|
},
|
|
1842
2005
|
countPendingTasks: () => {
|
|
@@ -1848,7 +2011,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1848
2011
|
workspaceConfig: execCtx.workspaceConfig,
|
|
1849
2012
|
});
|
|
1850
2013
|
return discovery.pending.size;
|
|
1851
|
-
} catch {
|
|
2014
|
+
} catch {
|
|
2015
|
+
return 0;
|
|
2016
|
+
}
|
|
1852
2017
|
},
|
|
1853
2018
|
});
|
|
1854
2019
|
|
|
@@ -1856,7 +2021,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1856
2021
|
if (detection.state === "active-batch") {
|
|
1857
2022
|
ctx.ui.notify(
|
|
1858
2023
|
`🔀 ${detection.contextMessage}\n\n` +
|
|
1859
|
-
|
|
2024
|
+
`Use /orch-status for full details, or /orch-pause to pause.`,
|
|
1860
2025
|
"info",
|
|
1861
2026
|
);
|
|
1862
2027
|
return;
|
|
@@ -1902,15 +2067,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1902
2067
|
if (!args?.trim()) {
|
|
1903
2068
|
ctx.ui.notify(
|
|
1904
2069
|
"Usage: /orch-plan <areas|paths|all> [--refresh]\n\n" +
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2070
|
+
"Shows the execution plan (tasks, waves, lane assignments)\n" +
|
|
2071
|
+
"without actually executing anything.\n\n" +
|
|
2072
|
+
"Options:\n" +
|
|
2073
|
+
" --refresh Force re-scan of areas (bypass dependency cache)\n\n" +
|
|
2074
|
+
"Examples:\n" +
|
|
2075
|
+
" /orch-plan all\n" +
|
|
2076
|
+
" /orch-plan time-off notifications\n" +
|
|
2077
|
+
" /orch-plan docs/task-management/domains/time-off/tasks\n" +
|
|
2078
|
+
" /orch-plan all --refresh",
|
|
1914
2079
|
"info",
|
|
1915
2080
|
);
|
|
1916
2081
|
return;
|
|
@@ -1924,7 +2089,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1924
2089
|
if (!cleanArgs) {
|
|
1925
2090
|
ctx.ui.notify(
|
|
1926
2091
|
"Usage: /orch-plan <areas|paths|all> [--refresh]\n" +
|
|
1927
|
-
|
|
2092
|
+
"Error: target argument required (e.g., 'all', area name, or path)",
|
|
1928
2093
|
"error",
|
|
1929
2094
|
);
|
|
1930
2095
|
return;
|
|
@@ -1948,7 +2113,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1948
2113
|
useDependencyCache: orchConfig.dependencies.cache,
|
|
1949
2114
|
workspaceConfig: execCtx!.workspaceConfig,
|
|
1950
2115
|
});
|
|
1951
|
-
ctx.ui.notify(
|
|
2116
|
+
ctx.ui.notify(
|
|
2117
|
+
formatDiscoveryResults(discovery),
|
|
2118
|
+
discovery.errors.length > 0 ? "warning" : "info",
|
|
2119
|
+
);
|
|
1952
2120
|
|
|
1953
2121
|
// Check for fatal errors
|
|
1954
2122
|
const fatalCodes = new Set<string>(FATAL_DISCOVERY_CODES);
|
|
@@ -1964,14 +2132,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1964
2132
|
"info",
|
|
1965
2133
|
);
|
|
1966
2134
|
}
|
|
1967
|
-
const hasStrictErrors = fatalErrors.some(
|
|
1968
|
-
(e) => e.code === "TASK_ROUTING_STRICT",
|
|
1969
|
-
);
|
|
2135
|
+
const hasStrictErrors = fatalErrors.some((e) => e.code === "TASK_ROUTING_STRICT");
|
|
1970
2136
|
if (hasStrictErrors) {
|
|
1971
2137
|
ctx.ui.notify(
|
|
1972
2138
|
"💡 Strict routing is enabled (routing.strict: true). Every task must declare an explicit execution target.\n" +
|
|
1973
|
-
|
|
1974
|
-
|
|
2139
|
+
" Add a `## Execution Target` section with `Repo: <id>` to each task's PROMPT.md.\n" +
|
|
2140
|
+
" To disable strict routing, set `routing.strict: false` in workspace config.",
|
|
1975
2141
|
"info",
|
|
1976
2142
|
);
|
|
1977
2143
|
}
|
|
@@ -1984,23 +2150,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1984
2150
|
}
|
|
1985
2151
|
|
|
1986
2152
|
// ── Section 3: Dependency Graph ──────────────────────────
|
|
1987
|
-
ctx.ui.notify(
|
|
1988
|
-
formatDependencyGraph(discovery.pending, discovery.completed),
|
|
1989
|
-
"info",
|
|
1990
|
-
);
|
|
2153
|
+
ctx.ui.notify(formatDependencyGraph(discovery.pending, discovery.completed), "info");
|
|
1991
2154
|
|
|
1992
2155
|
// ── Section 4: Waves + Estimate ──────────────────────────
|
|
1993
2156
|
// Uses computeWaveAssignments pipeline only — NO re-parsing
|
|
1994
|
-
const waveResult = computeWaveAssignments(
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
orchConfig,
|
|
1998
|
-
{
|
|
1999
|
-
workspaceRepoIds: execCtx!.workspaceConfig
|
|
2000
|
-
? execCtx!.workspaceConfig.repos.keys()
|
|
2001
|
-
: undefined,
|
|
2002
|
-
},
|
|
2003
|
-
);
|
|
2157
|
+
const waveResult = computeWaveAssignments(discovery.pending, discovery.completed, orchConfig, {
|
|
2158
|
+
workspaceRepoIds: execCtx!.workspaceConfig ? execCtx!.workspaceConfig.repos.keys() : undefined,
|
|
2159
|
+
});
|
|
2004
2160
|
|
|
2005
2161
|
ctx.ui.notify(
|
|
2006
2162
|
formatWavePlan(waveResult, orchConfig.assignment.size_weights),
|
|
@@ -2026,12 +2182,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2026
2182
|
*
|
|
2027
2183
|
* @since TP-061
|
|
2028
2184
|
*/
|
|
2029
|
-
async function doOrchStart(
|
|
2185
|
+
async function doOrchStart(
|
|
2186
|
+
target: string,
|
|
2187
|
+
ctx: ExtensionContext,
|
|
2188
|
+
): Promise<{ message: string; error?: boolean }> {
|
|
2030
2189
|
// Target validation
|
|
2031
2190
|
const trimmedTarget = target?.trim();
|
|
2032
2191
|
if (!trimmedTarget) {
|
|
2033
2192
|
return {
|
|
2034
|
-
message:
|
|
2193
|
+
message:
|
|
2194
|
+
'❌ Target is required. Use "all" to run all pending tasks, or specify a task area name or path.',
|
|
2035
2195
|
error: true,
|
|
2036
2196
|
};
|
|
2037
2197
|
}
|
|
@@ -2043,8 +2203,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2043
2203
|
// Skip if a batch is already active to avoid swapping config mid-run.
|
|
2044
2204
|
const _activePhase = orchBatchState.phase;
|
|
2045
2205
|
// Treat paused as active — config must not change for a resumable batch
|
|
2046
|
-
const _isActiveBatch =
|
|
2047
|
-
|
|
2206
|
+
const _isActiveBatch =
|
|
2207
|
+
_activePhase === "executing" ||
|
|
2208
|
+
_activePhase === "launching" ||
|
|
2209
|
+
_activePhase === "merging" ||
|
|
2210
|
+
_activePhase === "planning" ||
|
|
2211
|
+
_activePhase === "paused";
|
|
2048
2212
|
if (!_isActiveBatch) {
|
|
2049
2213
|
try {
|
|
2050
2214
|
// Build everything into temporaries first, then commit atomically
|
|
@@ -2052,10 +2216,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2052
2216
|
const freshCtx = buildExecutionContext(ctx.cwd, loadOrchestratorConfig, loadTaskRunnerConfig);
|
|
2053
2217
|
let freshSupervisor: SupervisorConfig;
|
|
2054
2218
|
try {
|
|
2055
|
-
freshSupervisor = loadSupervisorConfig(
|
|
2056
|
-
freshCtx.repoRoot,
|
|
2057
|
-
freshCtx.pointer?.configRoot,
|
|
2058
|
-
);
|
|
2219
|
+
freshSupervisor = loadSupervisorConfig(freshCtx.repoRoot, freshCtx.pointer?.configRoot);
|
|
2059
2220
|
} catch {
|
|
2060
2221
|
freshSupervisor = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
2061
2222
|
}
|
|
@@ -2086,7 +2247,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2086
2247
|
}
|
|
2087
2248
|
if (migrationResult.errors.length > 0) {
|
|
2088
2249
|
ctx.ui.notify(
|
|
2089
|
-
`⚠️ Migration warnings:\n${migrationResult.errors.map(e => ` ⚠ ${e.id}: ${e.error}`).join("\n")}`,
|
|
2250
|
+
`⚠️ Migration warnings:\n${migrationResult.errors.map((e) => ` ⚠ ${e.id}: ${e.error}`).join("\n")}`,
|
|
2090
2251
|
"warning",
|
|
2091
2252
|
);
|
|
2092
2253
|
}
|
|
@@ -2100,7 +2261,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2100
2261
|
}
|
|
2101
2262
|
|
|
2102
2263
|
// Prevent concurrent batch execution
|
|
2103
|
-
if (
|
|
2264
|
+
if (
|
|
2265
|
+
orchBatchState.phase !== "idle" &&
|
|
2266
|
+
orchBatchState.phase !== "completed" &&
|
|
2267
|
+
orchBatchState.phase !== "failed" &&
|
|
2268
|
+
orchBatchState.phase !== "stopped"
|
|
2269
|
+
) {
|
|
2104
2270
|
return {
|
|
2105
2271
|
message: `⚠️ A batch is already ${orchBatchState.phase} (${orchBatchState.batchId}). Use /orch-pause to pause or wait for completion.`,
|
|
2106
2272
|
error: true,
|
|
@@ -2110,10 +2276,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2110
2276
|
const { repoRoot } = execCtx;
|
|
2111
2277
|
|
|
2112
2278
|
// Orphan detection
|
|
2113
|
-
const orphanResult = detectOrphanSessions(
|
|
2114
|
-
orchConfig.orchestrator.sessionPrefix,
|
|
2115
|
-
repoRoot,
|
|
2116
|
-
);
|
|
2279
|
+
const orphanResult = detectOrphanSessions(orchConfig.orchestrator.sessionPrefix, repoRoot);
|
|
2117
2280
|
|
|
2118
2281
|
switch (orphanResult.recommendedAction) {
|
|
2119
2282
|
case "resume": {
|
|
@@ -2121,7 +2284,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2121
2284
|
const phase = orphanResult.loadedState?.phase ?? "";
|
|
2122
2285
|
const hasOrphans = orphanResult.orphanSessions.length > 0;
|
|
2123
2286
|
if (!hasOrphans && !resumablePhases.includes(phase)) {
|
|
2124
|
-
try {
|
|
2287
|
+
try {
|
|
2288
|
+
deleteBatchState(repoRoot);
|
|
2289
|
+
} catch {
|
|
2290
|
+
/* best effort */
|
|
2291
|
+
}
|
|
2125
2292
|
ctx.ui.notify(
|
|
2126
2293
|
`🧹 Cleared non-resumable stale batch (${orphanResult.loadedState?.batchId}, phase=${phase}). Starting fresh.`,
|
|
2127
2294
|
"info",
|
|
@@ -2133,7 +2300,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2133
2300
|
case "abort-orphans":
|
|
2134
2301
|
return { message: orphanResult.userMessage, error: true };
|
|
2135
2302
|
case "cleanup-stale":
|
|
2136
|
-
try {
|
|
2303
|
+
try {
|
|
2304
|
+
deleteBatchState(repoRoot);
|
|
2305
|
+
} catch {
|
|
2306
|
+
/* best effort */
|
|
2307
|
+
}
|
|
2137
2308
|
if (orphanResult.userMessage) {
|
|
2138
2309
|
ctx.ui.notify(orphanResult.userMessage, "info");
|
|
2139
2310
|
}
|
|
@@ -2155,14 +2326,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
2155
2326
|
workerModel: fullConfig.taskRunner.worker.model || "",
|
|
2156
2327
|
reviewerModel: fullConfig.taskRunner.reviewer.model || "",
|
|
2157
2328
|
};
|
|
2158
|
-
} catch {
|
|
2159
|
-
|
|
2160
|
-
|
|
2329
|
+
} catch {
|
|
2330
|
+
/* fall through */
|
|
2331
|
+
}
|
|
2332
|
+
const modelResults = validateModelAvailability(
|
|
2333
|
+
orchConfig,
|
|
2334
|
+
runnerConfig,
|
|
2335
|
+
supervisorConfig,
|
|
2336
|
+
ctx,
|
|
2337
|
+
agentModels,
|
|
2338
|
+
);
|
|
2339
|
+
const modelFailures = modelResults.filter((r) => r.status === "not-found");
|
|
2161
2340
|
ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
|
|
2162
2341
|
if (modelFailures.length > 0) {
|
|
2163
2342
|
return {
|
|
2164
|
-
message:
|
|
2165
|
-
|
|
2343
|
+
message:
|
|
2344
|
+
`❌ Cannot start batch — ${modelFailures.length} model(s) not found: ` +
|
|
2345
|
+
modelFailures.map((f) => `${f.role} (${f.modelStr})`).join(", ") +
|
|
2166
2346
|
`.\n\nFix the model configuration and try again.`,
|
|
2167
2347
|
error: true,
|
|
2168
2348
|
};
|
|
@@ -2172,11 +2352,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2172
2352
|
// This is a lightweight synchronous check before launching the async engine.
|
|
2173
2353
|
let pendingTaskCount = 0;
|
|
2174
2354
|
try {
|
|
2175
|
-
const preDiscovery = runDiscovery(
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2355
|
+
const preDiscovery = runDiscovery(
|
|
2356
|
+
trimmedTarget,
|
|
2357
|
+
runnerConfig.task_areas,
|
|
2358
|
+
execCtx.workspaceRoot,
|
|
2359
|
+
{
|
|
2360
|
+
dependencySource: orchConfig.dependencies.source,
|
|
2361
|
+
useDependencyCache: orchConfig.dependencies.cache,
|
|
2362
|
+
workspaceConfig: execCtx.workspaceConfig,
|
|
2363
|
+
},
|
|
2364
|
+
);
|
|
2180
2365
|
pendingTaskCount = preDiscovery.pending.size;
|
|
2181
2366
|
if (pendingTaskCount === 0) {
|
|
2182
2367
|
return {
|
|
@@ -2219,14 +2404,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
2219
2404
|
ctx,
|
|
2220
2405
|
updateOrchWidget,
|
|
2221
2406
|
(monState: MonitorState) => {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2407
|
+
// TP-195: GATED on E2 escalation. The previous change-detection
|
|
2408
|
+
// here compared `monState.totalDone/totalFailed` and
|
|
2409
|
+
// `lane.currentStep/completedChecks` — fields that do NOT exist
|
|
2410
|
+
// on `MonitorState` / `LaneMonitorSnapshot`. At runtime every
|
|
2411
|
+
// such read returned `undefined`, so the four comparisons were
|
|
2412
|
+
// always `undefined !== undefined` (false) and only
|
|
2413
|
+
// `currentTaskId` actually triggered refreshes. To preserve
|
|
2414
|
+
// historic behavior pending operator decision (would fix
|
|
2415
|
+
// rewire to `tasksDone`/`tasksFailed` and
|
|
2416
|
+
// `currentTaskSnapshot.currentStepNumber/totalChecked`), the
|
|
2417
|
+
// dead comparisons are dropped here. Observed behavior is
|
|
2418
|
+
// IDENTICAL — widget still refreshes only on `currentTaskId`
|
|
2419
|
+
// changes — but the source typechecks cleanly.
|
|
2420
|
+
const changed =
|
|
2421
|
+
!latestMonitorState ||
|
|
2422
|
+
latestMonitorState.lanes.some((l, i) => l.currentTaskId !== monState.lanes[i]?.currentTaskId);
|
|
2230
2423
|
latestMonitorState = monState;
|
|
2231
2424
|
if (changed) updateOrchWidget();
|
|
2232
2425
|
},
|
|
@@ -2236,17 +2429,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2236
2429
|
const sDeps: SummaryDeps = {
|
|
2237
2430
|
opId,
|
|
2238
2431
|
diagnostics: orchBatchState.diagnostics ?? null,
|
|
2239
|
-
mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
|
|
2432
|
+
mergeResults: (orchBatchState.mergeResults || []).map((mr) => ({
|
|
2240
2433
|
waveIndex: mr.waveIndex,
|
|
2241
2434
|
status: mr.status,
|
|
2242
2435
|
failedLane: mr.failedLane,
|
|
2243
2436
|
failureReason: mr.failureReason,
|
|
2244
2437
|
})),
|
|
2245
2438
|
};
|
|
2246
|
-
if (
|
|
2247
|
-
orchBatchState.phase === "completed" &&
|
|
2248
|
-
(mode === "supervised" || mode === "auto")
|
|
2249
|
-
) {
|
|
2439
|
+
if (orchBatchState.phase === "completed" && (mode === "supervised" || mode === "auto")) {
|
|
2250
2440
|
triggerSupervisorIntegration(
|
|
2251
2441
|
pi,
|
|
2252
2442
|
supervisorState,
|
|
@@ -2259,47 +2449,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
2259
2449
|
);
|
|
2260
2450
|
return;
|
|
2261
2451
|
}
|
|
2262
|
-
if (
|
|
2263
|
-
(mode === "supervised" || mode === "auto") &&
|
|
2264
|
-
orchBatchState.phase !== "completed"
|
|
2265
|
-
) {
|
|
2452
|
+
if ((mode === "supervised" || mode === "auto") && orchBatchState.phase !== "completed") {
|
|
2266
2453
|
pi.sendMessage(
|
|
2267
2454
|
{
|
|
2268
2455
|
customType: "supervisor-integration-skipped",
|
|
2269
|
-
content: [
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2456
|
+
content: [
|
|
2457
|
+
{
|
|
2458
|
+
type: "text",
|
|
2459
|
+
text:
|
|
2460
|
+
`📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
|
|
2461
|
+
`Integration skipped — only completed batches are eligible.\n` +
|
|
2462
|
+
`Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
|
|
2463
|
+
},
|
|
2464
|
+
],
|
|
2276
2465
|
display: `Integration skipped — batch ${orchBatchState.phase}`,
|
|
2277
2466
|
},
|
|
2278
2467
|
{ triggerTurn: false },
|
|
2279
2468
|
);
|
|
2280
2469
|
}
|
|
2281
|
-
presentBatchSummary(
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2470
|
+
presentBatchSummary(
|
|
2471
|
+
pi,
|
|
2472
|
+
orchBatchState,
|
|
2473
|
+
execCtx!.workspaceRoot,
|
|
2474
|
+
opId,
|
|
2475
|
+
orchBatchState.diagnostics,
|
|
2476
|
+
sDeps.mergeResults,
|
|
2477
|
+
);
|
|
2478
|
+
const postBatchContext: SupervisorRoutingContext =
|
|
2479
|
+
orchBatchState.phase === "completed"
|
|
2480
|
+
? {
|
|
2481
|
+
routingState: "completed-batch",
|
|
2482
|
+
contextMessage:
|
|
2483
|
+
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
2484
|
+
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
2485
|
+
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
2486
|
+
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
2487
|
+
`You can also:\n` +
|
|
2488
|
+
`• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
2489
|
+
`• Create new tasks for the next batch\n` +
|
|
2490
|
+
`• Run a health check`,
|
|
2491
|
+
}
|
|
2492
|
+
: {
|
|
2493
|
+
routingState: "no-tasks",
|
|
2494
|
+
contextMessage:
|
|
2495
|
+
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
2496
|
+
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
2497
|
+
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
2498
|
+
`What would you like to do next?`,
|
|
2499
|
+
};
|
|
2303
2500
|
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
2304
2501
|
},
|
|
2305
2502
|
// ── TP-076: Supervisor alert handler — injects alerts as user messages ──
|
|
@@ -2309,7 +2506,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2309
2506
|
if (isAlertSuppressed(alert)) {
|
|
2310
2507
|
process.stderr.write(
|
|
2311
2508
|
`[taskplane:zombie-filter] dropped alert (category=${alert.category}, ` +
|
|
2312
|
-
|
|
2509
|
+
`lane=${alert.context?.laneNumber ?? "?"}, agent=${alert.context?.agentId ?? "?"})\n`,
|
|
2313
2510
|
);
|
|
2314
2511
|
return;
|
|
2315
2512
|
}
|
|
@@ -2320,7 +2517,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2320
2517
|
if (!ipcBatchIdMatches(info.batchId)) {
|
|
2321
2518
|
process.stderr.write(
|
|
2322
2519
|
`[taskplane:zombie-filter] ignored stale lane-terminated IPC ` +
|
|
2323
|
-
|
|
2520
|
+
`(incoming batchId=${info.batchId}, current=${orchBatchState.batchId})\n`,
|
|
2324
2521
|
);
|
|
2325
2522
|
return;
|
|
2326
2523
|
}
|
|
@@ -2328,7 +2525,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2328
2525
|
if (info.agentId) terminatedAgents.set(info.agentId, info.terminatedAt);
|
|
2329
2526
|
process.stderr.write(
|
|
2330
2527
|
`[taskplane:zombie-filter] lane ${info.laneNumber} (${info.agentId}) terminated ` +
|
|
2331
|
-
|
|
2528
|
+
`(reason: ${info.reason}); ${terminatedLanes.size} lane(s) suppressed\n`,
|
|
2332
2529
|
);
|
|
2333
2530
|
},
|
|
2334
2531
|
// TP-187 (#538): Lane-respawned handler.
|
|
@@ -2336,7 +2533,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2336
2533
|
if (!ipcBatchIdMatches(incomingBatchId)) {
|
|
2337
2534
|
process.stderr.write(
|
|
2338
2535
|
`[taskplane:zombie-filter] ignored stale lane-respawned IPC ` +
|
|
2339
|
-
|
|
2536
|
+
`(incoming batchId=${incomingBatchId}, current=${orchBatchState.batchId})\n`,
|
|
2340
2537
|
);
|
|
2341
2538
|
return;
|
|
2342
2539
|
}
|
|
@@ -2357,7 +2554,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2357
2554
|
);
|
|
2358
2555
|
|
|
2359
2556
|
return {
|
|
2360
|
-
message:
|
|
2557
|
+
message:
|
|
2558
|
+
`🚀 Batch launching (target: "${trimmedTarget}", ${pendingTaskCount} pending task${pendingTaskCount === 1 ? "" : "s"}). ` +
|
|
2361
2559
|
`Batch ID will be assigned during planning. ` +
|
|
2362
2560
|
`The engine is running asynchronously — use orch_status() to check progress.`,
|
|
2363
2561
|
};
|
|
@@ -2370,13 +2568,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2370
2568
|
}
|
|
2371
2569
|
|
|
2372
2570
|
function buildTaskSegmentProgressLabel(
|
|
2373
|
-
task:
|
|
2374
|
-
|
|
2571
|
+
task:
|
|
2572
|
+
| { taskId: string; segmentIds?: string[]; activeSegmentId?: string | null; status?: string }
|
|
2573
|
+
| undefined,
|
|
2574
|
+
segments:
|
|
2575
|
+
| Array<{ taskId: string; segmentId: string; status: string; repoId: string }>
|
|
2576
|
+
| undefined,
|
|
2375
2577
|
preferredSegmentId?: string | null,
|
|
2376
2578
|
): string | null {
|
|
2377
2579
|
if (!task || !Array.isArray(task.segmentIds) || task.segmentIds.length <= 1) return null;
|
|
2378
2580
|
|
|
2379
|
-
const segmentIds = task.segmentIds.filter(
|
|
2581
|
+
const segmentIds = task.segmentIds.filter(
|
|
2582
|
+
(segmentId) => typeof segmentId === "string" && segmentId.trim().length > 0,
|
|
2583
|
+
);
|
|
2380
2584
|
if (segmentIds.length <= 1) return null;
|
|
2381
2585
|
|
|
2382
2586
|
const bySegmentId = new Map<string, { status: string; repoId: string }>();
|
|
@@ -2388,10 +2592,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2388
2592
|
|
|
2389
2593
|
let activeSegmentId = task.activeSegmentId ?? preferredSegmentId ?? null;
|
|
2390
2594
|
if (!activeSegmentId || !segmentIds.includes(activeSegmentId)) {
|
|
2391
|
-
activeSegmentId =
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2595
|
+
activeSegmentId =
|
|
2596
|
+
segmentIds.find((segmentId) => {
|
|
2597
|
+
const status = bySegmentId.get(segmentId)?.status;
|
|
2598
|
+
return !["succeeded", "failed", "stalled", "skipped"].includes(status || "pending");
|
|
2599
|
+
}) || segmentIds[segmentIds.length - 1];
|
|
2395
2600
|
}
|
|
2396
2601
|
|
|
2397
2602
|
const index = Math.max(0, segmentIds.indexOf(activeSegmentId));
|
|
@@ -2429,7 +2634,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2429
2634
|
];
|
|
2430
2635
|
|
|
2431
2636
|
const segmentRecords = diskState.segments || [];
|
|
2432
|
-
const multiSegmentTasks = (diskState.tasks || []).filter(
|
|
2637
|
+
const multiSegmentTasks = (diskState.tasks || []).filter(
|
|
2638
|
+
(task) => Array.isArray(task.segmentIds) && task.segmentIds.length > 1,
|
|
2639
|
+
);
|
|
2433
2640
|
if (multiSegmentTasks.length > 0) {
|
|
2434
2641
|
const byStatus = {
|
|
2435
2642
|
succeeded: segmentRecords.filter((segment) => segment.status === "succeeded").length,
|
|
@@ -2445,18 +2652,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
2445
2652
|
if (byStatus.pending > 0) segParts.push(`${byStatus.pending} pending`);
|
|
2446
2653
|
if (byStatus.skipped > 0) segParts.push(`${byStatus.skipped} skipped`);
|
|
2447
2654
|
if (byStatus.stalled > 0) segParts.push(`${byStatus.stalled} stalled`);
|
|
2448
|
-
lines.push(
|
|
2655
|
+
lines.push(
|
|
2656
|
+
` Segments: ${segParts.join(", ")} (${multiSegmentTasks.length} multi-segment task(s))`,
|
|
2657
|
+
);
|
|
2449
2658
|
}
|
|
2450
2659
|
|
|
2451
2660
|
const sortedDiskLanes = [...(diskState.lanes || [])].sort((a, b) => a.laneNumber - b.laneNumber);
|
|
2452
2661
|
if (sortedDiskLanes.length > 0) {
|
|
2453
2662
|
lines.push(" Lanes:");
|
|
2454
2663
|
for (const laneRec of sortedDiskLanes) {
|
|
2455
|
-
const laneTasks = (diskState.tasks || []).filter(
|
|
2664
|
+
const laneTasks = (diskState.tasks || []).filter(
|
|
2665
|
+
(task) => task.laneNumber === laneRec.laneNumber,
|
|
2666
|
+
);
|
|
2456
2667
|
const runningTask = laneTasks.find((task) => task.status === "running");
|
|
2457
2668
|
const activeTask = runningTask || laneTasks[laneTasks.length - 1];
|
|
2458
2669
|
const taskLabel = activeTask ? `${activeTask.taskId} (${activeTask.status})` : "idle";
|
|
2459
|
-
const segmentLabel = buildTaskSegmentProgressLabel(
|
|
2670
|
+
const segmentLabel = buildTaskSegmentProgressLabel(
|
|
2671
|
+
activeTask,
|
|
2672
|
+
segmentRecords,
|
|
2673
|
+
activeTask?.activeSegmentId ?? null,
|
|
2674
|
+
);
|
|
2460
2675
|
const segmentPart = segmentLabel ? ` · ${segmentLabel}` : "";
|
|
2461
2676
|
const repoPart = laneRec.repoId ? ` · repo: ${laneRec.repoId}` : "";
|
|
2462
2677
|
lines.push(` - Lane ${laneRec.laneNumber}: ${taskLabel}${segmentPart}${repoPart}`);
|
|
@@ -2483,7 +2698,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2483
2698
|
|
|
2484
2699
|
const segmentRecords = orchBatchState.segments || [];
|
|
2485
2700
|
const multiSegmentTaskCount = orchBatchState.currentLanes.reduce((count, laneRec) => {
|
|
2486
|
-
return
|
|
2701
|
+
return (
|
|
2702
|
+
count +
|
|
2703
|
+
laneRec.tasks.filter(
|
|
2704
|
+
(task) => Array.isArray(task.task.segmentIds) && task.task.segmentIds.length > 1,
|
|
2705
|
+
).length
|
|
2706
|
+
);
|
|
2487
2707
|
}, 0);
|
|
2488
2708
|
if (multiSegmentTaskCount > 0) {
|
|
2489
2709
|
const byStatus = {
|
|
@@ -2500,14 +2720,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2500
2720
|
if (byStatus.pending > 0) segParts.push(`${byStatus.pending} pending`);
|
|
2501
2721
|
if (byStatus.skipped > 0) segParts.push(`${byStatus.skipped} skipped`);
|
|
2502
2722
|
if (byStatus.stalled > 0) segParts.push(`${byStatus.stalled} stalled`);
|
|
2503
|
-
lines.push(
|
|
2723
|
+
lines.push(
|
|
2724
|
+
` Segments: ${segParts.join(", ")} (${multiSegmentTaskCount} multi-segment task(s))`,
|
|
2725
|
+
);
|
|
2504
2726
|
}
|
|
2505
2727
|
|
|
2506
2728
|
if (orchBatchState.currentLanes.length > 0) {
|
|
2507
2729
|
lines.push(" Lanes:");
|
|
2508
2730
|
const sortedLanes = [...orchBatchState.currentLanes].sort((a, b) => a.laneNumber - b.laneNumber);
|
|
2509
2731
|
for (const laneRec of sortedLanes) {
|
|
2510
|
-
const monLane = latestMonitorState?.lanes.find(
|
|
2732
|
+
const monLane = latestMonitorState?.lanes.find(
|
|
2733
|
+
(laneState) => laneState.laneNumber === laneRec.laneNumber,
|
|
2734
|
+
);
|
|
2511
2735
|
const currentTaskId = monLane?.currentTaskId || laneRec.tasks[0]?.taskId;
|
|
2512
2736
|
const allocatedTask = currentTaskId
|
|
2513
2737
|
? laneRec.tasks.find((task) => task.taskId === currentTaskId)
|
|
@@ -2537,7 +2761,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2537
2761
|
* Core logic for orch-pause. Returns a status message string.
|
|
2538
2762
|
*/
|
|
2539
2763
|
function doOrchPause(): string {
|
|
2540
|
-
if (
|
|
2764
|
+
if (
|
|
2765
|
+
orchBatchState.phase === "idle" ||
|
|
2766
|
+
orchBatchState.phase === "completed" ||
|
|
2767
|
+
orchBatchState.phase === "failed" ||
|
|
2768
|
+
orchBatchState.phase === "stopped"
|
|
2769
|
+
) {
|
|
2541
2770
|
return ORCH_MESSAGES.pauseNoBatch();
|
|
2542
2771
|
}
|
|
2543
2772
|
if (orchBatchState.phase === "paused" || orchBatchState.pauseSignal.paused) {
|
|
@@ -2555,7 +2784,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2555
2784
|
* The actual batch resume runs asynchronously via startBatchInWorker (TP-071).
|
|
2556
2785
|
* Returns null if execCtx is missing (caller must handle).
|
|
2557
2786
|
*/
|
|
2558
|
-
function doOrchResume(
|
|
2787
|
+
function doOrchResume(
|
|
2788
|
+
force: boolean,
|
|
2789
|
+
ctx: ExtensionContext,
|
|
2790
|
+
): { message: string; error?: boolean } {
|
|
2559
2791
|
if (!execCtx) {
|
|
2560
2792
|
return {
|
|
2561
2793
|
message: getExecCtxInitErrorMessage(),
|
|
@@ -2564,7 +2796,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2564
2796
|
}
|
|
2565
2797
|
|
|
2566
2798
|
// Prevent resume if a batch is actively running
|
|
2567
|
-
if (
|
|
2799
|
+
if (
|
|
2800
|
+
orchBatchState.phase === "launching" ||
|
|
2801
|
+
orchBatchState.phase === "executing" ||
|
|
2802
|
+
orchBatchState.phase === "merging" ||
|
|
2803
|
+
orchBatchState.phase === "planning"
|
|
2804
|
+
) {
|
|
2568
2805
|
return {
|
|
2569
2806
|
message: `⚠️ A batch is currently ${orchBatchState.phase} (${orchBatchState.batchId}). Cannot resume.`,
|
|
2570
2807
|
error: true,
|
|
@@ -2612,17 +2849,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2612
2849
|
const sDeps: SummaryDeps = {
|
|
2613
2850
|
opId,
|
|
2614
2851
|
diagnostics: orchBatchState.diagnostics ?? null,
|
|
2615
|
-
mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
|
|
2852
|
+
mergeResults: (orchBatchState.mergeResults || []).map((mr) => ({
|
|
2616
2853
|
waveIndex: mr.waveIndex,
|
|
2617
2854
|
status: mr.status,
|
|
2618
2855
|
failedLane: mr.failedLane,
|
|
2619
2856
|
failureReason: mr.failureReason,
|
|
2620
2857
|
})),
|
|
2621
2858
|
};
|
|
2622
|
-
if (
|
|
2623
|
-
orchBatchState.phase === "completed" &&
|
|
2624
|
-
(mode === "supervised" || mode === "auto")
|
|
2625
|
-
) {
|
|
2859
|
+
if (orchBatchState.phase === "completed" && (mode === "supervised" || mode === "auto")) {
|
|
2626
2860
|
triggerSupervisorIntegration(
|
|
2627
2861
|
pi,
|
|
2628
2862
|
supervisorState,
|
|
@@ -2635,47 +2869,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
2635
2869
|
);
|
|
2636
2870
|
return;
|
|
2637
2871
|
}
|
|
2638
|
-
if (
|
|
2639
|
-
(mode === "supervised" || mode === "auto") &&
|
|
2640
|
-
orchBatchState.phase !== "completed"
|
|
2641
|
-
) {
|
|
2872
|
+
if ((mode === "supervised" || mode === "auto") && orchBatchState.phase !== "completed") {
|
|
2642
2873
|
pi.sendMessage(
|
|
2643
2874
|
{
|
|
2644
2875
|
customType: "supervisor-integration-skipped",
|
|
2645
|
-
content: [
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2876
|
+
content: [
|
|
2877
|
+
{
|
|
2878
|
+
type: "text",
|
|
2879
|
+
text:
|
|
2880
|
+
`📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
|
|
2881
|
+
`Integration skipped — only completed batches are eligible.\n` +
|
|
2882
|
+
`Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
|
|
2883
|
+
},
|
|
2884
|
+
],
|
|
2652
2885
|
display: `Integration skipped — batch ${orchBatchState.phase}`,
|
|
2653
2886
|
},
|
|
2654
2887
|
{ triggerTurn: false },
|
|
2655
2888
|
);
|
|
2656
2889
|
}
|
|
2657
|
-
presentBatchSummary(
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2890
|
+
presentBatchSummary(
|
|
2891
|
+
pi,
|
|
2892
|
+
orchBatchState,
|
|
2893
|
+
execCtx!.workspaceRoot,
|
|
2894
|
+
opId,
|
|
2895
|
+
orchBatchState.diagnostics,
|
|
2896
|
+
sDeps.mergeResults,
|
|
2897
|
+
);
|
|
2898
|
+
const postBatchContext: SupervisorRoutingContext =
|
|
2899
|
+
orchBatchState.phase === "completed"
|
|
2900
|
+
? {
|
|
2901
|
+
routingState: "completed-batch",
|
|
2902
|
+
contextMessage:
|
|
2903
|
+
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
2904
|
+
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
2905
|
+
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
2906
|
+
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
2907
|
+
`You can also:\n` +
|
|
2908
|
+
`• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
2909
|
+
`• Create new tasks for the next batch\n` +
|
|
2910
|
+
`• Run a health check`,
|
|
2911
|
+
}
|
|
2912
|
+
: {
|
|
2913
|
+
routingState: "no-tasks",
|
|
2914
|
+
contextMessage:
|
|
2915
|
+
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
2916
|
+
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
2917
|
+
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
2918
|
+
`What would you like to do next?`,
|
|
2919
|
+
};
|
|
2679
2920
|
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
2680
2921
|
},
|
|
2681
2922
|
// ── TP-076: Supervisor alert handler — injects alerts as user messages ──
|
|
@@ -2685,7 +2926,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2685
2926
|
if (isAlertSuppressed(alert)) {
|
|
2686
2927
|
process.stderr.write(
|
|
2687
2928
|
`[taskplane:zombie-filter] dropped alert (category=${alert.category}, ` +
|
|
2688
|
-
|
|
2929
|
+
`lane=${alert.context?.laneNumber ?? "?"}, agent=${alert.context?.agentId ?? "?"})\n`,
|
|
2689
2930
|
);
|
|
2690
2931
|
return;
|
|
2691
2932
|
}
|
|
@@ -2696,7 +2937,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2696
2937
|
if (!ipcBatchIdMatches(info.batchId)) {
|
|
2697
2938
|
process.stderr.write(
|
|
2698
2939
|
`[taskplane:zombie-filter] ignored stale lane-terminated IPC ` +
|
|
2699
|
-
|
|
2940
|
+
`(incoming batchId=${info.batchId}, current=${orchBatchState.batchId})\n`,
|
|
2700
2941
|
);
|
|
2701
2942
|
return;
|
|
2702
2943
|
}
|
|
@@ -2704,7 +2945,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2704
2945
|
if (info.agentId) terminatedAgents.set(info.agentId, info.terminatedAt);
|
|
2705
2946
|
process.stderr.write(
|
|
2706
2947
|
`[taskplane:zombie-filter] lane ${info.laneNumber} (${info.agentId}) terminated ` +
|
|
2707
|
-
|
|
2948
|
+
`(reason: ${info.reason}); ${terminatedLanes.size} lane(s) suppressed\n`,
|
|
2708
2949
|
);
|
|
2709
2950
|
},
|
|
2710
2951
|
// TP-187 (#538): Lane-respawned handler.
|
|
@@ -2712,7 +2953,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2712
2953
|
if (!ipcBatchIdMatches(incomingBatchId)) {
|
|
2713
2954
|
process.stderr.write(
|
|
2714
2955
|
`[taskplane:zombie-filter] ignored stale lane-respawned IPC ` +
|
|
2715
|
-
|
|
2956
|
+
`(incoming batchId=${incomingBatchId}, current=${orchBatchState.batchId})\n`,
|
|
2716
2957
|
);
|
|
2717
2958
|
return;
|
|
2718
2959
|
}
|
|
@@ -2750,10 +2991,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2750
2991
|
const abortSignalFile = join(stateRoot, ".pi", "orch-abort-signal");
|
|
2751
2992
|
try {
|
|
2752
2993
|
mkdirSync(join(stateRoot, ".pi"), { recursive: true });
|
|
2753
|
-
writeFileSync(
|
|
2994
|
+
writeFileSync(
|
|
2995
|
+
abortSignalFile,
|
|
2996
|
+
`abort requested at ${new Date().toISOString()} (mode: ${mode})`,
|
|
2997
|
+
"utf-8",
|
|
2998
|
+
);
|
|
2754
2999
|
messages.push(" ✓ Abort signal file written (.pi/orch-abort-signal)");
|
|
2755
3000
|
} catch (err) {
|
|
2756
|
-
messages.push(
|
|
3001
|
+
messages.push(
|
|
3002
|
+
` ⚠ Failed to write abort signal file: ${err instanceof Error ? err.message : String(err)}`,
|
|
3003
|
+
);
|
|
2757
3004
|
}
|
|
2758
3005
|
|
|
2759
3006
|
// Step 2: Set pause signal and forward to worker
|
|
@@ -2773,7 +3020,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2773
3020
|
}
|
|
2774
3021
|
}
|
|
2775
3022
|
|
|
2776
|
-
const hasActiveBatch =
|
|
3023
|
+
const hasActiveBatch =
|
|
3024
|
+
orchBatchState.phase !== "idle" &&
|
|
2777
3025
|
orchBatchState.phase !== "completed" &&
|
|
2778
3026
|
orchBatchState.phase !== "failed" &&
|
|
2779
3027
|
orchBatchState.phase !== "stopped";
|
|
@@ -2787,11 +3035,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2787
3035
|
|
|
2788
3036
|
messages.push(
|
|
2789
3037
|
` Batch state: in-memory=${hasActiveBatch ? orchBatchState.phase : "none"}, ` +
|
|
2790
|
-
|
|
3038
|
+
`persisted=${persistedState ? persistedState.batchId : "none"}`,
|
|
2791
3039
|
);
|
|
2792
3040
|
|
|
2793
3041
|
if (!hasActiveBatch && !persistedState) {
|
|
2794
|
-
try {
|
|
3042
|
+
try {
|
|
3043
|
+
unlinkSync(abortSignalFile);
|
|
3044
|
+
} catch {}
|
|
2795
3045
|
return ORCH_MESSAGES.abortNoBatch();
|
|
2796
3046
|
}
|
|
2797
3047
|
|
|
@@ -2817,15 +3067,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
2817
3067
|
updateOrchWidget();
|
|
2818
3068
|
messages.push(" ✓ In-memory batch state set to 'stopped'");
|
|
2819
3069
|
} catch (err) {
|
|
2820
|
-
messages.push(
|
|
3070
|
+
messages.push(
|
|
3071
|
+
` ⚠ Failed to update in-memory state: ${err instanceof Error ? err.message : String(err)}`,
|
|
3072
|
+
);
|
|
2821
3073
|
}
|
|
2822
3074
|
|
|
2823
|
-
messages.push(
|
|
3075
|
+
messages.push(
|
|
3076
|
+
` Found ${abortResult.sessionsFound} session target(s) matching prefix "${prefix}-"`,
|
|
3077
|
+
);
|
|
2824
3078
|
if (mode === "graceful") {
|
|
2825
3079
|
const forceKilled = Math.max(0, abortResult.sessionsKilled - abortResult.gracefulExits);
|
|
2826
|
-
messages.push(
|
|
3080
|
+
messages.push(
|
|
3081
|
+
ORCH_MESSAGES.abortGracefulComplete(
|
|
3082
|
+
batchId,
|
|
3083
|
+
abortResult.gracefulExits,
|
|
3084
|
+
forceKilled,
|
|
3085
|
+
Math.round(abortResult.durationMs / 1000),
|
|
3086
|
+
),
|
|
3087
|
+
);
|
|
2827
3088
|
} else {
|
|
2828
|
-
messages.push(
|
|
3089
|
+
messages.push(
|
|
3090
|
+
ORCH_MESSAGES.abortHardComplete(
|
|
3091
|
+
batchId,
|
|
3092
|
+
abortResult.sessionsKilled,
|
|
3093
|
+
Math.round(abortResult.durationMs / 1000),
|
|
3094
|
+
),
|
|
3095
|
+
);
|
|
2829
3096
|
}
|
|
2830
3097
|
|
|
2831
3098
|
if (!abortResult.stateDeleted) {
|
|
@@ -2839,11 +3106,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2839
3106
|
}
|
|
2840
3107
|
|
|
2841
3108
|
// Step 7: Clean up abort signal file
|
|
2842
|
-
try {
|
|
3109
|
+
try {
|
|
3110
|
+
unlinkSync(abortSignalFile);
|
|
3111
|
+
} catch {}
|
|
2843
3112
|
|
|
2844
3113
|
messages.push(
|
|
2845
3114
|
`🏁 Abort (${mode}) complete for batch ${batchId}. ` +
|
|
2846
|
-
|
|
3115
|
+
`Worktrees and branches are preserved for inspection.`,
|
|
2847
3116
|
);
|
|
2848
3117
|
|
|
2849
3118
|
return messages.join("\n");
|
|
@@ -2891,15 +3160,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2891
3160
|
drainedAgents++;
|
|
2892
3161
|
drainedMessages += n;
|
|
2893
3162
|
}
|
|
2894
|
-
} catch {
|
|
3163
|
+
} catch {
|
|
3164
|
+
/* per-agent drain best-effort */
|
|
3165
|
+
}
|
|
2895
3166
|
}
|
|
2896
3167
|
messages.push(
|
|
2897
3168
|
` ✓ Drained on-disk outboxes (${drainedMessages} message(s) across ${drainedAgents} agent(s))`,
|
|
2898
3169
|
);
|
|
2899
3170
|
} catch (err) {
|
|
2900
|
-
messages.push(
|
|
2901
|
-
` ⚠ Drain failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2902
|
-
);
|
|
3171
|
+
messages.push(` ⚠ Drain failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2903
3172
|
}
|
|
2904
3173
|
} else {
|
|
2905
3174
|
messages.push(" — No active batch state; outbox drain skipped");
|
|
@@ -2964,9 +3233,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2964
3233
|
}
|
|
2965
3234
|
|
|
2966
3235
|
// Find the task
|
|
2967
|
-
const taskRecord = state.tasks.find(t => t.taskId === taskId);
|
|
3236
|
+
const taskRecord = state.tasks.find((t) => t.taskId === taskId);
|
|
2968
3237
|
if (!taskRecord) {
|
|
2969
|
-
const knownIds = state.tasks.map(t => t.taskId).join(", ");
|
|
3238
|
+
const knownIds = state.tasks.map((t) => t.taskId).join(", ");
|
|
2970
3239
|
return `❌ Task "${taskId}" not found in batch ${state.batchId}.\nKnown tasks: ${knownIds || "(none)"}`;
|
|
2971
3240
|
}
|
|
2972
3241
|
|
|
@@ -3001,7 +3270,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3001
3270
|
}
|
|
3002
3271
|
}
|
|
3003
3272
|
if (orchBatchState.dependencyGraph && orchBatchState.batchId === state.batchId) {
|
|
3004
|
-
const newBlocked = computeTransitiveDependents(
|
|
3273
|
+
const newBlocked = computeTransitiveDependents(
|
|
3274
|
+
remainingFailures,
|
|
3275
|
+
orchBatchState.dependencyGraph,
|
|
3276
|
+
);
|
|
3005
3277
|
state.blockedTaskIds = [...newBlocked].sort();
|
|
3006
3278
|
state.blockedTasks = newBlocked.size;
|
|
3007
3279
|
} else if (remainingFailures.size === 0) {
|
|
@@ -3037,13 +3309,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3037
3309
|
|
|
3038
3310
|
updateOrchWidget();
|
|
3039
3311
|
|
|
3040
|
-
const resumeHint =
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3312
|
+
const resumeHint =
|
|
3313
|
+
state.phase === "stopped"
|
|
3314
|
+
? "Use orch_resume(force=true) to re-execute the batch."
|
|
3315
|
+
: "Use orch_resume() to re-execute the batch.";
|
|
3316
|
+
return (
|
|
3317
|
+
`✅ Task "${taskId}" reset to pending for re-execution.\n` +
|
|
3044
3318
|
` Previous status: ${prevStatus}\n` +
|
|
3045
3319
|
` Batch phase: ${state.phase} | Failed: ${state.failedTasks}/${state.totalTasks}\n` +
|
|
3046
|
-
` ${resumeHint}
|
|
3320
|
+
` ${resumeHint}`
|
|
3321
|
+
);
|
|
3047
3322
|
}
|
|
3048
3323
|
|
|
3049
3324
|
/**
|
|
@@ -3074,14 +3349,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
3074
3349
|
}
|
|
3075
3350
|
|
|
3076
3351
|
// Find the task
|
|
3077
|
-
const taskRecord = state.tasks.find(t => t.taskId === taskId);
|
|
3352
|
+
const taskRecord = state.tasks.find((t) => t.taskId === taskId);
|
|
3078
3353
|
if (!taskRecord) {
|
|
3079
|
-
const knownIds = state.tasks.map(t => t.taskId).join(", ");
|
|
3354
|
+
const knownIds = state.tasks.map((t) => t.taskId).join(", ");
|
|
3080
3355
|
return `❌ Task "${taskId}" not found in batch ${state.batchId}.\nKnown tasks: ${knownIds || "(none)"}`;
|
|
3081
3356
|
}
|
|
3082
3357
|
|
|
3083
3358
|
// Validate: only failed, stalled, or pending tasks can be skipped
|
|
3084
|
-
if (
|
|
3359
|
+
if (
|
|
3360
|
+
taskRecord.status !== "failed" &&
|
|
3361
|
+
taskRecord.status !== "stalled" &&
|
|
3362
|
+
taskRecord.status !== "pending"
|
|
3363
|
+
) {
|
|
3085
3364
|
return `❌ Cannot skip task "${taskId}" — current status is "${taskRecord.status}". Only failed, stalled, or pending tasks can be skipped.`;
|
|
3086
3365
|
}
|
|
3087
3366
|
|
|
@@ -3114,7 +3393,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3114
3393
|
|
|
3115
3394
|
// Use in-memory dependency graph if available (batch IDs must match)
|
|
3116
3395
|
if (orchBatchState.dependencyGraph && orchBatchState.batchId === state.batchId) {
|
|
3117
|
-
const newBlocked = computeTransitiveDependents(
|
|
3396
|
+
const newBlocked = computeTransitiveDependents(
|
|
3397
|
+
remainingFailures,
|
|
3398
|
+
orchBatchState.dependencyGraph,
|
|
3399
|
+
);
|
|
3118
3400
|
|
|
3119
3401
|
// Find tasks that were blocked but are now unblocked
|
|
3120
3402
|
for (const id of prevBlocked) {
|
|
@@ -3191,7 +3473,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
3191
3473
|
* 4. Clears the failed merge entry and sets phase to "paused"
|
|
3192
3474
|
* 5. `orch_resume()` re-runs the merge using real git merge logic
|
|
3193
3475
|
*/
|
|
3194
|
-
function doOrchForceMerge(
|
|
3476
|
+
function doOrchForceMerge(
|
|
3477
|
+
waveIndex: number | undefined,
|
|
3478
|
+
skipFailed: boolean,
|
|
3479
|
+
ctx: ExtensionContext,
|
|
3480
|
+
): string {
|
|
3195
3481
|
// Reject while engine is actively running
|
|
3196
3482
|
const activePhases = new Set(["launching", "executing", "merging", "planning"]);
|
|
3197
3483
|
if (activePhases.has(orchBatchState.phase)) {
|
|
@@ -3215,8 +3501,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3215
3501
|
// Force-merge is a recovery action for non-running failed/paused batches.
|
|
3216
3502
|
const resumablePhases = new Set(["paused", "stopped", "failed"]);
|
|
3217
3503
|
if (!resumablePhases.has(state.phase)) {
|
|
3218
|
-
return
|
|
3219
|
-
|
|
3504
|
+
return (
|
|
3505
|
+
`❌ Cannot force merge when batch phase is "${state.phase}". ` +
|
|
3506
|
+
`Force merge is only valid for paused/stopped/failed batches.`
|
|
3507
|
+
);
|
|
3220
3508
|
}
|
|
3221
3509
|
|
|
3222
3510
|
// Determine target wave index (0-based). Default to currentWaveIndex.
|
|
@@ -3250,8 +3538,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3250
3538
|
// Only allow force merge for mixed-outcome failures (partial status).
|
|
3251
3539
|
// Other failures (conflicts, build failures, repo divergence) need different resolution.
|
|
3252
3540
|
if (mergeEntry.status !== "partial") {
|
|
3253
|
-
return
|
|
3254
|
-
|
|
3541
|
+
return (
|
|
3542
|
+
`❌ Wave ${targetWave} merge failed with status "${mergeEntry.status}": ${mergeEntry.failureReason || "unknown reason"}.\n` +
|
|
3543
|
+
`Force merge only applies to mixed-outcome lanes (partial). This failure needs manual resolution.`
|
|
3544
|
+
);
|
|
3255
3545
|
}
|
|
3256
3546
|
|
|
3257
3547
|
const failureReason = mergeEntry.failureReason || "";
|
|
@@ -3261,9 +3551,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
3261
3551
|
failureReasonLower.includes("mixed-outcome") ||
|
|
3262
3552
|
failureReasonLower.includes("automatic partial-branch merge is disabled");
|
|
3263
3553
|
if (!isMixedOutcomePartial) {
|
|
3264
|
-
return
|
|
3554
|
+
return (
|
|
3555
|
+
`❌ Wave ${targetWave} has partial merge status, but the failure reason does not match mixed-outcome lanes.\n` +
|
|
3265
3556
|
`Reason: ${failureReason || "unknown"}\n` +
|
|
3266
|
-
`Force merge is only valid for the mixed-outcome lane guard. Resolve this merge failure manually
|
|
3557
|
+
`Force merge is only valid for the mixed-outcome lane guard. Resolve this merge failure manually.`
|
|
3558
|
+
);
|
|
3267
3559
|
}
|
|
3268
3560
|
|
|
3269
3561
|
// Collect tasks in the target wave
|
|
@@ -3272,7 +3564,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3272
3564
|
const succeededInWave: string[] = [];
|
|
3273
3565
|
|
|
3274
3566
|
for (const taskId of waveTasks) {
|
|
3275
|
-
const task = state.tasks.find(t => t.taskId === taskId);
|
|
3567
|
+
const task = state.tasks.find((t) => t.taskId === taskId);
|
|
3276
3568
|
if (!task) continue;
|
|
3277
3569
|
if (task.status === "failed" || task.status === "stalled") {
|
|
3278
3570
|
failedInWave.push(taskId);
|
|
@@ -3289,7 +3581,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3289
3581
|
const skippedTasks: string[] = [];
|
|
3290
3582
|
if (skipFailed && failedInWave.length > 0) {
|
|
3291
3583
|
for (const taskId of failedInWave) {
|
|
3292
|
-
const task = state.tasks.find(t => t.taskId === taskId);
|
|
3584
|
+
const task = state.tasks.find((t) => t.taskId === taskId);
|
|
3293
3585
|
if (!task) continue;
|
|
3294
3586
|
const prevStatus = task.status;
|
|
3295
3587
|
task.status = "skipped";
|
|
@@ -3307,13 +3599,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3307
3599
|
// Recompute blocked tasks if dependency graph is available
|
|
3308
3600
|
const remainingFailures = new Set<string>();
|
|
3309
3601
|
for (const t of state.tasks) {
|
|
3310
|
-
if (
|
|
3602
|
+
if (t.status === "failed" || t.status === "stalled") {
|
|
3311
3603
|
remainingFailures.add(t.taskId);
|
|
3312
3604
|
}
|
|
3313
3605
|
}
|
|
3314
3606
|
|
|
3315
3607
|
if (orchBatchState.dependencyGraph && orchBatchState.batchId === state.batchId) {
|
|
3316
|
-
const newBlocked = computeTransitiveDependents(
|
|
3608
|
+
const newBlocked = computeTransitiveDependents(
|
|
3609
|
+
remainingFailures,
|
|
3610
|
+
orchBatchState.dependencyGraph,
|
|
3611
|
+
);
|
|
3317
3612
|
state.blockedTaskIds = [...newBlocked].sort();
|
|
3318
3613
|
state.blockedTasks = newBlocked.size;
|
|
3319
3614
|
} else if (remainingFailures.size === 0) {
|
|
@@ -3322,8 +3617,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3322
3617
|
state.blockedTasks = 0;
|
|
3323
3618
|
}
|
|
3324
3619
|
} else if (!skipFailed && failedInWave.length > 0) {
|
|
3325
|
-
return
|
|
3326
|
-
|
|
3620
|
+
return (
|
|
3621
|
+
`❌ Wave ${targetWave} has ${failedInWave.length} failed task(s): ${failedInWave.join(", ")}.\n` +
|
|
3622
|
+
`Use skipFailed=true to skip them, or use orch_skip_task to skip them individually first.`
|
|
3623
|
+
);
|
|
3327
3624
|
}
|
|
3328
3625
|
|
|
3329
3626
|
// Clear the failed merge result so resume will re-attempt the merge.
|
|
@@ -3335,7 +3632,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3335
3632
|
state.phase = "paused";
|
|
3336
3633
|
|
|
3337
3634
|
// Clear merge-related errors
|
|
3338
|
-
state.errors = state.errors.filter(
|
|
3635
|
+
state.errors = state.errors.filter(
|
|
3636
|
+
(e) => !e.includes("mixed") && !e.includes("merge") && !e.includes("Merge"),
|
|
3637
|
+
);
|
|
3339
3638
|
state.lastError = null;
|
|
3340
3639
|
|
|
3341
3640
|
// Update timestamp
|
|
@@ -3369,7 +3668,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3369
3668
|
lines.push(` Skipped tasks (were failed): ${skippedTasks.join(", ")}`);
|
|
3370
3669
|
}
|
|
3371
3670
|
|
|
3372
|
-
lines.push(
|
|
3671
|
+
lines.push(
|
|
3672
|
+
` Batch phase: paused | Failed: ${state.failedTasks}, Skipped: ${state.skippedTasks ?? 0} / ${state.totalTasks} total`,
|
|
3673
|
+
);
|
|
3373
3674
|
|
|
3374
3675
|
const resumeHint = "Use orch_resume() to re-run the merge with failed tasks skipped.";
|
|
3375
3676
|
lines.push(` ${resumeHint}`);
|
|
@@ -3407,7 +3708,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3407
3708
|
listOrchBranches: () => {
|
|
3408
3709
|
const result = runGit(["branch", "--list", "orch/*"], repoRoot);
|
|
3409
3710
|
return result.ok
|
|
3410
|
-
? result.stdout
|
|
3711
|
+
? result.stdout
|
|
3712
|
+
.split("\n")
|
|
3713
|
+
.map((b) => b.replace(/^\*?\s+/, "").trim())
|
|
3714
|
+
.filter(Boolean)
|
|
3411
3715
|
: [];
|
|
3412
3716
|
},
|
|
3413
3717
|
orchBranchExists: (branch: string) => {
|
|
@@ -3420,7 +3724,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3420
3724
|
return { message: resolution.error, error: severity !== "info" };
|
|
3421
3725
|
}
|
|
3422
3726
|
|
|
3423
|
-
const { orchBranch, baseBranch, batchId, currentBranch, notices } =
|
|
3727
|
+
const { orchBranch, baseBranch, batchId, currentBranch, notices } =
|
|
3728
|
+
resolution as IntegrationContext;
|
|
3424
3729
|
const outputLines: string[] = [];
|
|
3425
3730
|
let hasWarning = false;
|
|
3426
3731
|
|
|
@@ -3436,8 +3741,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3436
3741
|
hasWarning = true;
|
|
3437
3742
|
outputLines.push(
|
|
3438
3743
|
`⚠️ Branch \`${baseBranch}\` has branch protection rules enabled.\n` +
|
|
3439
|
-
|
|
3440
|
-
|
|
3744
|
+
`Direct merges may be blocked by your repository settings.\n\n` +
|
|
3745
|
+
`Recommended: use \`/orch-integrate --pr\` to create a pull request instead.`,
|
|
3441
3746
|
);
|
|
3442
3747
|
}
|
|
3443
3748
|
}
|
|
@@ -3449,24 +3754,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
3449
3754
|
);
|
|
3450
3755
|
const commitsAhead = revListResult.ok ? revListResult.stdout.trim() : "?";
|
|
3451
3756
|
|
|
3452
|
-
const diffStatResult = runGit(
|
|
3453
|
-
["diff", "--stat", `${currentBranch}...${orchBranch}`],
|
|
3454
|
-
repoRoot,
|
|
3455
|
-
);
|
|
3757
|
+
const diffStatResult = runGit(["diff", "--stat", `${currentBranch}...${orchBranch}`], repoRoot);
|
|
3456
3758
|
const diffSummary = diffStatResult.ok ? diffStatResult.stdout.trim() : "(unable to compute diff)";
|
|
3457
3759
|
|
|
3458
3760
|
outputLines.push(
|
|
3459
3761
|
`🔀 Integration Summary\n` +
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3762
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
3763
|
+
` Orch branch: ${orchBranch}\n` +
|
|
3764
|
+
` Target: ${currentBranch}\n` +
|
|
3765
|
+
` Commits: ${commitsAhead} ahead\n` +
|
|
3766
|
+
` Mode: ${parsed.mode === "ff" ? "fast-forward" : parsed.mode === "merge" ? "merge commit" : "pull request"}\n` +
|
|
3767
|
+
(batchId ? ` Batch: ${batchId}\n` : "") +
|
|
3768
|
+
(parsed.force ? ` ⚠ Force: branch safety check skipped\n` : "") +
|
|
3769
|
+
`\n` +
|
|
3770
|
+
(diffSummary ? `${diffSummary}\n` : "") +
|
|
3771
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
3470
3772
|
);
|
|
3471
3773
|
|
|
3472
3774
|
// Execute integration
|
|
@@ -3476,7 +3778,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3476
3778
|
|
|
3477
3779
|
if (wsConfig) {
|
|
3478
3780
|
for (const [repoId, repoConf] of wsConfig.repos) {
|
|
3479
|
-
const branchCheck = runGit(
|
|
3781
|
+
const branchCheck = runGit(
|
|
3782
|
+
["rev-parse", "--verify", `refs/heads/${resolvedOrchBranch}`],
|
|
3783
|
+
repoConf.path,
|
|
3784
|
+
);
|
|
3480
3785
|
if (branchCheck.ok) {
|
|
3481
3786
|
reposToIntegrate.push({ id: repoId, root: repoConf.path });
|
|
3482
3787
|
}
|
|
@@ -3490,7 +3795,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3490
3795
|
const repoMessages: string[] = [];
|
|
3491
3796
|
|
|
3492
3797
|
for (const repo of reposToIntegrate) {
|
|
3493
|
-
const preCountResult = runGit(
|
|
3798
|
+
const preCountResult = runGit(
|
|
3799
|
+
["rev-list", "--count", `HEAD..${resolvedOrchBranch}`],
|
|
3800
|
+
repo.root,
|
|
3801
|
+
);
|
|
3494
3802
|
const repoCommitsBefore = preCountResult.ok ? parseInt(preCountResult.stdout) || 0 : 0;
|
|
3495
3803
|
|
|
3496
3804
|
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
@@ -3513,11 +3821,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3513
3821
|
};
|
|
3514
3822
|
}
|
|
3515
3823
|
},
|
|
3516
|
-
deleteBatchState: () => {
|
|
3824
|
+
deleteBatchState: () => {
|
|
3825
|
+
/* handled once after all repos */
|
|
3826
|
+
},
|
|
3517
3827
|
});
|
|
3518
3828
|
|
|
3519
3829
|
if (!integrationResult.success) {
|
|
3520
|
-
return {
|
|
3830
|
+
return {
|
|
3831
|
+
ok: false as const,
|
|
3832
|
+
error: `❌ Integration failed in ${repo.id}:\n${integrationResult.error}`,
|
|
3833
|
+
};
|
|
3521
3834
|
}
|
|
3522
3835
|
|
|
3523
3836
|
totalCommits += repoCommitsBefore;
|
|
@@ -3551,17 +3864,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
3551
3864
|
const branchCleanupLines: string[] = [];
|
|
3552
3865
|
for (const repo of allRepos) {
|
|
3553
3866
|
const branchCleanup = deleteStaleBranches(repo.root, opId, batchId);
|
|
3554
|
-
const totalDeleted =
|
|
3867
|
+
const totalDeleted =
|
|
3868
|
+
branchCleanup.deletedTaskBranches.length + branchCleanup.deletedSavedBranches.length;
|
|
3555
3869
|
if (totalDeleted > 0 || branchCleanup.failedDeletes.length > 0) {
|
|
3556
3870
|
const label = repo.id === "(default)" ? "" : ` (${repo.id})`;
|
|
3557
3871
|
if (branchCleanup.deletedTaskBranches.length > 0) {
|
|
3558
|
-
branchCleanupLines.push(
|
|
3872
|
+
branchCleanupLines.push(
|
|
3873
|
+
` 🗑️ Deleted ${branchCleanup.deletedTaskBranches.length} task branch(es)${label}`,
|
|
3874
|
+
);
|
|
3559
3875
|
}
|
|
3560
3876
|
if (branchCleanup.deletedSavedBranches.length > 0) {
|
|
3561
|
-
branchCleanupLines.push(
|
|
3877
|
+
branchCleanupLines.push(
|
|
3878
|
+
` 🗑️ Deleted ${branchCleanup.deletedSavedBranches.length} saved branch(es)${label}`,
|
|
3879
|
+
);
|
|
3562
3880
|
}
|
|
3563
3881
|
if (branchCleanup.failedDeletes.length > 0) {
|
|
3564
|
-
branchCleanupLines.push(
|
|
3882
|
+
branchCleanupLines.push(
|
|
3883
|
+
` ⚠️ Failed to delete ${branchCleanup.failedDeletes.length} branch(es)${label}: ${branchCleanup.failedDeletes.join(", ")}`,
|
|
3884
|
+
);
|
|
3565
3885
|
}
|
|
3566
3886
|
}
|
|
3567
3887
|
}
|
|
@@ -3573,8 +3893,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3573
3893
|
const repoFindings: IntegrateCleanupRepoFindings[] = [];
|
|
3574
3894
|
for (const repo of allRepos) {
|
|
3575
3895
|
const findings = collectRepoCleanupFindings(
|
|
3576
|
-
repo.root,
|
|
3577
|
-
|
|
3896
|
+
repo.root,
|
|
3897
|
+
repo.id === "(default)" ? undefined : repo.id,
|
|
3898
|
+
opId,
|
|
3899
|
+
batchId,
|
|
3900
|
+
orchPrefix,
|
|
3901
|
+
resolvedOrchBranch,
|
|
3902
|
+
orchConfig,
|
|
3578
3903
|
{ skipOrchBranch },
|
|
3579
3904
|
);
|
|
3580
3905
|
repoFindings.push(findings);
|
|
@@ -3587,10 +3912,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
3587
3912
|
|
|
3588
3913
|
// TP-179: Write integratedAt to batch history before deleting state
|
|
3589
3914
|
if (batchId) {
|
|
3590
|
-
try {
|
|
3915
|
+
try {
|
|
3916
|
+
updateBatchHistoryIntegration(stateRoot, batchId, Date.now());
|
|
3917
|
+
} catch {
|
|
3918
|
+
/* best effort */
|
|
3919
|
+
}
|
|
3591
3920
|
}
|
|
3592
3921
|
|
|
3593
|
-
try {
|
|
3922
|
+
try {
|
|
3923
|
+
deleteBatchState(stateRoot);
|
|
3924
|
+
} catch {
|
|
3925
|
+
/* best effort */
|
|
3926
|
+
}
|
|
3594
3927
|
|
|
3595
3928
|
// ── TP-065: Post-integrate artifact cleanup (Layer 1) ────
|
|
3596
3929
|
// Delete batch-specific telemetry and merge result files.
|
|
@@ -3598,7 +3931,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
3598
3931
|
if (batchId) {
|
|
3599
3932
|
try {
|
|
3600
3933
|
const artifactCleanup = cleanupPostIntegrate(stateRoot, batchId);
|
|
3601
|
-
const totalCleaned =
|
|
3934
|
+
const totalCleaned =
|
|
3935
|
+
artifactCleanup.telemetryFilesDeleted +
|
|
3936
|
+
artifactCleanup.mergeFilesDeleted +
|
|
3937
|
+
artifactCleanup.promptFilesDeleted +
|
|
3938
|
+
artifactCleanup.mailboxDirsDeleted;
|
|
3602
3939
|
if (totalCleaned > 0) {
|
|
3603
3940
|
const cleanupParts = [
|
|
3604
3941
|
`${artifactCleanup.telemetryFilesDeleted} telemetry file(s)`,
|
|
@@ -3608,9 +3945,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3608
3945
|
if (artifactCleanup.mailboxDirsDeleted > 0) {
|
|
3609
3946
|
cleanupParts.push(`${artifactCleanup.mailboxDirsDeleted} mailbox dir(s)`);
|
|
3610
3947
|
}
|
|
3611
|
-
outputLines.push(
|
|
3612
|
-
`🧹 Cleaned up ${cleanupParts.join(", ")} for batch ${batchId}`,
|
|
3613
|
-
);
|
|
3948
|
+
outputLines.push(`🧹 Cleaned up ${cleanupParts.join(", ")} for batch ${batchId}`);
|
|
3614
3949
|
}
|
|
3615
3950
|
if (artifactCleanup.warnings.length > 0) {
|
|
3616
3951
|
hasWarning = true;
|
|
@@ -3632,7 +3967,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
3632
3967
|
const deps = supervisorState.pendingSummaryDeps;
|
|
3633
3968
|
supervisorState.pendingSummaryDeps = null;
|
|
3634
3969
|
if (supervisorState.batchStateRef && supervisorState.stateRoot) {
|
|
3635
|
-
presentBatchSummary(
|
|
3970
|
+
presentBatchSummary(
|
|
3971
|
+
pi,
|
|
3972
|
+
supervisorState.batchStateRef,
|
|
3973
|
+
supervisorState.stateRoot,
|
|
3974
|
+
deps.opId,
|
|
3975
|
+
deps.diagnostics,
|
|
3976
|
+
deps.mergeResults,
|
|
3977
|
+
);
|
|
3636
3978
|
}
|
|
3637
3979
|
deactivateSupervisor(pi, supervisorState);
|
|
3638
3980
|
}
|
|
@@ -3653,7 +3995,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3653
3995
|
handler: async (_args, ctx) => {
|
|
3654
3996
|
const result = doOrchPause();
|
|
3655
3997
|
// Determine notification level from result content
|
|
3656
|
-
const level =
|
|
3998
|
+
const level =
|
|
3999
|
+
result.includes("No batch") || result.includes("already paused") ? "warning" : "info";
|
|
3657
4000
|
ctx.ui.notify(result, level);
|
|
3658
4001
|
},
|
|
3659
4002
|
});
|
|
@@ -3686,7 +4029,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3686
4029
|
// Top-level catch: ensure the user ALWAYS sees something
|
|
3687
4030
|
ctx.ui.notify(
|
|
3688
4031
|
`❌ Abort failed with error: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
3689
|
-
|
|
4032
|
+
` Stack: ${err instanceof Error ? err.stack : "N/A"}`,
|
|
3690
4033
|
"error",
|
|
3691
4034
|
);
|
|
3692
4035
|
}
|
|
@@ -3699,15 +4042,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
3699
4042
|
if (!args?.trim()) {
|
|
3700
4043
|
ctx.ui.notify(
|
|
3701
4044
|
"Usage: /orch-deps <areas|paths|all> [--refresh] [--task <id>]\n\n" +
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
4045
|
+
"Shows the dependency graph for tasks in the specified areas.\n\n" +
|
|
4046
|
+
"Options:\n" +
|
|
4047
|
+
" --refresh Force re-scan of areas (bypass dependency cache)\n" +
|
|
4048
|
+
" --task <id> Show dependencies for a single task only\n\n" +
|
|
4049
|
+
"Examples:\n" +
|
|
4050
|
+
" /orch-deps all\n" +
|
|
4051
|
+
" /orch-deps all --task TO-014\n" +
|
|
4052
|
+
" /orch-deps time-off --refresh\n" +
|
|
4053
|
+
" /orch-deps all --task COMP-006 --refresh",
|
|
3711
4054
|
"info",
|
|
3712
4055
|
);
|
|
3713
4056
|
return;
|
|
@@ -3734,7 +4077,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3734
4077
|
if (!cleanArgs) {
|
|
3735
4078
|
ctx.ui.notify(
|
|
3736
4079
|
"Usage: /orch-deps <areas|paths|all> [--refresh] [--task <id>]\n" +
|
|
3737
|
-
|
|
4080
|
+
"Error: target argument required (e.g., 'all', area name, or path)",
|
|
3738
4081
|
"error",
|
|
3739
4082
|
);
|
|
3740
4083
|
return;
|
|
@@ -3760,11 +4103,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3760
4103
|
// Show dependency graph (full or filtered)
|
|
3761
4104
|
if (discovery.pending.size > 0) {
|
|
3762
4105
|
ctx.ui.notify(
|
|
3763
|
-
formatDependencyGraph(
|
|
3764
|
-
discovery.pending,
|
|
3765
|
-
discovery.completed,
|
|
3766
|
-
filterTaskId,
|
|
3767
|
-
),
|
|
4106
|
+
formatDependencyGraph(discovery.pending, discovery.completed, filterTaskId),
|
|
3768
4107
|
"info",
|
|
3769
4108
|
);
|
|
3770
4109
|
}
|
|
@@ -3790,8 +4129,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3790
4129
|
if (supervisorState.active) {
|
|
3791
4130
|
ctx.ui.notify(
|
|
3792
4131
|
"✅ This session is already the active supervisor.\n\n" +
|
|
3793
|
-
|
|
3794
|
-
|
|
4132
|
+
` Session: ${supervisorState.lockSessionId}\n` +
|
|
4133
|
+
` Batch: ${supervisorState.batchId || orchBatchState.batchId}`,
|
|
3795
4134
|
"info",
|
|
3796
4135
|
);
|
|
3797
4136
|
return;
|
|
@@ -3802,10 +4141,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3802
4141
|
|
|
3803
4142
|
switch (lockResult.status) {
|
|
3804
4143
|
case "no-active-batch":
|
|
3805
|
-
ctx.ui.notify(
|
|
3806
|
-
"No active batch to supervise.\n\nStart a batch with /orch first.",
|
|
3807
|
-
"info",
|
|
3808
|
-
);
|
|
4144
|
+
ctx.ui.notify("No active batch to supervise.\n\nStart a batch with /orch first.", "info");
|
|
3809
4145
|
return;
|
|
3810
4146
|
|
|
3811
4147
|
case "no-lockfile":
|
|
@@ -3816,17 +4152,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
3816
4152
|
const summary = buildTakeoverSummary(stateRoot, batchState);
|
|
3817
4153
|
const reason =
|
|
3818
4154
|
lockResult.status === "stale"
|
|
3819
|
-
?
|
|
4155
|
+
? isProcessAlive(lockResult.lock.pid)
|
|
3820
4156
|
? `Previous supervisor (PID ${lockResult.lock.pid}) has a stale heartbeat (last: ${lockResult.lock.heartbeat}).`
|
|
3821
|
-
: `Previous supervisor (PID ${lockResult.lock.pid}) process is dead.`
|
|
4157
|
+
: `Previous supervisor (PID ${lockResult.lock.pid}) process is dead.`
|
|
3822
4158
|
: lockResult.status === "corrupt"
|
|
3823
4159
|
? "Found a corrupt supervisor lockfile."
|
|
3824
4160
|
: "No supervisor lockfile found.";
|
|
3825
4161
|
|
|
3826
|
-
ctx.ui.notify(
|
|
3827
|
-
`🔄 **${reason}** Activating supervisor.\n\n` + summary,
|
|
3828
|
-
"info",
|
|
3829
|
-
);
|
|
4162
|
+
ctx.ui.notify(`🔄 **${reason}** Activating supervisor.\n\n` + summary, "info");
|
|
3830
4163
|
|
|
3831
4164
|
// Populate orchBatchState from persisted state
|
|
3832
4165
|
orchBatchState.batchId = batchState.batchId;
|
|
@@ -3867,10 +4200,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3867
4200
|
|
|
3868
4201
|
ctx.ui.notify(
|
|
3869
4202
|
`⚡ **Forcing supervisor takeover from PID ${lock.pid}.**\n\n` +
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
4203
|
+
` Previous session: ${lock.sessionId}\n` +
|
|
4204
|
+
` Previous heartbeat: ${lock.heartbeat}\n\n` +
|
|
4205
|
+
`The other session will yield on its next heartbeat check.\n\n` +
|
|
4206
|
+
summary,
|
|
3874
4207
|
"warning",
|
|
3875
4208
|
);
|
|
3876
4209
|
|
|
@@ -3916,19 +4249,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
3916
4249
|
if (args?.trim() === "--help" || args?.trim() === "-h") {
|
|
3917
4250
|
ctx.ui.notify(
|
|
3918
4251
|
"Usage: /orch-integrate [<orch-branch>] [--merge] [--pr] [--force]\n\n" +
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
4252
|
+
"Integrate a completed orch batch into your working branch.\n\n" +
|
|
4253
|
+
"Modes:\n" +
|
|
4254
|
+
" (default) Fast-forward merge (cleanest history)\n" +
|
|
4255
|
+
" --merge Create a real merge commit\n" +
|
|
4256
|
+
" --pr Push orch branch and create a pull request\n\n" +
|
|
4257
|
+
"Options:\n" +
|
|
4258
|
+
" --force Skip branch safety check\n" +
|
|
4259
|
+
" <branch> Orch branch name (auto-detected from batch state if omitted)\n\n" +
|
|
4260
|
+
"Examples:\n" +
|
|
4261
|
+
" /orch-integrate Auto-detect and fast-forward\n" +
|
|
4262
|
+
" /orch-integrate --merge Auto-detect with merge commit\n" +
|
|
4263
|
+
" /orch-integrate orch/op-abc123 --pr Specific branch, create PR\n" +
|
|
4264
|
+
" /orch-integrate --force Skip branch safety check",
|
|
3932
4265
|
"info",
|
|
3933
4266
|
);
|
|
3934
4267
|
return;
|
|
@@ -3962,7 +4295,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
3962
4295
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
3963
4296
|
} catch (err) {
|
|
3964
4297
|
return {
|
|
3965
|
-
content: [
|
|
4298
|
+
content: [
|
|
4299
|
+
{
|
|
4300
|
+
type: "text" as const,
|
|
4301
|
+
text: `Error checking status: ${err instanceof Error ? err.message : String(err)}`,
|
|
4302
|
+
},
|
|
4303
|
+
],
|
|
3966
4304
|
details: undefined,
|
|
3967
4305
|
};
|
|
3968
4306
|
}
|
|
@@ -3989,7 +4327,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
3989
4327
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
3990
4328
|
} catch (err) {
|
|
3991
4329
|
return {
|
|
3992
|
-
content: [
|
|
4330
|
+
content: [
|
|
4331
|
+
{
|
|
4332
|
+
type: "text" as const,
|
|
4333
|
+
text: `Error pausing batch: ${err instanceof Error ? err.message : String(err)}`,
|
|
4334
|
+
},
|
|
4335
|
+
],
|
|
3993
4336
|
details: undefined,
|
|
3994
4337
|
};
|
|
3995
4338
|
}
|
|
@@ -4011,9 +4354,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4011
4354
|
"The resume happens asynchronously — the tool returns immediately with a status message.",
|
|
4012
4355
|
],
|
|
4013
4356
|
parameters: Type.Object({
|
|
4014
|
-
force: Type.Optional(
|
|
4015
|
-
|
|
4016
|
-
|
|
4357
|
+
force: Type.Optional(
|
|
4358
|
+
Type.Boolean({
|
|
4359
|
+
description: "Resume from stopped or failed state (default: false)",
|
|
4360
|
+
}),
|
|
4361
|
+
),
|
|
4017
4362
|
}),
|
|
4018
4363
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4019
4364
|
try {
|
|
@@ -4021,7 +4366,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4021
4366
|
return { content: [{ type: "text" as const, text: result.message }], details: undefined };
|
|
4022
4367
|
} catch (err) {
|
|
4023
4368
|
return {
|
|
4024
|
-
content: [
|
|
4369
|
+
content: [
|
|
4370
|
+
{
|
|
4371
|
+
type: "text" as const,
|
|
4372
|
+
text: `Error resuming batch: ${err instanceof Error ? err.message : String(err)}`,
|
|
4373
|
+
},
|
|
4374
|
+
],
|
|
4025
4375
|
details: undefined,
|
|
4026
4376
|
};
|
|
4027
4377
|
}
|
|
@@ -4044,9 +4394,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4044
4394
|
"Worktrees and branches are preserved for inspection after abort.",
|
|
4045
4395
|
],
|
|
4046
4396
|
parameters: Type.Object({
|
|
4047
|
-
hard: Type.Optional(
|
|
4048
|
-
|
|
4049
|
-
|
|
4397
|
+
hard: Type.Optional(
|
|
4398
|
+
Type.Boolean({
|
|
4399
|
+
description: "Hard abort — immediate kill without grace period (default: false)",
|
|
4400
|
+
}),
|
|
4401
|
+
),
|
|
4050
4402
|
}),
|
|
4051
4403
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4052
4404
|
try {
|
|
@@ -4054,7 +4406,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4054
4406
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4055
4407
|
} catch (err) {
|
|
4056
4408
|
return {
|
|
4057
|
-
content: [
|
|
4409
|
+
content: [
|
|
4410
|
+
{
|
|
4411
|
+
type: "text" as const,
|
|
4412
|
+
text: `Error aborting batch: ${err instanceof Error ? err.message : String(err)}`,
|
|
4413
|
+
},
|
|
4414
|
+
],
|
|
4058
4415
|
details: undefined,
|
|
4059
4416
|
};
|
|
4060
4417
|
}
|
|
@@ -4097,7 +4454,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4097
4454
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4098
4455
|
} catch (err) {
|
|
4099
4456
|
return {
|
|
4100
|
-
content: [
|
|
4457
|
+
content: [
|
|
4458
|
+
{
|
|
4459
|
+
type: "text" as const,
|
|
4460
|
+
text: `Error during supervisor takeover: ${err instanceof Error ? err.message : String(err)}`,
|
|
4461
|
+
},
|
|
4462
|
+
],
|
|
4101
4463
|
details: undefined,
|
|
4102
4464
|
};
|
|
4103
4465
|
}
|
|
@@ -4121,16 +4483,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
4121
4483
|
"If the target branch has protection rules, prefer mode='pr'.",
|
|
4122
4484
|
],
|
|
4123
4485
|
parameters: Type.Object({
|
|
4124
|
-
mode: Type.Optional(
|
|
4125
|
-
[Type.Literal("fast-forward"), Type.Literal("merge"), Type.Literal("pr")],
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4486
|
+
mode: Type.Optional(
|
|
4487
|
+
Type.Union([Type.Literal("fast-forward"), Type.Literal("merge"), Type.Literal("pr")], {
|
|
4488
|
+
description: 'Integration mode (default: "fast-forward")',
|
|
4489
|
+
}),
|
|
4490
|
+
),
|
|
4491
|
+
force: Type.Optional(
|
|
4492
|
+
Type.Boolean({
|
|
4493
|
+
description: "Skip branch safety check (default: false)",
|
|
4494
|
+
}),
|
|
4495
|
+
),
|
|
4496
|
+
branch: Type.Optional(
|
|
4497
|
+
Type.String({
|
|
4498
|
+
description: "Orch branch name (auto-detected from batch state if omitted)",
|
|
4499
|
+
}),
|
|
4500
|
+
),
|
|
4134
4501
|
}),
|
|
4135
4502
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4136
4503
|
try {
|
|
@@ -4147,7 +4514,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4147
4514
|
return { content: [{ type: "text" as const, text: result.message }], details: undefined };
|
|
4148
4515
|
} catch (err) {
|
|
4149
4516
|
return {
|
|
4150
|
-
content: [
|
|
4517
|
+
content: [
|
|
4518
|
+
{
|
|
4519
|
+
type: "text" as const,
|
|
4520
|
+
text: `Error integrating batch: ${err instanceof Error ? err.message : String(err)}`,
|
|
4521
|
+
},
|
|
4522
|
+
],
|
|
4151
4523
|
details: undefined,
|
|
4152
4524
|
};
|
|
4153
4525
|
}
|
|
@@ -4158,7 +4530,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4158
4530
|
name: "orch_start",
|
|
4159
4531
|
label: "Start Batch",
|
|
4160
4532
|
description:
|
|
4161
|
-
|
|
4533
|
+
'Start a new orchestration batch. Target is "all" to run all pending tasks, ' +
|
|
4162
4534
|
"a task area name, a directory path, or one or more PROMPT.md paths. " +
|
|
4163
4535
|
"The batch runs asynchronously — use orch_status() to monitor progress.",
|
|
4164
4536
|
promptSnippet: "orch_start(target) — start a new batch",
|
|
@@ -4166,15 +4538,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
4166
4538
|
"Call orch_start to begin executing pending tasks as a batch.",
|
|
4167
4539
|
'Use target="all" to run all pending tasks.',
|
|
4168
4540
|
"Specify a task area name to run all pending tasks in that area.",
|
|
4169
|
-
|
|
4170
|
-
|
|
4541
|
+
'Specify a PROMPT.md path to run a single task: target="taskplane-tasks/TP-101/PROMPT.md"',
|
|
4542
|
+
'Specify multiple space-separated PROMPT.md paths to run specific tasks: target="path/TP-001/PROMPT.md path/TP-002/PROMPT.md"',
|
|
4171
4543
|
"Cannot start if a batch is already running — check orch_status() first.",
|
|
4172
4544
|
"The batch runs asynchronously. The tool returns immediately with an ACK.",
|
|
4173
4545
|
"After starting, use orch_status() to track progress.",
|
|
4174
4546
|
],
|
|
4175
4547
|
parameters: Type.Object({
|
|
4176
4548
|
target: Type.String({
|
|
4177
|
-
description:
|
|
4549
|
+
description:
|
|
4550
|
+
'Target to run: "all" for all pending tasks, a task area name, a directory path, or one or more PROMPT.md paths (space-separated)',
|
|
4178
4551
|
}),
|
|
4179
4552
|
}),
|
|
4180
4553
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -4183,7 +4556,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4183
4556
|
return { content: [{ type: "text" as const, text: result.message }], details: undefined };
|
|
4184
4557
|
} catch (err) {
|
|
4185
4558
|
return {
|
|
4186
|
-
content: [
|
|
4559
|
+
content: [
|
|
4560
|
+
{
|
|
4561
|
+
type: "text" as const,
|
|
4562
|
+
text: `Error starting batch: ${err instanceof Error ? err.message : String(err)}`,
|
|
4563
|
+
},
|
|
4564
|
+
],
|
|
4187
4565
|
details: undefined,
|
|
4188
4566
|
};
|
|
4189
4567
|
}
|
|
@@ -4216,7 +4594,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4216
4594
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4217
4595
|
} catch (err) {
|
|
4218
4596
|
return {
|
|
4219
|
-
content: [
|
|
4597
|
+
content: [
|
|
4598
|
+
{
|
|
4599
|
+
type: "text" as const,
|
|
4600
|
+
text: `Error retrying task: ${err instanceof Error ? err.message : String(err)}`,
|
|
4601
|
+
},
|
|
4602
|
+
],
|
|
4220
4603
|
details: undefined,
|
|
4221
4604
|
};
|
|
4222
4605
|
}
|
|
@@ -4249,7 +4632,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4249
4632
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4250
4633
|
} catch (err) {
|
|
4251
4634
|
return {
|
|
4252
|
-
content: [
|
|
4635
|
+
content: [
|
|
4636
|
+
{
|
|
4637
|
+
type: "text" as const,
|
|
4638
|
+
text: `Error skipping task: ${err instanceof Error ? err.message : String(err)}`,
|
|
4639
|
+
},
|
|
4640
|
+
],
|
|
4253
4641
|
details: undefined,
|
|
4254
4642
|
};
|
|
4255
4643
|
}
|
|
@@ -4265,7 +4653,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4265
4653
|
"Force merge a wave that was rejected due to mixed-outcome lanes (succeeded and failed tasks " +
|
|
4266
4654
|
"on the same lane). Updates the merge result to 'succeeded' so the batch can continue. " +
|
|
4267
4655
|
"Optionally skips failed tasks in the wave.",
|
|
4268
|
-
promptSnippet:
|
|
4656
|
+
promptSnippet:
|
|
4657
|
+
"orch_force_merge(waveIndex?, skipFailed?) — force merge a wave with mixed results",
|
|
4269
4658
|
promptGuidelines: [
|
|
4270
4659
|
"Call orch_force_merge when a wave merge was rejected because lanes had both succeeded and failed tasks.",
|
|
4271
4660
|
"The batch must be paused, stopped, or failed with a 'partial' merge result for the target wave.",
|
|
@@ -4275,12 +4664,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
4275
4664
|
"waveIndex is 0-based. Omit it to target the current wave.",
|
|
4276
4665
|
],
|
|
4277
4666
|
parameters: Type.Object({
|
|
4278
|
-
waveIndex: Type.Optional(
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4667
|
+
waveIndex: Type.Optional(
|
|
4668
|
+
Type.Number({
|
|
4669
|
+
description: "0-based wave index to force merge. Defaults to the current wave.",
|
|
4670
|
+
}),
|
|
4671
|
+
),
|
|
4672
|
+
skipFailed: Type.Optional(
|
|
4673
|
+
Type.Boolean({
|
|
4674
|
+
description:
|
|
4675
|
+
"If true, automatically skip all failed tasks in the wave before merging. Defaults to false.",
|
|
4676
|
+
}),
|
|
4677
|
+
),
|
|
4284
4678
|
}),
|
|
4285
4679
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4286
4680
|
try {
|
|
@@ -4288,7 +4682,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4288
4682
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4289
4683
|
} catch (err) {
|
|
4290
4684
|
return {
|
|
4291
|
-
content: [
|
|
4685
|
+
content: [
|
|
4686
|
+
{
|
|
4687
|
+
type: "text" as const,
|
|
4688
|
+
text: `Error force merging: ${err instanceof Error ? err.message : String(err)}`,
|
|
4689
|
+
},
|
|
4690
|
+
],
|
|
4292
4691
|
details: undefined,
|
|
4293
4692
|
};
|
|
4294
4693
|
}
|
|
@@ -4303,7 +4702,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4303
4702
|
description:
|
|
4304
4703
|
"Send a steering message to a running agent (worker, reviewer, or merger). " +
|
|
4305
4704
|
"The message is delivered into the agent's LLM context at the next turn boundary.",
|
|
4306
|
-
promptSnippet:
|
|
4705
|
+
promptSnippet:
|
|
4706
|
+
"send_agent_message(to, content, type?) — send steering message to a running agent",
|
|
4307
4707
|
promptGuidelines: [
|
|
4308
4708
|
"Call send_agent_message to course-correct a running agent (worker, reviewer, or merger).",
|
|
4309
4709
|
"The 'to' parameter must be a valid agent session name from the current batch.",
|
|
@@ -4318,10 +4718,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4318
4718
|
content: Type.String({
|
|
4319
4719
|
description: "Message content (max 4KB). Concise directive for the agent.",
|
|
4320
4720
|
}),
|
|
4321
|
-
type: Type.Optional(
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4721
|
+
type: Type.Optional(
|
|
4722
|
+
Type.Union(
|
|
4723
|
+
[Type.Literal("steer"), Type.Literal("query"), Type.Literal("abort"), Type.Literal("info")],
|
|
4724
|
+
{ description: 'Message type (default: "steer")' },
|
|
4725
|
+
),
|
|
4726
|
+
),
|
|
4325
4727
|
}),
|
|
4326
4728
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4327
4729
|
try {
|
|
@@ -4329,7 +4731,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4329
4731
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4330
4732
|
} catch (err) {
|
|
4331
4733
|
return {
|
|
4332
|
-
content: [
|
|
4734
|
+
content: [
|
|
4735
|
+
{
|
|
4736
|
+
type: "text" as const,
|
|
4737
|
+
text: `Error sending message: ${err instanceof Error ? err.message : String(err)}`,
|
|
4738
|
+
},
|
|
4739
|
+
],
|
|
4333
4740
|
details: undefined,
|
|
4334
4741
|
};
|
|
4335
4742
|
}
|
|
@@ -4343,7 +4750,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4343
4750
|
const registry = readRegistrySnapshot(stateRoot, state.batchId);
|
|
4344
4751
|
if (registry) {
|
|
4345
4752
|
for (const manifest of Object.values(registry.agents)) {
|
|
4346
|
-
if (manifest.role !== "worker" && manifest.role !== "reviewer" && manifest.role !== "merger")
|
|
4753
|
+
if (manifest.role !== "worker" && manifest.role !== "reviewer" && manifest.role !== "merger")
|
|
4754
|
+
continue;
|
|
4347
4755
|
if (isTerminalStatus(manifest.status) || !registryIsProcessAlive(manifest.pid)) continue;
|
|
4348
4756
|
ids.add(manifest.agentId);
|
|
4349
4757
|
}
|
|
@@ -4372,7 +4780,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4372
4780
|
*
|
|
4373
4781
|
* @since TP-089
|
|
4374
4782
|
*/
|
|
4375
|
-
function doSendAgentMessage(
|
|
4783
|
+
function doSendAgentMessage(
|
|
4784
|
+
to: string,
|
|
4785
|
+
content: string,
|
|
4786
|
+
messageType: string,
|
|
4787
|
+
ctx: ExtensionContext,
|
|
4788
|
+
): string {
|
|
4376
4789
|
const stateRoot = resolveToolStateRoot(ctx);
|
|
4377
4790
|
|
|
4378
4791
|
// Validate message type (outbound allowlist: steer, query, abort, info)
|
|
@@ -4452,11 +4865,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
4452
4865
|
contentPreview: content.slice(0, 200),
|
|
4453
4866
|
broadcast: false,
|
|
4454
4867
|
});
|
|
4455
|
-
return
|
|
4868
|
+
return (
|
|
4869
|
+
`✅ Message sent to \`${to}\` (batch ${state.batchId})\n` +
|
|
4456
4870
|
`- **ID:** ${msg.id}\n` +
|
|
4457
4871
|
`- **Type:** ${messageType}\n` +
|
|
4458
4872
|
`- **Size:** ${Buffer.byteLength(content, "utf8")} bytes\n` +
|
|
4459
|
-
`Message will be delivered at the agent's next turn boundary
|
|
4873
|
+
`Message will be delivered at the agent's next turn boundary.`
|
|
4874
|
+
);
|
|
4460
4875
|
} catch (err) {
|
|
4461
4876
|
return `❌ Failed to write message: ${err instanceof Error ? err.message : String(err)}`;
|
|
4462
4877
|
}
|
|
@@ -4471,7 +4886,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4471
4886
|
"Read reply and escalation messages from agents (non-consuming). " +
|
|
4472
4887
|
"Returns pending and already-acked outbox messages from a specific agent or all agents. " +
|
|
4473
4888
|
"Messages are never removed by reading — this is a durable history view.",
|
|
4474
|
-
promptSnippet:
|
|
4889
|
+
promptSnippet:
|
|
4890
|
+
"read_agent_replies(from?) \u2014 read replies/escalations from agents (read-only, non-consuming)",
|
|
4475
4891
|
promptGuidelines: [
|
|
4476
4892
|
"Call read_agent_replies to check if any agent has sent a reply or escalation.",
|
|
4477
4893
|
"Omit 'from' to read replies from all agents.",
|
|
@@ -4479,9 +4895,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4479
4895
|
"This is non-consuming: replies remain visible after reading (pending + acked history).",
|
|
4480
4896
|
],
|
|
4481
4897
|
parameters: Type.Object({
|
|
4482
|
-
from: Type.Optional(
|
|
4483
|
-
|
|
4484
|
-
|
|
4898
|
+
from: Type.Optional(
|
|
4899
|
+
Type.String({
|
|
4900
|
+
description: "Agent ID to read replies from (omit for all agents)",
|
|
4901
|
+
}),
|
|
4902
|
+
),
|
|
4485
4903
|
}),
|
|
4486
4904
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4487
4905
|
try {
|
|
@@ -4489,7 +4907,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4489
4907
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4490
4908
|
} catch (err) {
|
|
4491
4909
|
return {
|
|
4492
|
-
content: [
|
|
4910
|
+
content: [
|
|
4911
|
+
{
|
|
4912
|
+
type: "text" as const,
|
|
4913
|
+
text: `Error reading replies: ${err instanceof Error ? err.message : String(err)}`,
|
|
4914
|
+
},
|
|
4915
|
+
],
|
|
4493
4916
|
details: undefined,
|
|
4494
4917
|
};
|
|
4495
4918
|
}
|
|
@@ -4512,13 +4935,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
4512
4935
|
// so replies from agents no longer active are still visible.
|
|
4513
4936
|
const agentIds = from
|
|
4514
4937
|
? [from]
|
|
4515
|
-
: [
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4938
|
+
: [
|
|
4939
|
+
...new Set([
|
|
4940
|
+
...collectKnownAgentIds(stateRoot, state),
|
|
4941
|
+
...discoverMailboxAgentIds(stateRoot, state.batchId),
|
|
4942
|
+
]),
|
|
4943
|
+
];
|
|
4519
4944
|
|
|
4520
4945
|
// TP-091: read full outbox history (pending + processed) for durable visibility
|
|
4521
|
-
const allEntries: Array<{
|
|
4946
|
+
const allEntries: Array<{
|
|
4947
|
+
agentId: string;
|
|
4948
|
+
message: import("./types.ts").MailboxMessage;
|
|
4949
|
+
acked: boolean;
|
|
4950
|
+
}> = [];
|
|
4522
4951
|
for (const agentId of agentIds) {
|
|
4523
4952
|
const history = readOutboxHistory(stateRoot, state.batchId, agentId);
|
|
4524
4953
|
for (const entry of history) {
|
|
@@ -4566,10 +4995,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4566
4995
|
content: Type.String({
|
|
4567
4996
|
description: "Message content (max 4KB)",
|
|
4568
4997
|
}),
|
|
4569
|
-
type: Type.Optional(
|
|
4570
|
-
[Type.Literal("steer"), Type.Literal("info"), Type.Literal("abort")],
|
|
4571
|
-
|
|
4572
|
-
|
|
4998
|
+
type: Type.Optional(
|
|
4999
|
+
Type.Union([Type.Literal("steer"), Type.Literal("info"), Type.Literal("abort")], {
|
|
5000
|
+
description: 'Message type (default: "info")',
|
|
5001
|
+
}),
|
|
5002
|
+
),
|
|
4573
5003
|
}),
|
|
4574
5004
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4575
5005
|
try {
|
|
@@ -4577,7 +5007,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4577
5007
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4578
5008
|
} catch (err) {
|
|
4579
5009
|
return {
|
|
4580
|
-
content: [
|
|
5010
|
+
content: [
|
|
5011
|
+
{
|
|
5012
|
+
type: "text" as const,
|
|
5013
|
+
text: `Error broadcasting: ${err instanceof Error ? err.message : String(err)}`,
|
|
5014
|
+
},
|
|
5015
|
+
],
|
|
4581
5016
|
details: undefined,
|
|
4582
5017
|
};
|
|
4583
5018
|
}
|
|
@@ -4615,7 +5050,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4615
5050
|
retryAfterMs: b.check.retryAfterMs,
|
|
4616
5051
|
});
|
|
4617
5052
|
}
|
|
4618
|
-
const preview = blocked
|
|
5053
|
+
const preview = blocked
|
|
5054
|
+
.slice(0, 5)
|
|
5055
|
+
.map((b) => `${b.agentId} (${Math.ceil((b.check.retryAfterMs ?? 0) / 1000)}s)`)
|
|
5056
|
+
.join(", ");
|
|
4619
5057
|
return `⏳ Broadcast rate limited for ${blocked.length}/${recipients.length} agent(s): ${preview}${blocked.length > 5 ? " ..." : ""}`;
|
|
4620
5058
|
}
|
|
4621
5059
|
|
|
@@ -4637,12 +5075,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
4637
5075
|
contentPreview: content.slice(0, 200),
|
|
4638
5076
|
broadcast: true,
|
|
4639
5077
|
});
|
|
4640
|
-
return
|
|
5078
|
+
return (
|
|
5079
|
+
`✅ Broadcast sent (batch ${state.batchId})\n` +
|
|
4641
5080
|
`- **ID:** ${msg.id}\n` +
|
|
4642
5081
|
`- **Type:** ${messageType}\n` +
|
|
4643
5082
|
`- **Recipients:** ${recipients.length}\n` +
|
|
4644
5083
|
`- **Size:** ${Buffer.byteLength(content, "utf8")} bytes\n` +
|
|
4645
|
-
`Message will be delivered to all agents at their next turn boundary
|
|
5084
|
+
`Message will be delivered to all agents at their next turn boundary.`
|
|
5085
|
+
);
|
|
4646
5086
|
} catch (err) {
|
|
4647
5087
|
return `❌ Failed to broadcast: ${err instanceof Error ? err.message : String(err)}`;
|
|
4648
5088
|
}
|
|
@@ -4652,7 +5092,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4652
5092
|
return execCtx?.workspaceRoot ?? execCtx?.repoRoot ?? context.cwd;
|
|
4653
5093
|
}
|
|
4654
5094
|
|
|
4655
|
-
function resolveLaneRepoRootForTools(
|
|
5095
|
+
function resolveLaneRepoRootForTools(
|
|
5096
|
+
laneRec: PersistedBatchState["lanes"][number],
|
|
5097
|
+
stateRoot: string,
|
|
5098
|
+
): string {
|
|
4656
5099
|
if (execCtx?.workspaceConfig && laneRec.repoId) {
|
|
4657
5100
|
const repo = execCtx.workspaceConfig.repos[laneRec.repoId];
|
|
4658
5101
|
if (repo?.path) return repo.path;
|
|
@@ -4669,16 +5112,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
4669
5112
|
"Read STATUS.md and telemetry for a running agent's lane. " +
|
|
4670
5113
|
"Returns current step, checkbox progress, context %, cost, tool count, and elapsed time. " +
|
|
4671
5114
|
"If lane is omitted, returns status for all active lanes.",
|
|
4672
|
-
promptSnippet:
|
|
5115
|
+
promptSnippet:
|
|
5116
|
+
"read_agent_status(lane?) — read STATUS.md + context % + cost from a running agent",
|
|
4673
5117
|
promptGuidelines: [
|
|
4674
5118
|
"Call read_agent_status to check on a specific lane's worker progress.",
|
|
4675
5119
|
"Omit lane to get a summary of all active lanes.",
|
|
4676
5120
|
"Returns: current step, checked/total items, context %, cost, elapsed.",
|
|
4677
5121
|
],
|
|
4678
5122
|
parameters: Type.Object({
|
|
4679
|
-
lane: Type.Optional(
|
|
4680
|
-
|
|
4681
|
-
|
|
5123
|
+
lane: Type.Optional(
|
|
5124
|
+
Type.Number({
|
|
5125
|
+
description: "Lane number to check (omit for all lanes)",
|
|
5126
|
+
}),
|
|
5127
|
+
),
|
|
4682
5128
|
}),
|
|
4683
5129
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
4684
5130
|
try {
|
|
@@ -4686,7 +5132,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4686
5132
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4687
5133
|
} catch (err) {
|
|
4688
5134
|
return {
|
|
4689
|
-
content: [
|
|
5135
|
+
content: [
|
|
5136
|
+
{
|
|
5137
|
+
type: "text" as const,
|
|
5138
|
+
text: `Error reading agent status: ${err instanceof Error ? err.message : String(err)}`,
|
|
5139
|
+
},
|
|
5140
|
+
],
|
|
4690
5141
|
details: undefined,
|
|
4691
5142
|
};
|
|
4692
5143
|
}
|
|
@@ -4704,9 +5155,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4704
5155
|
const state = loadBatchState(stateRoot);
|
|
4705
5156
|
if (!state) return "❌ No batch state found.";
|
|
4706
5157
|
|
|
4707
|
-
const targetLanes = lane != null
|
|
4708
|
-
? state.lanes.filter(l => l.laneNumber === lane)
|
|
4709
|
-
: state.lanes;
|
|
5158
|
+
const targetLanes = lane != null ? state.lanes.filter((l) => l.laneNumber === lane) : state.lanes;
|
|
4710
5159
|
|
|
4711
5160
|
if (targetLanes.length === 0) {
|
|
4712
5161
|
return lane != null
|
|
@@ -4719,8 +5168,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4719
5168
|
|
|
4720
5169
|
for (const laneRec of targetLanes) {
|
|
4721
5170
|
// Find current task for this lane
|
|
4722
|
-
const laneTasks = state.tasks.filter(t => t.laneNumber === laneRec.laneNumber);
|
|
4723
|
-
const runningTask = laneTasks.find(t => t.status === "running");
|
|
5171
|
+
const laneTasks = state.tasks.filter((t) => t.laneNumber === laneRec.laneNumber);
|
|
5172
|
+
const runningTask = laneTasks.find((t) => t.status === "running");
|
|
4724
5173
|
const currentTask = runningTask || laneTasks[laneTasks.length - 1];
|
|
4725
5174
|
|
|
4726
5175
|
lines.push(`### Lane ${laneRec.laneNumber} — ${laneRec.laneSessionId}`);
|
|
@@ -4728,11 +5177,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
4728
5177
|
|
|
4729
5178
|
if (currentTask) {
|
|
4730
5179
|
lines.push(`**Task:** ${currentTask.taskId} (${currentTask.status})`);
|
|
4731
|
-
const segmentLabel = buildTaskSegmentProgressLabel(
|
|
5180
|
+
const segmentLabel = buildTaskSegmentProgressLabel(
|
|
5181
|
+
currentTask,
|
|
5182
|
+
state.segments || [],
|
|
5183
|
+
currentTask.activeSegmentId ?? null,
|
|
5184
|
+
);
|
|
4732
5185
|
if (segmentLabel) lines.push(`**Segment:** ${segmentLabel}`);
|
|
4733
5186
|
if (currentTask.activeSegmentId) lines.push(`**Segment ID:** ${currentTask.activeSegmentId}`);
|
|
4734
|
-
const packetHomeRepo =
|
|
4735
|
-
|
|
5187
|
+
const packetHomeRepo =
|
|
5188
|
+
typeof currentTask.packetRepoId === "string" ? currentTask.packetRepoId : "";
|
|
5189
|
+
const effectiveTaskRepo =
|
|
5190
|
+
currentTask.resolvedRepoId || currentTask.repoId || laneRec.repoId || "";
|
|
4736
5191
|
if (packetHomeRepo && packetHomeRepo !== effectiveTaskRepo) {
|
|
4737
5192
|
lines.push(`**Packet Home Repo:** ${packetHomeRepo}`);
|
|
4738
5193
|
}
|
|
@@ -4761,9 +5216,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4761
5216
|
|
|
4762
5217
|
if (stepMatch) lines.push(`**Step:** ${stepMatch[1].trim()}`);
|
|
4763
5218
|
if (statusMatch) lines.push(`**Step Status:** ${statusMatch[1].trim()}`);
|
|
4764
|
-
if (total > 0)
|
|
5219
|
+
if (total > 0)
|
|
5220
|
+
lines.push(`**Progress:** ${checked}/${total} (${Math.round((checked / total) * 100)}%)`);
|
|
4765
5221
|
if (iterMatch) lines.push(`**Iteration:** ${iterMatch[1]}`);
|
|
4766
|
-
if (reviewMatch && Number.parseInt(reviewMatch[1], 10) > 0)
|
|
5222
|
+
if (reviewMatch && Number.parseInt(reviewMatch[1], 10) > 0)
|
|
5223
|
+
lines.push(`**Reviews:** ${reviewMatch[1]}`);
|
|
4767
5224
|
}
|
|
4768
5225
|
}
|
|
4769
5226
|
} catch {
|
|
@@ -4784,7 +5241,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4784
5241
|
if (ls.workerToolCount) parts.push(`tools: ${ls.workerToolCount}`);
|
|
4785
5242
|
if (ls.workerElapsed) parts.push(`elapsed: ${Math.round(ls.workerElapsed / 1000)}s`);
|
|
4786
5243
|
if (ls.workerStatus) parts.push(`worker: ${ls.workerStatus}`);
|
|
4787
|
-
if (ls.reviewerStatus && ls.reviewerStatus !== "idle")
|
|
5244
|
+
if (ls.reviewerStatus && ls.reviewerStatus !== "idle")
|
|
5245
|
+
parts.push(`reviewer: ${ls.reviewerStatus}`);
|
|
4788
5246
|
if (parts.length > 0) lines.push(`**Telemetry:** ${parts.join(" · ")}`);
|
|
4789
5247
|
}
|
|
4790
5248
|
} catch {
|
|
@@ -4819,7 +5277,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4819
5277
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4820
5278
|
} catch (err) {
|
|
4821
5279
|
return {
|
|
4822
|
-
content: [
|
|
5280
|
+
content: [
|
|
5281
|
+
{
|
|
5282
|
+
type: "text" as const,
|
|
5283
|
+
text: `Error triggering wrap-up: ${err instanceof Error ? err.message : String(err)}`,
|
|
5284
|
+
},
|
|
5285
|
+
],
|
|
4823
5286
|
details: undefined,
|
|
4824
5287
|
};
|
|
4825
5288
|
}
|
|
@@ -4836,11 +5299,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4836
5299
|
const state = loadBatchState(stateRoot);
|
|
4837
5300
|
if (!state) return "❌ No batch state found.";
|
|
4838
5301
|
|
|
4839
|
-
const laneRec = state.lanes.find(l => l.laneNumber === lane);
|
|
5302
|
+
const laneRec = state.lanes.find((l) => l.laneNumber === lane);
|
|
4840
5303
|
if (!laneRec) return `❌ Lane ${lane} not found in batch ${state.batchId}.`;
|
|
4841
5304
|
|
|
4842
5305
|
// Find running task for this lane
|
|
4843
|
-
const runningTask = state.tasks.find(t => t.laneNumber === lane && t.status === "running");
|
|
5306
|
+
const runningTask = state.tasks.find((t) => t.laneNumber === lane && t.status === "running");
|
|
4844
5307
|
if (!runningTask) return `❌ No running task on lane ${lane}.`;
|
|
4845
5308
|
|
|
4846
5309
|
// Resolve task folder in the worktree using canonical path resolver
|
|
@@ -4863,9 +5326,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4863
5326
|
const dir = dirname(wrapUpPath);
|
|
4864
5327
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
4865
5328
|
writeFileSync(wrapUpPath, `wrap-up signal for ${runningTask.taskId}\n`, "utf-8");
|
|
4866
|
-
return
|
|
5329
|
+
return (
|
|
5330
|
+
`✅ Wrap-up signal written for **${runningTask.taskId}** on lane ${lane}.\n` +
|
|
4867
5331
|
`Path: \`${wrapUpPath}\`\n` +
|
|
4868
|
-
`The worker will finish its current step and exit gracefully
|
|
5332
|
+
`The worker will finish its current step and exit gracefully.`
|
|
5333
|
+
);
|
|
4869
5334
|
} catch (err) {
|
|
4870
5335
|
return `❌ Failed to write wrap-up file: ${err instanceof Error ? err.message : String(err)}`;
|
|
4871
5336
|
}
|
|
@@ -4874,8 +5339,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4874
5339
|
pi.registerTool({
|
|
4875
5340
|
name: "read_lane_logs",
|
|
4876
5341
|
label: "Read Lane Logs",
|
|
4877
|
-
description:
|
|
4878
|
-
"Read stderr/crash logs for a specific lane from .pi/telemetry/ directory.",
|
|
5342
|
+
description: "Read stderr/crash logs for a specific lane from .pi/telemetry/ directory.",
|
|
4879
5343
|
promptSnippet: "read_lane_logs(lane) — read stderr/crash logs for a lane",
|
|
4880
5344
|
promptGuidelines: [
|
|
4881
5345
|
"Call read_lane_logs to read crash/error logs from a lane's stderr capture.",
|
|
@@ -4892,7 +5356,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4892
5356
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
4893
5357
|
} catch (err) {
|
|
4894
5358
|
return {
|
|
4895
|
-
content: [
|
|
5359
|
+
content: [
|
|
5360
|
+
{
|
|
5361
|
+
type: "text" as const,
|
|
5362
|
+
text: `Error reading lane logs: ${err instanceof Error ? err.message : String(err)}`,
|
|
5363
|
+
},
|
|
5364
|
+
],
|
|
4896
5365
|
details: undefined,
|
|
4897
5366
|
};
|
|
4898
5367
|
}
|
|
@@ -4909,7 +5378,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4909
5378
|
const state = loadBatchState(stateRoot);
|
|
4910
5379
|
if (!state) return "❌ No batch state found.";
|
|
4911
5380
|
|
|
4912
|
-
const laneRec = state.lanes.find(l => l.laneNumber === lane);
|
|
5381
|
+
const laneRec = state.lanes.find((l) => l.laneNumber === lane);
|
|
4913
5382
|
if (!laneRec) return `❌ Lane ${lane} not found in batch ${state.batchId}.`;
|
|
4914
5383
|
|
|
4915
5384
|
const telemetryDir = join(stateRoot, ".pi", "telemetry");
|
|
@@ -4920,14 +5389,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
4920
5389
|
try {
|
|
4921
5390
|
if (existsSync(telemetryDir)) {
|
|
4922
5391
|
const allStderr = readdirSync(telemetryDir)
|
|
4923
|
-
.filter(f => f.endsWith("-stderr.log"))
|
|
4924
|
-
.filter(f => f.includes(`-lane-${lane}-worker`));
|
|
4925
|
-
const batchScoped = allStderr.filter(f => f.includes(`-${state.batchId}-`));
|
|
5392
|
+
.filter((f) => f.endsWith("-stderr.log"))
|
|
5393
|
+
.filter((f) => f.includes(`-lane-${lane}-worker`));
|
|
5394
|
+
const batchScoped = allStderr.filter((f) => f.includes(`-${state.batchId}-`));
|
|
4926
5395
|
const candidates = (batchScoped.length > 0 ? batchScoped : allStderr)
|
|
4927
|
-
.map(name => {
|
|
5396
|
+
.map((name) => {
|
|
4928
5397
|
const absPath = join(telemetryDir, name);
|
|
4929
5398
|
let mtime = 0;
|
|
4930
|
-
try {
|
|
5399
|
+
try {
|
|
5400
|
+
mtime = statSync(absPath).mtimeMs;
|
|
5401
|
+
} catch {}
|
|
4931
5402
|
return { name, mtime };
|
|
4932
5403
|
})
|
|
4933
5404
|
.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -4952,12 +5423,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
4952
5423
|
try {
|
|
4953
5424
|
if (existsSync(telemetryDir)) {
|
|
4954
5425
|
const files = readdirSync(telemetryDir)
|
|
4955
|
-
.filter(f => f.endsWith("-worker-exit.json"))
|
|
4956
|
-
.filter(f => f.includes(`-lane-${lane}-`));
|
|
4957
|
-
const batchScoped = files.filter(f => f.includes(`-${state.batchId}-`));
|
|
5426
|
+
.filter((f) => f.endsWith("-worker-exit.json"))
|
|
5427
|
+
.filter((f) => f.includes(`-lane-${lane}-`));
|
|
5428
|
+
const batchScoped = files.filter((f) => f.includes(`-${state.batchId}-`));
|
|
4958
5429
|
exitFiles.push(...(batchScoped.length > 0 ? batchScoped : files));
|
|
4959
5430
|
}
|
|
4960
|
-
} catch {
|
|
5431
|
+
} catch {
|
|
5432
|
+
/* directory not readable */
|
|
5433
|
+
}
|
|
4961
5434
|
|
|
4962
5435
|
const lines: string[] = [];
|
|
4963
5436
|
lines.push(`📜 **Lane ${lane} Logs** — batch ${state.batchId}\n`);
|
|
@@ -4966,9 +5439,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4966
5439
|
if (stderrPath && existsSync(stderrPath)) {
|
|
4967
5440
|
try {
|
|
4968
5441
|
const content = readFileSync(stderrPath, "utf-8");
|
|
4969
|
-
const truncated = content.length > 5000
|
|
4970
|
-
? "...\n" + content.slice(-5000)
|
|
4971
|
-
: content;
|
|
5442
|
+
const truncated = content.length > 5000 ? "...\n" + content.slice(-5000) : content;
|
|
4972
5443
|
lines.push("### Stderr Log");
|
|
4973
5444
|
lines.push("```");
|
|
4974
5445
|
lines.push(truncated.trim());
|
|
@@ -4978,16 +5449,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
4978
5449
|
lines.push("Stderr log found but unreadable.");
|
|
4979
5450
|
}
|
|
4980
5451
|
} else {
|
|
4981
|
-
lines.push(
|
|
5452
|
+
lines.push(
|
|
5453
|
+
`No stderr log found for lane ${lane} (pattern: \`*-lane-${lane}-worker-stderr.log\`).`,
|
|
5454
|
+
);
|
|
4982
5455
|
}
|
|
4983
5456
|
|
|
4984
5457
|
// Read most recent exit diagnostic
|
|
4985
5458
|
if (exitFiles.length > 0) {
|
|
4986
5459
|
const latestExit = exitFiles
|
|
4987
|
-
.map(name => {
|
|
5460
|
+
.map((name) => {
|
|
4988
5461
|
const absPath = join(telemetryDir, name);
|
|
4989
5462
|
let mtime = 0;
|
|
4990
|
-
try {
|
|
5463
|
+
try {
|
|
5464
|
+
mtime = statSync(absPath).mtimeMs;
|
|
5465
|
+
} catch {}
|
|
4991
5466
|
return { name, mtime };
|
|
4992
5467
|
})
|
|
4993
5468
|
.sort((a, b) => b.mtime - a.mtime)[0]?.name;
|
|
@@ -5000,7 +5475,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
5000
5475
|
if (exitData.errorMessage) lines.push(`**Error:** ${exitData.errorMessage}`);
|
|
5001
5476
|
if (exitData.durationSec) lines.push(`**Duration:** ${exitData.durationSec}s`);
|
|
5002
5477
|
lines.push("");
|
|
5003
|
-
} catch {
|
|
5478
|
+
} catch {
|
|
5479
|
+
/* skip malformed exit file */
|
|
5480
|
+
}
|
|
5004
5481
|
}
|
|
5005
5482
|
}
|
|
5006
5483
|
|
|
@@ -5012,7 +5489,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5012
5489
|
label: "List Active Agents",
|
|
5013
5490
|
description:
|
|
5014
5491
|
"List all active Runtime V2 agents with their role, lane, task, status, and elapsed time.",
|
|
5015
|
-
promptSnippet:
|
|
5492
|
+
promptSnippet:
|
|
5493
|
+
"list_active_agents() — show active Runtime V2 agents with role, lane, task, status, elapsed",
|
|
5016
5494
|
promptGuidelines: [
|
|
5017
5495
|
"Call list_active_agents to see all running agent sessions.",
|
|
5018
5496
|
"Shows: session name, role (worker/reviewer/merger/supervisor), lane, task, context %, elapsed.",
|
|
@@ -5024,7 +5502,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
5024
5502
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
5025
5503
|
} catch (err) {
|
|
5026
5504
|
return {
|
|
5027
|
-
content: [
|
|
5505
|
+
content: [
|
|
5506
|
+
{
|
|
5507
|
+
type: "text" as const,
|
|
5508
|
+
text: `Error listing agents: ${err instanceof Error ? err.message : String(err)}`,
|
|
5509
|
+
},
|
|
5510
|
+
],
|
|
5028
5511
|
details: undefined,
|
|
5029
5512
|
};
|
|
5030
5513
|
}
|
|
@@ -5050,10 +5533,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
5050
5533
|
return "❌ No active agents found (Runtime V2 registry is empty).";
|
|
5051
5534
|
}
|
|
5052
5535
|
|
|
5053
|
-
|
|
5054
5536
|
// ── TP-106: Registry-based agent list formatter ────────────────
|
|
5055
5537
|
|
|
5056
|
-
function formatRegistryAgents(
|
|
5538
|
+
function formatRegistryAgents(
|
|
5539
|
+
registry: import("./types.ts").RuntimeRegistry,
|
|
5540
|
+
_batchState: PersistedBatchState | null,
|
|
5541
|
+
): string {
|
|
5057
5542
|
const agents = Object.values(registry.agents);
|
|
5058
5543
|
if (agents.length === 0) return "❌ No agents in registry.";
|
|
5059
5544
|
|
|
@@ -5096,13 +5581,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
5096
5581
|
// Build everything into temporaries first, then commit atomically
|
|
5097
5582
|
// so a partial failure doesn't leave mixed-generation state.
|
|
5098
5583
|
try {
|
|
5099
|
-
const freshCtx = buildExecutionContext(
|
|
5584
|
+
const freshCtx = buildExecutionContext(
|
|
5585
|
+
reloadCwd,
|
|
5586
|
+
loadOrchestratorConfig,
|
|
5587
|
+
loadTaskRunnerConfig,
|
|
5588
|
+
);
|
|
5100
5589
|
let freshSupervisor: SupervisorConfig;
|
|
5101
5590
|
try {
|
|
5102
|
-
freshSupervisor = loadSupervisorConfig(
|
|
5103
|
-
freshCtx.repoRoot,
|
|
5104
|
-
freshCtx.pointer?.configRoot,
|
|
5105
|
-
);
|
|
5591
|
+
freshSupervisor = loadSupervisorConfig(freshCtx.repoRoot, freshCtx.pointer?.configRoot);
|
|
5106
5592
|
} catch {
|
|
5107
5593
|
freshSupervisor = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
5108
5594
|
}
|
|
@@ -5114,10 +5600,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5114
5600
|
} catch {
|
|
5115
5601
|
// Non-fatal — config was saved to disk but live reload failed.
|
|
5116
5602
|
// Existing in-memory config is preserved unchanged.
|
|
5117
|
-
ctx.ui.notify(
|
|
5118
|
-
"⚠️ Saved to disk but live reload failed. Restart to apply.",
|
|
5119
|
-
"warn",
|
|
5120
|
-
);
|
|
5603
|
+
ctx.ui.notify("⚠️ Saved to disk but live reload failed. Restart to apply.", "warn");
|
|
5121
5604
|
}
|
|
5122
5605
|
});
|
|
5123
5606
|
} catch (err: any) {
|
|
@@ -5158,17 +5641,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
5158
5641
|
// and must surface loudly so the user fixes it.
|
|
5159
5642
|
const setupError = err.code === "WORKSPACE_SETUP_REQUIRED";
|
|
5160
5643
|
execCtxInitError = setupError
|
|
5161
|
-
?
|
|
5162
|
-
`❌ Orchestrator startup blocked [${err.code}]\n\n` +
|
|
5644
|
+
? `❌ Orchestrator startup blocked [${err.code}]\n\n` +
|
|
5163
5645
|
`${err.message}\n\n` +
|
|
5164
5646
|
`Orchestrator commands are disabled until this setup issue is resolved.`
|
|
5165
|
-
|
|
5166
|
-
: (
|
|
5167
|
-
`❌ Workspace configuration error [${err.code}]\n\n` +
|
|
5647
|
+
: `❌ Workspace configuration error [${err.code}]\n\n` +
|
|
5168
5648
|
`${err.message}\n\n` +
|
|
5169
5649
|
`Fix the workspace config at .pi/taskplane-workspace.yaml (or taskplane-config.json workspace section), then restart.\n` +
|
|
5170
|
-
`Orchestrator commands are disabled until this is resolved
|
|
5171
|
-
);
|
|
5650
|
+
`Orchestrator commands are disabled until this is resolved.`;
|
|
5172
5651
|
|
|
5173
5652
|
if (setupError) {
|
|
5174
5653
|
// Soft-fail: no notify, quiet status line.
|
|
@@ -5198,10 +5677,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5198
5677
|
// established pattern — all config loading after buildExecutionContext
|
|
5199
5678
|
// uses the resolved execution context paths.
|
|
5200
5679
|
try {
|
|
5201
|
-
supervisorConfig = loadSupervisorConfig(
|
|
5202
|
-
execCtx.repoRoot,
|
|
5203
|
-
execCtx.pointer?.configRoot,
|
|
5204
|
-
);
|
|
5680
|
+
supervisorConfig = loadSupervisorConfig(execCtx.repoRoot, execCtx.pointer?.configRoot);
|
|
5205
5681
|
} catch {
|
|
5206
5682
|
// Non-fatal — use defaults if supervisor config fails to load
|
|
5207
5683
|
supervisorConfig = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
@@ -5257,17 +5733,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
5257
5733
|
const summary = buildTakeoverSummary(stateRoot, batchState);
|
|
5258
5734
|
const reason =
|
|
5259
5735
|
lockResult.status === "stale"
|
|
5260
|
-
?
|
|
5736
|
+
? isProcessAlive(lockResult.lock.pid)
|
|
5261
5737
|
? `Previous supervisor (PID ${lockResult.lock.pid}) has a stale heartbeat (last: ${lockResult.lock.heartbeat}). Process may be hung.`
|
|
5262
|
-
: `Previous supervisor (PID ${lockResult.lock.pid}) process is dead.`
|
|
5738
|
+
: `Previous supervisor (PID ${lockResult.lock.pid}) process is dead.`
|
|
5263
5739
|
: lockResult.status === "corrupt"
|
|
5264
5740
|
? "Found a corrupt supervisor lockfile (treating as stale)."
|
|
5265
5741
|
: "No supervisor lockfile found for the active batch.";
|
|
5266
5742
|
|
|
5267
5743
|
ctx.ui.notify(
|
|
5268
5744
|
`🔄 **Active batch detected — ${reason}**\n\n` +
|
|
5269
|
-
|
|
5270
|
-
|
|
5745
|
+
`Taking over supervisor duties for batch ${batchState.batchId}.\n\n` +
|
|
5746
|
+
summary,
|
|
5271
5747
|
"info",
|
|
5272
5748
|
);
|
|
5273
5749
|
|
|
@@ -5310,13 +5786,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
5310
5786
|
const batchState = lockResult.batchState;
|
|
5311
5787
|
ctx.ui.notify(
|
|
5312
5788
|
`⚠️ **Another supervisor is already monitoring batch ${batchState.batchId}.**\n\n` +
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5789
|
+
` PID: ${lock.pid}\n` +
|
|
5790
|
+
` Session: ${lock.sessionId}\n` +
|
|
5791
|
+
` Started: ${lock.startedAt}\n` +
|
|
5792
|
+
` Last heartbeat: ${lock.heartbeat}\n\n` +
|
|
5793
|
+
`To force takeover, run \`/orch-takeover\`.\n` +
|
|
5794
|
+
`The other session will yield on its next heartbeat.\n\n` +
|
|
5795
|
+
`Otherwise, use the other terminal or the dashboard to monitor the batch.`,
|
|
5320
5796
|
"warning",
|
|
5321
5797
|
);
|
|
5322
5798
|
|
|
@@ -5337,17 +5813,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
5337
5813
|
// Notify user of available commands
|
|
5338
5814
|
ctx.ui.notify(
|
|
5339
5815
|
"Task Orchestrator ready\n\n" +
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5816
|
+
`Mode: ${modeLabel}\n` +
|
|
5817
|
+
`Runtime: V2 default (configured spawn_mode: ${orchConfig.orchestrator.spawn_mode})\n` +
|
|
5818
|
+
`Config: ${orchConfig.orchestrator.max_lanes} lanes, ` +
|
|
5819
|
+
`${orchConfig.dependencies.source} deps\n` +
|
|
5820
|
+
`Areas: ${areaCount} registered\n\n` +
|
|
5821
|
+
"/orch <areas|all> Start batch execution\n" +
|
|
5822
|
+
"/orch-plan <areas|all> Preview execution plan\n" +
|
|
5823
|
+
"/orch-deps <areas|all> Show dependency graph\n" +
|
|
5824
|
+
"/orch-sessions List orchestrator sessions\n" +
|
|
5825
|
+
"/orch-takeover Force supervisor takeover\n" +
|
|
5826
|
+
"/orch-integrate Integrate orch branch into working branch",
|
|
5351
5827
|
"info",
|
|
5352
5828
|
);
|
|
5353
5829
|
|
|
@@ -5417,13 +5893,13 @@ async function checkForUpdate(ctx: ExtensionContext): Promise<void> {
|
|
|
5417
5893
|
|
|
5418
5894
|
const response = await fetch("https://registry.npmjs.org/taskplane/latest", {
|
|
5419
5895
|
signal: controller.signal,
|
|
5420
|
-
headers: {
|
|
5896
|
+
headers: { Accept: "application/json" },
|
|
5421
5897
|
});
|
|
5422
5898
|
clearTimeout(timeout);
|
|
5423
5899
|
|
|
5424
5900
|
if (!response.ok) return;
|
|
5425
5901
|
|
|
5426
|
-
const data = await response.json() as { version?: string };
|
|
5902
|
+
const data = (await response.json()) as { version?: string };
|
|
5427
5903
|
const latestVersion = data.version;
|
|
5428
5904
|
if (!latestVersion) return;
|
|
5429
5905
|
|
|
@@ -5431,9 +5907,9 @@ async function checkForUpdate(ctx: ExtensionContext): Promise<void> {
|
|
|
5431
5907
|
if (latestVersion !== installedVersion && isNewerVersion(latestVersion, installedVersion)) {
|
|
5432
5908
|
ctx.ui.notify(
|
|
5433
5909
|
`\n` +
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5910
|
+
` Update Available\n` +
|
|
5911
|
+
` New version ${latestVersion} is available (installed: ${installedVersion}).\n` +
|
|
5912
|
+
` Run: pi update\n`,
|
|
5437
5913
|
"info",
|
|
5438
5914
|
);
|
|
5439
5915
|
}
|
|
@@ -5456,4 +5932,3 @@ function isNewerVersion(a: string, b: string): boolean {
|
|
|
5456
5932
|
}
|
|
5457
5933
|
return false;
|
|
5458
5934
|
}
|
|
5459
|
-
|