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.
- package/README.md +5 -5
- package/agents/analyst.md +1 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/docs/next-upgrade-roadmap.md +733 -0
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-oh-my-pi-distillation.md +322 -0
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +107 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/install.mjs +52 -8
- package/package.json +1 -1
- package/schema.json +2 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +12 -11
- package/src/config/config.ts +48 -24
- package/src/config/defaults.ts +14 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +11 -9
- package/src/extension/registration/commands.ts +32 -25
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +8 -6
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +13 -5
- package/src/extension/run-maintenance.ts +62 -43
- package/src/extension/team-tool/api.ts +25 -8
- package/src/extension/team-tool/cancel.ts +33 -4
- package/src/extension/team-tool/context.ts +5 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +91 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +37 -17
- package/src/extension/team-tool/run.ts +52 -10
- package/src/extension/team-tool/status.ts +12 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +32 -11
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +8 -1
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +59 -53
- package/src/runtime/cancellation.ts +51 -0
- package/src/runtime/child-pi.ts +457 -444
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +1 -0
- package/src/runtime/crew-agent-records.ts +38 -6
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +46 -25
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +76 -0
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +88 -87
- package/src/runtime/live-agent-manager.ts +103 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +309 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +22 -6
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/subagent-manager.ts +3 -0
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +105 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +3 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +44 -5
- package/src/runtime/team-runner.ts +78 -15
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +3 -3
- package/src/state/active-run-registry.ts +165 -0
- package/src/state/contracts.ts +1 -1
- package/src/state/mailbox.ts +44 -4
- package/src/state/state-store.ts +8 -1
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +44 -2
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-config.ts +1 -0
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +4 -3
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +10 -2
- package/src/ui/snapshot-types.ts +2 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -33
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +4 -2
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
spawnOptions
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Phase 1.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (current
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
return subagentToolResult(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
44
|
+
const roots = new Set<string>();
|
|
45
|
+
roots.add(userCrewRoot());
|
|
44
46
|
const projectRoot = findRepoRoot(cwd);
|
|
45
|
-
if (projectRoot) roots.
|
|
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
|
/**
|