@pi-agents/orchid 0.1.0-beta.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/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/agents/AGENTS-MANIFEST.md +42 -0
- package/agents/brain.md +42 -0
- package/agents/context-builder.md +46 -0
- package/agents/delegate.md +12 -0
- package/agents/dev-1.md +42 -0
- package/agents/oracle.md +73 -0
- package/agents/planner.md +55 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/tester.md +45 -0
- package/agents/worker.md +55 -0
- package/extensions/ralph.ts +1 -0
- package/extensions/reviewer-extension.ts +125 -0
- package/extensions/task-orchestrator.ts +28 -0
- package/package.json +63 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-context-build.md +53 -0
- package/prompts/parallel-handoff-plan.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +41 -0
- package/skills/orchid/SKILL.md +214 -0
- package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
- package/skills/orchid/orchid-converge/SKILL.md +124 -0
- package/skills/orchid/orchid-decompose/SKILL.md +201 -0
- package/skills/orchid/orchid-doctor/SKILL.md +162 -0
- package/skills/orchid/orchid-investigate/SKILL.md +102 -0
- package/skills/orchid/orchid-launch/SKILL.md +147 -0
- package/skills/ralph/SKILL.md +73 -0
- package/skills/subagents/pi-subagents/SKILL.md +813 -0
- package/src/index.ts +7 -0
- package/src/orchestrator/abort.ts +534 -0
- package/src/orchestrator/agent-bridge-extension.ts +1020 -0
- package/src/orchestrator/agent-host.ts +954 -0
- package/src/orchestrator/cleanup.ts +776 -0
- package/src/orchestrator/config-loader.ts +1412 -0
- package/src/orchestrator/config-schema.ts +690 -0
- package/src/orchestrator/config.ts +81 -0
- package/src/orchestrator/context-window.ts +66 -0
- package/src/orchestrator/diagnostic-reports.ts +475 -0
- package/src/orchestrator/diagnostics.ts +394 -0
- package/src/orchestrator/discovery.ts +1833 -0
- package/src/orchestrator/engine-worker.ts +415 -0
- package/src/orchestrator/engine.ts +5940 -0
- package/src/orchestrator/execution.ts +3104 -0
- package/src/orchestrator/extension.ts +5934 -0
- package/src/orchestrator/formatting.ts +785 -0
- package/src/orchestrator/git.ts +88 -0
- package/src/orchestrator/index.ts +28 -0
- package/src/orchestrator/lane-runner.ts +1787 -0
- package/src/orchestrator/mailbox.ts +780 -0
- package/src/orchestrator/merge.ts +3414 -0
- package/src/orchestrator/messages.ts +1062 -0
- package/src/orchestrator/migrations.ts +278 -0
- package/src/orchestrator/naming.ts +117 -0
- package/src/orchestrator/path-resolver.ts +275 -0
- package/src/orchestrator/persistence.ts +2625 -0
- package/src/orchestrator/process-registry.ts +452 -0
- package/src/orchestrator/quality-gate.ts +1085 -0
- package/src/orchestrator/resume.ts +3488 -0
- package/src/orchestrator/sessions.ts +57 -0
- package/src/orchestrator/settings-loader.ts +136 -0
- package/src/orchestrator/settings-tui.ts +2208 -0
- package/src/orchestrator/sidecar-telemetry.ts +267 -0
- package/src/orchestrator/supervisor.ts +4548 -0
- package/src/orchestrator/task-executor-core.ts +675 -0
- package/src/orchestrator/tmux-compat.ts +37 -0
- package/src/orchestrator/tool-allowlist-constants.ts +37 -0
- package/src/orchestrator/types.ts +4465 -0
- package/src/orchestrator/verification.ts +547 -0
- package/src/orchestrator/waves.ts +1564 -0
- package/src/orchestrator/workspace.ts +707 -0
- package/src/orchestrator/worktree.ts +2725 -0
- package/src/ralph/index.ts +825 -0
- package/src/subagents/agents/agent-management.ts +648 -0
- package/src/subagents/agents/agent-scope.ts +6 -0
- package/src/subagents/agents/agent-selection.ts +23 -0
- package/src/subagents/agents/agent-serializer.ts +86 -0
- package/src/subagents/agents/agents.ts +832 -0
- package/src/subagents/agents/chain-serializer.ts +137 -0
- package/src/subagents/agents/frontmatter.ts +29 -0
- package/src/subagents/agents/identity.ts +30 -0
- package/src/subagents/agents/skills.ts +632 -0
- package/src/subagents/extension/config.ts +16 -0
- package/src/subagents/extension/control-notices.ts +92 -0
- package/src/subagents/extension/doctor.ts +199 -0
- package/src/subagents/extension/fanout-child.ts +170 -0
- package/src/subagents/extension/index.ts +573 -0
- package/src/subagents/extension/schemas.ts +168 -0
- package/src/subagents/intercom/intercom-bridge.ts +379 -0
- package/src/subagents/intercom/result-intercom.ts +377 -0
- package/src/subagents/runs/background/async-execution.ts +712 -0
- package/src/subagents/runs/background/async-job-tracker.ts +310 -0
- package/src/subagents/runs/background/async-resume.ts +345 -0
- package/src/subagents/runs/background/async-status.ts +325 -0
- package/src/subagents/runs/background/completion-dedupe.ts +63 -0
- package/src/subagents/runs/background/notify.ts +108 -0
- package/src/subagents/runs/background/parallel-groups.ts +45 -0
- package/src/subagents/runs/background/result-watcher.ts +307 -0
- package/src/subagents/runs/background/run-id-resolver.ts +83 -0
- package/src/subagents/runs/background/run-status.ts +269 -0
- package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
- package/src/subagents/runs/background/subagent-runner.ts +1808 -0
- package/src/subagents/runs/background/top-level-async.ts +13 -0
- package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
- package/src/subagents/runs/foreground/chain-execution.ts +938 -0
- package/src/subagents/runs/foreground/execution.ts +918 -0
- package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
- package/src/subagents/runs/shared/completion-guard.ts +147 -0
- package/src/subagents/runs/shared/long-running-guard.ts +175 -0
- package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/subagents/runs/shared/model-fallback.ts +103 -0
- package/src/subagents/runs/shared/nested-events.ts +819 -0
- package/src/subagents/runs/shared/nested-path.ts +52 -0
- package/src/subagents/runs/shared/nested-render.ts +115 -0
- package/src/subagents/runs/shared/parallel-utils.ts +109 -0
- package/src/subagents/runs/shared/pi-args.ts +220 -0
- package/src/subagents/runs/shared/pi-spawn.ts +115 -0
- package/src/subagents/runs/shared/run-history.ts +60 -0
- package/src/subagents/runs/shared/single-output.ts +164 -0
- package/src/subagents/runs/shared/subagent-control.ts +226 -0
- package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
- package/src/subagents/runs/shared/worktree.ts +577 -0
- package/src/subagents/shared/artifacts.ts +98 -0
- package/src/subagents/shared/atomic-json.ts +16 -0
- package/src/subagents/shared/file-coalescer.ts +40 -0
- package/src/subagents/shared/fork-context.ts +76 -0
- package/src/subagents/shared/formatters.ts +133 -0
- package/src/subagents/shared/jsonl-writer.ts +81 -0
- package/src/subagents/shared/model-info.ts +78 -0
- package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
- package/src/subagents/shared/session-identity.ts +10 -0
- package/src/subagents/shared/session-tokens.ts +44 -0
- package/src/subagents/shared/settings.ts +397 -0
- package/src/subagents/shared/status-format.ts +49 -0
- package/src/subagents/shared/types.ts +822 -0
- package/src/subagents/shared/utils.ts +450 -0
- package/src/subagents/slash/prompt-template-bridge.ts +397 -0
- package/src/subagents/slash/slash-bridge.ts +174 -0
- package/src/subagents/slash/slash-commands.ts +528 -0
- package/src/subagents/slash/slash-live-state.ts +292 -0
- package/src/subagents/tui/render-helpers.ts +80 -0
- package/src/subagents/tui/render.ts +1358 -0
- package/templates/agents/local/supervisor.md +33 -0
- package/templates/agents/local/task-merger.md +27 -0
- package/templates/agents/local/task-reviewer.md +30 -0
- package/templates/agents/local/task-worker.md +34 -0
- package/templates/agents/supervisor-routing.md +92 -0
- package/templates/agents/supervisor.md +229 -0
- package/templates/agents/task-merger.md +214 -0
- package/templates/agents/task-reviewer.md +260 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +557 -0
- package/templates/tasks/CONTEXT.md +30 -0
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
- package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
|
+
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
6
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
|
|
8
|
+
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
9
|
+
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
10
|
+
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
11
|
+
|
|
12
|
+
interface AsyncRunStepSummary {
|
|
13
|
+
index: number;
|
|
14
|
+
agent: string;
|
|
15
|
+
status: AsyncJobStep["status"];
|
|
16
|
+
activityState?: ActivityState;
|
|
17
|
+
lastActivityAt?: number;
|
|
18
|
+
currentTool?: string;
|
|
19
|
+
currentToolArgs?: string;
|
|
20
|
+
currentToolStartedAt?: number;
|
|
21
|
+
currentPath?: string;
|
|
22
|
+
recentTools?: Array<{ tool: string; args: string; endMs: number }>;
|
|
23
|
+
recentOutput?: string[];
|
|
24
|
+
turnCount?: number;
|
|
25
|
+
toolCount?: number;
|
|
26
|
+
durationMs?: number;
|
|
27
|
+
tokens?: TokenUsage;
|
|
28
|
+
skills?: string[];
|
|
29
|
+
model?: string;
|
|
30
|
+
thinking?: string;
|
|
31
|
+
attemptedModels?: string[];
|
|
32
|
+
error?: string;
|
|
33
|
+
children?: NestedRunSummary[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AsyncRunSummary {
|
|
37
|
+
id: string;
|
|
38
|
+
asyncDir: string;
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
41
|
+
activityState?: ActivityState;
|
|
42
|
+
lastActivityAt?: number;
|
|
43
|
+
currentTool?: string;
|
|
44
|
+
currentToolStartedAt?: number;
|
|
45
|
+
currentPath?: string;
|
|
46
|
+
turnCount?: number;
|
|
47
|
+
toolCount?: number;
|
|
48
|
+
mode: SubagentRunMode;
|
|
49
|
+
cwd?: string;
|
|
50
|
+
startedAt: number;
|
|
51
|
+
lastUpdate?: number;
|
|
52
|
+
endedAt?: number;
|
|
53
|
+
currentStep?: number;
|
|
54
|
+
chainStepCount?: number;
|
|
55
|
+
parallelGroups?: AsyncParallelGroupStatus[];
|
|
56
|
+
steps: AsyncRunStepSummary[];
|
|
57
|
+
sessionDir?: string;
|
|
58
|
+
outputFile?: string;
|
|
59
|
+
totalTokens?: TokenUsage;
|
|
60
|
+
sessionFile?: string;
|
|
61
|
+
nestedChildren?: NestedRunSummary[];
|
|
62
|
+
nestedWarnings?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AsyncRunListOptions {
|
|
66
|
+
states?: Array<AsyncRunSummary["state"]>;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
limit?: number;
|
|
69
|
+
resultsDir?: string;
|
|
70
|
+
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
71
|
+
now?: () => number;
|
|
72
|
+
reconcile?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getErrorMessage(error: unknown): string {
|
|
76
|
+
return error instanceof Error ? error.message : String(error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isNotFoundError(error: unknown): boolean {
|
|
80
|
+
return typeof error === "object"
|
|
81
|
+
&& error !== null
|
|
82
|
+
&& "code" in error
|
|
83
|
+
&& (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isAsyncRunDir(root: string, entry: string): boolean {
|
|
87
|
+
const entryPath = path.join(root, entry);
|
|
88
|
+
try {
|
|
89
|
+
return fs.statSync(entryPath).isDirectory();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (isNotFoundError(error)) return false;
|
|
92
|
+
throw new Error(`Failed to inspect async run path '${entryPath}': ${getErrorMessage(error)}`, {
|
|
93
|
+
cause: error instanceof Error ? error : undefined,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function outputFileMtime(outputFile: string | undefined): number | undefined {
|
|
99
|
+
if (!outputFile) return undefined;
|
|
100
|
+
try {
|
|
101
|
+
return fs.statSync(outputFile).mtimeMs;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (isNotFoundError(error)) return undefined;
|
|
104
|
+
throw new Error(`Failed to inspect async output file '${outputFile}': ${getErrorMessage(error)}`, {
|
|
105
|
+
cause: error instanceof Error ? error : undefined,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { activityState?: ActivityState; lastActivityAt?: number } {
|
|
111
|
+
if (status.state !== "running") return { activityState: status.activityState, lastActivityAt: status.lastActivityAt };
|
|
112
|
+
const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
|
|
113
|
+
const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : undefined;
|
|
114
|
+
return {
|
|
115
|
+
activityState: status.activityState,
|
|
116
|
+
lastActivityAt: status.lastActivityAt ?? outputFileMtime(outputPath) ?? currentStep?.lastActivityAt ?? currentStep?.startedAt ?? status.startedAt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = []): AsyncRunSummary {
|
|
121
|
+
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
122
|
+
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
123
|
+
}
|
|
124
|
+
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
125
|
+
const steps = status.steps ?? [];
|
|
126
|
+
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
127
|
+
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
128
|
+
let nestedChildren: NestedRunSummary[] = [];
|
|
129
|
+
if (nestedWarnings.length === 0) {
|
|
130
|
+
try {
|
|
131
|
+
nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
|
|
132
|
+
} catch (error) {
|
|
133
|
+
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const summarizedSteps = steps.map((step, index) => {
|
|
137
|
+
const stepActivityState = step.activityState;
|
|
138
|
+
const stepLastActivityAt = step.lastActivityAt;
|
|
139
|
+
return {
|
|
140
|
+
index,
|
|
141
|
+
agent: step.agent,
|
|
142
|
+
status: step.status,
|
|
143
|
+
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
144
|
+
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
145
|
+
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
146
|
+
...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
|
|
147
|
+
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
148
|
+
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
149
|
+
...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
|
|
150
|
+
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
151
|
+
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
152
|
+
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
153
|
+
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
154
|
+
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
155
|
+
...(step.skills ? { skills: step.skills } : {}),
|
|
156
|
+
...(step.model ? { model: step.model } : {}),
|
|
157
|
+
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
158
|
+
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
159
|
+
...(step.error ? { error: step.error } : {}),
|
|
160
|
+
...(step.children?.length ? { children: step.children } : {}),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
attachRootChildrenToSteps(status.runId || path.basename(asyncDir), summarizedSteps, nestedChildren);
|
|
164
|
+
return {
|
|
165
|
+
id: status.runId || path.basename(asyncDir),
|
|
166
|
+
asyncDir,
|
|
167
|
+
...(status.sessionId ? { sessionId: status.sessionId } : {}),
|
|
168
|
+
state: status.state,
|
|
169
|
+
activityState,
|
|
170
|
+
lastActivityAt,
|
|
171
|
+
currentTool: status.currentTool,
|
|
172
|
+
currentToolStartedAt: status.currentToolStartedAt,
|
|
173
|
+
currentPath: status.currentPath,
|
|
174
|
+
turnCount: status.turnCount,
|
|
175
|
+
toolCount: status.toolCount,
|
|
176
|
+
mode: status.mode,
|
|
177
|
+
cwd: status.cwd,
|
|
178
|
+
startedAt: status.startedAt,
|
|
179
|
+
lastUpdate: status.lastUpdate,
|
|
180
|
+
endedAt: status.endedAt,
|
|
181
|
+
currentStep: status.currentStep,
|
|
182
|
+
...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
|
|
183
|
+
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
184
|
+
steps: summarizedSteps,
|
|
185
|
+
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
186
|
+
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
187
|
+
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
188
|
+
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
189
|
+
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
190
|
+
...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
195
|
+
const rank = (state: AsyncRunSummary["state"]): number => {
|
|
196
|
+
switch (state) {
|
|
197
|
+
case "running": return 0;
|
|
198
|
+
case "queued": return 1;
|
|
199
|
+
case "failed": return 2;
|
|
200
|
+
case "paused": return 2;
|
|
201
|
+
case "complete": return 3;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return [...runs].sort((a, b) => {
|
|
205
|
+
const byState = rank(a.state) - rank(b.state);
|
|
206
|
+
if (byState !== 0) return byState;
|
|
207
|
+
const aTime = a.lastUpdate ?? a.endedAt ?? a.startedAt;
|
|
208
|
+
const bTime = b.lastUpdate ?? b.endedAt ?? b.startedAt;
|
|
209
|
+
return bTime - aTime;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
214
|
+
let entries: string[];
|
|
215
|
+
try {
|
|
216
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (isNotFoundError(error)) return [];
|
|
219
|
+
throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
|
|
220
|
+
cause: error instanceof Error ? error : undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
225
|
+
const runs: AsyncRunSummary[] = [];
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const asyncDir = path.join(asyncDirRoot, entry);
|
|
228
|
+
const reconciliation = options.reconcile === false
|
|
229
|
+
? undefined
|
|
230
|
+
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
231
|
+
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
232
|
+
if (!status) continue;
|
|
233
|
+
const nestedWarnings: string[] = [];
|
|
234
|
+
try {
|
|
235
|
+
const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
|
|
236
|
+
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
239
|
+
}
|
|
240
|
+
const summary = statusToSummary(asyncDir, status, nestedWarnings);
|
|
241
|
+
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
242
|
+
if (options.sessionId && summary.sessionId !== options.sessionId) continue;
|
|
243
|
+
runs.push(summary);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const sorted = sortRuns(runs);
|
|
247
|
+
return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number }): string | undefined {
|
|
251
|
+
const facts: string[] = [];
|
|
252
|
+
if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
|
|
253
|
+
else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
|
|
254
|
+
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
255
|
+
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
256
|
+
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
257
|
+
const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
|
|
258
|
+
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
262
|
+
const parts = [`${step.index + 1}. ${step.agent}`, step.status];
|
|
263
|
+
const activity = formatActivityFacts(step);
|
|
264
|
+
if (activity) parts.push(activity);
|
|
265
|
+
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
266
|
+
if (modelThinking) parts.push(modelThinking);
|
|
267
|
+
if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
|
|
268
|
+
if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
|
|
269
|
+
return parts.join(" | ");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
|
|
273
|
+
if (!run.outputFile) return undefined;
|
|
274
|
+
return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
|
|
278
|
+
const stepCount = run.steps.length || 1;
|
|
279
|
+
const chainStepCount = run.chainStepCount ?? stepCount;
|
|
280
|
+
const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
|
|
281
|
+
const activeGroup = run.currentStep !== undefined
|
|
282
|
+
? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
|
|
283
|
+
: undefined;
|
|
284
|
+
if (activeGroup) {
|
|
285
|
+
const groupSteps = run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count);
|
|
286
|
+
const groupLabel = formatParallelOutcome(groupSteps, activeGroup.count, { showRunning: run.state === "running" });
|
|
287
|
+
if (run.mode === "parallel") return groupLabel;
|
|
288
|
+
return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · parallel group: ${groupLabel}`;
|
|
289
|
+
}
|
|
290
|
+
if (run.mode === "parallel") return formatParallelOutcome(run.steps, stepCount, { showRunning: run.state === "running" });
|
|
291
|
+
if (run.mode === "chain" && run.currentStep !== undefined && groups.length > 0) {
|
|
292
|
+
const logicalStep = flatToLogicalStepIndex(run.currentStep, chainStepCount, groups);
|
|
293
|
+
return `step ${logicalStep + 1}/${chainStepCount}`;
|
|
294
|
+
}
|
|
295
|
+
return run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatRunHeader(run: AsyncRunSummary): string {
|
|
299
|
+
const stepLabel = formatAsyncRunProgressLabel(run);
|
|
300
|
+
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
301
|
+
const activity = formatActivityFacts(run);
|
|
302
|
+
return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
|
|
306
|
+
if (runs.length === 0) return `No ${heading.toLowerCase()}.`;
|
|
307
|
+
|
|
308
|
+
const lines = [`${heading}: ${runs.length}`, ""];
|
|
309
|
+
for (const run of runs) {
|
|
310
|
+
lines.push(`- ${formatRunHeader(run)}`);
|
|
311
|
+
for (const step of run.steps) {
|
|
312
|
+
lines.push(` ${formatStepLine(step)}`);
|
|
313
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
|
|
314
|
+
}
|
|
315
|
+
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
316
|
+
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
317
|
+
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
318
|
+
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
319
|
+
const outputPath = formatAsyncRunOutputPath(run);
|
|
320
|
+
if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
|
|
321
|
+
if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
|
|
322
|
+
lines.push("");
|
|
323
|
+
}
|
|
324
|
+
return lines.join("\n").trimEnd();
|
|
325
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface CompletionDataLike {
|
|
2
|
+
id?: unknown;
|
|
3
|
+
agent?: unknown;
|
|
4
|
+
timestamp?: unknown;
|
|
5
|
+
sessionId?: unknown;
|
|
6
|
+
taskIndex?: unknown;
|
|
7
|
+
totalTasks?: unknown;
|
|
8
|
+
success?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
12
|
+
if (typeof value !== "string") return undefined;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
18
|
+
if (typeof value !== "number") return undefined;
|
|
19
|
+
return Number.isFinite(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
|
+
const id = asNonEmptyString(data.id);
|
|
24
|
+
if (id) return `id:${id}`;
|
|
25
|
+
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
26
|
+
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
|
+
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
|
+
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
29
|
+
const totalTasks = asFiniteNumber(data.totalTasks);
|
|
30
|
+
const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
|
|
31
|
+
return [
|
|
32
|
+
"meta",
|
|
33
|
+
sessionId,
|
|
34
|
+
agent,
|
|
35
|
+
timestamp !== undefined ? String(timestamp) : "no-ts",
|
|
36
|
+
taskIndex !== undefined ? String(taskIndex) : "-",
|
|
37
|
+
totalTasks !== undefined ? String(totalTasks) : "-",
|
|
38
|
+
success,
|
|
39
|
+
fallback,
|
|
40
|
+
].join(":");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
|
|
44
|
+
for (const [key, ts] of seen.entries()) {
|
|
45
|
+
if (now - ts > ttlMs) seen.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
|
|
50
|
+
pruneSeenMap(seen, now, ttlMs);
|
|
51
|
+
if (seen.has(key)) return true;
|
|
52
|
+
seen.set(key, now);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
57
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
58
|
+
const existing = globalStore[storeKey];
|
|
59
|
+
if (existing instanceof Map) return existing as Map<string, number>;
|
|
60
|
+
const map = new Map<string, number>();
|
|
61
|
+
globalStore[storeKey] = map;
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent completion notifications.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
7
|
+
import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
|
|
8
|
+
|
|
9
|
+
interface ChainStepResult {
|
|
10
|
+
agent: string;
|
|
11
|
+
output: string;
|
|
12
|
+
success: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SubagentNotifyDetails {
|
|
16
|
+
agent: string;
|
|
17
|
+
status: "completed" | "failed" | "paused";
|
|
18
|
+
taskInfo?: string;
|
|
19
|
+
resultPreview: string;
|
|
20
|
+
durationMs?: number;
|
|
21
|
+
sessionLabel?: string;
|
|
22
|
+
sessionValue?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SubagentResult {
|
|
26
|
+
id: string | null;
|
|
27
|
+
agent: string | null;
|
|
28
|
+
success: boolean;
|
|
29
|
+
summary: string;
|
|
30
|
+
exitCode?: number;
|
|
31
|
+
state?: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
sessionFile?: string;
|
|
35
|
+
shareUrl?: string;
|
|
36
|
+
gistUrl?: string;
|
|
37
|
+
shareError?: string;
|
|
38
|
+
results?: ChainStepResult[];
|
|
39
|
+
taskIndex?: number;
|
|
40
|
+
totalTasks?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function registerSubagentNotify(pi: ExtensionAPI): void {
|
|
44
|
+
const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
|
|
45
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
46
|
+
const previousUnsubscribe = globalStore[unsubscribeStoreKey];
|
|
47
|
+
if (typeof previousUnsubscribe === "function") {
|
|
48
|
+
try {
|
|
49
|
+
previousUnsubscribe();
|
|
50
|
+
} catch {
|
|
51
|
+
// Best effort cleanup for stale handlers from an older reload.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
|
|
56
|
+
const ttlMs = 10 * 60 * 1000;
|
|
57
|
+
|
|
58
|
+
const handleComplete = (data: unknown) => {
|
|
59
|
+
const result = data as SubagentResult;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const key = buildCompletionKey(result, "notify");
|
|
62
|
+
if (markSeenWithTtl(seen, key, now, ttlMs)) return;
|
|
63
|
+
|
|
64
|
+
const agent = result.agent ?? "unknown";
|
|
65
|
+
const summary = typeof result.summary === "string" ? result.summary : "";
|
|
66
|
+
const paused = !result.success && (
|
|
67
|
+
result.exitCode === 0
|
|
68
|
+
|| result.state === "paused"
|
|
69
|
+
|| summary.startsWith("Paused after interrupt.")
|
|
70
|
+
);
|
|
71
|
+
const status = paused ? "paused" : result.success ? "completed" : "failed";
|
|
72
|
+
|
|
73
|
+
const taskInfo =
|
|
74
|
+
result.taskIndex !== undefined && result.totalTasks !== undefined
|
|
75
|
+
? ` (${result.taskIndex + 1}/${result.totalTasks})`
|
|
76
|
+
: "";
|
|
77
|
+
|
|
78
|
+
const sessionLine = result.shareUrl
|
|
79
|
+
? `Session: ${result.shareUrl}`
|
|
80
|
+
: result.shareError
|
|
81
|
+
? `Session share error: ${result.shareError}`
|
|
82
|
+
: result.sessionFile
|
|
83
|
+
? `Session file: ${result.sessionFile}`
|
|
84
|
+
: undefined;
|
|
85
|
+
|
|
86
|
+
const displaySummary = summary.trim() ? summary : "(no output)";
|
|
87
|
+
const content = [
|
|
88
|
+
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
89
|
+
"",
|
|
90
|
+
displaySummary,
|
|
91
|
+
sessionLine ? "" : undefined,
|
|
92
|
+
sessionLine,
|
|
93
|
+
]
|
|
94
|
+
.filter((line) => line !== undefined)
|
|
95
|
+
.join("\n");
|
|
96
|
+
|
|
97
|
+
pi.sendMessage(
|
|
98
|
+
{
|
|
99
|
+
customType: "subagent-notify",
|
|
100
|
+
content,
|
|
101
|
+
display: true,
|
|
102
|
+
},
|
|
103
|
+
{ triggerTurn: true },
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
globalStore[unsubscribeStoreKey] = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
|
|
108
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AsyncParallelGroupStatus } from "../../shared/types.ts";
|
|
2
|
+
|
|
3
|
+
function isValidParallelGroup(group: unknown, stepCount: number, chainStepCount: number): group is AsyncParallelGroupStatus {
|
|
4
|
+
if (typeof group !== "object" || group === null) return false;
|
|
5
|
+
const { start, count, stepIndex } = group as Partial<AsyncParallelGroupStatus>;
|
|
6
|
+
return typeof start === "number"
|
|
7
|
+
&& typeof count === "number"
|
|
8
|
+
&& typeof stepIndex === "number"
|
|
9
|
+
&& Number.isInteger(start)
|
|
10
|
+
&& Number.isInteger(count)
|
|
11
|
+
&& Number.isInteger(stepIndex)
|
|
12
|
+
&& start >= 0
|
|
13
|
+
&& count > 0
|
|
14
|
+
&& stepIndex >= 0
|
|
15
|
+
&& stepIndex < chainStepCount
|
|
16
|
+
&& start + count <= stepCount;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeParallelGroups(groups: unknown, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
20
|
+
if (!Array.isArray(groups)) return [];
|
|
21
|
+
return groups
|
|
22
|
+
.filter((group): group is AsyncParallelGroupStatus => isValidParallelGroup(group, stepCount, chainStepCount))
|
|
23
|
+
.sort((left, right) => left.stepIndex - right.stepIndex || left.start - right.start);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function flatToLogicalStepIndex(flatIndex: number, chainStepCount: number, groups: AsyncParallelGroupStatus[]): number {
|
|
27
|
+
let logicalIndex = 0;
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
for (const group of groups) {
|
|
30
|
+
while (cursor < group.start && logicalIndex < chainStepCount) {
|
|
31
|
+
if (cursor === flatIndex) return logicalIndex;
|
|
32
|
+
cursor++;
|
|
33
|
+
logicalIndex++;
|
|
34
|
+
}
|
|
35
|
+
if (flatIndex >= group.start && flatIndex < group.start + group.count) return group.stepIndex;
|
|
36
|
+
cursor = group.start + group.count;
|
|
37
|
+
logicalIndex = group.stepIndex + 1;
|
|
38
|
+
}
|
|
39
|
+
while (cursor <= flatIndex && logicalIndex < chainStepCount) {
|
|
40
|
+
if (cursor === flatIndex) return logicalIndex;
|
|
41
|
+
cursor++;
|
|
42
|
+
logicalIndex++;
|
|
43
|
+
}
|
|
44
|
+
return Math.max(0, chainStepCount - 1);
|
|
45
|
+
}
|