taskplane 0.23.16 → 0.24.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/taskplane.mjs +8 -41
- package/dashboard/public/app.js +24 -24
- package/dashboard/server.cjs +29 -7
- package/extensions/task-runner.ts +7 -3
- package/extensions/taskplane/abort.ts +93 -81
- package/extensions/taskplane/agent-host.ts +4 -5
- package/extensions/taskplane/config-loader.ts +86 -11
- package/extensions/taskplane/config-schema.ts +13 -13
- package/extensions/taskplane/diagnostic-reports.ts +1 -1
- package/extensions/taskplane/diagnostics.ts +3 -3
- package/extensions/taskplane/engine.ts +37 -16
- package/extensions/taskplane/execution.ts +86 -1000
- package/extensions/taskplane/extension.ts +60 -189
- package/extensions/taskplane/formatting.ts +5 -5
- package/extensions/taskplane/merge.ts +63 -371
- package/extensions/taskplane/messages.ts +1 -1
- package/extensions/taskplane/naming.ts +4 -4
- package/extensions/taskplane/persistence.ts +53 -26
- package/extensions/taskplane/process-registry.ts +2 -2
- package/extensions/taskplane/resume.ts +65 -130
- package/extensions/taskplane/sessions.ts +57 -92
- package/extensions/taskplane/settings-tui.ts +4 -4
- package/extensions/taskplane/tmux-compat.ts +37 -0
- package/extensions/taskplane/types.ts +43 -43
- package/extensions/taskplane/waves.ts +12 -10
- package/extensions/taskplane/worktree.ts +8 -66
- package/package.json +1 -1
- package/templates/config/task-orchestrator.yaml +3 -4
|
@@ -15,9 +15,9 @@ import { ORCH_MESSAGES, computeIntegrateCleanupResult } from "./messages.ts";
|
|
|
15
15
|
import type { IntegrateCleanupRepoFindings } from "./messages.ts";
|
|
16
16
|
import { computeWaveAssignments } from "./waves.ts";
|
|
17
17
|
import { createOrchWidget, formatDependencyGraph, formatWavePlan } from "./formatting.ts";
|
|
18
|
-
import { deleteBatchState, loadBatchState, saveBatchState, detectOrphanSessions
|
|
18
|
+
import { deleteBatchState, loadBatchState, saveBatchState, detectOrphanSessions } from "./persistence.ts";
|
|
19
19
|
import { deleteStaleBranches, listWorktrees, resolveWorktreeBasePath, formatPreflightResults, runPreflight } from "./worktree.ts";
|
|
20
|
-
import { computeTransitiveDependents,
|
|
20
|
+
import { computeTransitiveDependents, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
21
21
|
import { executeOrchBatch } from "./engine.ts";
|
|
22
22
|
import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
|
|
23
23
|
import { formatOrchSessions, listOrchSessions } from "./sessions.ts";
|
|
@@ -29,6 +29,7 @@ import { buildExecutionContext } from "./workspace.ts";
|
|
|
29
29
|
import { openSettingsTui } from "./settings-tui.ts";
|
|
30
30
|
import { loadProjectConfig } from "./config-loader.ts";
|
|
31
31
|
import { runMigrations } from "./migrations.ts";
|
|
32
|
+
import { executeAbort } from "./abort.ts";
|
|
32
33
|
import { serializeWorkspaceConfig, applySerializedState, deserializeWorkspaceConfig } from "./engine-worker.ts";
|
|
33
34
|
import type { EngineWorkerData, WorkerToMainMessage } from "./engine-worker.ts";
|
|
34
35
|
import { cleanupPostIntegrate, formatPostIntegrateCleanup, sweepStaleArtifacts, formatPreflightSweep, rotateSupervisorLogs, formatLogRotation } from "./cleanup.ts";
|
|
@@ -1481,7 +1482,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1481
1482
|
function updateOrchWidget() {
|
|
1482
1483
|
if (!orchWidgetCtx) return;
|
|
1483
1484
|
const ctx = orchWidgetCtx;
|
|
1484
|
-
const prefix = orchConfig.orchestrator.
|
|
1485
|
+
const prefix = orchConfig.orchestrator.sessionPrefix;
|
|
1485
1486
|
|
|
1486
1487
|
ctx.ui.setWidget(
|
|
1487
1488
|
"task-orchestrator",
|
|
@@ -1641,6 +1642,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1641
1642
|
}
|
|
1642
1643
|
|
|
1643
1644
|
// ── Section 1: Preflight ─────────────────────────────────
|
|
1645
|
+
ctx.ui.notify("ℹ️ Runtime V2 is the default backend (subprocess-only).", "info");
|
|
1644
1646
|
const preflight = runPreflight(orchConfig, execCtx!.repoRoot);
|
|
1645
1647
|
ctx.ui.notify(formatPreflightResults(preflight), preflight.passed ? "info" : "error");
|
|
1646
1648
|
if (!preflight.passed) return;
|
|
@@ -1783,7 +1785,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1783
1785
|
|
|
1784
1786
|
// Orphan detection
|
|
1785
1787
|
const orphanResult = detectOrphanSessions(
|
|
1786
|
-
orchConfig.orchestrator.
|
|
1788
|
+
orchConfig.orchestrator.sessionPrefix,
|
|
1787
1789
|
repoRoot,
|
|
1788
1790
|
);
|
|
1789
1791
|
|
|
@@ -2215,9 +2217,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2215
2217
|
* Core logic for orch-abort. Returns accumulated status messages.
|
|
2216
2218
|
* Works even without execCtx (safety-critical).
|
|
2217
2219
|
*/
|
|
2218
|
-
function doOrchAbort(hard: boolean, ctx: ExtensionContext): string {
|
|
2220
|
+
async function doOrchAbort(hard: boolean, ctx: ExtensionContext): Promise<string> {
|
|
2219
2221
|
const mode: AbortMode = hard ? "hard" : "graceful";
|
|
2220
|
-
const prefix = orchConfig.orchestrator.
|
|
2222
|
+
const prefix = orchConfig.orchestrator.sessionPrefix;
|
|
2221
2223
|
|
|
2222
2224
|
const stateRoot = execCtx?.repoRoot ?? ctx.cwd;
|
|
2223
2225
|
const messages: string[] = [`🛑 Abort requested (${mode} mode, prefix: ${prefix})...`];
|
|
@@ -2249,7 +2251,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
2249
2251
|
}
|
|
2250
2252
|
}
|
|
2251
2253
|
|
|
2252
|
-
// Step 3: Check what we're aborting
|
|
2253
2254
|
const hasActiveBatch = orchBatchState.phase !== "idle" &&
|
|
2254
2255
|
orchBatchState.phase !== "completed" &&
|
|
2255
2256
|
orchBatchState.phase !== "failed" &&
|
|
@@ -2267,67 +2268,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
2267
2268
|
`persisted=${persistedState ? persistedState.batchId : "none"}`,
|
|
2268
2269
|
);
|
|
2269
2270
|
|
|
2270
|
-
// If no batch AND no sessions, nothing to abort
|
|
2271
2271
|
if (!hasActiveBatch && !persistedState) {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
try {
|
|
2275
|
-
const tmuxOutput = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
2276
|
-
encoding: "utf-8",
|
|
2277
|
-
timeout: 5000,
|
|
2278
|
-
}).trim();
|
|
2279
|
-
const all = tmuxOutput ? tmuxOutput.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
2280
|
-
allSessionNames = all.filter(name => name.startsWith(`${prefix}-`));
|
|
2281
|
-
} catch {
|
|
2282
|
-
// tmux not available
|
|
2283
|
-
}
|
|
2284
|
-
if (allSessionNames.length === 0) {
|
|
2285
|
-
try { unlinkSync(abortSignalFile); } catch {}
|
|
2286
|
-
return ORCH_MESSAGES.abortNoBatch();
|
|
2287
|
-
}
|
|
2272
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
2273
|
+
return ORCH_MESSAGES.abortNoBatch();
|
|
2288
2274
|
}
|
|
2289
2275
|
|
|
2290
2276
|
const batchId = orchBatchState.batchId || persistedState?.batchId || "unknown";
|
|
2277
|
+
const gracePeriodMs = Math.max(0, orchConfig.failure.abort_grace_period * 1000);
|
|
2278
|
+
const pollIntervalMs = Math.max(250, orchConfig.monitoring.poll_interval * 1000);
|
|
2291
2279
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
messages.push(` Found ${allSessionNames.length} session(s) matching prefix "${prefix}-"`);
|
|
2302
|
-
} catch {
|
|
2303
|
-
messages.push(" ⚠ Could not list tmux sessions (tmux not available?)");
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
if (allSessionNames.length > 0) {
|
|
2307
|
-
messages.push(` Killing ${allSessionNames.length} tmux session(s)...`);
|
|
2308
|
-
let killed = 0;
|
|
2309
|
-
for (const name of allSessionNames) {
|
|
2310
|
-
try {
|
|
2311
|
-
execSync(`tmux kill-session -t "${name}-worker" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
2312
|
-
} catch {}
|
|
2313
|
-
try {
|
|
2314
|
-
execSync(`tmux kill-session -t "${name}-reviewer" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
2315
|
-
} catch {}
|
|
2316
|
-
try {
|
|
2317
|
-
execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
2318
|
-
killed++;
|
|
2319
|
-
messages.push(` ✓ Killed: ${name}`);
|
|
2320
|
-
} catch {
|
|
2321
|
-
messages.push(` · ${name} (already exited)`);
|
|
2322
|
-
killed++;
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
messages.push(` ✓ ${killed}/${allSessionNames.length} session(s) terminated`);
|
|
2326
|
-
} else {
|
|
2327
|
-
messages.push(" No tmux sessions to kill");
|
|
2328
|
-
}
|
|
2280
|
+
const abortResult = await executeAbort(
|
|
2281
|
+
mode,
|
|
2282
|
+
prefix,
|
|
2283
|
+
stateRoot,
|
|
2284
|
+
orchBatchState,
|
|
2285
|
+
persistedState,
|
|
2286
|
+
gracePeriodMs,
|
|
2287
|
+
pollIntervalMs,
|
|
2288
|
+
);
|
|
2329
2289
|
|
|
2330
|
-
// Step 6: Clean up batch state
|
|
2331
2290
|
deactivateSupervisor(pi, supervisorState);
|
|
2332
2291
|
|
|
2333
2292
|
try {
|
|
@@ -2339,19 +2298,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
2339
2298
|
messages.push(` ⚠ Failed to update in-memory state: ${err instanceof Error ? err.message : String(err)}`);
|
|
2340
2299
|
}
|
|
2341
2300
|
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2301
|
+
messages.push(` Found ${abortResult.sessionsFound} session target(s) matching prefix "${prefix}-"`);
|
|
2302
|
+
if (mode === "graceful") {
|
|
2303
|
+
const forceKilled = Math.max(0, abortResult.sessionsKilled - abortResult.gracefulExits);
|
|
2304
|
+
messages.push(ORCH_MESSAGES.abortGracefulComplete(batchId, abortResult.gracefulExits, forceKilled, Math.round(abortResult.durationMs / 1000)));
|
|
2305
|
+
} else {
|
|
2306
|
+
messages.push(ORCH_MESSAGES.abortHardComplete(batchId, abortResult.sessionsKilled, Math.round(abortResult.durationMs / 1000)));
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
if (!abortResult.stateDeleted) {
|
|
2310
|
+
messages.push(" ⚠ Batch state file was not deleted cleanly");
|
|
2311
|
+
}
|
|
2312
|
+
if (abortResult.errors.length > 0) {
|
|
2313
|
+
messages.push(ORCH_MESSAGES.abortPartialFailure(abortResult.errors.length));
|
|
2314
|
+
for (const err of abortResult.errors) {
|
|
2315
|
+
messages.push(` - ${err.code}: ${err.message}`);
|
|
2316
|
+
}
|
|
2347
2317
|
}
|
|
2348
2318
|
|
|
2349
2319
|
// Step 7: Clean up abort signal file
|
|
2350
2320
|
try { unlinkSync(abortSignalFile); } catch {}
|
|
2351
2321
|
|
|
2352
2322
|
messages.push(
|
|
2353
|
-
|
|
2354
|
-
`
|
|
2323
|
+
`🏁 Abort (${mode}) complete for batch ${batchId}. ` +
|
|
2324
|
+
`Worktrees and branches are preserved for inspection.`,
|
|
2355
2325
|
);
|
|
2356
2326
|
|
|
2357
2327
|
return messages.join("\n");
|
|
@@ -3091,15 +3061,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3091
3061
|
handler: async (args, ctx) => {
|
|
3092
3062
|
try {
|
|
3093
3063
|
const hard = args?.trim() === "--hard";
|
|
3094
|
-
const result = doOrchAbort(hard, ctx);
|
|
3064
|
+
const result = await doOrchAbort(hard, ctx);
|
|
3095
3065
|
ctx.ui.notify(result, "info");
|
|
3096
3066
|
} catch (err) {
|
|
3097
3067
|
// Top-level catch: ensure the user ALWAYS sees something
|
|
3098
3068
|
ctx.ui.notify(
|
|
3099
3069
|
`❌ Abort failed with error: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
3100
|
-
` Stack: ${err instanceof Error ? err.stack : "N/A"}
|
|
3101
|
-
` Manual cleanup: tmux kill-server (kills ALL tmux sessions)\n` +
|
|
3102
|
-
` Or: tmux kill-session -t <session-name> for each session`,
|
|
3070
|
+
` Stack: ${err instanceof Error ? err.stack : "N/A"}`,
|
|
3103
3071
|
"error",
|
|
3104
3072
|
);
|
|
3105
3073
|
}
|
|
@@ -3185,9 +3153,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3185
3153
|
});
|
|
3186
3154
|
|
|
3187
3155
|
pi.registerCommand("orch-sessions", {
|
|
3188
|
-
description: "List active orchestrator
|
|
3156
|
+
description: "List active orchestrator sessions",
|
|
3189
3157
|
handler: async (_args, ctx) => {
|
|
3190
|
-
const sessions = listOrchSessions(orchConfig.orchestrator.
|
|
3158
|
+
const sessions = listOrchSessions(orchConfig.orchestrator.sessionPrefix, orchBatchState);
|
|
3191
3159
|
ctx.ui.notify(formatOrchSessions(sessions), "info");
|
|
3192
3160
|
},
|
|
3193
3161
|
});
|
|
@@ -3445,13 +3413,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3445
3413
|
name: "orch_abort",
|
|
3446
3414
|
label: "Abort Batch",
|
|
3447
3415
|
description:
|
|
3448
|
-
"Abort the running batch.
|
|
3416
|
+
"Abort the running batch. Stops Runtime V2 lane + merge agents and cleans up state. " +
|
|
3449
3417
|
"Use hard=true for immediate kill (no grace period). " +
|
|
3450
3418
|
"Works even without execution context (safety-critical).",
|
|
3451
3419
|
promptSnippet: "orch_abort(hard?) — abort the running batch",
|
|
3452
3420
|
promptGuidelines: [
|
|
3453
3421
|
"Call orch_abort to stop a running batch.",
|
|
3454
|
-
"Default (hard=false) is graceful abort — writes signal file and
|
|
3422
|
+
"Default (hard=false) is graceful abort — writes signal file and waits for checkpoint exit before forced cleanup.",
|
|
3455
3423
|
"Set hard=true for immediate termination without grace period.",
|
|
3456
3424
|
"Use this when a batch is stuck, failing repeatedly, or the operator requests it.",
|
|
3457
3425
|
"Worktrees and branches are preserved for inspection after abort.",
|
|
@@ -3463,7 +3431,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3463
3431
|
}),
|
|
3464
3432
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
3465
3433
|
try {
|
|
3466
|
-
const result = doOrchAbort(params.hard ?? false, ctx);
|
|
3434
|
+
const result = await doOrchAbort(params.hard ?? false, ctx);
|
|
3467
3435
|
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
3468
3436
|
} catch (err) {
|
|
3469
3437
|
return {
|
|
@@ -3722,12 +3690,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
3722
3690
|
// Legacy fallback from lane naming when registry is absent/empty.
|
|
3723
3691
|
if (ids.size === 0) {
|
|
3724
3692
|
const orchConfig = execCtx?.orchestratorConfig;
|
|
3725
|
-
const
|
|
3693
|
+
const sessionPrefix = orchConfig?.orchestrator?.sessionPrefix ?? "orch";
|
|
3726
3694
|
const opId = orchConfig ? resolveOperatorId(orchConfig) : "op";
|
|
3727
3695
|
for (const lane of state.lanes) {
|
|
3728
|
-
ids.add(`${lane.
|
|
3729
|
-
ids.add(`${lane.
|
|
3730
|
-
ids.add(`${
|
|
3696
|
+
ids.add(`${lane.laneSessionId}-worker`);
|
|
3697
|
+
ids.add(`${lane.laneSessionId}-reviewer`);
|
|
3698
|
+
ids.add(`${sessionPrefix}-${opId}-merge-${lane.laneNumber}`);
|
|
3731
3699
|
}
|
|
3732
3700
|
}
|
|
3733
3701
|
|
|
@@ -3776,21 +3744,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3776
3744
|
return `❌ Unknown session "${to}" in batch ${state.batchId}.\nValid targets: ${examples}${validSessions.size > 5 ? ` (${validSessions.size} total)` : ""}`;
|
|
3777
3745
|
}
|
|
3778
3746
|
|
|
3779
|
-
// Guard: ensure the target agent is currently alive.
|
|
3780
|
-
// Check process registry first (Runtime V2), fall back to TMUX (legacy).
|
|
3747
|
+
// Guard: ensure the target agent is currently alive via Runtime V2 registry.
|
|
3781
3748
|
let agentAlive = false;
|
|
3782
3749
|
try {
|
|
3783
3750
|
const registry = readRegistrySnapshot(stateRoot, state.batchId);
|
|
3784
3751
|
if (registry && registry.agents[to]) {
|
|
3785
3752
|
const manifest = registry.agents[to];
|
|
3786
3753
|
agentAlive = !isTerminalStatus(manifest.status) && registryIsProcessAlive(manifest.pid);
|
|
3787
|
-
} else {
|
|
3788
|
-
// No registry entry — fall back to TMUX for legacy batches
|
|
3789
|
-
agentAlive = tmuxHasSession(to);
|
|
3790
3754
|
}
|
|
3791
3755
|
} catch {
|
|
3792
|
-
// Registry read
|
|
3793
|
-
agentAlive = tmuxHasSession(to);
|
|
3756
|
+
// Registry read failure — treat as not alive.
|
|
3794
3757
|
}
|
|
3795
3758
|
if (!agentAlive) {
|
|
3796
3759
|
return `❌ Agent "${to}" is not currently running. Use orch_status() or orch_resume() before sending messages.`;
|
|
@@ -4098,7 +4061,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4098
4061
|
const runningTask = laneTasks.find(t => t.status === "running");
|
|
4099
4062
|
const currentTask = runningTask || laneTasks[laneTasks.length - 1];
|
|
4100
4063
|
|
|
4101
|
-
lines.push(`### Lane ${laneRec.laneNumber} — ${laneRec.
|
|
4064
|
+
lines.push(`### Lane ${laneRec.laneNumber} — ${laneRec.laneSessionId}`);
|
|
4102
4065
|
lines.push(`**Branch:** ${laneRec.branch}`);
|
|
4103
4066
|
|
|
4104
4067
|
if (currentTask) {
|
|
@@ -4142,7 +4105,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4142
4105
|
|
|
4143
4106
|
// Read lane-state sidecar
|
|
4144
4107
|
try {
|
|
4145
|
-
const lsPath = join(stateRoot, ".pi", `lane-state-${laneRec.
|
|
4108
|
+
const lsPath = join(stateRoot, ".pi", `lane-state-${laneRec.laneSessionId}.json`);
|
|
4146
4109
|
if (existsSync(lsPath)) {
|
|
4147
4110
|
const ls = JSON.parse(readFileSync(lsPath, "utf-8"));
|
|
4148
4111
|
const parts: string[] = [];
|
|
@@ -4378,8 +4341,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
4378
4341
|
name: "list_active_agents",
|
|
4379
4342
|
label: "List Active Agents",
|
|
4380
4343
|
description:
|
|
4381
|
-
"List all
|
|
4382
|
-
promptSnippet: "list_active_agents() — show
|
|
4344
|
+
"List all active Runtime V2 agents with their role, lane, task, status, and elapsed time.",
|
|
4345
|
+
promptSnippet: "list_active_agents() — show active Runtime V2 agents with role, lane, task, status, elapsed",
|
|
4383
4346
|
promptGuidelines: [
|
|
4384
4347
|
"Call list_active_agents to see all running agent sessions.",
|
|
4385
4348
|
"Shows: session name, role (worker/reviewer/merger/supervisor), lane, task, context %, elapsed.",
|
|
@@ -4399,7 +4362,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4399
4362
|
});
|
|
4400
4363
|
|
|
4401
4364
|
/**
|
|
4402
|
-
* List all active
|
|
4365
|
+
* List all active Runtime V2 agents using the persisted registry.
|
|
4403
4366
|
* @since TP-096
|
|
4404
4367
|
*/
|
|
4405
4368
|
function doListActiveAgents(ctx: ExtensionContext): string {
|
|
@@ -4414,99 +4377,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4414
4377
|
}
|
|
4415
4378
|
}
|
|
4416
4379
|
|
|
4417
|
-
|
|
4418
|
-
let sessions: string[] = [];
|
|
4419
|
-
try {
|
|
4420
|
-
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
4421
|
-
encoding: "utf-8",
|
|
4422
|
-
timeout: 5000,
|
|
4423
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
4424
|
-
}).trim();
|
|
4425
|
-
sessions = output ? output.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
4426
|
-
} catch {
|
|
4427
|
-
return "❌ No active agents found (no registry and no tmux sessions).";
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
if (sessions.length === 0) return "❌ No active agents found.";
|
|
4431
|
-
|
|
4432
|
-
// state already loaded above for registry check (may be null)
|
|
4433
|
-
|
|
4434
|
-
// Build a map of session name → lane-state data
|
|
4435
|
-
const laneStates: Record<string, any> = {};
|
|
4436
|
-
try {
|
|
4437
|
-
const piDir = join(stateRoot, ".pi");
|
|
4438
|
-
if (existsSync(piDir)) {
|
|
4439
|
-
const files = readdirSync(piDir).filter(f => f.startsWith("lane-state-") && f.endsWith(".json"));
|
|
4440
|
-
for (const file of files) {
|
|
4441
|
-
try {
|
|
4442
|
-
const data = JSON.parse(readFileSync(join(piDir, file), "utf-8"));
|
|
4443
|
-
if (data.prefix) laneStates[data.prefix] = data;
|
|
4444
|
-
} catch { continue; }
|
|
4445
|
-
}
|
|
4446
|
-
}
|
|
4447
|
-
} catch { /* .pi dir missing */ }
|
|
4448
|
-
|
|
4449
|
-
const lines: string[] = [];
|
|
4450
|
-
lines.push(`👥 **Active Agents** (${sessions.length} sessions)\n`);
|
|
4451
|
-
|
|
4452
|
-
// Parse each session name to extract role, lane, etc.
|
|
4453
|
-
for (const sess of sessions) {
|
|
4454
|
-
let role = "unknown";
|
|
4455
|
-
let laneNum = "";
|
|
4456
|
-
let taskId = "";
|
|
4457
|
-
let contextPct = "";
|
|
4458
|
-
let elapsed = "";
|
|
4459
|
-
let costStr = "";
|
|
4460
|
-
|
|
4461
|
-
// Parse session name pattern:
|
|
4462
|
-
// Workers/reviewers: orch-{opId}-lane-{N} (or -worker/-reviewer suffix)
|
|
4463
|
-
// Mergers: orch-{opId}-merge-{N}
|
|
4464
|
-
// Supervisor: pi-supervisor-{...}
|
|
4465
|
-
const laneMatch = sess.match(/-lane-(\d+)/);
|
|
4466
|
-
const mergeMatch = sess.match(/-merge-(\d+)/);
|
|
4467
|
-
|
|
4468
|
-
if (mergeMatch) {
|
|
4469
|
-
role = "merger";
|
|
4470
|
-
laneNum = mergeMatch[1];
|
|
4471
|
-
} else if (laneMatch) {
|
|
4472
|
-
if (sess.includes("-reviewer")) {
|
|
4473
|
-
role = "reviewer";
|
|
4474
|
-
} else {
|
|
4475
|
-
role = "worker";
|
|
4476
|
-
}
|
|
4477
|
-
laneNum = laneMatch[1];
|
|
4478
|
-
} else if (sess.includes("supervisor")) {
|
|
4479
|
-
role = "supervisor";
|
|
4480
|
-
}
|
|
4481
|
-
|
|
4482
|
-
// Find matching task and lane-state
|
|
4483
|
-
if (state && laneNum) {
|
|
4484
|
-
const ln = parseInt(laneNum);
|
|
4485
|
-
const task = state.tasks.find(t => t.laneNumber === ln && t.status === "running");
|
|
4486
|
-
if (task) taskId = task.taskId;
|
|
4487
|
-
|
|
4488
|
-
// Find lane-state prefix (may be the session name or a prefix of it)
|
|
4489
|
-
const laneRec = state.lanes.find(l => l.laneNumber === ln);
|
|
4490
|
-
const prefix = laneRec?.tmuxSessionName || sess;
|
|
4491
|
-
const ls = laneStates[prefix];
|
|
4492
|
-
if (ls) {
|
|
4493
|
-
if (ls.workerContextPct) contextPct = `${Math.round(ls.workerContextPct)}%`;
|
|
4494
|
-
if (ls.workerElapsed) elapsed = `${Math.round(ls.workerElapsed / 1000)}s`;
|
|
4495
|
-
if (ls.workerCostUsd) costStr = `$${ls.workerCostUsd.toFixed(3)}`;
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
|
|
4499
|
-
const parts: string[] = [`**${sess}**`];
|
|
4500
|
-
parts.push(`role: ${role}`);
|
|
4501
|
-
if (laneNum) parts.push(`lane: ${laneNum}`);
|
|
4502
|
-
if (taskId) parts.push(`task: ${taskId}`);
|
|
4503
|
-
if (contextPct) parts.push(`ctx: ${contextPct}`);
|
|
4504
|
-
if (elapsed) parts.push(`elapsed: ${elapsed}`);
|
|
4505
|
-
if (costStr) parts.push(`cost: ${costStr}`);
|
|
4506
|
-
lines.push(`- ${parts.join(" · ")}`);
|
|
4507
|
-
}
|
|
4508
|
-
|
|
4509
|
-
return lines.join("\n");
|
|
4380
|
+
return "❌ No active agents found (Runtime V2 registry is empty).";
|
|
4510
4381
|
}
|
|
4511
4382
|
|
|
4512
4383
|
|
|
@@ -4744,14 +4615,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
4744
4615
|
ctx.ui.notify(
|
|
4745
4616
|
"Task Orchestrator ready\n\n" +
|
|
4746
4617
|
`Mode: ${modeLabel}\n` +
|
|
4618
|
+
`Runtime: V2 default (configured spawn_mode: ${orchConfig.orchestrator.spawn_mode})\n` +
|
|
4747
4619
|
`Config: ${orchConfig.orchestrator.max_lanes} lanes, ` +
|
|
4748
|
-
`${orchConfig.orchestrator.spawn_mode} mode, ` +
|
|
4749
4620
|
`${orchConfig.dependencies.source} deps\n` +
|
|
4750
4621
|
`Areas: ${areaCount} registered\n\n` +
|
|
4751
4622
|
"/orch <areas|all> Start batch execution\n" +
|
|
4752
4623
|
"/orch-plan <areas|all> Preview execution plan\n" +
|
|
4753
4624
|
"/orch-deps <areas|all> Show dependency graph\n" +
|
|
4754
|
-
"/orch-sessions List
|
|
4625
|
+
"/orch-sessions List orchestrator sessions\n" +
|
|
4755
4626
|
"/orch-takeover Force supervisor takeover\n" +
|
|
4756
4627
|
"/orch-integrate Integrate orch branch into working branch",
|
|
4757
4628
|
"info",
|
|
@@ -401,7 +401,7 @@ export function buildDashboardViewModel(
|
|
|
401
401
|
laneCards.push({
|
|
402
402
|
laneNumber: lane.laneNumber,
|
|
403
403
|
laneId: lane.laneId,
|
|
404
|
-
sessionName: lane.
|
|
404
|
+
sessionName: lane.laneSessionId,
|
|
405
405
|
sessionAlive: true, // assumed alive during allocation
|
|
406
406
|
currentTaskId: lane.tasks.length > 0 ? lane.tasks[0].taskId : null,
|
|
407
407
|
currentStepName: null,
|
|
@@ -419,9 +419,9 @@ export function buildDashboardViewModel(
|
|
|
419
419
|
let attachHint = "";
|
|
420
420
|
const aliveLane = laneCards.find(l => l.sessionAlive && l.status === "running");
|
|
421
421
|
if (aliveLane) {
|
|
422
|
-
attachHint = `
|
|
422
|
+
attachHint = `Use /orch-sessions to inspect active lane sessions (${aliveLane.sessionName})`;
|
|
423
423
|
} else if (laneCards.length > 0) {
|
|
424
|
-
attachHint = "/orch-sessions for session list";
|
|
424
|
+
attachHint = "Use /orch-sessions for active lane session list";
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
// Determine failure policy if batch was stopped
|
|
@@ -545,12 +545,12 @@ export function renderLaneCard(card: OrchLaneCardData, colWidth: number, theme:
|
|
|
545
545
|
*
|
|
546
546
|
* @param getBatchState - Getter for current batch state
|
|
547
547
|
* @param getMonitorState - Getter for current monitor state (may be null)
|
|
548
|
-
* @param
|
|
548
|
+
* @param sessionPrefix - Session prefix for lane identification
|
|
549
549
|
*/
|
|
550
550
|
export function createOrchWidget(
|
|
551
551
|
getBatchState: () => OrchBatchRuntimeState,
|
|
552
552
|
getMonitorState: () => MonitorState | null,
|
|
553
|
-
|
|
553
|
+
sessionPrefix: string,
|
|
554
554
|
): (_tui: any, theme: any) => { render(width: number): string[]; invalidate(): void } {
|
|
555
555
|
return (_tui: any, theme: any) => {
|
|
556
556
|
return {
|