taskplane 0.22.17 → 0.23.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/dashboard/public/app.js +365 -7
- package/dashboard/public/index.html +16 -0
- package/dashboard/public/style.css +105 -0
- package/dashboard/server.cjs +199 -0
- package/extensions/task-runner.ts +64 -349
- package/extensions/taskplane/abort.ts +11 -1
- package/extensions/taskplane/agent-bridge-extension.ts +159 -0
- package/extensions/taskplane/agent-host.ts +686 -0
- package/extensions/taskplane/engine.ts +75 -3
- package/extensions/taskplane/execution.ts +403 -9
- package/extensions/taskplane/extension.ts +322 -28
- package/extensions/taskplane/lane-runner.ts +567 -0
- package/extensions/taskplane/mailbox.ts +349 -1
- package/extensions/taskplane/merge.ts +208 -51
- package/extensions/taskplane/process-registry.ts +345 -0
- package/extensions/taskplane/resume.ts +185 -47
- package/extensions/taskplane/supervisor.ts +16 -12
- package/extensions/taskplane/task-executor-core.ts +553 -0
- package/extensions/taskplane/types.ts +517 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +41 -33
- package/skills/create-taskplane-task/references/prompt-template.md +3 -3
|
@@ -32,7 +32,21 @@ import { runMigrations } from "./migrations.ts";
|
|
|
32
32
|
import { serializeWorkspaceConfig, applySerializedState, deserializeWorkspaceConfig } from "./engine-worker.ts";
|
|
33
33
|
import type { EngineWorkerData, WorkerToMainMessage } from "./engine-worker.ts";
|
|
34
34
|
import { cleanupPostIntegrate, formatPostIntegrateCleanup, sweepStaleArtifacts, formatPreflightSweep, rotateSupervisorLogs, formatLogRotation } from "./cleanup.ts";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
writeMailboxMessage,
|
|
37
|
+
readOutbox,
|
|
38
|
+
readOutboxHistory,
|
|
39
|
+
discoverMailboxAgentIds,
|
|
40
|
+
writeBroadcastMessage,
|
|
41
|
+
checkRateLimit,
|
|
42
|
+
recordSend,
|
|
43
|
+
appendMailboxAuditEvent,
|
|
44
|
+
} from "./mailbox.ts";
|
|
45
|
+
import {
|
|
46
|
+
readRegistrySnapshot,
|
|
47
|
+
isProcessAlive as registryIsProcessAlive,
|
|
48
|
+
isTerminalStatus,
|
|
49
|
+
} from "./process-registry.ts";
|
|
36
50
|
import type { MailboxMessageType } from "./types.ts";
|
|
37
51
|
import {
|
|
38
52
|
activateSupervisor,
|
|
@@ -3515,19 +3529,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
3515
3529
|
label: "Start Batch",
|
|
3516
3530
|
description:
|
|
3517
3531
|
"Start a new orchestration batch. Target is \"all\" to run all pending tasks, " +
|
|
3518
|
-
"
|
|
3519
|
-
"use orch_status() to monitor progress.",
|
|
3532
|
+
"a task area name, a directory path, or one or more PROMPT.md paths. " +
|
|
3533
|
+
"The batch runs asynchronously — use orch_status() to monitor progress.",
|
|
3520
3534
|
promptSnippet: "orch_start(target) — start a new batch",
|
|
3521
3535
|
promptGuidelines: [
|
|
3522
3536
|
"Call orch_start to begin executing pending tasks as a batch.",
|
|
3523
|
-
'Use target="all" to run all pending tasks
|
|
3537
|
+
'Use target="all" to run all pending tasks.',
|
|
3538
|
+
"Specify a task area name to run all pending tasks in that area.",
|
|
3539
|
+
"Specify a PROMPT.md path to run a single task: target=\"taskplane-tasks/TP-101/PROMPT.md\"",
|
|
3540
|
+
"Specify multiple space-separated PROMPT.md paths to run specific tasks: target=\"path/TP-001/PROMPT.md path/TP-002/PROMPT.md\"",
|
|
3524
3541
|
"Cannot start if a batch is already running — check orch_status() first.",
|
|
3525
3542
|
"The batch runs asynchronously. The tool returns immediately with an ACK.",
|
|
3526
3543
|
"After starting, use orch_status() to track progress.",
|
|
3527
3544
|
],
|
|
3528
3545
|
parameters: Type.Object({
|
|
3529
3546
|
target: Type.String({
|
|
3530
|
-
description: 'Target to run: "all" for all pending tasks,
|
|
3547
|
+
description: 'Target to run: "all" for all pending tasks, a task area name, a directory path, or one or more PROMPT.md paths (space-separated)',
|
|
3531
3548
|
}),
|
|
3532
3549
|
}),
|
|
3533
3550
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -3689,6 +3706,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
3689
3706
|
},
|
|
3690
3707
|
});
|
|
3691
3708
|
|
|
3709
|
+
function collectKnownAgentIds(stateRoot: string, state: PersistedBatchState): string[] {
|
|
3710
|
+
const ids = new Set<string>();
|
|
3711
|
+
|
|
3712
|
+
// Runtime V2 source of truth first.
|
|
3713
|
+
const registry = readRegistrySnapshot(stateRoot, state.batchId);
|
|
3714
|
+
if (registry) {
|
|
3715
|
+
for (const manifest of Object.values(registry.agents)) {
|
|
3716
|
+
if (manifest.role !== "worker" && manifest.role !== "reviewer" && manifest.role !== "merger") continue;
|
|
3717
|
+
if (isTerminalStatus(manifest.status) || !registryIsProcessAlive(manifest.pid)) continue;
|
|
3718
|
+
ids.add(manifest.agentId);
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// Legacy fallback from lane naming when registry is absent/empty.
|
|
3723
|
+
if (ids.size === 0) {
|
|
3724
|
+
const orchConfig = execCtx?.orchestratorConfig;
|
|
3725
|
+
const tmuxPrefix = orchConfig?.orchestrator?.tmux_prefix ?? "orch";
|
|
3726
|
+
const opId = orchConfig ? resolveOperatorId(orchConfig) : "op";
|
|
3727
|
+
for (const lane of state.lanes) {
|
|
3728
|
+
ids.add(`${lane.tmuxSessionName}-worker`);
|
|
3729
|
+
ids.add(`${lane.tmuxSessionName}-reviewer`);
|
|
3730
|
+
ids.add(`${tmuxPrefix}-${opId}-merge-${lane.laneNumber}`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
return [...ids];
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3692
3737
|
/**
|
|
3693
3738
|
* Send a steering message to a running agent via the mailbox system.
|
|
3694
3739
|
*
|
|
@@ -3722,19 +3767,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3722
3767
|
return `❌ Batch ${state.batchId} is in terminal phase (${state.phase}). Start or resume a batch before sending messages.`;
|
|
3723
3768
|
}
|
|
3724
3769
|
|
|
3725
|
-
// Build
|
|
3726
|
-
const validSessions = new Set<string>();
|
|
3727
|
-
const orchConfig = execCtx?.orchestratorConfig;
|
|
3728
|
-
const tmuxPrefix = orchConfig?.orchestrator?.tmux_prefix ?? "orch";
|
|
3729
|
-
const opId = orchConfig ? resolveOperatorId(orchConfig) : "op";
|
|
3730
|
-
|
|
3731
|
-
for (const lane of state.lanes) {
|
|
3732
|
-
// Worker and reviewer are derived from lane session name
|
|
3733
|
-
validSessions.add(`${lane.tmuxSessionName}-worker`);
|
|
3734
|
-
validSessions.add(`${lane.tmuxSessionName}-reviewer`);
|
|
3735
|
-
// Merger: {tmuxPrefix}-{opId}-merge-{laneNumber}
|
|
3736
|
-
validSessions.add(`${tmuxPrefix}-${opId}-merge-${lane.laneNumber}`);
|
|
3737
|
-
}
|
|
3770
|
+
// Build valid runtime agent IDs (registry-first, legacy fallback).
|
|
3771
|
+
const validSessions = new Set<string>(collectKnownAgentIds(stateRoot, state));
|
|
3738
3772
|
|
|
3739
3773
|
// Validate target session
|
|
3740
3774
|
if (!validSessions.has(to)) {
|
|
@@ -3742,11 +3776,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
3742
3776
|
return `❌ Unknown session "${to}" in batch ${state.batchId}.\nValid targets: ${examples}${validSessions.size > 5 ? ` (${validSessions.size} total)` : ""}`;
|
|
3743
3777
|
}
|
|
3744
3778
|
|
|
3745
|
-
// Guard: ensure the target
|
|
3746
|
-
//
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3779
|
+
// Guard: ensure the target agent is currently alive.
|
|
3780
|
+
// Check process registry first (Runtime V2), fall back to TMUX (legacy).
|
|
3781
|
+
let agentAlive = false;
|
|
3782
|
+
try {
|
|
3783
|
+
const registry = readRegistrySnapshot(stateRoot, state.batchId);
|
|
3784
|
+
if (registry && registry.agents[to]) {
|
|
3785
|
+
const manifest = registry.agents[to];
|
|
3786
|
+
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
|
+
}
|
|
3791
|
+
} catch {
|
|
3792
|
+
// Registry read failed — fall back to TMUX
|
|
3793
|
+
agentAlive = tmuxHasSession(to);
|
|
3794
|
+
}
|
|
3795
|
+
if (!agentAlive) {
|
|
3796
|
+
return `❌ Agent "${to}" is not currently running. Use orch_status() or orch_resume() before sending messages.`;
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
// Rate limiting (TP-106)
|
|
3800
|
+
const rateCheck = checkRateLimit(to);
|
|
3801
|
+
if (!rateCheck.allowed) {
|
|
3802
|
+
const waitSec = Math.ceil((rateCheck.retryAfterMs ?? 0) / 1000);
|
|
3803
|
+
appendMailboxAuditEvent(stateRoot, state.batchId, {
|
|
3804
|
+
type: "message_rate_limited",
|
|
3805
|
+
from: "supervisor",
|
|
3806
|
+
to,
|
|
3807
|
+
reason: "per-agent rate limit",
|
|
3808
|
+
retryAfterMs: rateCheck.retryAfterMs,
|
|
3809
|
+
});
|
|
3810
|
+
return `⏳ Rate limited: wait ${waitSec}s before sending another message to \`${to}\`.`;
|
|
3750
3811
|
}
|
|
3751
3812
|
|
|
3752
3813
|
// Write message to inbox
|
|
@@ -3756,6 +3817,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3756
3817
|
type: messageType as MailboxMessageType,
|
|
3757
3818
|
content,
|
|
3758
3819
|
});
|
|
3820
|
+
recordSend(to);
|
|
3821
|
+
appendMailboxAuditEvent(stateRoot, state.batchId, {
|
|
3822
|
+
type: "message_sent",
|
|
3823
|
+
from: "supervisor",
|
|
3824
|
+
to,
|
|
3825
|
+
messageId: msg.id,
|
|
3826
|
+
messageType,
|
|
3827
|
+
contentPreview: content.slice(0, 200),
|
|
3828
|
+
broadcast: false,
|
|
3829
|
+
});
|
|
3759
3830
|
return `✅ Message sent to \`${to}\` (batch ${state.batchId})\n` +
|
|
3760
3831
|
`- **ID:** ${msg.id}\n` +
|
|
3761
3832
|
`- **Type:** ${messageType}\n` +
|
|
@@ -3766,6 +3837,192 @@ export default function (pi: ExtensionAPI) {
|
|
|
3766
3837
|
}
|
|
3767
3838
|
}
|
|
3768
3839
|
|
|
3840
|
+
// ── TP-106: read_agent_replies tool ───────────────────────
|
|
3841
|
+
|
|
3842
|
+
pi.registerTool({
|
|
3843
|
+
name: "read_agent_replies",
|
|
3844
|
+
label: "Read Agent Replies",
|
|
3845
|
+
description:
|
|
3846
|
+
"Read reply and escalation messages from agents (non-consuming). " +
|
|
3847
|
+
"Returns pending and already-acked outbox messages from a specific agent or all agents. " +
|
|
3848
|
+
"Messages are never removed by reading — this is a durable history view.",
|
|
3849
|
+
promptSnippet: "read_agent_replies(from?) \u2014 read replies/escalations from agents (read-only, non-consuming)",
|
|
3850
|
+
promptGuidelines: [
|
|
3851
|
+
"Call read_agent_replies to check if any agent has sent a reply or escalation.",
|
|
3852
|
+
"Omit 'from' to read replies from all agents.",
|
|
3853
|
+
"Provide 'from' with an agent ID to read replies from a specific agent.",
|
|
3854
|
+
"This is non-consuming: replies remain visible after reading (pending + acked history).",
|
|
3855
|
+
],
|
|
3856
|
+
parameters: Type.Object({
|
|
3857
|
+
from: Type.Optional(Type.String({
|
|
3858
|
+
description: "Agent ID to read replies from (omit for all agents)",
|
|
3859
|
+
})),
|
|
3860
|
+
}),
|
|
3861
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
3862
|
+
try {
|
|
3863
|
+
const result = doReadAgentReplies(params.from, ctx);
|
|
3864
|
+
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
3865
|
+
} catch (err) {
|
|
3866
|
+
return {
|
|
3867
|
+
content: [{ type: "text" as const, text: `Error reading replies: ${err instanceof Error ? err.message : String(err)}` }],
|
|
3868
|
+
details: undefined,
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
},
|
|
3872
|
+
});
|
|
3873
|
+
|
|
3874
|
+
/**
|
|
3875
|
+
* Read agent replies/escalations. Non-consuming (read-only):
|
|
3876
|
+
* reads both pending outbox and processed (acked) messages so
|
|
3877
|
+
* replies are never lost from the supervisor's view.
|
|
3878
|
+
*
|
|
3879
|
+
* @since TP-091 (lifecycle semantics hardened)
|
|
3880
|
+
*/
|
|
3881
|
+
function doReadAgentReplies(from: string | undefined, ctx: ExtensionContext): string {
|
|
3882
|
+
const stateRoot = resolveToolStateRoot(ctx);
|
|
3883
|
+
const state = loadBatchState(stateRoot);
|
|
3884
|
+
if (!state) return "❌ No batch state found.";
|
|
3885
|
+
|
|
3886
|
+
// TP-091: when from is omitted, union live agents + mailbox history roots
|
|
3887
|
+
// so replies from agents no longer active are still visible.
|
|
3888
|
+
const agentIds = from
|
|
3889
|
+
? [from]
|
|
3890
|
+
: [...new Set([
|
|
3891
|
+
...collectKnownAgentIds(stateRoot, state),
|
|
3892
|
+
...discoverMailboxAgentIds(stateRoot, state.batchId),
|
|
3893
|
+
])];
|
|
3894
|
+
|
|
3895
|
+
// TP-091: read full outbox history (pending + processed) for durable visibility
|
|
3896
|
+
const allEntries: Array<{ agentId: string; message: import("./types.ts").MailboxMessage; acked: boolean }> = [];
|
|
3897
|
+
for (const agentId of agentIds) {
|
|
3898
|
+
const history = readOutboxHistory(stateRoot, state.batchId, agentId);
|
|
3899
|
+
for (const entry of history) {
|
|
3900
|
+
allEntries.push({ agentId, ...entry });
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
if (allEntries.length === 0) {
|
|
3905
|
+
return from
|
|
3906
|
+
? `No replies from \`${from}\` in batch ${state.batchId}.`
|
|
3907
|
+
: `No agent replies in batch ${state.batchId}.`;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
const lines: string[] = [`📨 **Agent Replies** (${allEntries.length} message(s))\n`];
|
|
3911
|
+
for (const { agentId, message, acked } of allEntries) {
|
|
3912
|
+
const ts = new Date(message.timestamp).toISOString().slice(0, 16).replace("T", " ");
|
|
3913
|
+
const statusTag = acked ? " *(acked)*" : " *(pending)*";
|
|
3914
|
+
lines.push(`### ${message.type.toUpperCase()} from \`${agentId}\`${statusTag}`);
|
|
3915
|
+
lines.push(`- **Time:** ${ts}`);
|
|
3916
|
+
lines.push(`- **ID:** ${message.id}`);
|
|
3917
|
+
if (message.replyTo) lines.push(`- **Reply to:** ${message.replyTo}`);
|
|
3918
|
+
lines.push(`- **Content:** ${message.content.slice(0, 500)}`);
|
|
3919
|
+
lines.push("");
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
return lines.join("\n");
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// ── TP-106: broadcast_message tool ────────────────────────
|
|
3926
|
+
|
|
3927
|
+
pi.registerTool({
|
|
3928
|
+
name: "broadcast_message",
|
|
3929
|
+
label: "Broadcast Message",
|
|
3930
|
+
description:
|
|
3931
|
+
"Send a message to all active agents. " +
|
|
3932
|
+
"The message is written to the broadcast directory and delivered to all agents at their next turn boundary.",
|
|
3933
|
+
promptSnippet: "broadcast_message(content, type?) \u2014 send message to all agents",
|
|
3934
|
+
promptGuidelines: [
|
|
3935
|
+
"Call broadcast_message to send a message to all active agents at once.",
|
|
3936
|
+
"Default type is 'info'. Other types: 'steer', 'abort'.",
|
|
3937
|
+
"Messages are limited to 4KB.",
|
|
3938
|
+
"Rate limiting is all-or-none: if ANY recipient is rate-limited, the entire broadcast is rejected.",
|
|
3939
|
+
],
|
|
3940
|
+
parameters: Type.Object({
|
|
3941
|
+
content: Type.String({
|
|
3942
|
+
description: "Message content (max 4KB)",
|
|
3943
|
+
}),
|
|
3944
|
+
type: Type.Optional(Type.Union(
|
|
3945
|
+
[Type.Literal("steer"), Type.Literal("info"), Type.Literal("abort")],
|
|
3946
|
+
{ description: 'Message type (default: "info")' },
|
|
3947
|
+
)),
|
|
3948
|
+
}),
|
|
3949
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
3950
|
+
try {
|
|
3951
|
+
const result = doBroadcastMessage(params.content, params.type ?? "info", ctx);
|
|
3952
|
+
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
3953
|
+
} catch (err) {
|
|
3954
|
+
return {
|
|
3955
|
+
content: [{ type: "text" as const, text: `Error broadcasting: ${err instanceof Error ? err.message : String(err)}` }],
|
|
3956
|
+
details: undefined,
|
|
3957
|
+
};
|
|
3958
|
+
}
|
|
3959
|
+
},
|
|
3960
|
+
});
|
|
3961
|
+
|
|
3962
|
+
function doBroadcastMessage(content: string, messageType: string, ctx: ExtensionContext): string {
|
|
3963
|
+
const stateRoot = resolveToolStateRoot(ctx);
|
|
3964
|
+
const state = loadBatchState(stateRoot);
|
|
3965
|
+
if (!state) return "❌ No batch state found.";
|
|
3966
|
+
if (isBatchTerminal(state.phase)) {
|
|
3967
|
+
return `❌ Batch ${state.batchId} is in terminal phase (${state.phase}).`;
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
const validTypes = new Set(["steer", "info", "abort"]);
|
|
3971
|
+
if (!validTypes.has(messageType)) {
|
|
3972
|
+
return `❌ Invalid broadcast type "${messageType}". Valid types: steer, info, abort.`;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
const recipients = collectKnownAgentIds(stateRoot, state);
|
|
3976
|
+
if (recipients.length === 0) {
|
|
3977
|
+
return `❌ No known agents found in batch ${state.batchId} for broadcast delivery.`;
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
const blocked = recipients
|
|
3981
|
+
.map((agentId) => ({ agentId, check: checkRateLimit(agentId) }))
|
|
3982
|
+
.filter(({ check }) => !check.allowed);
|
|
3983
|
+
if (blocked.length > 0) {
|
|
3984
|
+
for (const b of blocked) {
|
|
3985
|
+
appendMailboxAuditEvent(stateRoot, state.batchId, {
|
|
3986
|
+
type: "message_rate_limited",
|
|
3987
|
+
from: "supervisor",
|
|
3988
|
+
to: b.agentId,
|
|
3989
|
+
reason: "broadcast blocked by per-agent rate limit",
|
|
3990
|
+
retryAfterMs: b.check.retryAfterMs,
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
const preview = blocked.slice(0, 5).map(b => `${b.agentId} (${Math.ceil((b.check.retryAfterMs ?? 0) / 1000)}s)`).join(", ");
|
|
3994
|
+
return `⏳ Broadcast rate limited for ${blocked.length}/${recipients.length} agent(s): ${preview}${blocked.length > 5 ? " ..." : ""}`;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
try {
|
|
3998
|
+
const msg = writeBroadcastMessage(stateRoot, state.batchId, {
|
|
3999
|
+
from: "supervisor",
|
|
4000
|
+
type: messageType as MailboxMessageType,
|
|
4001
|
+
content,
|
|
4002
|
+
});
|
|
4003
|
+
for (const agentId of recipients) {
|
|
4004
|
+
recordSend(agentId);
|
|
4005
|
+
}
|
|
4006
|
+
appendMailboxAuditEvent(stateRoot, state.batchId, {
|
|
4007
|
+
type: "message_sent",
|
|
4008
|
+
from: "supervisor",
|
|
4009
|
+
to: "_broadcast",
|
|
4010
|
+
messageId: msg.id,
|
|
4011
|
+
messageType,
|
|
4012
|
+
contentPreview: content.slice(0, 200),
|
|
4013
|
+
broadcast: true,
|
|
4014
|
+
});
|
|
4015
|
+
return `✅ Broadcast sent (batch ${state.batchId})\n` +
|
|
4016
|
+
`- **ID:** ${msg.id}\n` +
|
|
4017
|
+
`- **Type:** ${messageType}\n` +
|
|
4018
|
+
`- **Recipients:** ${recipients.length}\n` +
|
|
4019
|
+
`- **Size:** ${Buffer.byteLength(content, "utf8")} bytes\n` +
|
|
4020
|
+
`Message will be delivered to all agents at their next turn boundary.`;
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
return `❌ Failed to broadcast: ${err instanceof Error ? err.message : String(err)}`;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
|
|
3769
4026
|
function resolveToolStateRoot(context: ExtensionContext): string {
|
|
3770
4027
|
return execCtx?.workspaceRoot ?? execCtx?.repoRoot ?? context.cwd;
|
|
3771
4028
|
}
|
|
@@ -4148,7 +4405,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
4148
4405
|
function doListActiveAgents(ctx: ExtensionContext): string {
|
|
4149
4406
|
const stateRoot = resolveToolStateRoot(ctx);
|
|
4150
4407
|
|
|
4151
|
-
//
|
|
4408
|
+
// Try Runtime V2 registry first
|
|
4409
|
+
const state = loadBatchState(stateRoot);
|
|
4410
|
+
if (state) {
|
|
4411
|
+
const registry = readRegistrySnapshot(stateRoot, state.batchId);
|
|
4412
|
+
if (registry && Object.keys(registry.agents).length > 0) {
|
|
4413
|
+
return formatRegistryAgents(registry, state);
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
// Fall back to TMUX-based discovery for legacy batches
|
|
4152
4418
|
let sessions: string[] = [];
|
|
4153
4419
|
try {
|
|
4154
4420
|
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
@@ -4158,13 +4424,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4158
4424
|
}).trim();
|
|
4159
4425
|
sessions = output ? output.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
4160
4426
|
} catch {
|
|
4161
|
-
return "❌
|
|
4427
|
+
return "❌ No active agents found (no registry and no tmux sessions).";
|
|
4162
4428
|
}
|
|
4163
4429
|
|
|
4164
|
-
if (sessions.length === 0) return "❌ No
|
|
4430
|
+
if (sessions.length === 0) return "❌ No active agents found.";
|
|
4165
4431
|
|
|
4166
|
-
//
|
|
4167
|
-
const state = loadBatchState(stateRoot);
|
|
4432
|
+
// state already loaded above for registry check (may be null)
|
|
4168
4433
|
|
|
4169
4434
|
// Build a map of session name → lane-state data
|
|
4170
4435
|
const laneStates: Record<string, any> = {};
|
|
@@ -4244,6 +4509,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
4244
4509
|
return lines.join("\n");
|
|
4245
4510
|
}
|
|
4246
4511
|
|
|
4512
|
+
|
|
4513
|
+
// ── TP-106: Registry-based agent list formatter ────────────────
|
|
4514
|
+
|
|
4515
|
+
function formatRegistryAgents(registry: import("./types.ts").RuntimeRegistry, _batchState: PersistedBatchState | null): string {
|
|
4516
|
+
const agents = Object.values(registry.agents);
|
|
4517
|
+
if (agents.length === 0) return "❌ No agents in registry.";
|
|
4518
|
+
|
|
4519
|
+
const lines: string[] = [];
|
|
4520
|
+
lines.push(`👥 **Active Agents** (${agents.length} registered)
|
|
4521
|
+
`);
|
|
4522
|
+
|
|
4523
|
+
for (const m of agents) {
|
|
4524
|
+
const alive = !isTerminalStatus(m.status) && registryIsProcessAlive(m.pid);
|
|
4525
|
+
const icon = alive ? "🟢" : "🔴";
|
|
4526
|
+
const parts: string[] = [`**${m.agentId}**`];
|
|
4527
|
+
parts.push(`role: ${m.role}`);
|
|
4528
|
+
if (m.laneNumber != null) parts.push(`lane: ${m.laneNumber}`);
|
|
4529
|
+
if (m.taskId) parts.push(`task: ${m.taskId}`);
|
|
4530
|
+
parts.push(`status: ${m.status}`);
|
|
4531
|
+
if (alive) {
|
|
4532
|
+
const elapsed = Math.round((Date.now() - m.startedAt) / 1000);
|
|
4533
|
+
parts.push(`elapsed: ${elapsed}s`);
|
|
4534
|
+
}
|
|
4535
|
+
lines.push(`- ${icon} ${parts.join(" · ")}`);
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
return lines.join("\n");
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4247
4541
|
// ── Settings TUI ─────────────────────────────────────────────────
|
|
4248
4542
|
|
|
4249
4543
|
pi.registerCommand("taskplane-settings", {
|