pi-crew 0.1.45 → 0.1.46

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 (198) hide show
  1. package/README.md +5 -5
  2. package/agents/analyst.md +1 -1
  3. package/agents/critic.md +1 -1
  4. package/agents/executor.md +1 -1
  5. package/agents/explorer.md +1 -1
  6. package/agents/planner.md +1 -1
  7. package/agents/reviewer.md +1 -1
  8. package/agents/security-reviewer.md +1 -1
  9. package/agents/test-engineer.md +1 -1
  10. package/agents/verifier.md +1 -1
  11. package/agents/writer.md +1 -1
  12. package/docs/next-upgrade-roadmap.md +733 -0
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-oh-my-pi-distillation.md +322 -0
  21. package/docs/research-optimization-plan.md +548 -548
  22. package/docs/research-phase10-distillation.md +198 -198
  23. package/docs/research-phase11-distillation.md +201 -201
  24. package/docs/research-pi-coding-agent.md +357 -357
  25. package/docs/research-source-pi-crew-reference.md +174 -174
  26. package/docs/runtime-flow.md +148 -148
  27. package/docs/source-runtime-refactor-map.md +107 -83
  28. package/docs/usage.md +3 -3
  29. package/index.ts +6 -6
  30. package/install.mjs +52 -8
  31. package/package.json +1 -1
  32. package/schema.json +2 -1
  33. package/skills/async-worker-recovery/SKILL.md +42 -0
  34. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  35. package/skills/delegation-patterns/SKILL.md +54 -0
  36. package/skills/mailbox-interactive/SKILL.md +40 -0
  37. package/skills/model-routing-context/SKILL.md +39 -0
  38. package/skills/multi-perspective-review/SKILL.md +58 -0
  39. package/skills/observability-reliability/SKILL.md +41 -0
  40. package/skills/ownership-session-security/SKILL.md +41 -0
  41. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  42. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  43. package/skills/resource-discovery-config/SKILL.md +41 -0
  44. package/skills/runtime-state-reader/SKILL.md +44 -0
  45. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  46. package/skills/state-mutation-locking/SKILL.md +42 -0
  47. package/skills/systematic-debugging/SKILL.md +67 -0
  48. package/skills/ui-render-performance/SKILL.md +39 -0
  49. package/skills/verification-before-done/SKILL.md +57 -0
  50. package/skills/worktree-isolation/SKILL.md +39 -0
  51. package/src/agents/agent-serializer.ts +34 -34
  52. package/src/agents/discover-agents.ts +12 -11
  53. package/src/config/config.ts +48 -24
  54. package/src/config/defaults.ts +14 -0
  55. package/src/extension/cross-extension-rpc.ts +82 -82
  56. package/src/extension/project-init.ts +62 -2
  57. package/src/extension/register.ts +11 -9
  58. package/src/extension/registration/commands.ts +32 -25
  59. package/src/extension/registration/compaction-guard.ts +125 -125
  60. package/src/extension/registration/subagent-helpers.ts +8 -0
  61. package/src/extension/registration/subagent-tools.ts +149 -148
  62. package/src/extension/registration/team-tool.ts +8 -6
  63. package/src/extension/run-bundle-schema.ts +89 -89
  64. package/src/extension/run-index.ts +13 -5
  65. package/src/extension/run-maintenance.ts +62 -43
  66. package/src/extension/team-tool/api.ts +25 -8
  67. package/src/extension/team-tool/cancel.ts +33 -4
  68. package/src/extension/team-tool/context.ts +5 -0
  69. package/src/extension/team-tool/handle-settings.ts +188 -188
  70. package/src/extension/team-tool/inspect.ts +41 -41
  71. package/src/extension/team-tool/lifecycle-actions.ts +91 -79
  72. package/src/extension/team-tool/plan.ts +19 -19
  73. package/src/extension/team-tool/respond.ts +37 -17
  74. package/src/extension/team-tool/run.ts +52 -10
  75. package/src/extension/team-tool/status.ts +12 -1
  76. package/src/extension/team-tool-types.ts +2 -0
  77. package/src/extension/team-tool.ts +32 -11
  78. package/src/i18n.ts +184 -184
  79. package/src/observability/event-to-metric.ts +8 -1
  80. package/src/observability/exporters/otlp-exporter.ts +77 -77
  81. package/src/prompt/prompt-runtime.ts +72 -72
  82. package/src/runtime/agent-control.ts +63 -63
  83. package/src/runtime/agent-memory.ts +72 -72
  84. package/src/runtime/agent-observability.ts +114 -114
  85. package/src/runtime/async-marker.ts +26 -26
  86. package/src/runtime/attention-events.ts +28 -28
  87. package/src/runtime/background-runner.ts +59 -53
  88. package/src/runtime/cancellation.ts +51 -0
  89. package/src/runtime/child-pi.ts +457 -444
  90. package/src/runtime/completion-guard.ts +190 -190
  91. package/src/runtime/crash-recovery.ts +1 -0
  92. package/src/runtime/crew-agent-records.ts +38 -6
  93. package/src/runtime/deadletter.ts +1 -0
  94. package/src/runtime/delivery-coordinator.ts +46 -25
  95. package/src/runtime/direct-run.ts +35 -35
  96. package/src/runtime/effectiveness.ts +76 -0
  97. package/src/runtime/foreground-control.ts +82 -82
  98. package/src/runtime/green-contract.ts +46 -46
  99. package/src/runtime/group-join.ts +106 -106
  100. package/src/runtime/heartbeat-gradient.ts +28 -28
  101. package/src/runtime/heartbeat-watcher.ts +124 -124
  102. package/src/runtime/live-agent-control.ts +88 -87
  103. package/src/runtime/live-agent-manager.ts +103 -85
  104. package/src/runtime/live-control-realtime.ts +36 -36
  105. package/src/runtime/live-session-runtime.ts +309 -305
  106. package/src/runtime/manifest-cache.ts +17 -2
  107. package/src/runtime/model-fallback.ts +6 -4
  108. package/src/runtime/parallel-research.ts +44 -44
  109. package/src/runtime/pi-args.ts +18 -3
  110. package/src/runtime/pi-json-output.ts +111 -111
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/process-status.ts +5 -1
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/retry-executor.ts +81 -64
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/runtime-resolver.ts +22 -6
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/skill-instructions.ts +222 -0
  123. package/src/runtime/stale-reconciler.ts +4 -14
  124. package/src/runtime/subagent-manager.ts +3 -0
  125. package/src/runtime/supervisor-contact.ts +59 -59
  126. package/src/runtime/task-display.ts +38 -38
  127. package/src/runtime/task-output-context.ts +127 -127
  128. package/src/runtime/task-runner/capabilities.ts +78 -0
  129. package/src/runtime/task-runner/live-executor.ts +105 -101
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-builder.ts +3 -1
  132. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  133. package/src/runtime/task-runner/result-utils.ts +14 -14
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/task-runner.ts +44 -5
  136. package/src/runtime/team-runner.ts +78 -15
  137. package/src/runtime/worker-heartbeat.ts +21 -21
  138. package/src/runtime/worker-startup.ts +57 -57
  139. package/src/schema/config-schema.ts +1 -0
  140. package/src/schema/team-tool-schema.ts +3 -3
  141. package/src/state/active-run-registry.ts +165 -0
  142. package/src/state/contracts.ts +1 -1
  143. package/src/state/mailbox.ts +44 -4
  144. package/src/state/state-store.ts +8 -1
  145. package/src/state/task-claims.ts +44 -44
  146. package/src/state/types.ts +44 -2
  147. package/src/state/usage.ts +29 -29
  148. package/src/subagents/async-entry.ts +1 -1
  149. package/src/subagents/index.ts +3 -3
  150. package/src/subagents/live/control.ts +1 -1
  151. package/src/subagents/live/manager.ts +1 -1
  152. package/src/subagents/live/realtime.ts +1 -1
  153. package/src/subagents/live/session-runtime.ts +1 -1
  154. package/src/subagents/manager.ts +1 -1
  155. package/src/subagents/spawn.ts +1 -1
  156. package/src/teams/team-config.ts +1 -0
  157. package/src/teams/team-serializer.ts +38 -38
  158. package/src/types/diff.d.ts +18 -18
  159. package/src/ui/crew-footer.ts +101 -101
  160. package/src/ui/crew-select-list.ts +111 -111
  161. package/src/ui/crew-widget.ts +4 -3
  162. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  163. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  164. package/src/ui/dynamic-border.ts +25 -25
  165. package/src/ui/layout-primitives.ts +106 -106
  166. package/src/ui/loaders.ts +158 -158
  167. package/src/ui/render-diff.ts +119 -119
  168. package/src/ui/render-scheduler.ts +143 -143
  169. package/src/ui/run-snapshot-cache.ts +10 -2
  170. package/src/ui/snapshot-types.ts +2 -0
  171. package/src/ui/spinner.ts +17 -17
  172. package/src/ui/status-colors.ts +58 -58
  173. package/src/ui/syntax-highlight.ts +116 -116
  174. package/src/utils/atomic-write.ts +33 -33
  175. package/src/utils/completion-dedupe.ts +63 -63
  176. package/src/utils/frontmatter.ts +68 -68
  177. package/src/utils/git.ts +262 -262
  178. package/src/utils/ids.ts +12 -12
  179. package/src/utils/names.ts +27 -27
  180. package/src/utils/paths.ts +4 -2
  181. package/src/utils/redaction.ts +44 -44
  182. package/src/utils/safe-paths.ts +47 -47
  183. package/src/utils/sleep.ts +32 -32
  184. package/src/workflows/validate-workflow.ts +40 -40
  185. package/src/workflows/workflow-config.ts +1 -0
  186. package/src/worktree/branch-freshness.ts +45 -45
  187. package/teams/default.team.md +12 -12
  188. package/teams/fast-fix.team.md +11 -11
  189. package/teams/implementation.team.md +18 -18
  190. package/teams/parallel-research.team.md +14 -14
  191. package/teams/research.team.md +11 -11
  192. package/teams/review.team.md +12 -12
  193. package/workflows/default.workflow.md +29 -29
  194. package/workflows/fast-fix.workflow.md +22 -22
  195. package/workflows/implementation.workflow.md +38 -38
  196. package/workflows/parallel-research.workflow.md +46 -46
  197. package/workflows/research.workflow.md +22 -22
  198. package/workflows/review.workflow.md +30 -30
