@leing2021/super-pi 0.22.1 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +66 -293
  2. package/extensions/ce-core/index.ts +276 -48
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -20
  6. package/rules/common/naming.md +154 -0
  7. package/skills/01-brainstorm/SKILL.md +42 -79
  8. package/skills/01-brainstorm/references/ce-brainstorm-mode.md +47 -0
  9. package/skills/02-plan/SKILL.md +43 -61
  10. package/skills/02-plan/references/solution-search.md +46 -0
  11. package/skills/03-work/SKILL.md +34 -52
  12. package/skills/03-work/references/completion-report.md +51 -0
  13. package/skills/04-review/SKILL.md +44 -51
  14. package/skills/04-review/references/solution-search.md +46 -0
  15. package/skills/06-next/SKILL.md +35 -20
  16. package/skills/06-next/references/recommendation-logic.md +47 -2
  17. package/skills/07-worktree/SKILL.md +21 -20
  18. package/skills/08-help/SKILL.md +46 -0
  19. package/skills/08-help/references/workflow-sequence.md +135 -0
  20. package/skills/references/language-detection.md +44 -0
  21. package/LICENSE +0 -21
  22. package/THIRD-PARTY-NOTICES.md +0 -35
  23. package/extensions/subagent/__tests__/async-job-tracker.test.ts +0 -21
  24. package/extensions/subagent/__tests__/execution-activity.test.ts +0 -15
  25. package/extensions/subagent/__tests__/parallel-render-stress.test.ts +0 -82
  26. package/extensions/subagent/__tests__/render-dedup.test.ts +0 -98
  27. package/extensions/subagent/__tests__/render-widget.test.ts +0 -28
  28. package/extensions/subagent/__tests__/throttle.test.ts +0 -90
  29. package/extensions/subagent/agent-management.ts +0 -596
  30. package/extensions/subagent/agent-manager-chain-detail.ts +0 -163
  31. package/extensions/subagent/agent-manager-detail.ts +0 -232
  32. package/extensions/subagent/agent-manager-edit.ts +0 -391
  33. package/extensions/subagent/agent-manager-list.ts +0 -279
  34. package/extensions/subagent/agent-manager-parallel.ts +0 -305
  35. package/extensions/subagent/agent-manager.ts +0 -706
  36. package/extensions/subagent/agent-scope.ts +0 -9
  37. package/extensions/subagent/agent-selection.ts +0 -26
  38. package/extensions/subagent/agent-serializer.ts +0 -124
  39. package/extensions/subagent/agent-templates.ts +0 -63
  40. package/extensions/subagent/agents/context-builder.md +0 -37
  41. package/extensions/subagent/agents/delegate.md +0 -9
  42. package/extensions/subagent/agents/oracle.md +0 -73
  43. package/extensions/subagent/agents/planner.md +0 -52
  44. package/extensions/subagent/agents/researcher.md +0 -50
  45. package/extensions/subagent/agents/reviewer.md +0 -38
  46. package/extensions/subagent/agents/scout.md +0 -48
  47. package/extensions/subagent/agents/worker.md +0 -52
  48. package/extensions/subagent/agents.ts +0 -762
  49. package/extensions/subagent/artifacts.ts +0 -101
  50. package/extensions/subagent/async-execution.ts +0 -521
  51. package/extensions/subagent/async-job-tracker.ts +0 -240
  52. package/extensions/subagent/async-status.ts +0 -242
  53. package/extensions/subagent/chain-clarify.ts +0 -1365
  54. package/extensions/subagent/chain-execution.ts +0 -854
  55. package/extensions/subagent/chain-serializer.ts +0 -127
  56. package/extensions/subagent/completion-dedupe.ts +0 -66
  57. package/extensions/subagent/doctor.ts +0 -201
  58. package/extensions/subagent/execution.ts +0 -748
  59. package/extensions/subagent/file-coalescer.ts +0 -43
  60. package/extensions/subagent/fork-context.ts +0 -64
  61. package/extensions/subagent/formatters.ts +0 -123
  62. package/extensions/subagent/frontmatter.ts +0 -32
  63. package/extensions/subagent/index.ts +0 -586
  64. package/extensions/subagent/intercom-bridge.ts +0 -241
  65. package/extensions/subagent/jsonl-writer.ts +0 -84
  66. package/extensions/subagent/model-fallback.ts +0 -109
  67. package/extensions/subagent/notify.ts +0 -111
  68. package/extensions/subagent/parallel-utils.ts +0 -109
  69. package/extensions/subagent/pi-args.ts +0 -139
  70. package/extensions/subagent/pi-spawn.ts +0 -101
  71. package/extensions/subagent/post-exit-stdio-guard.ts +0 -88
  72. package/extensions/subagent/prompt-template-bridge.ts +0 -400
  73. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  74. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  75. package/extensions/subagent/prompts/parallel-research.md +0 -50
  76. package/extensions/subagent/prompts/parallel-review.md +0 -40
  77. package/extensions/subagent/render-helpers.ts +0 -83
  78. package/extensions/subagent/render.ts +0 -908
  79. package/extensions/subagent/result-intercom.ts +0 -238
  80. package/extensions/subagent/result-watcher.ts +0 -172
  81. package/extensions/subagent/run-history.ts +0 -58
  82. package/extensions/subagent/run-status.ts +0 -137
  83. package/extensions/subagent/schemas.ts +0 -165
  84. package/extensions/subagent/session-tokens.ts +0 -51
  85. package/extensions/subagent/settings.ts +0 -368
  86. package/extensions/subagent/single-output.ts +0 -98
  87. package/extensions/subagent/skills.ts +0 -627
  88. package/extensions/subagent/slash-bridge.ts +0 -177
  89. package/extensions/subagent/slash-commands.ts +0 -304
  90. package/extensions/subagent/slash-live-state.ts +0 -295
  91. package/extensions/subagent/subagent-control.ts +0 -151
  92. package/extensions/subagent/subagent-executor.ts +0 -1915
  93. package/extensions/subagent/subagent-prompt-runtime.ts +0 -76
  94. package/extensions/subagent/subagent-runner.ts +0 -1471
  95. package/extensions/subagent/subagents-status.ts +0 -473
  96. package/extensions/subagent/text-editor.ts +0 -273
  97. package/extensions/subagent/throttle.ts +0 -77
  98. package/extensions/subagent/top-level-async.ts +0 -16
  99. package/extensions/subagent/types.ts +0 -624
  100. package/extensions/subagent/utils.ts +0 -457
  101. package/extensions/subagent/worktree.ts +0 -580
  102. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  103. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  104. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  105. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  106. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  107. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  108. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  109. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  110. package/extensions/super-pi-extension/index.ts +0 -129
  111. package/extensions/super-pi-extension/model-sync.ts +0 -204
  112. package/skills/08-status/SKILL.md +0 -41
  113. package/skills/08-status/references/artifact-locations.md +0 -10
  114. package/skills/09-help/SKILL.md +0 -37
  115. package/skills/09-help/references/workflow-sequence.md +0 -9
  116. package/skills/10-rules/SKILL.md +0 -76
  117. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,240 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
