pi-subagents 0.23.1 → 0.24.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 (44) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +13 -76
  3. package/package.json +14 -13
  4. package/prompts/parallel-cleanup.md +11 -1
  5. package/prompts/parallel-review.md +11 -1
  6. package/skills/pi-subagents/SKILL.md +11 -12
  7. package/src/agents/agent-management.ts +2 -2
  8. package/src/agents/agent-serializer.ts +0 -42
  9. package/src/agents/agents.ts +1 -1
  10. package/src/extension/control-notices.ts +1 -1
  11. package/src/extension/index.ts +6 -6
  12. package/src/intercom/intercom-bridge.ts +1 -2
  13. package/src/runs/background/async-execution.ts +37 -11
  14. package/src/runs/background/async-job-tracker.ts +1 -1
  15. package/src/runs/background/async-status.ts +16 -50
  16. package/src/runs/background/notify.ts +1 -1
  17. package/src/runs/background/result-watcher.ts +1 -1
  18. package/src/runs/background/run-status.ts +9 -10
  19. package/src/runs/background/subagent-runner.ts +5 -19
  20. package/src/runs/foreground/chain-clarify.ts +186 -221
  21. package/src/runs/foreground/chain-execution.ts +2 -2
  22. package/src/runs/foreground/execution.ts +1 -1
  23. package/src/runs/foreground/subagent-executor.ts +6 -6
  24. package/src/runs/shared/completion-guard.ts +1 -1
  25. package/src/runs/shared/pi-spawn.ts +32 -15
  26. package/src/runs/shared/subagent-prompt-runtime.ts +1 -1
  27. package/src/shared/fork-context.ts +22 -7
  28. package/src/shared/status-format.ts +49 -0
  29. package/src/shared/types.ts +3 -8
  30. package/src/shared/utils.ts +1 -1
  31. package/src/slash/slash-bridge.ts +2 -2
  32. package/src/slash/slash-commands.ts +10 -77
  33. package/src/slash/slash-live-state.ts +2 -2
  34. package/src/tui/render-helpers.ts +2 -2
  35. package/src/tui/render.ts +35 -61
  36. package/src/agents/agent-templates.ts +0 -60
  37. package/src/manager-ui/agent-manager-chain-detail.ts +0 -164
  38. package/src/manager-ui/agent-manager-detail.ts +0 -235
  39. package/src/manager-ui/agent-manager-edit.ts +0 -456
  40. package/src/manager-ui/agent-manager-list.ts +0 -283
  41. package/src/manager-ui/agent-manager-parallel.ts +0 -302
  42. package/src/manager-ui/agent-manager.ts +0 -732
  43. package/src/tui/subagents-status.ts +0 -621
  44. package/src/tui/text-editor.ts +0 -286
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
- import { type ActivityState, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
4
+ import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
5
6
  import { readStatus } from "../../shared/utils.ts";
6
7
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
7
8
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
@@ -9,7 +10,7 @@ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
9
10
  interface AsyncRunStepSummary {
10
11
  index: number;
11
12
  agent: string;
12
- status: string;
13
+ status: AsyncJobStep["status"];
13
14
  activityState?: ActivityState;
14
15
  lastActivityAt?: number;
15
16
  currentTool?: string;
@@ -65,16 +66,6 @@ interface AsyncRunListOptions {
65
66
  reconcile?: boolean;
66
67
  }
67
68
 
68
- export interface AsyncRunOverlayData {
69
- active: AsyncRunSummary[];
70
- recent: AsyncRunSummary[];
71
- }
72
-
73
- export interface AsyncRunOverlayOptions {
74
- recentLimit?: number;
75
- sessionId?: string;
76
- }
77
-
78
69
  function getErrorMessage(error: unknown): string {
79
70
  return error instanceof Error ? error.message : String(error);
80
71
  }
@@ -185,9 +176,9 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
185
176
  switch (state) {
186
177
  case "running": return 0;
187
178
  case "queued": return 1;
188
- case "failed": return 2;
189
- case "paused": return 2;
190
- case "complete": return 3;
179
+ case "failed": return 2;
180
+ case "paused": return 2;
181
+ case "complete": return 3;
191
182
  }
192
183
  };
193
184
  return [...runs].sort((a, b) => {
@@ -229,35 +220,15 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
229
220
  return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
230
221
  }
231
222
 
232
- export function listAsyncRunsForOverlay(asyncDirRoot: string, options: AsyncRunOverlayOptions = {}): AsyncRunOverlayData {
233
- const recentLimit = options.recentLimit ?? 5;
234
- const all = listAsyncRuns(asyncDirRoot, { sessionId: options.sessionId });
235
- const recent = all
236
- .filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
237
- .sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
238
- .slice(0, recentLimit);
239
- return {
240
- active: all.filter((run) => run.state === "queued" || run.state === "running"),
241
- recent,
242
- };
243
- }
244
-
245
223
  function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number }): string | undefined {
246
224
  const facts: string[] = [];
247
- if (input.currentTool && input.currentToolStartedAt) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
225
+ if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
248
226
  else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
249
227
  if (input.currentPath) facts.push(shortenPath(input.currentPath));
250
228
  if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
251
229
  if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
252
- if (!input.lastActivityAt) {
253
- if (input.activityState === "needs_attention") return ["needs attention", ...facts].join(" | ");
254
- if (input.activityState === "active_long_running") return ["active but long-running", ...facts].join(" | ");
255
- return facts.length ? facts.join(" | ") : undefined;
256
- }
257
- const elapsed = formatDuration(Math.max(0, Date.now() - input.lastActivityAt));
258
- if (input.activityState === "needs_attention") return [`no activity for ${elapsed}`, ...facts].join(" | ");
259
- if (input.activityState === "active_long_running") return [`active but long-running; last activity ${elapsed} ago`, ...facts].join(" | ");
260
- return [`active ${elapsed} ago`, ...facts].join(" | ");
230
+ const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
231
+ return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
261
232
  }