@@ -81,6 +81,13 @@ export function subagentToolResult(text: string, details: Record<string, unknown
81
81
  return { content: [{ type: "text" as const, text }], details, isError };
82
82
  }
83
83
 
84
+ function parseSkillParam(value: unknown): string | string[] | false | undefined {
85
+ if (value === false) return false;
86
+ if (typeof value === "string") return value;
87
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) return value;
88
+ return undefined;
89
+ }
90
+
84
91
  export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx: Pick<ExtensionContext, "cwd">): SubagentSpawnOptions {
85
92
  return {
86
93
  cwd: ctx.cwd,
@@ -89,6 +96,7 @@ export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx
89
96
  prompt: typeof params.prompt === "string" ? params.prompt : "",
90
97
  background: params.run_in_background === true,
91
98
  model: typeof params.model === "string" && params.model.trim() ? params.model.trim() : undefined,
99
+ skill: parseSkillParam(params.skill),
92
100
  maxTurns: typeof params.max_turns === "number" && Number.isFinite(params.max_turns) ? params.max_turns : undefined,
93
101
  };
94
102
  }
@@ -1,148 +1,149 @@
1
- import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "typebox";
3
- import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
- import { handleTeamTool } from "../team-tool.ts";
5
- import { checkSubagentSpawnPermission, currentCrewRole } from "../../runtime/role-permission.ts";
6
- import { readPersistedSubagentRecord, savePersistedSubagentRecord, type SubagentManager, type SubagentSpawnOptions } from "../../subagents/manager.ts";
7
- import { loadConfig } from "../../config/config.ts";
8
- import { logInternalError } from "../../utils/internal-error.ts";
9
- import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
10
- import { t } from "../../i18n.ts";
11
-
12
- export interface SubagentToolRegistrationOptions {
13
- ownerSessionGeneration?: () => number;
14
- }
15
-
16
- export function registerSubagentTools(pi: ExtensionAPI, subagentManager: SubagentManager, options: SubagentToolRegistrationOptions = {}): void {
17
- const agentTool: ToolDefinition = {
18
- name: "Agent",
19
- label: "Agent",
20
- description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
21
- promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
22
- promptGuidelines: [
23
- "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
24
- "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
25
- "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
26
- ],
27
- parameters: Type.Object({
28
- prompt: Type.String({ description: "The task for the subagent to perform." }),
29
- description: Type.String({ description: "Short 3-5 word task description." }),
30
- subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
31
- model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
32
- max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
33
- run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
34
- }) as never,
35
- async execute(_id, params, signal, _onUpdate, ctx) {
36
- const currentRole = currentCrewRole();
37
- const permission = checkSubagentSpawnPermission(currentRole);
38
- if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
39
- const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
40
- spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
41
- if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
42
- const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
43
- const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
44
- if (spawnOptions.background || record.status === "queued") {
45
- // Phase 1.1a: Terminate turn for background queued — no LLM follow-up needed.
46
- // Phase 1.6: Record was terminated for telemetry.
47
- record.terminated = true;
48
- savePersistedSubagentRecord(ctx.cwd, record);
49
- return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
50
- }
51
- await record.promise;
52
- const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
53
- const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
54
- if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
55
- record.terminated = true;
56
- savePersistedSubagentRecord(ctx.cwd, record);
57
- return { ...foregroundResult, terminate: true };
58
- }
59
- return foregroundResult;
60
- },
61
- };
62
-
63
- const getSubagentResultTool: ToolDefinition = {
64
- name: "get_subagent_result",
65
- label: "Get Agent Result",
66
- description: "Check status and retrieve results from a pi-crew background subagent.",
67
- parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
68
- async execute(_id, params, signal, _onUpdate, ctx) {
69
- const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
70
- if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
71
- const inMemory = subagentManager.getRecord(p.agent_id);
72
- const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
73
- if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
74
- let current = refreshPersistedSubagentRecord(ctx, record);
75
- if (inMemory && current !== inMemory) Object.assign(inMemory, current);
76
- if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
77
- current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
78
- savePersistedSubagentRecord(ctx.cwd, current);
79
- }
80
- if (p.wait && (current.status === "running" || current.status === "queued")) {
81
- const waited = await subagentManager.waitForRecord(current.id);
82
- if (waited) current = waited;
83
- if (current.status === "blocked") {
84
- current.resultConsumed = false;
85
- if (inMemory) inMemory.resultConsumed = false;
86
- savePersistedSubagentRecord(ctx.cwd, current);
87
- } else {
88
- const waitStartMs = Date.now();
89
- const maxWaitMs = 300_000; // 5 minutes
90
- while (current.status === "running" || current.status === "queued") {
91
- if (signal?.aborted) {
92
- current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
93
- savePersistedSubagentRecord(ctx.cwd, current);
94
- break;
95
- }
96
- if (Date.now() - waitStartMs > maxWaitMs) {
97
- current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
98
- savePersistedSubagentRecord(ctx.cwd, current);
99
- break;
100
- }
101
- await new Promise((resolve) => setTimeout(resolve, 1000));
102
- current = refreshPersistedSubagentRecord(ctx, current);
103
- if (!current.runId) break;
104
- }
105
- }
106
- }
107
- const output = readSubagentRunResult(ctx, current);
108
- if (current.status !== "running" && current.status !== "queued" && current.status !== "blocked") {
109
- current.resultConsumed = true;
110
- if (inMemory) inMemory.resultConsumed = true;
111
- savePersistedSubagentRecord(ctx.cwd, current);
112
- }
113
- const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
114
- return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
115
- },
116
- };
117
-
118
- const steerSubagentTool: ToolDefinition = {
119
- name: "steer_subagent",
120
- label: "Steer Agent",
121
- description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
122
- parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
123
- async execute(_id, params, _signal, _onUpdate, ctx) {
124
- const p = params as { agent_id?: string; message?: string };
125
- const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
126
- if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
127
- return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
128
- },
129
- };
130
-
131
- const crewAgentTool: ToolDefinition = { ...agentTool, name: "crew_agent", label: "Crew Agent", description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.", promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool." };
132
- const crewAgentResultTool: ToolDefinition = { ...getSubagentResultTool, name: "crew_agent_result", label: "Get Crew Agent Result", description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name." };
133
- const crewAgentSteerTool: ToolDefinition = { ...steerSubagentTool, name: "crew_agent_steer", label: "Steer Crew Agent", description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name." };
134
- const toolConfig = loadConfig(process.cwd()).config.tools;
135
- const enableSteer = toolConfig?.enableSteer !== false;
136
- const enableClaudeStyleAliases = toolConfig?.enableClaudeStyleAliases !== false;
137
-
138
- for (const extraTool of enableSteer ? [crewAgentTool, crewAgentResultTool, crewAgentSteerTool] : [crewAgentTool, crewAgentResultTool]) pi.registerTool(extraTool);
139
- if (enableClaudeStyleAliases) {
140
- for (const extraTool of enableSteer ? [agentTool, getSubagentResultTool, steerSubagentTool] : [agentTool, getSubagentResultTool]) {
141
- try {
142
- pi.registerTool(extraTool);
143
- } catch (error) {
144
- logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
145
- }
146
- }
147
- }
148
- }
1
+ import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
+ import { handleTeamTool } from "../team-tool.ts";
5
+ import { checkSubagentSpawnPermission, currentCrewRole } from "../../runtime/role-permission.ts";
6
+ import { readPersistedSubagentRecord, savePersistedSubagentRecord, type SubagentManager, type SubagentSpawnOptions } from "../../subagents/manager.ts";
7
+ import { loadConfig } from "../../config/config.ts";
8
+ import { logInternalError } from "../../utils/internal-error.ts";
9
+ import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
10
+ import { t } from "../../i18n.ts";
11
+
12
+ export interface SubagentToolRegistrationOptions {
13
+ ownerSessionGeneration?: () => number;
14
+ }
15
+
16
+ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: SubagentManager, options: SubagentToolRegistrationOptions = {}): void {
17
+ const agentTool: ToolDefinition = {
18
+ name: "Agent",
19
+ label: "Agent",
20
+ description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
21
+ promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
22
+ promptGuidelines: [
23
+ "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
24
+ "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
25
+ "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
26
+ ],
27
+ parameters: Type.Object({
28
+ prompt: Type.String({ description: "The task for the subagent to perform." }),
29
+ description: Type.String({ description: "Short 3-5 word task description." }),
30
+ subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
31
+ model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
32
+ skill: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String()), Type.Boolean()], { description: "Skill name(s) to inject for this subagent, or false to disable selected/default skills." })),
33
+ max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
34
+ run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
35
+ }) as never,
36
+ async execute(_id, params, signal, _onUpdate, ctx) {
37
+ const currentRole = currentCrewRole();
38
+ const permission = checkSubagentSpawnPermission(currentRole);
39
+ if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
40
+ const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
41
+ spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
42
+ if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
43
+ const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, skill: currentOptions.skill, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
44
+ const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
45
+ if (spawnOptions.background || record.status === "queued") {
46
+ // Phase 1.1a: Terminate turn for background queued — no LLM follow-up needed.
47
+ // Phase 1.6: Record was terminated for telemetry.
48
+ record.terminated = true;
49
+ savePersistedSubagentRecord(ctx.cwd, record);
50
+ return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
51
+ }
52
+ await record.promise;
53
+ const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
54
+ const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
55
+ if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
56
+ record.terminated = true;
57
+ savePersistedSubagentRecord(ctx.cwd, record);
58
+ return { ...foregroundResult, terminate: true };
59
+ }
60
+ return foregroundResult;
61
+ },
62
+ };
63
+
64
+ const getSubagentResultTool: ToolDefinition = {
65
+ name: "get_subagent_result",
66
+ label: "Get Agent Result",
67
+ description: "Check status and retrieve results from a pi-crew background subagent.",
68
+ parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
69
+ async execute(_id, params, signal, _onUpdate, ctx) {
70
+ const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
71
+ if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
72
+ const inMemory = subagentManager.getRecord(p.agent_id);
73
+ const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
74
+ if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
75
+ let current = refreshPersistedSubagentRecord(ctx, record);
76
+ if (inMemory && current !== inMemory) Object.assign(inMemory, current);
77
+ if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
78
+ current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
79
+ savePersistedSubagentRecord(ctx.cwd, current);
80
+ }
81
+ if (p.wait && (current.status === "running" || current.status === "queued")) {
82
+ const waited = await subagentManager.waitForRecord(current.id);
83
+ if (waited) current = waited;
84
+ if (current.status === "blocked") {
85
+ current.resultConsumed = false;
86
+ if (inMemory) inMemory.resultConsumed = false;
87
+ savePersistedSubagentRecord(ctx.cwd, current);
88
+ } else {
89
+ const waitStartMs = Date.now();
90
+ const maxWaitMs = 300_000; // 5 minutes
91
+ while (current.status === "running" || current.status === "queued") {
92
+ if (signal?.aborted) {
93
+ current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
94
+ savePersistedSubagentRecord(ctx.cwd, current);
95
+ break;
96
+ }
97
+ if (Date.now() - waitStartMs > maxWaitMs) {
98
+ current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
99
+ savePersistedSubagentRecord(ctx.cwd, current);
100
+ break;
101
+ }
102
+ await new Promise((resolve) => setTimeout(resolve, 1000));
103
+ current = refreshPersistedSubagentRecord(ctx, current);
104
+ if (!current.runId) break;
105
+ }
106
+ }
107
+ }
108
+ const output = readSubagentRunResult(ctx, current);
109
+ if (current.status !== "running" && current.status !== "queued" && current.status !== "blocked") {
110
+ current.resultConsumed = true;
111
+ if (inMemory) inMemory.resultConsumed = true;
112
+ savePersistedSubagentRecord(ctx.cwd, current);
113
+ }
114
+ const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
115
+ return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
116
+ },
117
+ };
118
+
119
+ const steerSubagentTool: ToolDefinition = {
120
+ name: "steer_subagent",
121
+ label: "Steer Agent",
122
+ description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
123
+ parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
124
+ async execute(_id, params, _signal, _onUpdate, ctx) {
125
+ const p = params as { agent_id?: string; message?: string };
126
+ const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
127
+ if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
128
+ return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
129
+ },
130
+ };
131
+
132
+ const crewAgentTool: ToolDefinition = { ...agentTool, name: "crew_agent", label: "Crew Agent", description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.", promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool." };
133
+ const crewAgentResultTool: ToolDefinition = { ...getSubagentResultTool, name: "crew_agent_result", label: "Get Crew Agent Result", description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name." };
134
+ const crewAgentSteerTool: ToolDefinition = { ...steerSubagentTool, name: "crew_agent_steer", label: "Steer Crew Agent", description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name." };
135
+ const toolConfig = loadConfig(process.cwd()).config.tools;
136
+ const enableSteer = toolConfig?.enableSteer !== false;
137
+ const enableClaudeStyleAliases = toolConfig?.enableClaudeStyleAliases !== false;
138
+
139
+ for (const extraTool of enableSteer ? [crewAgentTool, crewAgentResultTool, crewAgentSteerTool] : [crewAgentTool, crewAgentResultTool]) pi.registerTool(extraTool);
140
+ if (enableClaudeStyleAliases) {
141
+ for (const extraTool of enableSteer ? [agentTool, getSubagentResultTool, steerSubagentTool] : [agentTool, getSubagentResultTool]) {
142
+ try {
143
+ pi.registerTool(extraTool);
144
+ } catch (error) {
145
+ logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
146
+ }
147
+ }
148
+ }
149
+ }
@@ -1,5 +1,4 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
2
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
4
3
  import { loadConfig } from "../../config/config.ts";
5
4
  import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
@@ -9,7 +8,9 @@ import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
9
8
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
10
9
  import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
11
10
  import type { MetricRegistry } from "../../observability/metric-registry.ts";
11
+ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
12
12
  import { handleTeamTool } from "../team-tool.ts";
13
+ import { withSessionId } from "../team-tool/context.ts";
13
14
  import { toolResult } from "../tool-result.ts";
14
15
 
15
16
  export interface RegisterTeamToolDeps {
@@ -23,15 +24,16 @@ export interface RegisterTeamToolDeps {
23
24
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
24
25
  }
25
26
 
26
- function resolveCwdOverride(baseCwd: string, override: string | undefined): { ok: true; cwd: string } | { ok: false; error: string } {
27
+ export function resolveCwdOverride(baseCwd: string, override: string | undefined): { ok: true; cwd: string } | { ok: false; error: string } {
27
28
  if (!override) return { ok: true, cwd: baseCwd };
28
- const resolved = path.resolve(baseCwd, override);
29
29
  try {
30
+ const resolved = resolveRealContainedPath(baseCwd, override);
30
31
  const stat = fs.statSync(resolved);
31
32
  if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
32
33
  return { ok: true, cwd: resolved };
33
- } catch {
34
- return { ok: false, error: `cwd override does not exist: ${resolved}` };
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ return { ok: false, error: `Invalid cwd override: ${message}` };
35
37
  }
36
38
  }
37
39
 
@@ -51,7 +53,7 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
51
53
  const resolved = params as TeamToolParamsValue;
52
54
  const cwdOverride = resolveCwdOverride(ctx.cwd, resolved.cwd);
53
55
  if (!cwdOverride.ok) return toolResult(cwdOverride.error, { action: resolved.action ?? "list", status: "error" }, true);
54
- const toolCtx = { ...ctx, cwd: cwdOverride.cwd };
56
+ const toolCtx = withSessionId({ ...ctx, cwd: cwdOverride.cwd });
55
57
  // Phase 1.5: Auto-set session name from team run context
56
58
  if (resolved.action === "run" && resolved.goal && !pi.getSessionName()) {
57
59
  const runLabel = resolved.team ?? resolved.agent ?? "direct";
@@ -1,89 +1,89 @@
1
- import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
2
- import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
3
- import type { TeamEvent } from "../state/event-log.ts";
4
- import type { ExportedRunBundle } from "./run-export.ts";
5
-
6
- export interface BundleValidationResult {
7
- ok: boolean;
8
- errors: string[];
9
- }
10
-
11
- function isRecord(value: unknown): value is Record<string, unknown> {
12
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
- }
14
-
15
- function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
16
- if (!isRecord(value)) {
17
- errors.push(`manifest.artifacts[${index}] must be an object.`);
18
- return false;
19
- }
20
- const before = errors.length;
21
- if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
22
- if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
23
- if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
24
- if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
25
- if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
26
- return errors.length === before;
27
- }
28
-
29
- function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
30
- if (!isRecord(value)) {
31
- errors.push("manifest must be an object.");
32
- return false;
33
- }
34
- const before = errors.length;
35
- if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
36
- for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
37
- if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
38
- }
39
- if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
40
- if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
41
- if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
42
- else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
43
- return errors.length === before;
44
- }
45
-
46
- function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
47
- if (!isRecord(value)) {
48
- errors.push(`tasks[${index}] must be an object.`);
49
- return false;
50
- }
51
- const before = errors.length;
52
- for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
53
- if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
54
- }
55
- if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
56
- if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
57
- return errors.length === before;
58
- }
59
-
60
- function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
61
- if (!isRecord(value)) {
62
- errors.push(`events[${index}] must be an object.`);
63
- return false;
64
- }
65
- const before = errors.length;
66
- for (const field of ["time", "type", "runId"] as const) {
67
- if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
68
- }
69
- return errors.length === before;
70
- }
71
-
72
- export function validateRunBundle(value: unknown): BundleValidationResult {
73
- const errors: string[] = [];
74
- if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
75
- if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
76
- if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
77
- validateManifest(value.manifest, errors);
78
- if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
79
- else value.tasks.forEach((task, index) => validateTask(task, index, errors));
80
- if (!Array.isArray(value.events)) errors.push("events must be an array.");
81
- else value.events.forEach((event, index) => validateEvent(event, index, errors));
82
- if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
83
- return { ok: errors.length === 0, errors };
84
- }
85
-
86
- export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
87
- const validation = validateRunBundle(value);
88
- if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
89
- }
1
+ import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
2
+ import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
3
+ import type { TeamEvent } from "../state/event-log.ts";
4
+ import type { ExportedRunBundle } from "./run-export.ts";
5
+
6
+ export interface BundleValidationResult {
7
+ ok: boolean;
8
+ errors: string[];
9
+ }
10
+
11
+ function isRecord(value: unknown): value is Record<string, unknown> {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+
15
+ function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
16
+ if (!isRecord(value)) {
17
+ errors.push(`manifest.artifacts[${index}] must be an object.`);
18
+ return false;
19
+ }
20
+ const before = errors.length;
21
+ if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
22
+ if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
23
+ if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
24
+ if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
25
+ if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
26
+ return errors.length === before;
27
+ }
28
+
29
+ function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
30
+ if (!isRecord(value)) {
31
+ errors.push("manifest must be an object.");
32
+ return false;
33
+ }
34
+ const before = errors.length;
35
+ if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
36
+ for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
37
+ if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
38
+ }
39
+ if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
40
+ if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
41
+ if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
42
+ else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
43
+ return errors.length === before;
44
+ }
45
+
46
+ function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
47
+ if (!isRecord(value)) {
48
+ errors.push(`tasks[${index}] must be an object.`);
49
+ return false;
50
+ }
51
+ const before = errors.length;
52
+ for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
53
+ if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
54
+ }
55
+ if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
56
+ if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
57
+ return errors.length === before;
58
+ }
59
+
60
+ function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
61
+ if (!isRecord(value)) {
62
+ errors.push(`events[${index}] must be an object.`);
63
+ return false;
64
+ }
65
+ const before = errors.length;
66
+ for (const field of ["time", "type", "runId"] as const) {
67
+ if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
68
+ }
69
+ return errors.length === before;
70
+ }
71
+
72
+ export function validateRunBundle(value: unknown): BundleValidationResult {
73
+ const errors: string[] = [];
74
+ if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
75
+ if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
76
+ if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
77
+ validateManifest(value.manifest, errors);
78
+ if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
79
+ else value.tasks.forEach((task, index) => validateTask(task, index, errors));
80
+ if (!Array.isArray(value.events)) errors.push("events must be an array.");
81
+ else value.events.forEach((event, index) => validateEvent(event, index, errors));
82
+ if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
83
+ return { ok: errors.length === 0, errors };
84
+ }
85
+
86
+ export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
87
+ const validation = validateRunBundle(value);
88
+ if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
89
+ }
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { TeamRunManifest } from "../state/types.ts";
4
4
  import { DEFAULT_PATHS } from "../config/defaults.ts";