- import * as fs from "node:fs";
6
- import * as path from "node:path";
7
- import { renderWidget } from "./render.ts";
8
- import { formatControlNoticeMessage } from "./subagent-control.ts";
9
- import {
10
- type AsyncJobState,
11
- type ControlEvent,
12
- type SubagentState,
13
- POLL_INTERVAL_MS,
14
- SUBAGENT_CONTROL_EVENT,
15
- SUBAGENT_CONTROL_INTERCOM_EVENT,
16
- } from "./types.ts";
17
- import { readStatus } from "./utils.ts";
18
-
19
- /**
20
- * Resolve poll interval based on number of running jobs.
21
- * Higher parallelism → longer interval to reduce I/O and render pressure.
22
- */
23
- export function resolvePollInterval(runningCount: number): number {
24
- if (runningCount <= 2) return 1000;
25
- if (runningCount <= 4) return 1500;
26
- return 2000;
27
- }
28
-
29
- interface AsyncJobTrackerOptions {
30
- completionRetentionMs?: number;
31
- pollIntervalMs?: number;
32
- }
33
-
34
- export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
35
- ensurePoller: () => void;
36
- handleStarted: (data: unknown) => void;
37
- handleComplete: (data: unknown) => void;
38
- resetJobs: (ctx?: ExtensionContext) => void;
39
- } {
40
- const completionRetentionMs = options.completionRetentionMs ?? 10000;
41
- const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
42
- const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
43
- renderWidget(ctx, jobs);
44
- ctx.ui.requestRender?.();
45
- };
46
- const scheduleCleanup = (asyncId: string) => {
47
- const existingTimer = state.cleanupTimers.get(asyncId);
48
- if (existingTimer) clearTimeout(existingTimer);
49
- const timer = setTimeout(() => {
50
- state.cleanupTimers.delete(asyncId);
51
- state.asyncJobs.delete(asyncId);
52
- if (state.lastUiContext) {
53
- rerenderWidget(state.lastUiContext);
54
- }
55
- }, completionRetentionMs);
56
- state.cleanupTimers.set(asyncId, timer);
57
- };
58
- const emitNewControlEvents = (job: AsyncJobState) => {
59
- const eventsPath = path.join(job.asyncDir, "events.jsonl");
60
- let fd: number;
61
- try {
62
- fd = fs.openSync(eventsPath, "r");
63
- } catch (error) {
64
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
65
- console.error(`Failed to open async control events for '${job.asyncDir}':`, error);
66
- return;
67
- }
68
- try {
69
- const stat = fs.fstatSync(fd);
70
- const cursor = stat.size < (job.controlEventCursor ?? 0) ? 0 : (job.controlEventCursor ?? 0);
71
- if (stat.size <= cursor) return;
72
- const buffer = Buffer.alloc(stat.size - cursor);
73
- fs.readSync(fd, buffer, 0, buffer.length, cursor);
74
- const lastNewline = buffer.lastIndexOf(0x0a);
75
- if (lastNewline === -1) return;
76
- job.controlEventCursor = cursor + lastNewline + 1;
77
- for (const line of buffer.subarray(0, lastNewline).toString("utf-8").split("\n")) {
78
- if (!line.trim()) continue;
79
- let parsed: unknown;
80
- try {
81
- parsed = JSON.parse(line);
82
- } catch {
83
- // Ignore malformed completed records but keep the poller alive for later events.
84
- continue;
85
- }
86
- if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") continue;
87
- const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
88
- if (!record.event || !Array.isArray(record.channels)) continue;
89
- const payload = {
90
- event: record.event,
91
- source: "async" as const,
92
- asyncDir: job.asyncDir,
93
- childIntercomTarget: record.childIntercomTarget,
94
- noticeText: record.noticeText ?? formatControlNoticeMessage(record.event, record.childIntercomTarget),
95
- };
96
- if (record.channels.includes("event")) {
97
- pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
98
- }
99
- if (record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
100
- pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
101
- ...payload,
102
- to: record.intercom.to,
103
- message: record.intercom.message,
104
- });
105
- }
106
- }
107
- } catch (error) {
108
- console.error(`Failed to read async control events for '${job.asyncDir}':`, error);
109
- } finally {
110
- fs.closeSync(fd);
111
- }
112
- };
113
-
114
- const ensurePoller = () => {
115
- if (state.poller) return;
116
- const pollOnce = () => {
117
- if (state.asyncJobs.size === 0) {
118
- if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext, []);
119
- if (state.poller) {
120
- clearInterval(state.poller);
121
- state.poller = null;
122
- }
123
- return;
124
- }
125
-
126
- for (const job of state.asyncJobs.values()) {
127
- try {
128
- emitNewControlEvents(job);
129
- const status = readStatus(job.asyncDir);
130
- if (status) {
131
- const previousStatus = job.status;
132
- job.status = status.state;
133
- job.activityState = status.activityState;
134
- job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
135
- job.currentTool = status.currentTool ?? job.currentTool;
136
- job.currentToolStartedAt = status.currentToolStartedAt ?? job.currentToolStartedAt;
137
- job.mode = status.mode;
138
- job.currentStep = status.currentStep ?? job.currentStep;
139
- job.stepsTotal = status.steps?.length ?? job.stepsTotal;
140
- job.startedAt = status.startedAt ?? job.startedAt;
141
- job.updatedAt = status.lastUpdate ?? Date.now();
142
- if (status.steps?.length) {
143
- job.agents = status.steps.map((step) => step.agent);
144
- }
145
- job.sessionDir = status.sessionDir ?? job.sessionDir;
146
- job.outputFile = status.outputFile ?? job.outputFile;
147
- job.totalTokens = status.totalTokens ?? job.totalTokens;
148
- job.sessionFile = status.sessionFile ?? job.sessionFile;
149
- if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && previousStatus !== job.status) {
150
- scheduleCleanup(job.asyncId);
151
- }
152
- continue;
153
- }
154
- job.status = job.status === "queued" ? "running" : job.status;
155
- job.updatedAt = Date.now();
156
- } catch (error) {
157
- console.error(`Failed to read async status for '${job.asyncDir}':`, error);
158
- job.status = "failed";
159
- job.updatedAt = Date.now();
160
- }
161
- }
162
-
163
- if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
164
-
165
- // Dynamically adjust poll interval based on running count
166
- const runningCount = Array.from(state.asyncJobs.values()).filter((j) => j.status === "running").length;
167
- const adaptiveInterval = resolvePollInterval(runningCount);
168
- if (state.poller) {
169
- clearInterval(state.poller);
170
- state.poller = setInterval(pollOnce, adaptiveInterval);
171
- state.poller.unref?.();
172
- }
173
- };
174
-
175
- const runningCount = Array.from(state.asyncJobs.values()).filter((j) => j.status === "running").length;
176
- const initialInterval = resolvePollInterval(runningCount);
177
- state.poller = setInterval(pollOnce, initialInterval);
178
- state.poller.unref?.();
179
- };
180
-
181
- const handleStarted = (data: unknown) => {
182
- const info = data as {
183
- id?: string;
184
- asyncDir?: string;
185
- agent?: string;
186
- chain?: string[];
187
- };
188
- if (!info.id) return;
189
- const now = Date.now();
190
- const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
191
- const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
192
- state.asyncJobs.set(info.id, {
193
- asyncId: info.id,
194
- asyncDir,
195
- status: "queued",
196
- mode: info.chain ? "chain" : "single",
197
- agents,
198
- stepsTotal: agents?.length,
199
- startedAt: now,
200
- updatedAt: now,
201
- });
202
- ensurePoller();
203
- if (state.lastUiContext) {
204
- rerenderWidget(state.lastUiContext);
205
- }
206
- };
207
-
208
- const handleComplete = (data: unknown) => {
209
- const result = data as { id?: string; success?: boolean; asyncDir?: string };
210
- const asyncId = result.id;
211
- if (!asyncId) return;
212
- const job = state.asyncJobs.get(asyncId);
213
- if (job) {
214
- job.status = result.success ? "complete" : "failed";
215
- job.updatedAt = Date.now();
216
- if (result.asyncDir) job.asyncDir = result.asyncDir;
217
- }
218
- if (state.lastUiContext) {
219
- rerenderWidget(state.lastUiContext);
220
- }
221
- scheduleCleanup(asyncId);
222
- };
223
-
224
- const resetJobs = (ctx?: ExtensionContext) => {
225
- for (const timer of state.cleanupTimers.values()) {
226
- clearTimeout(timer);
227
- }
228
- state.cleanupTimers.clear();
229
- state.asyncJobs.clear();
230
- state.foregroundControls?.clear();
231
- state.lastForegroundControlId = null;
232
- state.resultFileCoalescer.clear();
233
- if (ctx?.hasUI) {
234
- state.lastUiContext = ctx;
235
- rerenderWidget(ctx, []);
236
- }
237
- };
238
-
239
- return { ensurePoller, handleStarted, handleComplete, resetJobs };
240
- }
@@ -1,242 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
7
- import { type ActivityState, type AsyncStatus, type TokenUsage } from "./types.ts";
8
- import { DEFAULT_CONTROL_CONFIG, deriveActivityState } from "./subagent-control.ts";
9
- import { readStatus } from "./utils.ts";
10
-
11
- export interface AsyncRunStepSummary {
12
- index: number;
13
- agent: string;
14
- status: string;
15
- activityState?: ActivityState;
16
- lastActivityAt?: number;
17
- currentTool?: string;
18
- currentToolStartedAt?: number;
19
- durationMs?: number;
20
- tokens?: TokenUsage;
21
- skills?: string[];
22
- model?: string;
23
- attemptedModels?: string[];
24
- error?: string;
25
- }
26
-
27
- export interface AsyncRunSummary {
28
- id: string;
29
- asyncDir: string;
30
- state: "queued" | "running" | "complete" | "failed" | "paused";
31
- activityState?: ActivityState;
32
- lastActivityAt?: number;
33
- currentTool?: string;
34
- currentToolStartedAt?: number;
35
- mode: "single" | "chain";
36
- cwd?: string;
37
- startedAt: number;
38
- lastUpdate?: number;
39
- endedAt?: number;
40
- currentStep?: number;
41
- steps: AsyncRunStepSummary[];
42
- sessionDir?: string;
43
- outputFile?: string;
44
- totalTokens?: TokenUsage;
45
- sessionFile?: string;
46
- }
47
-
48
- export interface AsyncRunListOptions {
49
- states?: Array<AsyncRunSummary["state"]>;
50
- limit?: number;
51
- }
52
-
53
- export interface AsyncRunOverlayData {
54
- active: AsyncRunSummary[];
55
- recent: AsyncRunSummary[];
56
- }
57
-
58
- function getErrorMessage(error: unknown): string {
59
- return error instanceof Error ? error.message : String(error);
60
- }
61
-
62
- function isNotFoundError(error: unknown): boolean {
63
- return typeof error === "object"
64
- && error !== null
65
- && "code" in error
66
- && (error as NodeJS.ErrnoException).code === "ENOENT";
67
- }
68
-
69
- function isAsyncRunDir(root: string, entry: string): boolean {
70
- const entryPath = path.join(root, entry);
71
- try {
72
- return fs.statSync(entryPath).isDirectory();
73
- } catch (error) {
74
- if (isNotFoundError(error)) return false;
75
- throw new Error(`Failed to inspect async run path '${entryPath}': ${getErrorMessage(error)}`, {
76
- cause: error instanceof Error ? error : undefined,
77
- });
78
- }
79
- }
80
-
81
- function outputFileMtime(outputFile: string | undefined): number | undefined {
82
- if (!outputFile) return undefined;
83
- try {
84
- return fs.statSync(outputFile).mtimeMs;
85
- } catch {
86
- return undefined;
87
- }
88
- }
89
-
90
- function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { activityState?: ActivityState; lastActivityAt?: number } {
91
- if (status.state !== "running") return { activityState: status.activityState, lastActivityAt: status.lastActivityAt };
92
- const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
93
- const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : undefined;
94
- const lastActivityAt = status.lastActivityAt ?? outputFileMtime(outputPath) ?? currentStep?.lastActivityAt ?? currentStep?.startedAt ?? status.startedAt;
95
- return {
96
- lastActivityAt,
97
- activityState: status.activityState ?? deriveActivityState({
98
- config: DEFAULT_CONTROL_CONFIG,
99
- startedAt: status.startedAt,
100
- lastActivityAt,
101
- }),
102
- };
103
- }
104
-
105
- function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
106
- const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
107
- return {
108
- id: status.runId || path.basename(asyncDir),
109
- asyncDir,
110
- state: status.state,
111
- activityState,
112
- lastActivityAt,
113
- currentTool: status.currentTool,
114
- currentToolStartedAt: status.currentToolStartedAt,
115
- mode: status.mode,
116
- cwd: status.cwd,
117
- startedAt: status.startedAt,
118
- lastUpdate: status.lastUpdate,
119
- endedAt: status.endedAt,
120
- currentStep: status.currentStep,
121
- steps: (status.steps ?? []).map((step, index) => {
122
- const stepActivityState = step.activityState ?? (step.status === "running" ? activityState : undefined);
123
- const stepLastActivityAt = step.lastActivityAt ?? (step.status === "running" ? lastActivityAt : undefined);
124
- return {
125
- index,
126
- agent: step.agent,
127
- status: step.status,
128
- ...(stepActivityState ? { activityState: stepActivityState } : {}),
129
- ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
130
- ...(step.currentTool ? { currentTool: step.currentTool } : {}),
131
- ...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
132
- ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
133
- ...(step.tokens ? { tokens: step.tokens } : {}),
134
- ...(step.skills ? { skills: step.skills } : {}),
135
- ...(step.model ? { model: step.model } : {}),
136
- ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
137
- ...(step.error ? { error: step.error } : {}),
138
- };
139
- }),
140
- ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
141
- ...(status.outputFile ? { outputFile: status.outputFile } : {}),
142
- ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
143
- ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
144
- };
145
- }
146
-
147
- function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
148
- const rank = (state: AsyncRunSummary["state"]): number => {
149
- switch (state) {
150
- case "running": return 0;
151
- case "queued": return 1;
152
- case "failed": return 2;
153
- case "paused": return 2;
154
- case "complete": return 3;
155
- }
156
- };
157
- return [...runs].sort((a, b) => {
158
- const byState = rank(a.state) - rank(b.state);
159
- if (byState !== 0) return byState;
160
- const aTime = a.lastUpdate ?? a.endedAt ?? a.startedAt;
161
- const bTime = b.lastUpdate ?? b.endedAt ?? b.startedAt;
162
- return bTime - aTime;
163
- });
164
- }
165
-
166
- export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
167
- let entries: string[];
168
- try {
169
- entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
170
- } catch (error) {
171
- if (isNotFoundError(error)) return [];
172
- throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
173
- cause: error instanceof Error ? error : undefined,
174
- });
175
- }
176
-
177
- const allowedStates = options.states ? new Set(options.states) : undefined;
178
- const runs: AsyncRunSummary[] = [];
179
- for (const entry of entries) {
180
- const asyncDir = path.join(asyncDirRoot, entry);
181
- const status = readStatus(asyncDir) as (AsyncStatus & { cwd?: string }) | null;
182
- if (!status) continue;
183
- const summary = statusToSummary(asyncDir, status);
184
- if (allowedStates && !allowedStates.has(summary.state)) continue;
185
- runs.push(summary);
186
- }
187
-
188
- const sorted = sortRuns(runs);
189
- return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
190
- }
191
-
192
- export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5): AsyncRunOverlayData {
193
- const all = listAsyncRuns(asyncDirRoot);
194
- const recent = all
195
- .filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
196
- .sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
197
- .slice(0, recentLimit);
198
- return {
199
- active: all.filter((run) => run.state === "queued" || run.state === "running"),
200
- recent,
201
- };
202
- }
203
-
204
- function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number }): string | undefined {
205
- if (input.currentTool && input.currentToolStartedAt) return `tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`;
206
- if (!input.lastActivityAt) return input.activityState === "needs_attention" ? "needs attention" : undefined;
207
- const elapsed = formatDuration(Math.max(0, Date.now() - input.lastActivityAt));
208
- return input.activityState === "needs_attention" ? `no activity for ${elapsed}` : `active ${elapsed} ago`;
209
- }
210
-
211
- function formatStepLine(step: AsyncRunStepSummary): string {
212
- const parts = [`${step.index + 1}. ${step.agent}`, step.status];
213
- const activity = formatActivityFacts(step);
214
- if (activity) parts.push(activity);
215
- if (step.model) parts.push(step.model);
216
- if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
217
- if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
218
- return parts.join(" | ");
219
- }
220
-
221
- function formatRunHeader(run: AsyncRunSummary): string {
222
- const stepCount = run.steps.length || 1;
223
- const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
224
- const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
225
- const activity = formatActivityFacts(run);
226
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
227
- }
228
-
229
- export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
230
- if (runs.length === 0) return `No ${heading.toLowerCase()}.`;
231
-
232
- const lines = [`${heading}: ${runs.length}`, ""];
233
- for (const run of runs) {
234
- lines.push(`- ${formatRunHeader(run)}`);
235
- for (const step of run.steps) {
236
- lines.push(` ${formatStepLine(step)}`);
237
- }
238
- if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
239
- lines.push("");
240
- }
241
- return lines.join("\n").trimEnd();
242
- }