pi-crew 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/README.md +70 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +194 -34
- package/src/extension/registration/commands.ts +2 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +60 -11
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -17,7 +17,7 @@ import * as path from "node:path";
|
|
|
17
17
|
|
|
18
18
|
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
19
19
|
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
20
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
20
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
21
21
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
|
|
22
22
|
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
23
23
|
const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
|
|
@@ -46,7 +46,7 @@ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
46
46
|
|
|
47
47
|
export async function handleExport(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
48
48
|
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
49
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
49
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
50
50
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
|
|
51
51
|
|
|
52
52
|
// SECURITY: Ownership check — only the owner session may export a run.
|
|
@@ -95,7 +95,7 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
95
95
|
if (intentError) return intentError;
|
|
96
96
|
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
97
97
|
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
98
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
98
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
99
99
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
100
100
|
|
|
101
101
|
// Ownership check — prevent cross-session deletion unless force is set
|
|
@@ -125,7 +125,7 @@ export async function handleCleanup(params: TeamToolParamsValue, ctx: TeamContex
|
|
|
125
125
|
const intentError = enforceDestructiveIntent("cleanup", params, ctx.config);
|
|
126
126
|
if (intentError) return intentError;
|
|
127
127
|
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
128
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
128
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
129
129
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
130
130
|
|
|
131
131
|
// Ownership check — prevent cross-session worktree cleanup unless force is set
|
|
@@ -20,11 +20,11 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
20
20
|
|
|
21
21
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
22
22
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
23
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
23
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
24
24
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
25
25
|
|
|
26
26
|
return withRunLockSync(loaded.manifest, () => {
|
|
27
|
-
const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!);
|
|
27
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!); // NOTE: inside withRunLockSync - consistent read
|
|
28
28
|
if (!fresh) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
29
29
|
const foreignRun = typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId;
|
|
30
30
|
if (foreignRun && !params.force) return result(`Run ${fresh.manifest.runId} belongs to another session. Use force: true to override.`, { action: "respond", status: "error", runId: fresh.manifest.runId }, true);
|
|
@@ -2,6 +2,7 @@ import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
|
2
2
|
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
3
3
|
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
4
4
|
import { loadConfig } from "../../config/config.ts";
|
|
5
|
+
import { findGitRoot, assertCleanLeader } from "../../worktree/worktree-manager.ts";
|
|
5
6
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
6
7
|
import { writeArtifact } from "../../state/artifact-store.ts";
|
|
7
8
|
import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-registry.ts";
|
|
@@ -85,9 +86,57 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
85
86
|
const { ensureCrewDirectory } = await import("../../state/crew-init.ts");
|
|
86
87
|
await ensureCrewDirectory(workingDir);
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// WORKTREE FIX: If worktree mode is needed but cwd is not a git repo,
|
|
90
|
+
// auto-correct to the nearest git repo root. This prevents "not a git repository"
|
|
91
|
+
// errors when ctx.cwd points to a parent directory that isn't a git repo.
|
|
92
|
+
let resolvedCtx = ctx;
|
|
93
|
+
if (workingDir) {
|
|
94
|
+
try {
|
|
95
|
+
const gitRoot = findGitRoot(workingDir);
|
|
96
|
+
if (gitRoot && gitRoot !== workingDir) {
|
|
97
|
+
resolvedCtx = { ...ctx, cwd: gitRoot };
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// cwd is not in a git repo — validate below if worktree mode is needed
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// WORKTREE PRECONDITION CHECK: validate git repo exists and is clean
|
|
105
|
+
// BEFORE creating the run manifest, so we return a friendly error
|
|
106
|
+
// instead of crashing mid-execution in prepareTaskWorkspace.
|
|
107
|
+
if (params.workspaceMode === "worktree") {
|
|
108
|
+
let gitRoot: string | undefined;
|
|
109
|
+
try {
|
|
110
|
+
gitRoot = findGitRoot(resolvedCtx.cwd);
|
|
111
|
+
} catch {
|
|
112
|
+
// not a git repo
|
|
113
|
+
}
|
|
114
|
+
if (!gitRoot) {
|
|
115
|
+
return result(
|
|
116
|
+
`Worktree mode requires a git repository. '${resolvedCtx.cwd}' is not inside a git repo.\nUse workspaceMode: 'single' or run from a git repository.`,
|
|
117
|
+
{ action: "run", status: "error" },
|
|
118
|
+
true,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Check if clean leader is required (can be disabled via config)
|
|
122
|
+
const preCheckConfig = loadConfig(resolvedCtx.cwd);
|
|
123
|
+
if (preCheckConfig.config.requireCleanWorktreeLeader !== false) {
|
|
124
|
+
try {
|
|
125
|
+
assertCleanLeader(gitRoot);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
128
|
+
return result(
|
|
129
|
+
`${msg}\nCommit or stash changes before using worktree mode, or use workspaceMode: 'single'.`,
|
|
130
|
+
{ action: "run", status: "error" },
|
|
131
|
+
true,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const teams = allTeams(discoverTeams(resolvedCtx.cwd));
|
|
138
|
+
const workflows = allWorkflows(discoverWorkflows(resolvedCtx.cwd));
|
|
139
|
+
const agents = allAgents(discoverAgents(resolvedCtx.cwd));
|
|
91
140
|
const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
|
|
92
141
|
if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
|
|
93
142
|
const teamName = params.team ?? "default";
|
|
@@ -110,7 +159,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
110
159
|
steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
|
|
111
160
|
} : workflows.find((item) => item.name === workflowName);
|
|
112
161
|
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
113
|
-
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow,
|
|
162
|
+
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, resolvedCtx.cwd);
|
|
114
163
|
|
|
115
164
|
// Check if this is a pipeline workflow - special handling for multi-stage execution
|
|
116
165
|
const isPipelineWorkflow = workflowName === "pipeline" && !directAgent;
|
|
@@ -152,7 +201,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
152
201
|
|
|
153
202
|
const skillOverride = normalizeSkillOverride(params.skill);
|
|
154
203
|
const { manifest, tasks, paths } = createRunManifest({
|
|
155
|
-
cwd:
|
|
204
|
+
cwd: resolvedCtx.cwd,
|
|
156
205
|
team,
|
|
157
206
|
workflow,
|
|
158
207
|
goal,
|
|
@@ -169,7 +218,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
169
218
|
atomicWriteJson(paths.manifestPath, updatedManifest);
|
|
170
219
|
registerActiveRun(updatedManifest);
|
|
171
220
|
|
|
172
|
-
const loadedConfig = loadConfig(
|
|
221
|
+
const loadedConfig = loadConfig(resolvedCtx.cwd);
|
|
173
222
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
174
223
|
const runtime = await resolveCrewRuntime(executedConfig);
|
|
175
224
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
@@ -205,11 +254,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
205
254
|
const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
|
|
206
255
|
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
207
256
|
void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
208
|
-
scheduleBackgroundEarlyExitGuard(
|
|
257
|
+
scheduleBackgroundEarlyExitGuard(resolvedCtx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
|
|
209
258
|
// Wait for the async run to complete and return actual results.
|
|
210
259
|
try {
|
|
211
|
-
const completed = await waitForRun(updatedManifest.runId,
|
|
212
|
-
const metrics = collectRunMetrics(
|
|
260
|
+
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
|
|
261
|
+
const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
|
|
213
262
|
const lines: string[] = [
|
|
214
263
|
`pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
|
|
215
264
|
`Goal: ${goal.slice(0, 100)}`,
|
|
@@ -341,8 +390,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
341
390
|
|
|
342
391
|
// Wait for the foreground run to complete and return actual results.
|
|
343
392
|
try {
|
|
344
|
-
const completed = await waitForRun(updatedManifest.runId,
|
|
345
|
-
const metrics = collectRunMetrics(
|
|
393
|
+
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
|
|
394
|
+
const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
|
|
346
395
|
const lines: string[] = [
|
|
347
396
|
`pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
|
|
348
397
|
`Goal: ${goal.slice(0, 100)}`,
|
|
@@ -18,7 +18,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
18
18
|
if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
|
|
19
19
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
20
20
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
|
|
21
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
21
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
22
22
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
|
|
23
23
|
let { manifest, tasks } = loaded;
|
|
24
24
|
let asyncLivenessLine: string | undefined;
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
|
|
19
19
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
20
20
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
21
|
-
import { appendEvent } from "../state/event-log.ts";
|
|
21
|
+
import { appendEvent, appendEventFireAndForget } from "../state/event-log.ts";
|
|
22
22
|
import { withRunLock } from "../state/locks.ts";
|
|
23
23
|
import { replayPendingMailboxMessages } from "../state/mailbox.ts";
|
|
24
24
|
import {
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
TeamTaskState,
|
|
34
34
|
} from "../state/types.ts";
|
|
35
35
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
36
|
+
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
36
37
|
import {
|
|
37
38
|
allWorkflows,
|
|
38
39
|
discoverWorkflows,
|
|
@@ -99,10 +100,31 @@ async function handleRun(
|
|
|
99
100
|
return _cachedHandleRun(...args);
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
import { FileCheckpointStore } from "../runtime/checkpoint.ts";
|
|
102
104
|
import { waitForRun } from "../runtime/run-tracker.ts";
|
|
103
105
|
import { normalizeSkillOverride } from "../runtime/skill-instructions.ts";
|
|
106
|
+
import {
|
|
107
|
+
computeRunCacheKey,
|
|
108
|
+
getCachedRun,
|
|
109
|
+
getCacheStats,
|
|
110
|
+
} from "../state/run-cache.ts";
|
|
111
|
+
import { listRunGraphs, loadRunGraph } from "../state/run-graph.ts";
|
|
104
112
|
import { searchAgents, searchTeams } from "../utils/bm25-search.ts";
|
|
105
113
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
114
|
+
import { buildTeamOnboarding } from "./team-onboard.ts";
|
|
115
|
+
import {
|
|
116
|
+
handleAnchorAccumulate,
|
|
117
|
+
handleAnchorClear,
|
|
118
|
+
handleAnchorSet,
|
|
119
|
+
handleAnchorStatus,
|
|
120
|
+
} from "./team-tool/anchor.ts";
|
|
121
|
+
import {
|
|
122
|
+
createAutoSummarizeService,
|
|
123
|
+
handleAutoSummarizeConfig,
|
|
124
|
+
handleAutoSummarizeOff,
|
|
125
|
+
handleAutoSummarizeOn,
|
|
126
|
+
handleAutoSummarizeStatus,
|
|
127
|
+
} from "./team-tool/auto-summarize.ts";
|
|
106
128
|
import {
|
|
107
129
|
type CacheControlDeps,
|
|
108
130
|
invalidateSnapshot,
|
|
@@ -110,6 +132,10 @@ import {
|
|
|
110
132
|
import { handleCancel, handleRetry } from "./team-tool/cancel.ts";
|
|
111
133
|
import { handleDoctor } from "./team-tool/doctor.ts";
|
|
112
134
|
import { handleExplain } from "./team-tool/explain.ts";
|
|
135
|
+
import {
|
|
136
|
+
handleListScheduled,
|
|
137
|
+
handleSchedule,
|
|
138
|
+
} from "./team-tool/handle-schedule.ts";
|
|
113
139
|
import { handleHealthMonitor } from "./team-tool/health-monitor.ts";
|
|
114
140
|
import {
|
|
115
141
|
handleArtifacts,
|
|
@@ -125,30 +151,17 @@ import {
|
|
|
125
151
|
handlePrune,
|
|
126
152
|
handleWorktrees,
|
|
127
153
|
} from "./team-tool/lifecycle-actions.ts";
|
|
128
|
-
import {
|
|
129
|
-
getCachedRun,
|
|
130
|
-
computeRunCacheKey,
|
|
131
|
-
getCacheStats,
|
|
132
|
-
} from "../state/run-cache.ts";
|
|
133
|
-
import {
|
|
134
|
-
loadRunGraph,
|
|
135
|
-
listRunGraphs,
|
|
136
|
-
} from "../state/run-graph.ts";
|
|
137
|
-
import { FileCheckpointStore } from "../runtime/checkpoint.ts";
|
|
138
|
-
import { buildTeamOnboarding } from "./team-onboard.ts";
|
|
154
|
+
import { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
139
155
|
import { handleParallel } from "./team-tool/parallel-dispatch.ts";
|
|
140
|
-
import { handleSchedule, handleListScheduled } from "./team-tool/handle-schedule.ts";
|
|
141
156
|
import { handlePlan } from "./team-tool/plan.ts";
|
|
142
|
-
import { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
143
157
|
import { handleRespond } from "./team-tool/respond.ts";
|
|
144
158
|
import { handleStatus } from "./team-tool/status.ts";
|
|
145
|
-
import { handleAnchorSet, handleAnchorClear, handleAnchorStatus, handleAnchorAccumulate } from "./team-tool/anchor.ts";
|
|
146
|
-
import { handleAutoSummarizeOn, handleAutoSummarizeOff, handleAutoSummarizeStatus, handleAutoSummarizeConfig, createAutoSummarizeService } from "./team-tool/auto-summarize.ts";
|
|
147
159
|
|
|
148
160
|
export { handleApi } from "./team-tool/api.ts";
|
|
149
161
|
export { handleRetry } from "./team-tool/cancel.ts";
|
|
150
162
|
export type { TeamContext } from "./team-tool/context.ts";
|
|
151
163
|
export { handleDoctor } from "./team-tool/doctor.ts";
|
|
164
|
+
export { handleSchedule } from "./team-tool/handle-schedule.ts";
|
|
152
165
|
export {
|
|
153
166
|
handleArtifacts,
|
|
154
167
|
handleEvents,
|
|
@@ -163,12 +176,11 @@ export {
|
|
|
163
176
|
handlePrune,
|
|
164
177
|
handleWorktrees,
|
|
165
178
|
} from "./team-tool/lifecycle-actions.ts";
|
|
166
|
-
export {
|
|
179
|
+
export { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
167
180
|
export { handlePlan } from "./team-tool/plan.ts";
|
|
168
181
|
export { handleStatus } from "./team-tool/status.ts";
|
|
169
182
|
export type { TeamToolDetails } from "./team-tool-types.ts";
|
|
170
183
|
export { handleRun };
|
|
171
|
-
export { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
172
184
|
|
|
173
185
|
export function handleList(
|
|
174
186
|
params: TeamToolParamsValue,
|
|
@@ -451,7 +463,7 @@ export async function handleResume(
|
|
|
451
463
|
{ action: "resume", status: "error" },
|
|
452
464
|
true,
|
|
453
465
|
);
|
|
454
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
466
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
455
467
|
if (!loaded)
|
|
456
468
|
return result(
|
|
457
469
|
`Run '${params.runId}' not found.`,
|
|
@@ -674,7 +686,7 @@ export function handleSteer(
|
|
|
674
686
|
{ action: "steer", status: "error" },
|
|
675
687
|
true,
|
|
676
688
|
);
|
|
677
|
-
const loaded = loadRunManifestById(runCwd, runId);
|
|
689
|
+
const loaded = loadRunManifestById(runCwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
678
690
|
if (!loaded)
|
|
679
691
|
return result(
|
|
680
692
|
`Run '${runId}' not found`,
|
|
@@ -689,12 +701,23 @@ export function handleSteer(
|
|
|
689
701
|
true,
|
|
690
702
|
);
|
|
691
703
|
if (!task.pendingSteers) task.pendingSteers = [];
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
704
|
+
// HIGH-04: Cap pendingSteers array to prevent unbounded memory growth
|
|
705
|
+
const MAX_PENDING_STEERS = 100;
|
|
706
|
+
if (task.pendingSteers.length >= MAX_PENDING_STEERS) {
|
|
707
|
+
// Log warning before dropping the oldest message
|
|
708
|
+
appendEventFireAndForget(loaded.manifest.eventsPath, {
|
|
709
|
+
type: "task.steer_dropped",
|
|
710
|
+
runId,
|
|
711
|
+
taskId,
|
|
712
|
+
data: {
|
|
713
|
+
droppedMessage: task.pendingSteers[0],
|
|
714
|
+
reason: "pendingSteers cap exceeded",
|
|
715
|
+
queueDepth: task.pendingSteers.length,
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
task.pendingSteers = task.pendingSteers.slice(-(MAX_PENDING_STEERS - 1));
|
|
719
|
+
}
|
|
720
|
+
task.pendingSteers.push(message);
|
|
698
721
|
saveRunTasks(loaded.manifest, loaded.tasks);
|
|
699
722
|
appendEvent(loaded.manifest.eventsPath, {
|
|
700
723
|
type: "task.steer_queued",
|
|
@@ -751,17 +774,57 @@ function handleInvalidate(
|
|
|
751
774
|
/**
|
|
752
775
|
* Locate the CWD where a run's state is stored.
|
|
753
776
|
* Tries ctx.cwd first, then scans immediate child directories for .crew/state/runs/<runId>.
|
|
777
|
+
*
|
|
778
|
+
* Defensive bounds (prevent hang on large dirs like /tmp in CI):
|
|
779
|
+
* - Skips entries that are well-known system/ephemeral dirs (e.g. .npm, node_modules, .git)
|
|
780
|
+
* - Caps the scan at MAX_SCAN_ENTRIES to avoid pathological scans
|
|
781
|
+
* - Skips hidden entries (starting with `.`) unless they look like run directories
|
|
782
|
+
* (e.g. .crew, .pi, .tmp-crew-runs)
|
|
754
783
|
*/
|
|
755
|
-
|
|
784
|
+
const MAX_SCAN_ENTRIES = 1000;
|
|
785
|
+
const SKIP_SCAN_DIRS = new Set([
|
|
786
|
+
"node_modules",
|
|
787
|
+
".git",
|
|
788
|
+
".npm",
|
|
789
|
+
".cache",
|
|
790
|
+
".local",
|
|
791
|
+
"proc",
|
|
792
|
+
"sys",
|
|
793
|
+
"dev",
|
|
794
|
+
"Library",
|
|
795
|
+
"Applications",
|
|
796
|
+
]);
|
|
797
|
+
|
|
798
|
+
export function locateRunCwd(
|
|
799
|
+
runId: string,
|
|
800
|
+
baseCwd: string,
|
|
801
|
+
): string | undefined {
|
|
756
802
|
// Fast path: run is in the current CWD
|
|
757
|
-
if (loadRunManifestById(baseCwd, runId))
|
|
803
|
+
if (loadRunManifestById(baseCwd, runId)) {
|
|
804
|
+
return baseCwd;
|
|
805
|
+
}
|
|
758
806
|
|
|
759
|
-
// Scan immediate child directories
|
|
807
|
+
// Scan immediate child directories, but with defensive bounds.
|
|
760
808
|
try {
|
|
761
|
-
|
|
809
|
+
const entries = fs.readdirSync(baseCwd, { withFileTypes: true });
|
|
810
|
+
const boundedEntries = entries.length > MAX_SCAN_ENTRIES
|
|
811
|
+
? entries.slice(0, MAX_SCAN_ENTRIES)
|
|
812
|
+
: entries;
|
|
813
|
+
for (const entry of boundedEntries) {
|
|
762
814
|
if (!entry.isDirectory()) continue;
|
|
815
|
+
if (SKIP_SCAN_DIRS.has(entry.name)) continue;
|
|
816
|
+
// Skip hidden entries except well-known run-storage prefixes
|
|
817
|
+
if (entry.name.startsWith(".")) {
|
|
818
|
+
if (
|
|
819
|
+
!entry.name.startsWith(".crew") &&
|
|
820
|
+
!entry.name.startsWith(".pi") &&
|
|
821
|
+
!entry.name.startsWith(".tmp-crew")
|
|
822
|
+
) continue;
|
|
823
|
+
}
|
|
763
824
|
const candidate = path.join(baseCwd, entry.name);
|
|
764
|
-
if (loadRunManifestById(candidate, runId))
|
|
825
|
+
if (loadRunManifestById(candidate, runId)) {
|
|
826
|
+
return candidate;
|
|
827
|
+
}
|
|
765
828
|
}
|
|
766
829
|
} catch {
|
|
767
830
|
/* ignore unreadable dirs */
|
|
@@ -1090,23 +1153,32 @@ export async function handleTeamTool(
|
|
|
1090
1153
|
return handleWait(params, ctx);
|
|
1091
1154
|
case "graph": {
|
|
1092
1155
|
if (params.runId) {
|
|
1156
|
+
assertSafePathId("runId", params.runId);
|
|
1093
1157
|
const graph = loadRunGraph(ctx.cwd, params.runId);
|
|
1094
1158
|
return result(
|
|
1095
|
-
graph
|
|
1159
|
+
graph
|
|
1160
|
+
? JSON.stringify(graph, null, 2)
|
|
1161
|
+
: "No graph found for this run.",
|
|
1096
1162
|
{ action: "graph", status: graph ? "ok" : "error" },
|
|
1097
1163
|
!graph,
|
|
1098
1164
|
);
|
|
1099
1165
|
}
|
|
1100
1166
|
const graphs = listRunGraphs(ctx.cwd);
|
|
1101
1167
|
return result(
|
|
1102
|
-
graphs.length
|
|
1168
|
+
graphs.length
|
|
1169
|
+
? `Available graphs:\n${graphs.join("\n")}`
|
|
1170
|
+
: "No graphs available.",
|
|
1103
1171
|
{ action: "graph", status: "ok" },
|
|
1104
1172
|
);
|
|
1105
1173
|
}
|
|
1106
1174
|
case "search": {
|
|
1107
1175
|
const query = params.goal ?? params.task ?? "";
|
|
1108
1176
|
if (!query) {
|
|
1109
|
-
return result(
|
|
1177
|
+
return result(
|
|
1178
|
+
"Search requires goal or task query.",
|
|
1179
|
+
{ action: "search", status: "error" },
|
|
1180
|
+
true,
|
|
1181
|
+
);
|
|
1110
1182
|
}
|
|
1111
1183
|
try {
|
|
1112
1184
|
const [agentResults, teamResults] = await Promise.all([
|
|
@@ -1117,19 +1189,30 @@ export async function handleTeamTool(
|
|
|
1117
1189
|
if (teamResults.length) {
|
|
1118
1190
|
lines.push("## Teams");
|
|
1119
1191
|
for (const r of teamResults) {
|
|
1120
|
-
lines.push(
|
|
1192
|
+
lines.push(
|
|
1193
|
+
`- [${r.team.name}] score=${r.score.toFixed(2)}: ${r.team.description ?? "(no description)"}`,
|
|
1194
|
+
);
|
|
1121
1195
|
}
|
|
1122
1196
|
}
|
|
1123
1197
|
if (agentResults.length) {
|
|
1124
1198
|
lines.push("## Agents");
|
|
1125
1199
|
for (const r of agentResults) {
|
|
1126
|
-
lines.push(
|
|
1200
|
+
lines.push(
|
|
1201
|
+
`- [${r.agent.name}] score=${r.score.toFixed(2)}: ${r.agent.description ?? "(no description)"}`,
|
|
1202
|
+
);
|
|
1127
1203
|
}
|
|
1128
1204
|
}
|
|
1129
|
-
return result(
|
|
1205
|
+
return result(
|
|
1206
|
+
lines.length ? lines.join("\n") : "No results found.",
|
|
1207
|
+
{ action: "search", status: "ok" },
|
|
1208
|
+
);
|
|
1130
1209
|
} catch (err) {
|
|
1131
1210
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1132
|
-
return result(
|
|
1211
|
+
return result(
|
|
1212
|
+
`Search failed: ${msg}`,
|
|
1213
|
+
{ action: "search", status: "error" },
|
|
1214
|
+
true,
|
|
1215
|
+
);
|
|
1133
1216
|
}
|
|
1134
1217
|
}
|
|
1135
1218
|
case "schedule":
|
|
@@ -1137,7 +1220,10 @@ export async function handleTeamTool(
|
|
|
1137
1220
|
case "scheduled":
|
|
1138
1221
|
return handleListScheduled(params, ctx);
|
|
1139
1222
|
case "anchor": {
|
|
1140
|
-
const subAction =
|
|
1223
|
+
const subAction =
|
|
1224
|
+
typeof params.config?.subAction === "string"
|
|
1225
|
+
? params.config.subAction
|
|
1226
|
+
: "status";
|
|
1141
1227
|
switch (subAction) {
|
|
1142
1228
|
case "set":
|
|
1143
1229
|
return handleAnchorSet(params, ctx);
|
|
@@ -1151,7 +1237,12 @@ export async function handleTeamTool(
|
|
|
1151
1237
|
}
|
|
1152
1238
|
case "auto-summarize":
|
|
1153
1239
|
case "auto_boomerang": {
|
|
1154
|
-
const subAction =
|
|
1240
|
+
const subAction =
|
|
1241
|
+
typeof params.config?.subAction === "string"
|
|
1242
|
+
? params.config.subAction
|
|
1243
|
+
: (params.action as string) === "auto_boomerang"
|
|
1244
|
+
? "toggle"
|
|
1245
|
+
: "status";
|
|
1155
1246
|
switch (subAction) {
|
|
1156
1247
|
case "on":
|
|
1157
1248
|
return handleAutoSummarizeOn(params, ctx);
|
|
@@ -1178,7 +1269,14 @@ export async function handleTeamTool(
|
|
|
1178
1269
|
}
|
|
1179
1270
|
case "explain": {
|
|
1180
1271
|
const explainResult = handleExplain(params, ctx.cwd);
|
|
1181
|
-
return result(
|
|
1272
|
+
return result(
|
|
1273
|
+
explainResult.text,
|
|
1274
|
+
{
|
|
1275
|
+
action: "explain",
|
|
1276
|
+
status: explainResult.isError ? "error" : "ok",
|
|
1277
|
+
},
|
|
1278
|
+
explainResult.isError,
|
|
1279
|
+
);
|
|
1182
1280
|
}
|
|
1183
1281
|
case "cache": {
|
|
1184
1282
|
if (params.goal) {
|
|
@@ -1192,10 +1290,24 @@ export async function handleTeamTool(
|
|
|
1192
1290
|
if (cached) {
|
|
1193
1291
|
return result(
|
|
1194
1292
|
`Cached run found (${new Date(cached.cachedAt).toISOString()}): runId=${cached.runId}, status=${cached.status}, ${cached.tasks.length} tasks`,
|
|
1195
|
-
{
|
|
1293
|
+
{
|
|
1294
|
+
action: "cache",
|
|
1295
|
+
status: "ok",
|
|
1296
|
+
data: {
|
|
1297
|
+
cacheKey: key,
|
|
1298
|
+
cacheHit: true,
|
|
1299
|
+
runId: cached.runId,
|
|
1300
|
+
status: cached.status,
|
|
1301
|
+
taskCount: cached.tasks.length,
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1196
1304
|
);
|
|
1197
1305
|
}
|
|
1198
|
-
return result(`No cached result for key: ${key}`, {
|
|
1306
|
+
return result(`No cached result for key: ${key}`, {
|
|
1307
|
+
action: "cache",
|
|
1308
|
+
status: "ok",
|
|
1309
|
+
data: { cacheKey: key, cacheHit: false },
|
|
1310
|
+
});
|
|
1199
1311
|
}
|
|
1200
1312
|
const stats = getCacheStats(ctx.cwd);
|
|
1201
1313
|
return result(
|
|
@@ -1205,13 +1317,28 @@ export async function handleTeamTool(
|
|
|
1205
1317
|
}
|
|
1206
1318
|
case "checkpoint": {
|
|
1207
1319
|
if (!params.runId || !params.taskId) {
|
|
1208
|
-
return result(
|
|
1320
|
+
return result(
|
|
1321
|
+
"Checkpoint requires runId and taskId.",
|
|
1322
|
+
{ action: "checkpoint", status: "error" },
|
|
1323
|
+
true,
|
|
1324
|
+
);
|
|
1209
1325
|
}
|
|
1210
|
-
|
|
1326
|
+
assertSafePathId("runId", params.runId);
|
|
1327
|
+
assertSafePathId("taskId", params.taskId);
|
|
1328
|
+
const stateRoot = path.join(
|
|
1329
|
+
projectCrewRoot(ctx.cwd),
|
|
1330
|
+
"state",
|
|
1331
|
+
"runs",
|
|
1332
|
+
params.runId,
|
|
1333
|
+
);
|
|
1211
1334
|
const store = new FileCheckpointStore(stateRoot);
|
|
1212
1335
|
const checkpoint = store.load(params.runId, params.taskId);
|
|
1213
1336
|
if (!checkpoint) {
|
|
1214
|
-
return result(
|
|
1337
|
+
return result(
|
|
1338
|
+
"No checkpoint found.",
|
|
1339
|
+
{ action: "checkpoint", status: "error" },
|
|
1340
|
+
true,
|
|
1341
|
+
);
|
|
1215
1342
|
}
|
|
1216
1343
|
return result(
|
|
1217
1344
|
`Checkpoint: step=${checkpoint.step}, progress=${checkpoint.progress}, savedAt=${new Date(checkpoint.savedAt).toISOString()}`,
|