262
233
 
263
234
  function formatStepLine(step: AsyncRunStepSummary): string {
@@ -270,16 +241,9 @@ function formatStepLine(step: AsyncRunStepSummary): string {
270
241
  return parts.join(" | ");
271
242
  }
272
243
 
273
- function formatParallelProgress(steps: Pick<AsyncRunStepSummary, "status">[], total: number, showRunning: boolean): string {
274
- const running = steps.filter((step) => step.status === "running").length;
275
- const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
276
- const failed = steps.filter((step) => step.status === "failed").length;
277
- const paused = steps.filter((step) => step.status === "paused").length;
278
- const parts = [`${done}/${total} done`];
279
- if (showRunning && running > 0) parts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
280
- if (failed > 0) parts.push(`${failed} failed`);
281
- if (paused > 0) parts.push(`${paused} paused`);
282
- return parts.join(" · ");
244
+ export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
245
+ if (!run.outputFile) return undefined;
246
+ return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
283
247
  }
284
248
 
285
249
  export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
@@ -291,11 +255,11 @@ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" |
291
255
  : undefined;
292
256
  if (activeGroup) {
293
257
  const groupSteps = run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count);
294
- const groupLabel = formatParallelProgress(groupSteps, activeGroup.count, run.state === "running");
258
+ const groupLabel = formatParallelOutcome(groupSteps, activeGroup.count, { showRunning: run.state === "running" });
295
259
  if (run.mode === "parallel") return groupLabel;
296
260
  return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · parallel group: ${groupLabel}`;
297
261
  }
298
- if (run.mode === "parallel") return formatParallelProgress(run.steps, stepCount, run.state === "running");
262
+ if (run.mode === "parallel") return formatParallelOutcome(run.steps, stepCount, { showRunning: run.state === "running" });
299
263
  if (run.mode === "chain" && run.currentStep !== undefined && groups.length > 0) {
300
264
  const logicalStep = flatToLogicalStepIndex(run.currentStep, chainStepCount, groups);
301
265
  return `step ${logicalStep + 1}/${chainStepCount}`;
@@ -319,6 +283,8 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
319
283
  for (const step of run.steps) {
320
284
  lines.push(` ${formatStepLine(step)}`);
321
285
  }
286
+ const outputPath = formatAsyncRunOutputPath(run);
287
+ if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
322
288
  if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
323
289
  lines.push("");
324
290
  }
@@ -2,7 +2,7 @@
2
2
  * Subagent completion notifications.
3
3
  */
4
4
 
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
7
7
  import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
8
8
 
@@ -87,7 +87,7 @@ export function createResultWatcher(
87
87
  intercomTarget?: string;
88
88
  };
89
89
  if (data.sessionId && data.sessionId !== state.currentSessionId) return;
90
- if (!data.sessionId && data.cwd && data.cwd !== state.baseCwd) return;
90
+ if (!data.sessionId && data.cwd && (!state.baseCwd || data.cwd !== state.baseCwd)) return;
91
91
 
92
92
  const now = Date.now();
93
93
  const completionKey = buildCompletionKey(data, `result:${file}`);
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { AgentToolResult } from "@mariozechner/pi-agent-core";
4
- import { formatAsyncRunList, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
3
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { formatActivityLabel } from "../../shared/status-format.ts";
5
6
  import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details } from "../../shared/types.ts";
6
7
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
7
8
  import { resolveAsyncRunLocation } from "./async-resume.ts";
@@ -22,12 +23,6 @@ interface RunStatusDeps {
22
23
  now?: () => number;
23
24
  }
24
25
 
25
- function activityText(activityState: unknown, lastActivityAt: unknown): string | undefined {
26
- if (typeof lastActivityAt !== "number") return undefined;
27
- const seconds = Math.floor(Math.max(0, Date.now() - lastActivityAt) / 1000);
28
- return activityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
29
- }
30
-
31
26
  function hasExistingSessionFile(value: unknown): value is string {
32
27
  return typeof value === "string" && fs.existsSync(value);
33
28
  }
@@ -119,6 +114,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
119
114
  const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
120
115
  const eventsPath = path.join(asyncDir, "events.jsonl");
121
116
  if (status) {
117
+ const outputPath = formatAsyncRunOutputPath({ asyncDir, outputFile: status.outputFile });
122
118
  const progressLabel = formatAsyncRunProgressLabel({
123
119
  mode: status.mode,
124
120
  state: status.state,
@@ -129,7 +125,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
129
125
  });
130
126
  const started = new Date(status.startedAt).toISOString();
131
127
  const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
132
- const statusActivityText = status.state === "running" ? activityText(status.activityState, status.lastActivityAt) : undefined;
128
+ const statusActivityText = status.state === "running" ? formatActivityLabel(status.lastActivityAt, status.activityState) : undefined;
133
129
 
134
130
  const lines = [
135
131
  `Run: ${status.runId}`,
@@ -140,13 +136,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
140
136
  `Started: ${started}`,
141
137
  `Updated: ${updated}`,
142
138
  `Dir: ${asyncDir}`,
139
+ outputPath ? `Output: ${outputPath}` : undefined,
143
140
  reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
144
141
  reconciliation.resultPath && fs.existsSync(reconciliation.resultPath) ? `Result: ${reconciliation.resultPath}` : undefined,
145
142
  ].filter((line): line is string => Boolean(line));
146
143
  for (const [index, step] of (status.steps ?? []).entries()) {
147
- const stepActivityText = step.status === "running" ? activityText(step.activityState, step.lastActivityAt) : undefined;
144
+ const stepActivityText = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
148
145
  const errorText = step.error ? `, error: ${step.error}` : "";
149
146
  lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
147
+ const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
148
+ if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
150
149
  if (step.status === "running") {
151
150
  lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
152
151
  }
@@ -1,12 +1,11 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
- import { createRequire } from "node:module";
4
3
  import * as path from "node:path";
5
4
  import { pathToFileURL } from "node:url";
6
- import type { Message } from "@mariozechner/pi-ai";
5
+ import type { Message } from "@earendil-works/pi-ai";
7
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
7
  import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
- import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
8
+ import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
10
9
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
11
10
  import {
12
11
  type ActivityState,
@@ -109,7 +108,6 @@ interface StepResult {
109
108
  truncated?: boolean;
110
109
  }
111
110
 
112
- const require = createRequire(import.meta.url);
113
111
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
114
112
 
115
113
  function findLatestSessionFile(sessionDir: string): string | null {
@@ -424,21 +422,9 @@ function runPiStreaming(
424
422
  }
425
423
 
426
424
  function resolvePiPackageRootFallback(): string {
427
- // Try to resolve the main entry point and walk up to find the package root
428
- const entryPoint = require.resolve("@mariozechner/pi-coding-agent");
429
- // Entry point is typically /path/to/dist/index.js, so go up to find package root
430
- let dir = path.dirname(entryPoint);
431
- while (dir !== path.dirname(dir)) {
432
- const pkgJsonPath = path.join(dir, "package.json");
433
- try {
434
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
435
- if (pkg.name === "@mariozechner/pi-coding-agent") return dir;
436
- } catch {
437
- // Keep walking up until a readable package.json is found.
438
- }
439
- dir = path.dirname(dir);
440
- }
441
- throw new Error("Could not resolve @mariozechner/pi-coding-agent package root");
425
+ const root = resolveInstalledPiPackageRoot();
426
+ if (root) return root;
427
+ throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
442
428
  }
443
429
 
444
430
  async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot?: string): Promise<string> {