pi-crew 0.1.28 → 0.1.30
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 +42 -0
- package/NOTICE.md +1 -0
- package/docs/architecture.md +164 -92
- package/docs/refactor-tasks-phase6.md +662 -0
- package/docs/runtime-flow.md +148 -0
- package/package.json +1 -1
- package/schema.json +1 -0
- package/skills/git-master/SKILL.md +19 -0
- package/skills/read-only-explorer/SKILL.md +21 -0
- package/skills/safe-bash/SKILL.md +16 -0
- package/skills/task-packet/SKILL.md +23 -0
- package/skills/verify-evidence/SKILL.md +22 -0
- package/src/config/config.ts +2 -0
- package/src/config/defaults.ts +1 -0
- package/src/extension/async-notifier.ts +33 -4
- package/src/extension/register.ts +15 -522
- package/src/extension/registration/artifact-cleanup.ts +14 -0
- package/src/extension/registration/commands.ts +208 -0
- package/src/extension/registration/subagent-helpers.ts +1 -1
- package/src/extension/registration/subagent-tools.ts +110 -0
- package/src/extension/registration/team-tool.ts +44 -0
- package/src/extension/team-tool/api.ts +4 -4
- package/src/extension/team-tool/cancel.ts +31 -0
- package/src/extension/team-tool/inspect.ts +41 -0
- package/src/extension/team-tool/lifecycle-actions.ts +79 -0
- package/src/extension/team-tool/plan.ts +19 -0
- package/src/extension/team-tool/run.ts +41 -3
- package/src/extension/team-tool/status.ts +73 -0
- package/src/extension/team-tool.ts +57 -224
- package/src/runtime/async-marker.ts +26 -0
- package/src/runtime/async-runner.ts +44 -9
- package/src/runtime/background-runner.ts +2 -0
- package/src/runtime/child-pi.ts +5 -1
- package/src/runtime/concurrency.ts +9 -3
- package/src/runtime/crew-agent-records.ts +1 -0
- package/src/runtime/crew-agent-runtime.ts +2 -1
- package/src/runtime/model-fallback.ts +21 -4
- package/src/runtime/pi-args.ts +2 -0
- package/src/runtime/process-status.ts +1 -0
- package/src/runtime/role-permission.ts +11 -0
- package/src/runtime/task-runner/live-executor.ts +98 -0
- package/src/runtime/task-runner/progress.ts +111 -0
- package/src/runtime/task-runner/prompt-builder.ts +72 -0
- package/src/runtime/task-runner/result-utils.ts +14 -0
- package/src/runtime/task-runner/state-helpers.ts +22 -0
- package/src/runtime/task-runner.ts +38 -283
- package/src/runtime/team-runner.ts +116 -7
- package/src/schema/config-schema.ts +1 -0
- package/src/state/mailbox.ts +28 -0
- package/src/state/types.ts +16 -0
- package/src/subagents/async-entry.ts +1 -0
- package/src/subagents/index.ts +3 -0
- package/src/subagents/live/control.ts +1 -0
- package/src/subagents/live/manager.ts +1 -0
- package/src/subagents/live/realtime.ts +1 -0
- package/src/subagents/live/session-runtime.ts +1 -0
- package/src/subagents/manager.ts +1 -0
- package/src/subagents/spawn.ts +1 -0
- package/src/ui/live-run-sidebar.ts +1 -1
|
@@ -4,18 +4,55 @@ import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workfl
|
|
|
4
4
|
import { loadConfig } from "../../config/config.ts";
|
|
5
5
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
6
6
|
import { writeArtifact } from "../../state/artifact-store.ts";
|
|
7
|
-
import { createRunManifest } from "../../state/state-store.ts";
|
|
7
|
+
import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
8
8
|
import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
9
9
|
import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
|
|
10
10
|
import { executeTeamRun } from "../../runtime/team-runner.ts";
|
|
11
|
-
import { spawnBackgroundTeamRun } from "../../
|
|
12
|
-
import { appendEvent } from "../../state/event-log.ts";
|
|
11
|
+
import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
|
|
12
|
+
import { appendEvent, readEvents } from "../../state/event-log.ts";
|
|
13
13
|
import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
|
|
14
14
|
import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
|
|
15
|
+
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
16
|
+
import { hasAsyncStartMarker } from "../../runtime/async-marker.ts";
|
|
17
|
+
import * as fs from "node:fs";
|
|
15
18
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
16
19
|
import { buildParentContext, result, type TeamContext } from "./context.ts";
|
|
17
20
|
import { effectiveRunConfig } from "./config-patch.ts";
|
|
18
21
|
|
|
22
|
+
function tailFile(filePath: string, maxBytes = 4096): string | undefined {
|
|
23
|
+
try {
|
|
24
|
+
const stat = fs.statSync(filePath);
|
|
25
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
26
|
+
const fd = fs.openSync(filePath, "r");
|
|
27
|
+
try {
|
|
28
|
+
const buffer = Buffer.alloc(stat.size - start);
|
|
29
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
30
|
+
return buffer.toString("utf-8").trim();
|
|
31
|
+
} finally {
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: number | undefined, logPath: string): void {
|
|
40
|
+
if (process.env.PI_CREW_ASYNC_EARLY_EXIT_GUARD === "0") return;
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
43
|
+
if (!loaded || !isActiveRunStatus(loaded.manifest.status)) return;
|
|
44
|
+
if (hasAsyncStartMarker(loaded.manifest)) return;
|
|
45
|
+
if (readEvents(loaded.manifest.eventsPath).some((event) => event.type === "async.started" || event.type === "async.completed" || event.type === "async.failed")) return;
|
|
46
|
+
const liveness = checkProcessLiveness(pid);
|
|
47
|
+
if (liveness.alive) return;
|
|
48
|
+
const tail = tailFile(logPath);
|
|
49
|
+
const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
|
|
50
|
+
const failed = updateRunStatus(loaded.manifest, "failed", "Background runner exited within 3s; see background.log");
|
|
51
|
+
appendEvent(failed.eventsPath, { type: "async.failed", runId: failed.runId, message, data: { pid, detail: liveness.detail } });
|
|
52
|
+
}, 3000);
|
|
53
|
+
timer.unref?.();
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
20
57
|
const goal = params.goal ?? params.task;
|
|
21
58
|
if (!goal) return result("Run requires goal or task.", { action: "run", status: "error" }, true);
|
|
@@ -75,6 +112,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
75
112
|
const asyncManifest = { ...updatedManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
|
|
76
113
|
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
77
114
|
appendEvent(updatedManifest.eventsPath, { type: "async.spawned", runId: updatedManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
115
|
+
scheduleBackgroundEarlyExitGuard(ctx.cwd, updatedManifest.runId, spawned.pid, spawned.logPath);
|
|
78
116
|
const text = [
|
|
79
117
|
`Started async pi-crew run ${updatedManifest.runId}.`,
|
|
80
118
|
`Team: ${team.name}`,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { loadConfig } from "../../config/config.ts";
|
|
2
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
|
+
import { appendEvent, readEvents } from "../../state/event-log.ts";
|
|
4
|
+
import { loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
5
|
+
import { aggregateUsage, formatUsage } from "../../state/usage.ts";
|
|
6
|
+
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../../runtime/agent-control.ts";
|
|
7
|
+
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
8
|
+
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
9
|
+
import { formatTaskGraphLines, waitingReason } from "../../runtime/task-display.ts";
|
|
10
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
|
+
import { result, type TeamContext } from "./context.ts";
|
|
12
|
+
|
|
13
|
+
export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
14
|
+
if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
|
|
15
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
16
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
|
|
17
|
+
let { manifest, tasks } = loaded;
|
|
18
|
+
let asyncLivenessLine: string | undefined;
|
|
19
|
+
if (manifest.async) {
|
|
20
|
+
const asyncState = manifest.async;
|
|
21
|
+
const liveness = checkProcessLiveness(asyncState.pid);
|
|
22
|
+
asyncLivenessLine = `Async: pid=${asyncState.pid ?? "unknown"} alive=${liveness.alive ? "true" : "false"} detail=${liveness.detail} log=${asyncState.logPath} spawnedAt=${asyncState.spawnedAt}`;
|
|
23
|
+
if (!liveness.alive && isActiveRunStatus(manifest.status)) {
|
|
24
|
+
manifest = updateRunStatus(manifest, "failed", `Async process stale: ${liveness.detail}`);
|
|
25
|
+
appendEvent(manifest.eventsPath, { type: "async.stale", runId: manifest.runId, message: liveness.detail, data: { pid: asyncState.pid } });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const counts = new Map<string, number>();
|
|
29
|
+
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
30
|
+
const events = readEvents(manifest.eventsPath).slice(-8);
|
|
31
|
+
const controlConfig = resolveCrewControlConfig(loadConfig(ctx.cwd).config);
|
|
32
|
+
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
33
|
+
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
34
|
+
const totalUsage = aggregateUsage(tasks);
|
|
35
|
+
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
36
|
+
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
37
|
+
const waitingTasks = tasks.filter((task) => task.status === "queued");
|
|
38
|
+
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
39
|
+
const lines = [
|
|
40
|
+
`Run: ${manifest.runId}`,
|
|
41
|
+
`Team: ${manifest.team}`,
|
|
42
|
+
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
43
|
+
`Status: ${manifest.status}`,
|
|
44
|
+
`Workspace mode: ${manifest.workspaceMode}`,
|
|
45
|
+
`Goal: ${manifest.goal}`,
|
|
46
|
+
`Created: ${manifest.createdAt}`,
|
|
47
|
+
`Updated: ${manifest.updatedAt}`,
|
|
48
|
+
`State: ${manifest.stateRoot}`,
|
|
49
|
+
`Artifacts: ${manifest.artifactsRoot}`,
|
|
50
|
+
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|
|
51
|
+
"Task graph:",
|
|
52
|
+
...formatTaskGraphLines(tasks),
|
|
53
|
+
"Tasks:",
|
|
54
|
+
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
55
|
+
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
56
|
+
"Active agents:",
|
|
57
|
+
...(activeAgents.length ? activeAgents.map(agentLine) : ["- (none)"]),
|
|
58
|
+
"Waiting tasks:",
|
|
59
|
+
...(waitingTasks.length ? waitingTasks.map((task) => `- ${task.id} [queued] ${task.role} -> ${task.agent} ${waitingReason(task, tasks) ?? "waiting"}`) : ["- (none)"]),
|
|
60
|
+
"Completed agents:",
|
|
61
|
+
...(completedAgents.length ? completedAgents.map(agentLine) : ["- (none)"]),
|
|
62
|
+
"Policy decisions:",
|
|
63
|
+
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
64
|
+
`Total usage: ${formatUsage(totalUsage)}`,
|
|
65
|
+
"",
|
|
66
|
+
"Recent artifacts:",
|
|
67
|
+
...(artifactLines.length ? artifactLines : ["- (none)"]),
|
|
68
|
+
"",
|
|
69
|
+
"Recent events:",
|
|
70
|
+
...(events.length ? events.map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`) : ["- (none)"]),
|
|
71
|
+
];
|
|
72
|
+
return result(lines.join("\n"), { action: "status", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
|
|
73
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
3
4
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
4
5
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
5
6
|
import { loadConfig, updateAutonomousConfig, updateConfig } from "../config/config.ts";
|
|
6
7
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
7
|
-
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
8
|
+
import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
8
9
|
import { withRunLock, withRunLockSync } from "../state/locks.ts";
|
|
9
10
|
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
10
11
|
import { appendEvent, readEvents } from "../state/event-log.ts";
|
|
12
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
13
|
+
import { replayPendingMailboxMessages } from "../state/mailbox.ts";
|
|
11
14
|
import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
|
|
12
15
|
import { piTeamsHelp } from "./help.ts";
|
|
13
16
|
import { initializeProject } from "./project-init.ts";
|
|
@@ -21,6 +24,7 @@ import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
|
|
|
21
24
|
import { formatValidationReport, validateResources } from "./validate-resources.ts";
|
|
22
25
|
import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
|
|
23
26
|
import type { PiTeamsToolResult } from "./tool-result.ts";
|
|
27
|
+
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
24
28
|
import { executeTeamRun } from "../runtime/team-runner.ts";
|
|
25
29
|
import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
|
|
26
30
|
import { saveCrewAgents, readCrewAgents, recordFromTask } from "../runtime/crew-agent-records.ts";
|
|
@@ -29,17 +33,28 @@ import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from
|
|
|
29
33
|
import { writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
|
|
30
34
|
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
31
35
|
import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
|
|
36
|
+
import { parsePiJsonOutput } from "../runtime/pi-json-output.ts";
|
|
32
37
|
import { buildParentContext, configRecord, formatScoped, result, type TeamContext } from "./team-tool/context.ts";
|
|
33
38
|
import { autonomousPatchFromConfig, configPatchFromConfig, formatAutonomyStatus } from "./team-tool/config-patch.ts";
|
|
34
39
|
import { handleApi } from "./team-tool/api.ts";
|
|
35
40
|
import { handleRun } from "./team-tool/run.ts";
|
|
36
41
|
import { handleDoctor } from "./team-tool/doctor.ts";
|
|
42
|
+
import { handleStatus } from "./team-tool/status.ts";
|
|
43
|
+
import { handleArtifacts, handleEvents, handleSummary } from "./team-tool/inspect.ts";
|
|
44
|
+
import { handleCleanup, handleExport, handleForget, handleImport, handleImports, handlePrune, handleWorktrees } from "./team-tool/lifecycle-actions.ts";
|
|
45
|
+
import { handleCancel } from "./team-tool/cancel.ts";
|
|
46
|
+
import { handlePlan } from "./team-tool/plan.ts";
|
|
37
47
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
38
48
|
|
|
39
49
|
export type { TeamToolDetails } from "./team-tool-types.ts";
|
|
40
50
|
export type { TeamContext } from "./team-tool/context.ts";
|
|
41
51
|
export { handleRun } from "./team-tool/run.ts";
|
|
42
52
|
export { handleDoctor } from "./team-tool/doctor.ts";
|
|
53
|
+
export { handleStatus } from "./team-tool/status.ts";
|
|
54
|
+
export { handleArtifacts, handleEvents, handleSummary } from "./team-tool/inspect.ts";
|
|
55
|
+
export { handleCleanup, handleExport, handleForget, handleImport, handleImports, handlePrune, handleWorktrees } from "./team-tool/lifecycle-actions.ts";
|
|
56
|
+
export { handleCancel } from "./team-tool/cancel.ts";
|
|
57
|
+
export { handlePlan } from "./team-tool/plan.ts";
|
|
43
58
|
export { handleApi } from "./team-tool/api.ts";
|
|
44
59
|
|
|
45
60
|
export function handleList(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
@@ -108,111 +123,43 @@ export function handleGet(params: TeamToolParamsValue, ctx: TeamContext): PiTeam
|
|
|
108
123
|
return result("Specify team, workflow, or agent for get.", { action: "get", status: "error" }, true);
|
|
109
124
|
}
|
|
110
125
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
114
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
|
|
115
|
-
let { manifest, tasks } = loaded;
|
|
116
|
-
let asyncLivenessLine: string | undefined;
|
|
117
|
-
if (manifest.async) {
|
|
118
|
-
const asyncState = manifest.async;
|
|
119
|
-
const liveness = checkProcessLiveness(asyncState.pid);
|
|
120
|
-
asyncLivenessLine = `Async: pid=${asyncState.pid ?? "unknown"} alive=${liveness.alive ? "true" : "false"} detail=${liveness.detail} log=${asyncState.logPath} spawnedAt=${asyncState.spawnedAt}`;
|
|
121
|
-
if (!liveness.alive && isActiveRunStatus(manifest.status)) {
|
|
122
|
-
manifest = updateRunStatus(manifest, "failed", `Async process stale: ${liveness.detail}`);
|
|
123
|
-
appendEvent(manifest.eventsPath, { type: "async.stale", runId: manifest.runId, message: liveness.detail, data: { pid: asyncState.pid } });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const counts = new Map<string, number>();
|
|
127
|
-
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
128
|
-
const events = readEvents(manifest.eventsPath).slice(-8);
|
|
129
|
-
const controlConfig = resolveCrewControlConfig(loadConfig(ctx.cwd).config);
|
|
130
|
-
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
131
|
-
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
132
|
-
const totalUsage = aggregateUsage(tasks);
|
|
133
|
-
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
134
|
-
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
135
|
-
const waitingTasks = tasks.filter((task) => task.status === "queued");
|
|
136
|
-
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
137
|
-
const lines = [
|
|
138
|
-
`Run: ${manifest.runId}`,
|
|
139
|
-
`Team: ${manifest.team}`,
|
|
140
|
-
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
141
|
-
`Status: ${manifest.status}`,
|
|
142
|
-
`Workspace mode: ${manifest.workspaceMode}`,
|
|
143
|
-
`Goal: ${manifest.goal}`,
|
|
144
|
-
`Created: ${manifest.createdAt}`,
|
|
145
|
-
`Updated: ${manifest.updatedAt}`,
|
|
146
|
-
`State: ${manifest.stateRoot}`,
|
|
147
|
-
`Artifacts: ${manifest.artifactsRoot}`,
|
|
148
|
-
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|
|
149
|
-
"Task graph:",
|
|
150
|
-
...formatTaskGraphLines(tasks),
|
|
151
|
-
"Tasks:",
|
|
152
|
-
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
153
|
-
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
154
|
-
"Active agents:",
|
|
155
|
-
...(activeAgents.length ? activeAgents.map(agentLine) : ["- (none)"]),
|
|
156
|
-
"Waiting tasks:",
|
|
157
|
-
...(waitingTasks.length ? waitingTasks.map((task) => `- ${task.id} [queued] ${task.role} -> ${task.agent} ${waitingReason(task, tasks) ?? "waiting"}`) : ["- (none)"]),
|
|
158
|
-
"Completed agents:",
|
|
159
|
-
...(completedAgents.length ? completedAgents.map(agentLine) : ["- (none)"]),
|
|
160
|
-
"Policy decisions:",
|
|
161
|
-
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
162
|
-
`Total usage: ${formatUsage(totalUsage)}`,
|
|
163
|
-
"",
|
|
164
|
-
"Recent artifacts:",
|
|
165
|
-
...(artifactLines.length ? artifactLines : ["- (none)"]),
|
|
166
|
-
"",
|
|
167
|
-
"Recent events:",
|
|
168
|
-
...(events.length ? events.map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`) : ["- (none)"]),
|
|
169
|
-
];
|
|
170
|
-
return result(lines.join("\n"), { action: "status", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
174
|
-
const teamName = params.team ?? "default";
|
|
175
|
-
const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === teamName);
|
|
176
|
-
if (!team) return result(`Team '${teamName}' not found.`, { action: "plan", status: "error" }, true);
|
|
177
|
-
const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
|
|
178
|
-
const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === workflowName);
|
|
179
|
-
if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "plan", status: "error" }, true);
|
|
180
|
-
const errors = validateWorkflowForTeam(workflow, team);
|
|
181
|
-
if (errors.length > 0) return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...errors.map((error) => `- ${error}`)].join("\n"), { action: "plan", status: "error" }, true);
|
|
182
|
-
const lines = [
|
|
183
|
-
`Team plan: ${team.name}`,
|
|
184
|
-
`Workflow: ${workflow.name}`,
|
|
185
|
-
`Goal: ${params.goal ?? params.task ?? "(not provided)"}`,
|
|
186
|
-
"",
|
|
187
|
-
"Steps:",
|
|
188
|
-
...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`),
|
|
189
|
-
];
|
|
190
|
-
return result(lines.join("\n"), { action: "plan", status: "ok" });
|
|
126
|
+
function artifactKey(artifact: ArtifactDescriptor): string {
|
|
127
|
+
return `${artifact.kind}:${artifact.path}`;
|
|
191
128
|
}
|
|
192
129
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
202
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
203
|
-
try {
|
|
204
|
-
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
205
|
-
} catch (error) {
|
|
206
|
-
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
130
|
+
function recoverCheckpointedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): { manifest: TeamRunManifest; tasks: TeamTaskState[]; recovered: string[] } {
|
|
131
|
+
const recovered: string[] = [];
|
|
132
|
+
let nextManifest = manifest;
|
|
133
|
+
let nextTasks = tasks.map((task) => {
|
|
134
|
+
if (task.status !== "running" || !task.checkpoint) return task;
|
|
135
|
+
if (task.checkpoint.phase === "artifact-written" && task.resultArtifact) {
|
|
136
|
+
recovered.push(task.id);
|
|
137
|
+
return { ...task, status: "completed" as const, finishedAt: task.finishedAt ?? task.checkpoint.updatedAt, error: undefined, claim: undefined };
|
|
207
138
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
139
|
+
if (task.checkpoint.phase === "child-stdout-final") {
|
|
140
|
+
const transcriptPath = path.join(manifest.artifactsRoot, "transcripts", `${task.id}.jsonl`);
|
|
141
|
+
if (!fs.existsSync(transcriptPath)) return task;
|
|
142
|
+
const transcript = fs.readFileSync(transcriptPath, "utf-8");
|
|
143
|
+
const parsed = parsePiJsonOutput(transcript);
|
|
144
|
+
if (!parsed.finalText && !parsed.usage) return task;
|
|
145
|
+
const resultArtifact = writeArtifact(manifest.artifactsRoot, { kind: "result", relativePath: `results/${task.id}.txt`, content: parsed.finalText ?? "(recovered from completed child transcript)", producer: task.id });
|
|
146
|
+
const transcriptArtifact = writeArtifact(manifest.artifactsRoot, { kind: "log", relativePath: `transcripts/${task.id}.jsonl`, content: transcript, producer: task.id });
|
|
147
|
+
recovered.push(task.id);
|
|
148
|
+
return { ...task, status: "completed" as const, finishedAt: task.finishedAt ?? task.checkpoint.updatedAt, error: undefined, claim: undefined, resultArtifact, transcriptArtifact, usage: parsed.usage, jsonEvents: parsed.jsonEvents };
|
|
212
149
|
}
|
|
213
|
-
|
|
214
|
-
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
150
|
+
return task;
|
|
215
151
|
});
|
|
152
|
+
if (recovered.length) {
|
|
153
|
+
const artifacts = new Map(nextManifest.artifacts.map((artifact) => [artifactKey(artifact), artifact]));
|
|
154
|
+
for (const task of nextTasks) {
|
|
155
|
+
if (!recovered.includes(task.id)) continue;
|
|
156
|
+
for (const artifact of [task.promptArtifact, task.resultArtifact, task.logArtifact, task.transcriptArtifact].filter(Boolean) as ArtifactDescriptor[]) artifacts.set(artifactKey(artifact), artifact);
|
|
157
|
+
}
|
|
158
|
+
nextManifest = { ...nextManifest, artifacts: [...artifacts.values()], updatedAt: new Date().toISOString() };
|
|
159
|
+
saveRunManifest(nextManifest);
|
|
160
|
+
saveRunTasks(nextManifest, nextTasks);
|
|
161
|
+
}
|
|
162
|
+
return { manifest: nextManifest, tasks: nextTasks, recovered };
|
|
216
163
|
}
|
|
217
164
|
|
|
218
165
|
export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
@@ -227,136 +174,22 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
227
174
|
const workflow = direct?.workflow ?? allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
|
|
228
175
|
if (!workflow) return result(`Workflow '${loaded.manifest.workflow}' not found.`, { action: "resume", status: "error" }, true);
|
|
229
176
|
return await withRunLock(loaded.manifest, async () => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
177
|
+
const recovered = recoverCheckpointedTasks(loaded.manifest, loaded.tasks);
|
|
178
|
+
const resumeManifest = recovered.manifest;
|
|
179
|
+
const resetTasks = recovered.tasks.map((task) => task.status === "failed" || task.status === "cancelled" || task.status === "skipped" || task.status === "running" ? { ...task, status: "queued" as const, error: undefined, startedAt: undefined, finishedAt: undefined, claim: undefined } : task);
|
|
180
|
+
saveRunTasks(resumeManifest, resetTasks);
|
|
181
|
+
const replay = replayPendingMailboxMessages(resumeManifest);
|
|
182
|
+
appendEvent(resumeManifest.eventsPath, { type: "run.resume_requested", runId: resumeManifest.runId, data: { replayedMailboxMessages: replay.messages.length, recoveredCheckpointTasks: recovered.recovered } });
|
|
183
|
+
if (recovered.recovered.length) appendEvent(resumeManifest.eventsPath, { type: "task.checkpoint_recovered", runId: resumeManifest.runId, message: `Recovered ${recovered.recovered.length} task(s) from artifact-written checkpoints.`, data: { taskIds: recovered.recovered } });
|
|
184
|
+
if (replay.messages.length) appendEvent(resumeManifest.eventsPath, { type: "mailbox.replayed", runId: resumeManifest.runId, message: `Replayed ${replay.messages.length} pending inbox message(s).`, data: { messageIds: replay.messages.map((message) => message.id), taskIds: replay.messages.map((message) => message.taskId).filter(Boolean) } });
|
|
233
185
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
234
186
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
235
187
|
const executeWorkers = runtime.kind !== "scaffold";
|
|
236
|
-
const executed = await executeTeamRun({ manifest:
|
|
188
|
+
const executed = await executeTeamRun({ manifest: resumeManifest, tasks: resetTasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
237
189
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
238
190
|
});
|
|
239
191
|
}
|
|
240
192
|
|
|
241
|
-
export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
242
|
-
if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
|
|
243
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
244
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
245
|
-
const events = readEvents(loaded.manifest.eventsPath);
|
|
246
|
-
const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
|
|
247
|
-
return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
251
|
-
if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
|
|
252
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
253
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
254
|
-
const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
|
|
255
|
-
return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
259
|
-
if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
|
|
260
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
261
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
262
|
-
const usage = aggregateUsage(loaded.tasks);
|
|
263
|
-
const lines = [
|
|
264
|
-
`Summary for ${loaded.manifest.runId}`,
|
|
265
|
-
`Status: ${loaded.manifest.status}`,
|
|
266
|
-
`Team: ${loaded.manifest.team}`,
|
|
267
|
-
`Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
|
|
268
|
-
`Goal: ${loaded.manifest.goal}`,
|
|
269
|
-
`Usage: ${formatUsage(usage)}`,
|
|
270
|
-
"Tasks:",
|
|
271
|
-
...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
272
|
-
];
|
|
273
|
-
return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
277
|
-
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
278
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
279
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
|
|
280
|
-
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
281
|
-
const lines = [
|
|
282
|
-
`Worktrees for ${loaded.manifest.runId}:`,
|
|
283
|
-
...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"]),
|
|
284
|
-
];
|
|
285
|
-
return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
289
|
-
const imports = listImportedRuns(ctx.cwd);
|
|
290
|
-
const lines = [
|
|
291
|
-
"Imported pi-crew runs:",
|
|
292
|
-
...(imports.length ? imports.map((entry) => `- ${entry.runId} (${entry.scope})${entry.status ? ` [${entry.status}]` : ""} ${entry.team ?? "unknown"}/${entry.workflow ?? "none"}: ${entry.goal ?? ""}\n Bundle: ${entry.bundlePath}\n Summary: ${entry.summaryPath}`) : ["- (none)"]),
|
|
293
|
-
];
|
|
294
|
-
return result(lines.join("\n"), { action: "imports", status: "ok" });
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
298
|
-
const cfg = configRecord(params.config);
|
|
299
|
-
const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
|
|
300
|
-
if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
|
|
301
|
-
const scope = cfg.scope === "user" ? "user" : "project";
|
|
302
|
-
try {
|
|
303
|
-
const imported = importRunBundle(ctx.cwd, bundlePath, scope);
|
|
304
|
-
return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
|
|
305
|
-
} catch (error) {
|
|
306
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
307
|
-
return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
312
|
-
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
313
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
314
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
|
|
315
|
-
const exported = exportRunBundle(loaded.manifest, loaded.tasks);
|
|
316
|
-
appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
|
|
317
|
-
return result([`Exported run ${loaded.manifest.runId}.`, `JSON: ${exported.jsonPath}`, `Markdown: ${exported.markdownPath}`].join("\n"), { action: "export", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
321
|
-
const keep = params.keep ?? 20;
|
|
322
|
-
if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
|
|
323
|
-
if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
|
|
324
|
-
const pruned = pruneFinishedRuns(ctx.cwd, keep);
|
|
325
|
-
return result([`Pruned finished pi-crew runs.`, `Kept: ${pruned.kept.length}`, `Removed: ${pruned.removed.length}`, ...(pruned.removed.length ? ["Removed runs:", ...pruned.removed.map((runId) => `- ${runId}`)] : [])].join("\n"), { action: "prune", status: "ok" });
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
329
|
-
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
330
|
-
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
331
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
332
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
333
|
-
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
334
|
-
if (cleanup.preserved.length > 0 && !params.force) {
|
|
335
|
-
return result([`Run '${params.runId}' has preserved worktrees. Use force: true to forget anyway.`, ...cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`)].join("\n"), { action: "forget", status: "error", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, true);
|
|
336
|
-
}
|
|
337
|
-
fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
|
|
338
|
-
fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
|
|
339
|
-
return result([`Forgot run ${loaded.manifest.runId}.`, `Removed state: ${loaded.manifest.stateRoot}`, `Removed artifacts: ${loaded.manifest.artifactsRoot}`, ...(cleanup.removed.length ? ["Removed worktrees:", ...cleanup.removed.map((item) => `- ${item}`)] : [])].join("\n"), { action: "forget", status: "ok", runId: loaded.manifest.runId });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
343
|
-
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
344
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
345
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
346
|
-
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
347
|
-
appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths } });
|
|
348
|
-
const lines = [
|
|
349
|
-
`Worktree cleanup for ${loaded.manifest.runId}:`,
|
|
350
|
-
"Removed:",
|
|
351
|
-
...(cleanup.removed.length ? cleanup.removed.map((item) => `- ${item}`) : ["- (none)"]),
|
|
352
|
-
"Preserved:",
|
|
353
|
-
...(cleanup.preserved.length ? cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`) : ["- (none)"]),
|
|
354
|
-
"Artifacts:",
|
|
355
|
-
...(cleanup.artifactPaths.length ? cleanup.artifactPaths.map((item) => `- ${item}`) : ["- (none)"]),
|
|
356
|
-
];
|
|
357
|
-
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
358
|
-
}
|
|
359
|
-
|
|
360
193
|
export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
361
194
|
const action = params.action ?? "list";
|
|
362
195
|
switch (action) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { atomicWriteJson } from "../state/atomic-write.ts";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
|
|
6
|
+
export interface AsyncStartMarker {
|
|
7
|
+
pid: number;
|
|
8
|
+
startedAt: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function asyncStartMarkerPath(manifest: Pick<TeamRunManifest, "stateRoot">): string {
|
|
12
|
+
return path.join(manifest.stateRoot, "async.pid");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function writeAsyncStartMarker(manifest: Pick<TeamRunManifest, "stateRoot">, marker: AsyncStartMarker): void {
|
|
16
|
+
atomicWriteJson(asyncStartMarkerPath(manifest), marker);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasAsyncStartMarker(manifest: Pick<TeamRunManifest, "stateRoot">): boolean {
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(asyncStartMarkerPath(manifest), "utf-8")) as Partial<AsyncStartMarker>;
|
|
22
|
+
return typeof raw.pid === "number" && Number.isInteger(raw.pid) && raw.pid > 0 && typeof raw.startedAt === "string" && raw.startedAt.length > 0;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { spawn, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
5
7
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
6
8
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
export type FileExists = (filePath: string) => boolean;
|
|
10
|
+
|
|
11
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
function packageRootFromRuntime(): string {
|
|
14
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function jitiRegisterPathFromPackageJson(packageJsonPath: string): string {
|
|
18
|
+
return path.join(path.dirname(packageJsonPath), "lib", "jiti-register.mjs");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveJitiRegisterPath(packageRoot = packageRootFromRuntime(), exists: FileExists = fs.existsSync): string | undefined {
|
|
22
|
+
const candidates = [
|
|
23
|
+
path.join(packageRoot, "node_modules", "jiti", "lib", "jiti-register.mjs"),
|
|
24
|
+
path.join(packageRoot, "..", "..", "node_modules", "jiti", "lib", "jiti-register.mjs"),
|
|
25
|
+
];
|
|
26
|
+
try {
|
|
27
|
+
candidates.push(jitiRegisterPathFromPackageJson(requireFromHere.resolve("jiti/package.json")));
|
|
28
|
+
} catch {
|
|
29
|
+
// Fall through to explicit candidate checks.
|
|
30
|
+
}
|
|
31
|
+
return [...new Set(candidates)].find((candidate) => exists(candidate));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getBackgroundRunnerCommand(runnerPath: string, cwd: string, runId: string, jitiRegisterPath: string | false | undefined = resolveJitiRegisterPath()): { args: string[]; loader: "jiti" } {
|
|
35
|
+
if (!jitiRegisterPath) throw new Error("pi-crew background runner cannot start: jiti loader not found. Reinstall pi-crew (`pi install npm:pi-crew`) or ensure node_modules/jiti is present.");
|
|
10
36
|
return {
|
|
11
37
|
args: ["--import", pathToFileURL(jitiRegisterPath).href, runnerPath, "--cwd", cwd, "--run-id", runId],
|
|
12
38
|
loader: "jiti",
|
|
@@ -33,10 +59,19 @@ export function spawnBackgroundTeamRun(manifest: TeamRunManifest): SpawnBackgrou
|
|
|
33
59
|
const logPath = path.join(manifest.stateRoot, "background.log");
|
|
34
60
|
fs.mkdirSync(manifest.stateRoot, { recursive: true });
|
|
35
61
|
const logFd = fs.openSync(logPath, "a");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
try {
|
|
63
|
+
const jitiRegisterPath = resolveJitiRegisterPath();
|
|
64
|
+
if (!jitiRegisterPath) {
|
|
65
|
+
const message = "pi-crew background runner cannot start: jiti loader not found. Reinstall pi-crew (`pi install npm:pi-crew`) or ensure node_modules/jiti is present.";
|
|
66
|
+
appendEvent(manifest.eventsPath, { type: "async.failed", runId: manifest.runId, message });
|
|
67
|
+
throw new Error(message);
|
|
68
|
+
}
|
|
69
|
+
const command = getBackgroundRunnerCommand(runnerPath, manifest.cwd, manifest.runId, jitiRegisterPath);
|
|
70
|
+
fs.appendFileSync(logPath, `[pi-crew] background loader=${command.loader}\n`, "utf-8");
|
|
71
|
+
const child = spawn(process.execPath, command.args, buildBackgroundSpawnOptions(manifest, logFd));
|
|
72
|
+
child.unref();
|
|
73
|
+
return { pid: child.pid, logPath };
|
|
74
|
+
} finally {
|
|
75
|
+
fs.closeSync(logFd);
|
|
76
|
+
}
|
|
42
77
|
}
|
|
@@ -8,6 +8,7 @@ import { executeTeamRun } from "./team-runner.ts";
|
|
|
8
8
|
import { resolveCrewRuntime } from "./runtime-resolver.ts";
|
|
9
9
|
import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
|
|
10
10
|
import { expandParallelResearchWorkflow } from "./parallel-research.ts";
|
|
11
|
+
import { writeAsyncStartMarker } from "./async-marker.ts";
|
|
11
12
|
|
|
12
13
|
function argValue(name: string): string | undefined {
|
|
13
14
|
const index = process.argv.indexOf(name);
|
|
@@ -24,6 +25,7 @@ async function main(): Promise<void> {
|
|
|
24
25
|
if (!loaded) throw new Error(`Run '${runId}' not found.`);
|
|
25
26
|
let { manifest, tasks } = loaded;
|
|
26
27
|
appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
|
|
28
|
+
writeAsyncStartMarker(manifest, { pid: process.pid, startedAt: new Date().toISOString() });
|
|
27
29
|
|
|
28
30
|
try {
|
|
29
31
|
const agents = allAgents(discoverAgents(cwd));
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -91,6 +91,7 @@ export interface ChildPiRunInput {
|
|
|
91
91
|
transcriptPath?: string;
|
|
92
92
|
onStdoutLine?: (line: string) => void;
|
|
93
93
|
onJsonEvent?: (event: unknown) => void;
|
|
94
|
+
onSpawn?: (pid: number) => void;
|
|
94
95
|
maxDepth?: number;
|
|
95
96
|
finalDrainMs?: number;
|
|
96
97
|
hardKillMs?: number;
|
|
@@ -286,7 +287,10 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
286
287
|
try {
|
|
287
288
|
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
288
289
|
const child = spawn(spawnSpec.command, spawnSpec.args, buildChildPiSpawnOptions(input.cwd, { ...process.env, ...built.env }));
|
|
289
|
-
if (child.pid)
|
|
290
|
+
if (child.pid) {
|
|
291
|
+
activeChildProcesses.set(child.pid, child);
|
|
292
|
+
input.onSpawn?.(child.pid);
|
|
293
|
+
}
|
|
290
294
|
let stdout = "";
|
|
291
295
|
let stderr = "";
|
|
292
296
|
let settled = false;
|