pi-crew 0.1.45 → 0.1.49
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 +97 -0
- package/README.md +5 -5
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/next-upgrade-roadmap.md +808 -0
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
- package/docs/research/AUDIT_OH_MY_PI.md +261 -0
- package/docs/research/AUDIT_PI_CREW.md +457 -0
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
- package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
- package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-oh-my-pi-distillation.md +369 -0
- package/docs/source-runtime-refactor-map.md +24 -0
- package/docs/usage.md +3 -3
- package/install.mjs +52 -8
- package/package.json +99 -98
- package/schema.json +10 -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/orchestration/SKILL.md +157 -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-config.ts +6 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -34
- package/src/agents/discover-agents.ts +29 -15
- package/src/config/config.ts +72 -24
- package/src/config/defaults.ts +25 -0
- package/src/extension/autonomous-policy.ts +26 -33
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +5 -0
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +69 -22
- package/src/extension/registration/commands.ts +64 -25
- package/src/extension/registration/compaction-guard.ts +1 -1
- 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 +14 -10
- package/src/extension/run-index.ts +35 -21
- package/src/extension/run-maintenance.ts +30 -5
- package/src/extension/team-tool/api.ts +47 -9
- package/src/extension/team-tool/cancel.ts +109 -5
- package/src/extension/team-tool/context.ts +8 -0
- package/src/extension/team-tool/intent-policy.ts +42 -0
- package/src/extension/team-tool/lifecycle-actions.ts +120 -79
- package/src/extension/team-tool/parallel-dispatch.ts +156 -0
- package/src/extension/team-tool/respond.ts +46 -18
- package/src/extension/team-tool/run.ts +55 -12
- package/src/extension/team-tool/status.ts +13 -2
- package/src/extension/team-tool-types.ts +3 -0
- package/src/extension/team-tool.ts +45 -14
- package/src/hooks/registry.ts +61 -0
- package/src/hooks/types.ts +41 -0
- package/src/observability/event-to-metric.ts +8 -1
- package/src/runtime/agent-control.ts +169 -63
- package/src/runtime/async-runner.ts +3 -1
- package/src/runtime/background-runner.ts +78 -53
- package/src/runtime/cancellation-token.ts +89 -0
- package/src/runtime/cancellation.ts +61 -0
- package/src/runtime/capability-inventory.ts +116 -0
- package/src/runtime/child-pi.ts +458 -444
- package/src/runtime/code-summary.ts +247 -0
- package/src/runtime/crash-recovery.ts +182 -0
- package/src/runtime/crew-agent-records.ts +70 -10
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +48 -25
- package/src/runtime/effectiveness.ts +81 -0
- package/src/runtime/event-stream-bridge.ts +90 -0
- package/src/runtime/live-agent-control.ts +2 -1
- package/src/runtime/live-agent-manager.ts +179 -85
- package/src/runtime/live-control-realtime.ts +1 -1
- package/src/runtime/live-extension-bridge.ts +150 -0
- package/src/runtime/live-irc.ts +92 -0
- package/src/runtime/live-session-health.ts +100 -0
- package/src/runtime/live-session-runtime.ts +599 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/mcp-proxy.ts +113 -0
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/notebook-helpers.ts +90 -0
- package/src/runtime/orphan-sentinel.ts +7 -0
- package/src/runtime/output-validator.ts +187 -0
- package/src/runtime/parallel-utils.ts +57 -0
- package/src/runtime/parent-guard.ts +80 -0
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/prose-compressor.ts +164 -0
- package/src/runtime/result-extractor.ts +121 -0
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/runtime-resolver.ts +23 -10
- package/src/runtime/semaphore.ts +131 -0
- package/src/runtime/sensitive-paths.ts +92 -0
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/stream-preview.ts +177 -0
- package/src/runtime/subagent-manager.ts +6 -2
- package/src/runtime/subprocess-tool-registry.ts +67 -0
- package/src/runtime/task-output-context.ts +177 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +107 -101
- package/src/runtime/task-runner/prompt-builder.ts +72 -8
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/run-projection.ts +104 -0
- package/src/runtime/task-runner.ts +115 -5
- package/src/runtime/team-runner.ts +134 -19
- package/src/runtime/workspace-tree.ts +298 -0
- package/src/runtime/yield-handler.ts +189 -0
- package/src/schema/config-schema.ts +7 -0
- package/src/schema/team-tool-schema.ts +14 -4
- package/src/skills/discover-skills.ts +67 -0
- package/src/state/active-run-registry.ts +167 -0
- package/src/state/artifact-store.ts +4 -1
- package/src/state/atomic-write.ts +50 -1
- package/src/state/blob-store.ts +117 -0
- package/src/state/contracts.ts +2 -1
- package/src/state/event-log-rotation.ts +158 -0
- package/src/state/event-log.ts +52 -2
- package/src/state/mailbox.ts +129 -9
- package/src/state/state-store.ts +32 -5
- package/src/state/types.ts +64 -2
- package/src/teams/team-config.ts +1 -0
- package/src/ui/agent-management-overlay.ts +144 -0
- package/src/ui/crew-widget.ts +15 -5
- package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
- package/src/ui/dashboard-panes/capability-pane.ts +60 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/live-run-sidebar.ts +4 -0
- package/src/ui/powerbar-publisher.ts +77 -15
- package/src/ui/render-coalescer.ts +51 -0
- package/src/ui/run-dashboard.ts +4 -0
- package/src/ui/run-event-bus.ts +209 -0
- package/src/ui/run-snapshot-cache.ts +78 -18
- package/src/ui/snapshot-types.ts +10 -0
- package/src/ui/transcript-entries.ts +258 -0
- package/src/utils/ids.ts +5 -0
- package/src/utils/incremental-reader.ts +104 -0
- package/src/utils/paths.ts +4 -2
- package/src/utils/scan-cache.ts +137 -0
- package/src/utils/sse-parser.ts +134 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/visual.ts +33 -2
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/cleanup.ts +2 -1
|
@@ -5,7 +5,7 @@ import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } f
|
|
|
5
5
|
import { withRunLockSync } from "../../state/locks.ts";
|
|
6
6
|
import { canTransitionTaskStatus, isTeamTaskStatus } from "../../state/contracts.ts";
|
|
7
7
|
import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../state/task-claims.ts";
|
|
8
|
-
import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, readMailboxMessage, validateMailbox, type MailboxDirection } from "../../state/mailbox.ts";
|
|
8
|
+
import { acknowledgeMailboxMessage, appendFollowUpMessage, appendMailboxMessage, appendSteeringMessage, readDeliveryState, readMailbox, readMailboxMessage, validateMailbox, type MailboxDirection, type MailboxMessageKind } from "../../state/mailbox.ts";
|
|
9
9
|
import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log.ts";
|
|
10
10
|
import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
|
|
11
11
|
import { probeLiveSessionRuntime } from "../../subagents/live/session-runtime.ts";
|
|
@@ -14,9 +14,10 @@ import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
|
|
|
14
14
|
import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
15
15
|
import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
|
|
16
16
|
import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
17
|
-
import { getLiveAgent, listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
|
|
17
|
+
import { followUpLiveAgent, getLiveAgent, listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
|
|
18
18
|
import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
|
|
19
19
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
|
|
20
|
+
import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
|
|
20
21
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
21
22
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
22
23
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
@@ -76,6 +77,10 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
76
77
|
});
|
|
77
78
|
return result(JSON.stringify(filtered, null, 2), { action: "api", status: "ok", ...(runIdFilter ? { runId: runIdFilter } : {}) });
|
|
78
79
|
}
|
|
80
|
+
if (operation === "inventory") {
|
|
81
|
+
const inventory = buildCapabilityInventory(ctx.cwd, ctx.config);
|
|
82
|
+
return result(JSON.stringify(inventory, null, 2), { action: "api", status: "ok" });
|
|
83
|
+
}
|
|
79
84
|
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
80
85
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
81
86
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
@@ -212,7 +217,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
212
217
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
213
218
|
if (!agent) return result("API nudge-agent requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
214
219
|
const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
|
|
215
|
-
const message =
|
|
220
|
+
const message = appendSteeringMessage(loaded.manifest, { taskId: agent.taskId, to: agent.taskId, body: messageText, priority: "normal", data: { source: "nudge-agent" } });
|
|
216
221
|
appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
|
|
217
222
|
ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction: message.direction, from: message.from, to: message.to, taskId: message.taskId, source: "nudge-agent" });
|
|
218
223
|
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -220,7 +225,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
220
225
|
if (operation === "list-live-agents") {
|
|
221
226
|
return result(JSON.stringify(listLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
222
227
|
}
|
|
223
|
-
if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
|
|
228
|
+
if (operation === "steer-agent" || operation === "follow-up-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
|
|
224
229
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
225
230
|
if (!agentId) return result(`API ${operation} requires config.agentId.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
226
231
|
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
@@ -228,7 +233,22 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
228
233
|
try {
|
|
229
234
|
const live = getLiveAgent(agentId);
|
|
230
235
|
if (live && live.runId !== loaded.manifest.runId) return result(`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
231
|
-
if (operation === "steer-agent"
|
|
236
|
+
if (!live && (operation === "steer-agent" || operation === "follow-up-agent")) throw new Error(`Live agent '${agentId}' not found.`);
|
|
237
|
+
const liveTaskId = live?.taskId;
|
|
238
|
+
if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId) throw new Error(`Live agent '${agentId}' not found.`);
|
|
239
|
+
const targetTaskId = liveTaskId ?? agentId;
|
|
240
|
+
if (operation === "steer-agent") {
|
|
241
|
+
const text = message ?? "Please report current status and wrap up if possible.";
|
|
242
|
+
const realtime = await steerLiveAgent(agentId, text);
|
|
243
|
+
const mailboxMessage = appendSteeringMessage(loaded.manifest, { taskId: targetTaskId, body: text, status: "delivered", data: { source: "steer-agent", realtime: true } });
|
|
244
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
245
|
+
}
|
|
246
|
+
if (operation === "follow-up-agent") {
|
|
247
|
+
if (!prompt) return result("API follow-up-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
248
|
+
const realtime = await followUpLiveAgent(agentId, prompt);
|
|
249
|
+
const mailboxMessage = appendFollowUpMessage(loaded.manifest, { taskId: targetTaskId, body: prompt, status: "delivered", data: { source: "follow-up-agent", realtime: true } });
|
|
250
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
251
|
+
}
|
|
232
252
|
if (operation === "resume-agent") {
|
|
233
253
|
if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
234
254
|
return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -243,12 +263,14 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
243
263
|
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
244
264
|
if (!task) return result(`API ${operation} agent '${agentId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
245
265
|
if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
266
|
+
if (operation === "follow-up-agent" && !prompt) return result("API follow-up-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
246
267
|
try {
|
|
247
|
-
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
|
|
268
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "follow-up-agent" ? "follow-up" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message });
|
|
269
|
+
const mailboxMessage = operation === "steer-agent" ? appendSteeringMessage(loaded.manifest, { taskId: task.id, to: agent.id, body: message ?? "Please report current status and wrap up if possible.", status: "delivered", data: { source: "steer-agent", liveControlRequestId: request.id } }) : operation === "follow-up-agent" && prompt ? appendFollowUpMessage(loaded.manifest, { taskId: task.id, to: agent.id, body: prompt, status: "delivered", data: { source: "follow-up-agent", liveControlRequestId: request.id } }) : undefined;
|
|
248
270
|
publishLiveControlRealtime(request);
|
|
249
271
|
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
250
|
-
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
|
|
251
|
-
return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
272
|
+
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, mailboxMessageId: mailboxMessage?.id, realtime: true } });
|
|
273
|
+
return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
252
274
|
} catch (queueError) {
|
|
253
275
|
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
254
276
|
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
@@ -258,9 +280,10 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
258
280
|
if (operation === "read-mailbox") {
|
|
259
281
|
const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
|
|
260
282
|
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
283
|
+
const kind = typeof cfg.kind === "string" && ["message", "steer", "follow-up", "response", "group_join"].includes(cfg.kind) ? cfg.kind as MailboxMessageKind : undefined;
|
|
261
284
|
if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API read-mailbox taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
262
285
|
try {
|
|
263
|
-
return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
286
|
+
return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId, kind), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
264
287
|
} catch (error) {
|
|
265
288
|
const message = error instanceof Error ? error.message : String(error);
|
|
266
289
|
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
@@ -399,5 +422,20 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
399
422
|
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
400
423
|
}
|
|
401
424
|
}
|
|
425
|
+
if (operation === "diff") {
|
|
426
|
+
const diffArtifacts = loaded.manifest.artifacts.filter(a => a.kind === "diff" || a.kind === "patch");
|
|
427
|
+
if (diffArtifacts.length === 0) {
|
|
428
|
+
return result(`No diff artifacts found for run ${loaded.manifest.runId}. Diffs are captured in worktree mode.`, { action: "api", status: "ok", runId: loaded.manifest.runId, intent: `diff ${loaded.manifest.runId}: no diffs` });
|
|
429
|
+
}
|
|
430
|
+
const parts: string[] = [`Diff artifacts for run ${loaded.manifest.runId}:`];
|
|
431
|
+
for (const artifact of diffArtifacts) {
|
|
432
|
+
const content = safeReadContainedFile(loaded.manifest.artifactsRoot, artifact.path);
|
|
433
|
+
if (content) {
|
|
434
|
+
const display = content.length > 4000 ? content.slice(0, 4000) + "\n... (truncated)" : content;
|
|
435
|
+
parts.push(`\n--- ${artifact.path} ---\n${display}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return result(parts.join("\n"), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot, intent: `diff ${loaded.manifest.runId}` });
|
|
439
|
+
}
|
|
402
440
|
return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
403
441
|
}
|
|
@@ -3,9 +3,13 @@ import { withRunLockSync } from "../../state/locks.ts";
|
|
|
3
3
|
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
4
|
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
5
|
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
+
import { cancellationReasonFromUnknown, buildSyntheticTerminalEvidence, type CancellationReason } from "../../runtime/cancellation.ts";
|
|
7
|
+
import { appendEvent } from "../../state/event-log.ts";
|
|
6
8
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
9
|
+
import { executeHook, appendHookEvent } from "../../hooks/registry.ts";
|
|
7
10
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
11
|
import { result, type TeamContext } from "./context.ts";
|
|
12
|
+
import { enforceDestructiveIntent, intentFromConfig } from "./intent-policy.ts";
|
|
9
13
|
|
|
10
14
|
export interface AbortOwnedResult {
|
|
11
15
|
abortedIds: string[];
|
|
@@ -55,23 +59,118 @@ export function abortOwned(
|
|
|
55
59
|
return result;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
function configFromParams(params: TeamToolParamsValue): Record<string, unknown> | undefined {
|
|
63
|
+
return params.config && typeof params.config === "object" && !Array.isArray(params.config) ? params.config : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cancelReasonFromParams(params: TeamToolParamsValue): CancellationReason {
|
|
67
|
+
const config = configFromParams(params);
|
|
68
|
+
const rawReason = config?.reason ?? config?.cancelReason;
|
|
69
|
+
const reason = rawReason === undefined ? { code: "caller_cancelled" as const, message: "Run cancelled by user request." } : cancellationReasonFromUnknown(rawReason);
|
|
70
|
+
return { code: reason.code, message: reason.message };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
74
|
+
if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
|
|
75
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
76
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
77
|
+
|
|
78
|
+
// Pre-lock ownership check: reject foreign-owned runs
|
|
79
|
+
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
80
|
+
if (foreignRun) {
|
|
81
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session; not retried.`, { action: "retry", status: "error", runId: loaded.manifest.runId }, true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute before_retry hook after ownership confirmed, before mutation lock
|
|
85
|
+
const hookReport = await executeHook("before_retry", { runId: loaded.manifest.runId, cwd: ctx.cwd });
|
|
86
|
+
appendHookEvent(loaded.manifest, hookReport);
|
|
87
|
+
if (hookReport.outcome === "block") {
|
|
88
|
+
return result(`Retry blocked by hook: ${hookReport.reason ?? "before_retry hook blocked the operation."}`, { action: "retry", status: "error", runId: loaded.manifest.runId }, true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const targetTaskId = typeof params.taskId === "string" ? params.taskId : undefined;
|
|
92
|
+
|
|
93
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
94
|
+
const retryableStatuses: ReadonlySet<string> = new Set(["failed", "cancelled"]);
|
|
95
|
+
|
|
96
|
+
const matchingTasks = loaded.tasks.filter((task) => {
|
|
97
|
+
if (targetTaskId && task.id !== targetTaskId) return false;
|
|
98
|
+
return retryableStatuses.has(task.status);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (matchingTasks.length === 0) {
|
|
102
|
+
return result(targetTaskId ? `Task '${targetTaskId}' is not failed/cancelled; nothing to retry.` : "No failed/cancelled tasks to retry.", { action: "retry", status: "error", runId: loaded.manifest.runId }, true);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const retriedIds = new Set(matchingTasks.map((t) => t.id));
|
|
106
|
+
const tasks = loaded.tasks.map((task) => {
|
|
107
|
+
if (!retriedIds.has(task.id)) return task;
|
|
108
|
+
const { error: _error, finishedAt: _finishedAt, terminalEvidence: _terminalEvidence, ...rest } = task;
|
|
109
|
+
return { ...rest, status: "queued" as const };
|
|
110
|
+
});
|
|
111
|
+
saveRunTasks(loaded.manifest, tasks);
|
|
112
|
+
try {
|
|
113
|
+
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logInternalError("team-tool.handleRetry.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const retriedTaskIds = [...retriedIds];
|
|
119
|
+
for (const taskId of retriedTaskIds) {
|
|
120
|
+
appendEvent(loaded.manifest.eventsPath, { type: "task.retried", runId: loaded.manifest.runId, taskId, message: `Task ${taskId} queued for retry.` });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result(`Retried ${retriedTaskIds.length} task(s) in run ${loaded.manifest.runId}.`, {
|
|
124
|
+
action: "retry",
|
|
125
|
+
status: "ok",
|
|
126
|
+
runId: loaded.manifest.runId,
|
|
127
|
+
retriedTaskIds: retriedTaskIds,
|
|
128
|
+
intent: `retrying ${retriedTaskIds.length} task(s) in ${loaded.manifest.runId}`,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
134
|
+
const intentError = enforceDestructiveIntent("cancel", params, ctx.config);
|
|
135
|
+
if (intentError) return intentError;
|
|
59
136
|
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
60
137
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
61
138
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
139
|
+
|
|
140
|
+
// Pre-lock ownership check: reject foreign-owned runs before executing hooks
|
|
141
|
+
const preCheck = abortOwned(loaded.manifest.runId, undefined, ctx);
|
|
142
|
+
if (preCheck.abortedIds.length === 0 && preCheck.foreignIds.length > 0) {
|
|
143
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session; not cancelled.`, { action: "cancel", status: "error", runId: loaded.manifest.runId, foreignIds: preCheck.foreignIds }, true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Execute before_cancel hook after ownership confirmed, before mutation lock
|
|
147
|
+
const hookReport = await executeHook("before_cancel", { runId: loaded.manifest.runId, cwd: ctx.cwd });
|
|
148
|
+
appendHookEvent(loaded.manifest, hookReport);
|
|
149
|
+
if (hookReport.outcome === "block") {
|
|
150
|
+
return result(`Cancel blocked by hook: ${hookReport.reason ?? "before_cancel hook blocked the operation."}`, { action: "cancel", status: "error", runId: loaded.manifest.runId }, true);
|
|
151
|
+
}
|
|
152
|
+
|
|
62
153
|
return withRunLockSync(loaded.manifest, () => {
|
|
63
154
|
if ((loaded.manifest.status === "completed" || loaded.manifest.status === "cancelled") && !params.force) return result(`Run ${loaded.manifest.runId} is already ${loaded.manifest.status}; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
64
155
|
|
|
65
156
|
// Classify tasks for foreign-aware cancellation
|
|
66
157
|
const abortResult = abortOwned(loaded.manifest.runId, undefined, ctx);
|
|
67
158
|
if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0) {
|
|
68
|
-
return result(`Run ${loaded.manifest.runId} belongs to another session; not cancelled.`, { action: "cancel", status: "error", runId: loaded.manifest.runId, foreignIds: abortResult.foreignIds }
|
|
159
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session; not cancelled.`, { action: "cancel", status: "error", runId: loaded.manifest.runId, foreignIds: abortResult.foreignIds }, true);
|
|
69
160
|
}
|
|
70
161
|
const cancellableIds = new Set(abortResult.abortedIds);
|
|
162
|
+
const cancelReason = cancelReasonFromParams(params);
|
|
163
|
+
const cancelIntent = intentFromConfig(params.config);
|
|
164
|
+
const cancelData = cancelIntent ? { reason: cancelReason.code, intent: cancelIntent } : { reason: cancelReason.code };
|
|
165
|
+
const cancelMessage = `${cancelReason.message} (${cancelReason.code})`;
|
|
71
166
|
|
|
72
167
|
const tasks = loaded.tasks.map((task) => {
|
|
73
168
|
if (cancellableIds.has(task.id) && (task.status === "queued" || task.status === "running" || task.status === "waiting")) {
|
|
74
|
-
|
|
169
|
+
const base = { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: cancelMessage };
|
|
170
|
+
if (task.status === "running") {
|
|
171
|
+
return { ...base, terminalEvidence: [...(task.terminalEvidence ?? []), buildSyntheticTerminalEvidence("worker", cancelReason, task.startedAt)] };
|
|
172
|
+
}
|
|
173
|
+
return base;
|
|
75
174
|
}
|
|
76
175
|
return task;
|
|
77
176
|
});
|
|
@@ -82,11 +181,15 @@ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
82
181
|
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
83
182
|
}
|
|
84
183
|
try {
|
|
85
|
-
writeForegroundInterruptRequest(loaded.manifest,
|
|
184
|
+
writeForegroundInterruptRequest(loaded.manifest, cancelMessage);
|
|
86
185
|
} catch (error) {
|
|
87
186
|
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
88
187
|
}
|
|
89
|
-
|
|
188
|
+
ctx.abortForegroundRun?.(loaded.manifest.runId);
|
|
189
|
+
for (const taskId of abortResult.abortedIds) {
|
|
190
|
+
appendEvent(loaded.manifest.eventsPath, { type: "task.cancelled", runId: loaded.manifest.runId, taskId, message: cancelMessage, data: cancelData });
|
|
191
|
+
}
|
|
192
|
+
const updated = updateRunStatus(loaded.manifest, "cancelled", `${cancelMessage} Already-finished worker processes are not retroactively changed.`, { data: cancelData });
|
|
90
193
|
|
|
91
194
|
// Build descriptive message including foreign/missing info
|
|
92
195
|
const parts = [`Cancelled run ${updated.runId}.`];
|
|
@@ -101,6 +204,7 @@ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
101
204
|
abortedIds: abortResult.abortedIds,
|
|
102
205
|
missingIds: abortResult.missingIds,
|
|
103
206
|
foreignIds: abortResult.foreignIds,
|
|
207
|
+
intent: cancelIntent,
|
|
104
208
|
});
|
|
105
209
|
});
|
|
106
210
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
2
3
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
3
4
|
import type { TeamToolDetails } from "../team-tool-types.ts";
|
|
4
5
|
import { toolResult, type PiTeamsToolResult } from "../tool-result.ts";
|
|
@@ -11,10 +12,17 @@ export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<Extension
|
|
|
11
12
|
metricRegistry?: MetricRegistry;
|
|
12
13
|
signal?: AbortSignal;
|
|
13
14
|
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
15
|
+
abortForegroundRun?: (runId: string) => boolean;
|
|
14
16
|
onRunStarted?: (runId: string) => void;
|
|
15
17
|
onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
|
|
18
|
+
config?: PiTeamsConfig;
|
|
16
19
|
};
|
|
17
20
|
|
|
21
|
+
export function withSessionId<T extends Pick<ExtensionContext, "sessionManager">>(ctx: T): T & { sessionId?: string } {
|
|
22
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
23
|
+
return sessionId ? { ...ctx, sessionId } : { ...ctx };
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
export function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
19
27
|
return toolResult(text, details, isError);
|
|
20
28
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
2
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
4
|
+
import { configRecord, result } from "./context.ts";
|
|
5
|
+
|
|
6
|
+
export type DestructiveIntentAction = "cancel" | "cleanup" | "delete" | "forget" | "prune";
|
|
7
|
+
|
|
8
|
+
const DESTRUCTIVE_ACTION_LABELS: Record<DestructiveIntentAction, string> = {
|
|
9
|
+
cancel: "cancel",
|
|
10
|
+
cleanup: "forced cleanup",
|
|
11
|
+
delete: "delete",
|
|
12
|
+
forget: "forget",
|
|
13
|
+
prune: "prune",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function intentFromConfig(config: unknown): string | undefined {
|
|
17
|
+
const cfg = configRecord(config);
|
|
18
|
+
const rawIntent = cfg.intent ?? cfg._intent;
|
|
19
|
+
if (typeof rawIntent !== "string") return undefined;
|
|
20
|
+
const intent = rawIntent.replace(/\s+/g, " ").trim();
|
|
21
|
+
return intent ? intent.slice(0, 500) : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldRequireIntent(config: PiTeamsConfig | undefined): boolean {
|
|
25
|
+
return config?.policy?.requireIntentForDestructiveActions === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function enforceDestructiveIntent(
|
|
29
|
+
action: DestructiveIntentAction,
|
|
30
|
+
params: TeamToolParamsValue,
|
|
31
|
+
config: PiTeamsConfig | undefined,
|
|
32
|
+
): PiTeamsToolResult | undefined {
|
|
33
|
+
if (!shouldRequireIntent(config)) return undefined;
|
|
34
|
+
if (action === "cleanup" && params.force !== true) return undefined;
|
|
35
|
+
if (intentFromConfig(params.config)) return undefined;
|
|
36
|
+
const label = DESTRUCTIVE_ACTION_LABELS[action];
|
|
37
|
+
return result(
|
|
38
|
+
`Destructive action '${label}' requires config.intent when policy.requireIntentForDestructiveActions is enabled.`,
|
|
39
|
+
{ action: action === "delete" ? "management" : action, status: "error" },
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -1,79 +1,120 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
|
-
import { appendEvent } from "../../state/event-log.ts";
|
|
4
|
-
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
5
|
-
import { cleanupRunWorktrees } from "../../worktree/cleanup.ts";
|
|
6
|
-
import { listImportedRuns } from "../import-index.ts";
|
|
7
|
-
import { exportRunBundle } from "../run-export.ts";
|
|
8
|
-
import { importRunBundle } from "../run-import.ts";
|
|
9
|
-
import { pruneFinishedRuns } from "../run-maintenance.ts";
|
|
10
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
|
-
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
|
+
import { appendEvent } from "../../state/event-log.ts";
|
|
4
|
+
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
5
|
+
import { cleanupRunWorktrees } from "../../worktree/cleanup.ts";
|
|
6
|
+
import { listImportedRuns } from "../import-index.ts";
|
|
7
|
+
import { exportRunBundle } from "../run-export.ts";
|
|
8
|
+
import { importRunBundle } from "../run-import.ts";
|
|
9
|
+
import { pruneFinishedRuns } from "../run-maintenance.ts";
|
|
10
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
|
+
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
12
|
+
import { enforceDestructiveIntent, intentFromConfig } from "./intent-policy.ts";
|
|
13
|
+
import { executeHook, appendHookEvent } from "../../hooks/registry.ts";
|
|
14
|
+
|
|
15
|
+
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
16
|
+
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
17
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
18
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
|
|
19
|
+
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
20
|
+
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)"])];
|
|
21
|
+
return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
25
|
+
const imports = listImportedRuns(ctx.cwd);
|
|
26
|
+
const lines = ["Imported pi-crew runs:", ...(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)"])];
|
|
27
|
+
return result(lines.join("\n"), { action: "imports", status: "ok" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
31
|
+
const cfg = configRecord(params.config);
|
|
32
|
+
const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
|
|
33
|
+
if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
|
|
34
|
+
const scope = cfg.scope === "user" ? "user" : "project";
|
|
35
|
+
try {
|
|
36
|
+
const imported = importRunBundle(ctx.cwd, bundlePath, scope);
|
|
37
|
+
return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function handleExport(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
45
|
+
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
46
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
47
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
|
|
48
|
+
|
|
49
|
+
const hookReport = await executeHook("before_publish", { runId: loaded.manifest.runId, cwd: ctx.cwd });
|
|
50
|
+
appendHookEvent(loaded.manifest, hookReport);
|
|
51
|
+
if (hookReport.outcome === "block") {
|
|
52
|
+
return result(`Export blocked by hook: ${hookReport.reason ?? "before_publish hook blocked the operation."}`, { action: "export", status: "error", runId: loaded.manifest.runId }, true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const exported = exportRunBundle(loaded.manifest, loaded.tasks);
|
|
56
|
+
appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
|
|
57
|
+
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 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
61
|
+
const intentError = enforceDestructiveIntent("prune", params, ctx.config);
|
|
62
|
+
if (intentError) return intentError;
|
|
63
|
+
if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
|
|
64
|
+
const keep = params.keep ?? 20;
|
|
65
|
+
if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
|
|
66
|
+
const intent = intentFromConfig(params.config);
|
|
67
|
+
const pruned = pruneFinishedRuns(ctx.cwd, keep, { intent, signal: ctx.signal });
|
|
68
|
+
const firstRunId = pruned.kept[0] ?? pruned.removed[0];
|
|
69
|
+
if (firstRunId) {
|
|
70
|
+
const manifest = loadRunManifestById(ctx.cwd, firstRunId)?.manifest;
|
|
71
|
+
if (manifest) {
|
|
72
|
+
const hookReport = await executeHook("before_cleanup", { runId: manifest.runId, cwd: ctx.cwd });
|
|
73
|
+
appendHookEvent(manifest, hookReport);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result([`Pruned finished pi-crew runs.`, `Kept: ${pruned.kept.length}`, `Removed: ${pruned.removed.length}`, ...(pruned.auditPath ? [`Audit: ${pruned.auditPath}`] : []), ...(pruned.removed.length ? ["Removed runs:", ...pruned.removed.map((runId) => `- ${runId}`)] : [])].join("\n"), { action: "prune", status: "ok", intent });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
80
|
+
const intentError = enforceDestructiveIntent("forget", params, ctx.config);
|
|
81
|
+
if (intentError) return intentError;
|
|
82
|
+
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
83
|
+
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
84
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
85
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
86
|
+
|
|
87
|
+
const hookReport = await executeHook("before_forget", { runId: loaded.manifest.runId, cwd: ctx.cwd });
|
|
88
|
+
appendHookEvent(loaded.manifest, hookReport);
|
|
89
|
+
if (hookReport.outcome === "block") {
|
|
90
|
+
return result(`Forget blocked by hook: ${hookReport.reason ?? "before_forget hook blocked the operation."}`, { action: "forget", status: "error", runId: loaded.manifest.runId }, true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
94
|
+
if (cleanup.preserved.length > 0 && !params.force) 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);
|
|
95
|
+
const intent = intentFromConfig(params.config);
|
|
96
|
+
appendEvent(loaded.manifest.eventsPath, { type: "run.forget_requested", runId: loaded.manifest.runId, message: "Run state and artifacts are being forgotten.", data: { force: params.force === true, removedWorktrees: cleanup.removed, preservedWorktrees: cleanup.preserved, intent } });
|
|
97
|
+
fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
|
|
98
|
+
fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
|
|
99
|
+
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, intent });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
103
|
+
const intentError = enforceDestructiveIntent("cleanup", params, ctx.config);
|
|
104
|
+
if (intentError) return intentError;
|
|
105
|
+
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
106
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
107
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
108
|
+
|
|
109
|
+
const hookReport = await executeHook("before_cleanup", { runId: loaded.manifest.runId, cwd: ctx.cwd });
|
|
110
|
+
appendHookEvent(loaded.manifest, hookReport);
|
|
111
|
+
if (hookReport.outcome === "block") {
|
|
112
|
+
return result(`Cleanup blocked by hook: ${hookReport.reason ?? "before_cleanup hook blocked the operation."}`, { action: "cleanup", status: "error", runId: loaded.manifest.runId }, true);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force, signal: ctx.signal });
|
|
116
|
+
const intent = intentFromConfig(params.config);
|
|
117
|
+
appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths, intent } });
|
|
118
|
+
const lines = [`Worktree cleanup for ${loaded.manifest.runId}:`, "Removed:", ...(cleanup.removed.length ? cleanup.removed.map((item) => `- ${item}`) : ["- (none)"]), "Preserved:", ...(cleanup.preserved.length ? cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`) : ["- (none)"]), "Artifacts:", ...(cleanup.artifactPaths.length ? cleanup.artifactPaths.map((item) => `- ${item}`) : ["- (none)"])];
|
|
119
|
+
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot, intent });
|
|
120
|
+
}
|