5
5
  import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
6
+ import { activeRunEntries } from "../state/active-run-registry.ts";
6
7
  import { isSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
7
8
 
8
9
  function readManifest(filePath: string): TeamRunManifest | undefined {
@@ -40,20 +41,27 @@ function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[
40
41
  }
41
42
 
42
43
  function scopedRunRoots(cwd: string): string[] {
43
- const roots: string[] = [userCrewRoot()];
44
+ const roots = new Set<string>();
45
+ roots.add(userCrewRoot());
44
46
  const projectRoot = findRepoRoot(cwd);
45
- if (projectRoot) roots.unshift(projectCrewRoot(cwd));
46
- return roots;
47
+ if (projectRoot) roots.add(projectCrewRoot(cwd));
48
+ return [...roots];
49
+ }
50
+
51
+ function collectActiveRuns(): TeamRunManifest[] {
52
+ return activeRunEntries()
53
+ .map((entry) => readManifest(entry.manifestPath))
54
+ .filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
47
55
  }
48
56
 
49
57
  export function listRuns(cwd: string): TeamRunManifest[] {
50
58
  const roots = scopedRunRoots(cwd);
51
- return mergeRuns(roots.map((root) => collectRuns(root)));
59
+ return mergeRuns([...roots.map((root) => collectRuns(root)), collectActiveRuns()]);
52
60
  }
53
61
 
54
62
  export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
55
63
  const roots = scopedRunRoots(cwd);
56
- return mergeRuns(roots.map((root) => collectRuns(root, max)), max);
64
+ return mergeRuns([...roots.map((root) => collectRuns(root, max)), collectActiveRuns()], max);
57
65
  }
58
66
 
59
67
  /**