pi-crew 0.1.44 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -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 +733 -0
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-oh-my-pi-distillation.md +322 -0
- package/docs/source-runtime-refactor-map.md +24 -0
- package/docs/usage.md +3 -3
- package/install.mjs +52 -8
- package/package.json +1 -1
- package/schema.json +2 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/discover-agents.ts +12 -11
- package/src/config/config.ts +48 -24
- package/src/config/defaults.ts +14 -0
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +19 -10
- package/src/extension/registration/commands.ts +49 -26
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +2 -1
- package/src/extension/registration/team-tool.ts +28 -8
- package/src/extension/run-index.ts +13 -5
- package/src/extension/run-maintenance.ts +22 -3
- package/src/extension/team-tool/api.ts +25 -8
- package/src/extension/team-tool/cancel.ts +134 -102
- package/src/extension/team-tool/context.ts +6 -0
- package/src/extension/team-tool/lifecycle-actions.ts +17 -5
- package/src/extension/team-tool/respond.ts +103 -66
- package/src/extension/team-tool/run.ts +53 -10
- package/src/extension/team-tool/status.ts +12 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +32 -11
- package/src/observability/event-to-metric.ts +8 -1
- package/src/runtime/background-runner.ts +10 -4
- package/src/runtime/cancellation.ts +51 -0
- package/src/runtime/child-pi.ts +17 -4
- package/src/runtime/crash-recovery.ts +1 -0
- package/src/runtime/crew-agent-records.ts +41 -1
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +174 -142
- package/src/runtime/effectiveness.ts +76 -0
- package/src/runtime/live-agent-control.ts +2 -1
- package/src/runtime/live-agent-manager.ts +20 -2
- package/src/runtime/live-control-realtime.ts +1 -1
- package/src/runtime/live-session-runtime.ts +5 -1
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/overflow-recovery.ts +175 -156
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/retry-executor.ts +26 -9
- package/src/runtime/runtime-resolver.ts +22 -6
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +189 -179
- package/src/runtime/subagent-manager.ts +3 -0
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +4 -0
- package/src/runtime/task-runner/prompt-builder.ts +3 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner.ts +44 -5
- package/src/runtime/team-runner.ts +91 -19
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +3 -3
- package/src/state/active-run-registry.ts +165 -0
- package/src/state/contracts.ts +1 -1
- package/src/state/mailbox.ts +44 -4
- package/src/state/state-store.ts +51 -1
- package/src/state/types.ts +46 -2
- package/src/teams/team-config.ts +1 -0
- package/src/ui/crew-widget.ts +9 -4
- package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/run-snapshot-cache.ts +66 -39
- package/src/ui/snapshot-types.ts +7 -0
- package/src/utils/paths.ts +4 -2
- package/src/workflows/workflow-config.ts +1 -0
|
@@ -1,103 +1,135 @@
|
|
|
1
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
-
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
-
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
-
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
|
-
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
* - Tasks with status "queued" or "running" that belong to
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
+
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
+
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
+
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
|
+
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
+
import { cancellationReasonFromUnknown } from "../../runtime/cancellation.ts";
|
|
7
|
+
import { appendEvent } from "../../state/event-log.ts";
|
|
8
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
9
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
10
|
+
import { result, type TeamContext } from "./context.ts";
|
|
11
|
+
|
|
12
|
+
export interface AbortOwnedResult {
|
|
13
|
+
abortedIds: string[];
|
|
14
|
+
missingIds: string[];
|
|
15
|
+
foreignIds: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Classify task IDs by ownership.
|
|
20
|
+
* - Tasks with status "queued" or "running" that belong to the current session → abortedIds
|
|
21
|
+
* - Task IDs not found in the run → missingIds
|
|
22
|
+
* - Tasks with status "queued" or "running" that belong to a different session → foreignIds
|
|
23
|
+
* - Tasks already completed/failed/cancelled → neither (not included in any list)
|
|
24
|
+
*
|
|
25
|
+
* Currently, task ownership is determined by the manifest's run-level ownership.
|
|
26
|
+
* Since tasks in a single run are all owned by the session that created the run,
|
|
27
|
+
* the ownerSessionId comes from the context. Foreign detection compares
|
|
28
|
+
* the requesting session against the run's creating session.
|
|
29
|
+
*/
|
|
30
|
+
export function abortOwned(
|
|
31
|
+
runId: string,
|
|
32
|
+
taskIds: string[] | undefined,
|
|
33
|
+
ctx: TeamContext,
|
|
34
|
+
): AbortOwnedResult {
|
|
35
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
36
|
+
if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
37
|
+
|
|
38
|
+
const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
|
|
39
|
+
const taskMap = new Map(loaded.tasks.map((t) => [t.id, t] as const));
|
|
40
|
+
const targetIds = taskIds ?? loaded.tasks.map((t) => t.id);
|
|
41
|
+
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
42
|
+
|
|
43
|
+
for (const id of targetIds) {
|
|
44
|
+
const task = taskMap.get(id);
|
|
45
|
+
if (!task) {
|
|
46
|
+
result.missingIds.push(id);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") continue;
|
|
50
|
+
if (foreignRun) {
|
|
51
|
+
result.foreignIds.push(id);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result.abortedIds.push(id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function configFromParams(params: TeamToolParamsValue): Record<string, unknown> | undefined {
|
|
61
|
+
return params.config && typeof params.config === "object" && !Array.isArray(params.config) ? params.config : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cancelReasonFromParams(params: TeamToolParamsValue): { code: string; message: string } {
|
|
65
|
+
const config = configFromParams(params);
|
|
66
|
+
const rawReason = config?.reason ?? config?.cancelReason;
|
|
67
|
+
const reason = rawReason === undefined ? { code: "caller_cancelled" as const, message: "Run cancelled by user request." } : cancellationReasonFromUnknown(rawReason);
|
|
68
|
+
return { code: reason.code, message: reason.message };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function intentFromParams(params: TeamToolParamsValue): string | undefined {
|
|
72
|
+
const config = configFromParams(params);
|
|
73
|
+
const rawIntent = config?.intent ?? config?._intent;
|
|
74
|
+
if (typeof rawIntent !== "string") return undefined;
|
|
75
|
+
const intent = rawIntent.replace(/\s+/g, " ").trim();
|
|
76
|
+
return intent ? intent.slice(0, 500) : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
80
|
+
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
81
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
82
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
83
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
84
|
+
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 });
|
|
85
|
+
|
|
86
|
+
// Classify tasks for foreign-aware cancellation
|
|
87
|
+
const abortResult = abortOwned(loaded.manifest.runId, undefined, ctx);
|
|
88
|
+
if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0) {
|
|
89
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session; not cancelled.`, { action: "cancel", status: "error", runId: loaded.manifest.runId, foreignIds: abortResult.foreignIds }, true);
|
|
90
|
+
}
|
|
91
|
+
const cancellableIds = new Set(abortResult.abortedIds);
|
|
92
|
+
const cancelReason = cancelReasonFromParams(params);
|
|
93
|
+
const cancelIntent = intentFromParams(params);
|
|
94
|
+
const cancelData = cancelIntent ? { reason: cancelReason.code, intent: cancelIntent } : { reason: cancelReason.code };
|
|
95
|
+
const cancelMessage = `${cancelReason.message} (${cancelReason.code})`;
|
|
96
|
+
|
|
97
|
+
const tasks = loaded.tasks.map((task) => {
|
|
98
|
+
if (cancellableIds.has(task.id) && (task.status === "queued" || task.status === "running" || task.status === "waiting")) {
|
|
99
|
+
return { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: cancelMessage };
|
|
100
|
+
}
|
|
101
|
+
return task;
|
|
102
|
+
});
|
|
103
|
+
saveRunTasks(loaded.manifest, tasks);
|
|
104
|
+
try {
|
|
105
|
+
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
writeForegroundInterruptRequest(loaded.manifest, cancelMessage);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
113
|
+
}
|
|
114
|
+
for (const taskId of abortResult.abortedIds) {
|
|
115
|
+
appendEvent(loaded.manifest.eventsPath, { type: "task.cancelled", runId: loaded.manifest.runId, taskId, message: cancelMessage, data: cancelData });
|
|
116
|
+
}
|
|
117
|
+
const updated = updateRunStatus(loaded.manifest, "cancelled", `${cancelMessage} Already-finished worker processes are not retroactively changed.`, { data: cancelData });
|
|
118
|
+
|
|
119
|
+
// Build descriptive message including foreign/missing info
|
|
120
|
+
const parts = [`Cancelled run ${updated.runId}.`];
|
|
121
|
+
if (abortResult.foreignIds.length > 0) parts.push(` ${abortResult.foreignIds.length} task(s) belong to another session and were not cancelled: ${abortResult.foreignIds.join(", ")}.`);
|
|
122
|
+
if (abortResult.missingIds.length > 0) parts.push(` ${abortResult.missingIds.length} task ID(s) not found: ${abortResult.missingIds.join(", ")}.`);
|
|
123
|
+
|
|
124
|
+
return result(parts.join(""), {
|
|
125
|
+
action: "cancel",
|
|
126
|
+
status: "ok",
|
|
127
|
+
runId: updated.runId,
|
|
128
|
+
artifactsRoot: updated.artifactsRoot,
|
|
129
|
+
abortedIds: abortResult.abortedIds,
|
|
130
|
+
missingIds: abortResult.missingIds,
|
|
131
|
+
foreignIds: abortResult.foreignIds,
|
|
132
|
+
intent: cancelIntent,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
103
135
|
}
|
|
@@ -4,6 +4,7 @@ import type { TeamToolDetails } from "../team-tool-types.ts";
|
|
|
4
4
|
import { toolResult, type PiTeamsToolResult } from "../tool-result.ts";
|
|
5
5
|
|
|
6
6
|
export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model">> & {
|
|
7
|
+
sessionId?: string;
|
|
7
8
|
modelRegistry?: unknown;
|
|
8
9
|
sessionManager?: { getBranch?: () => unknown[] };
|
|
9
10
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
@@ -14,6 +15,11 @@ export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<Extension
|
|
|
14
15
|
onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
export function withSessionId<T extends Pick<ExtensionContext, "sessionManager">>(ctx: T): T & { sessionId?: string } {
|
|
19
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
20
|
+
return sessionId ? { ...ctx, sessionId } : { ...ctx };
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
18
24
|
return toolResult(text, details, isError);
|
|
19
25
|
}
|
|
@@ -10,6 +10,14 @@ import { pruneFinishedRuns } from "../run-maintenance.ts";
|
|
|
10
10
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
11
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
12
12
|
|
|
13
|
+
function intentFromParams(params: TeamToolParamsValue): string | undefined {
|
|
14
|
+
const cfg = configRecord(params.config);
|
|
15
|
+
const rawIntent = cfg.intent ?? cfg._intent;
|
|
16
|
+
if (typeof rawIntent !== "string") return undefined;
|
|
17
|
+
const intent = rawIntent.replace(/\s+/g, " ").trim();
|
|
18
|
+
return intent ? intent.slice(0, 500) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
14
22
|
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
15
23
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
@@ -52,8 +60,9 @@ export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTe
|
|
|
52
60
|
const keep = params.keep ?? 20;
|
|
53
61
|
if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
|
|
54
62
|
if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
|
|
55
|
-
const
|
|
56
|
-
|
|
63
|
+
const intent = intentFromParams(params);
|
|
64
|
+
const pruned = pruneFinishedRuns(ctx.cwd, keep, { intent });
|
|
65
|
+
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 });
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
@@ -63,9 +72,11 @@ export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
63
72
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
64
73
|
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
65
74
|
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);
|
|
75
|
+
const intent = intentFromParams(params);
|
|
76
|
+
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 } });
|
|
66
77
|
fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
|
|
67
78
|
fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
|
|
68
|
-
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 });
|
|
79
|
+
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 });
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
@@ -73,7 +84,8 @@ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
73
84
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
74
85
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
75
86
|
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
76
|
-
|
|
87
|
+
const intent = intentFromParams(params);
|
|
88
|
+
appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths, intent } });
|
|
77
89
|
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)"])];
|
|
78
|
-
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
90
|
+
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot, intent });
|
|
79
91
|
}
|
|
@@ -1,67 +1,104 @@
|
|
|
1
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
-
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
-
import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
+
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
+
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
+
import { appendEvent } from "../../state/event-log.ts";
|
|
5
|
+
import { appendMailboxMessage } from "../../state/mailbox.ts";
|
|
6
|
+
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
7
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
8
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
9
|
+
import { result, type TeamContext } from "./context.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle `respond` action: send a message to a waiting (interactive) task.
|
|
13
|
+
* The task must be in "waiting" status. The message is stored in the task's
|
|
14
|
+
* mailbox and the task is re-queued for durable scheduler resume.
|
|
15
|
+
*/
|
|
16
|
+
export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
17
|
+
if (!params.runId) return result("Respond requires runId.", { action: "respond", status: "error" }, true);
|
|
18
|
+
if (!params.message && !params.taskId) return result("Respond requires taskId and/or message.", { action: "respond", status: "error" }, true);
|
|
19
|
+
|
|
20
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
21
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
22
|
+
|
|
23
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
24
|
+
const fresh = loadRunManifestById(ctx.cwd, params.runId!);
|
|
25
|
+
if (!fresh) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
26
|
+
const foreignRun = typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId;
|
|
27
|
+
if (foreignRun) return result(`Run ${fresh.manifest.runId} belongs to another session; not responding.`, { action: "respond", status: "error", runId: fresh.manifest.runId }, true);
|
|
28
|
+
|
|
29
|
+
const taskId = params.taskId;
|
|
30
|
+
const message = params.message ?? "";
|
|
31
|
+
|
|
32
|
+
const targetTasks = taskId
|
|
33
|
+
? fresh.tasks.filter((t) => t.id === taskId && t.status === "waiting")
|
|
34
|
+
: fresh.tasks.filter((t) => t.status === "waiting");
|
|
35
|
+
|
|
36
|
+
if (targetTasks.length === 0) {
|
|
37
|
+
const existing = taskId ? fresh.tasks.find((t) => t.id === taskId) : undefined;
|
|
38
|
+
const hint = " Use api operation=follow-up-agent for continuation prompts or api operation=steer-agent to interrupt active work.";
|
|
39
|
+
return result(
|
|
40
|
+
(taskId
|
|
41
|
+
? existing
|
|
42
|
+
? `Task '${taskId}' is ${existing.status}, not waiting.`
|
|
43
|
+
: `Task '${taskId}' not found.`
|
|
44
|
+
: `No waiting tasks in run ${fresh.manifest.runId}.`) + hint,
|
|
45
|
+
{ action: "respond", status: "error", runId: fresh.manifest.runId },
|
|
46
|
+
true,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resumed = new Set(targetTasks.map((t) => t.id));
|
|
51
|
+
const mailboxIds: string[] = [];
|
|
52
|
+
for (const task of targetTasks) {
|
|
53
|
+
const mailbox = appendMailboxMessage(fresh.manifest, {
|
|
54
|
+
direction: "inbox",
|
|
55
|
+
from: "leader",
|
|
56
|
+
to: task.id,
|
|
57
|
+
taskId: task.id,
|
|
58
|
+
body: message || "(resume)",
|
|
59
|
+
kind: "response",
|
|
60
|
+
priority: "normal",
|
|
61
|
+
deliveryMode: "next_turn",
|
|
62
|
+
data: { action: "respond", kind: "response" },
|
|
63
|
+
});
|
|
64
|
+
mailboxIds.push(mailbox.id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Re-queue waiting tasks so durable scheduler/resume can pick them up again.
|
|
68
|
+
const updatedTasks = fresh.tasks.map((task) => {
|
|
69
|
+
if (!resumed.has(task.id)) return task;
|
|
70
|
+
return {
|
|
71
|
+
...task,
|
|
72
|
+
status: "queued" as const,
|
|
73
|
+
startedAt: undefined,
|
|
74
|
+
finishedAt: undefined,
|
|
75
|
+
error: undefined,
|
|
76
|
+
adaptive: {
|
|
77
|
+
...task.adaptive,
|
|
78
|
+
phase: "resumed",
|
|
79
|
+
task: message || task.adaptive?.task || "",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
saveRunTasks(fresh.manifest, updatedTasks);
|
|
85
|
+
let manifest = fresh.manifest;
|
|
86
|
+
if (manifest.status === "blocked" || manifest.status === "completed" || manifest.status === "failed" || manifest.status === "cancelled") {
|
|
87
|
+
manifest = updateRunStatus(manifest, "running", `Resumed ${resumed.size} waiting task(s).`);
|
|
88
|
+
}
|
|
89
|
+
for (const taskId of resumed) {
|
|
90
|
+
appendEvent(manifest.eventsPath, { type: "task.resumed", runId: manifest.runId, taskId, message: message || "Task re-queued after respond.", data: { mailboxIds } });
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
saveCrewAgents(fresh.manifest, updatedTasks.map((task) => recordFromTask(fresh.manifest, task, "child-process")));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logInternalError("team-tool.handleRespond.crewAgents", error, `runId=${fresh.manifest.runId}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const resumedIds = targetTasks.map((t) => t.id);
|
|
99
|
+
return result(
|
|
100
|
+
`Resumed ${resumedIds.length} waiting task(s): ${resumedIds.join(", ")}. Message: ${message || "(no message)"}`,
|
|
101
|
+
{ action: "respond", status: "ok", runId: fresh.manifest.runId, resumedIds, mailboxIds },
|
|
102
|
+
);
|
|
103
|
+
});
|
|
67
104
|
}
|
|
@@ -4,13 +4,15 @@ 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 { registerActiveRun, unregisterActiveRun } from "../../state/active-run-registry.ts";
|
|
7
8
|
import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
8
9
|
import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
9
10
|
import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
|
|
10
11
|
import { executeTeamRun } from "../../runtime/team-runner.ts";
|
|
11
12
|
import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
|
|
12
13
|
import { appendEvent, readEvents } from "../../state/event-log.ts";
|
|
13
|
-
import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
|
|
14
|
+
import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtime-resolver.ts";
|
|
15
|
+
import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
|
|
14
16
|
import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
|
|
15
17
|
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
16
18
|
import { hasAsyncStartMarker } from "../../runtime/async-marker.ts";
|
|
@@ -91,12 +93,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
91
93
|
return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...validationErrors.map((error) => `- ${error}`)].join("\n"), { action: "run", status: "error" }, true);
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
const skillOverride = normalizeSkillOverride(params.skill);
|
|
94
97
|
const { manifest, tasks, paths } = createRunManifest({
|
|
95
98
|
cwd: ctx.cwd,
|
|
96
99
|
team,
|
|
97
100
|
workflow,
|
|
98
101
|
goal,
|
|
99
102
|
workspaceMode: params.workspaceMode,
|
|
103
|
+
ownerSessionId: ctx.sessionId,
|
|
100
104
|
});
|
|
101
105
|
const goalArtifact = writeArtifact(paths.artifactsRoot, {
|
|
102
106
|
kind: "prompt",
|
|
@@ -104,17 +108,35 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
104
108
|
content: `${goal}\n`,
|
|
105
109
|
producer: "team-tool",
|
|
106
110
|
});
|
|
107
|
-
const updatedManifest = { ...manifest, artifacts: [goalArtifact], summary: "Run manifest created; worker execution is not implemented yet." };
|
|
111
|
+
const updatedManifest = { ...manifest, ...(skillOverride !== undefined ? { skillOverride } : {}), artifacts: [goalArtifact], summary: "Run manifest created; worker execution is not implemented yet." };
|
|
108
112
|
atomicWriteJson(paths.manifestPath, updatedManifest);
|
|
113
|
+
registerActiveRun(updatedManifest);
|
|
109
114
|
|
|
110
115
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
116
|
+
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
117
|
+
const runtime = await resolveCrewRuntime(executedConfig);
|
|
118
|
+
const runtimeResolution = runtimeResolutionState(runtime);
|
|
119
|
+
const executionManifest = { ...updatedManifest, runtimeResolution, runConfig: executedConfig, updatedAt: new Date().toISOString() };
|
|
120
|
+
atomicWriteJson(paths.manifestPath, executionManifest);
|
|
121
|
+
appendEvent(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } });
|
|
111
122
|
const runAsync = params.async ?? loadedConfig.config.asyncByDefault ?? false;
|
|
112
123
|
if (runAsync) {
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
if (runtime.safety === "blocked") {
|
|
125
|
+
const runningManifest = updateRunStatus(executionManifest, "running", "Checking worker runtime availability.");
|
|
126
|
+
const blocked = updateRunStatus(runningManifest, "blocked", runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
|
|
127
|
+
appendEvent(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime, runtimeResolution, async: true } });
|
|
128
|
+
unregisterActiveRun(blocked.runId);
|
|
129
|
+
return result([
|
|
130
|
+
`Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
|
|
131
|
+
`Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
|
|
132
|
+
runtime.reason ?? "Child worker execution is disabled.",
|
|
133
|
+
].join("\n"), { action: "run", status: "error", runId: blocked.runId, artifactsRoot: blocked.artifactsRoot }, true);
|
|
134
|
+
}
|
|
135
|
+
const spawned = spawnBackgroundTeamRun(executionManifest);
|
|
136
|
+
const asyncManifest = { ...executionManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
|
|
115
137
|
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
116
|
-
appendEvent(
|
|
117
|
-
scheduleBackgroundEarlyExitGuard(ctx.cwd,
|
|
138
|
+
appendEvent(executionManifest.eventsPath, { type: "async.spawned", runId: executionManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
139
|
+
scheduleBackgroundEarlyExitGuard(ctx.cwd, executionManifest.runId, spawned.pid, spawned.logPath);
|
|
118
140
|
const text = [
|
|
119
141
|
`Started async pi-crew run ${updatedManifest.runId}.`,
|
|
120
142
|
`Team: ${team.name}`,
|
|
@@ -130,13 +152,29 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
130
152
|
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
131
153
|
}
|
|
132
154
|
|
|
133
|
-
|
|
155
|
+
if (runtime.safety === "blocked") {
|
|
156
|
+
const runningManifest = updateRunStatus(executionManifest, "running", "Checking worker runtime availability.");
|
|
157
|
+
const blocked = updateRunStatus(runningManifest, "blocked", runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
|
|
158
|
+
appendEvent(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime, runtimeResolution } });
|
|
159
|
+
unregisterActiveRun(blocked.runId);
|
|
160
|
+
return result([
|
|
161
|
+
`Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
|
|
162
|
+
`Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
|
|
163
|
+
runtime.reason ?? "Child worker execution is disabled.",
|
|
164
|
+
"",
|
|
165
|
+
"To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
|
|
166
|
+
"Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
|
|
167
|
+
].join("\n"), { action: "run", status: "error", runId: blocked.runId, artifactsRoot: blocked.artifactsRoot }, true);
|
|
168
|
+
}
|
|
134
169
|
const executeWorkers = runtime.kind !== "scaffold";
|
|
135
|
-
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
136
170
|
if (executeWorkers && ctx.startForegroundRun) {
|
|
137
171
|
ctx.onRunStarted?.(updatedManifest.runId);
|
|
138
172
|
ctx.startForegroundRun(async (signal) => {
|
|
139
|
-
|
|
173
|
+
try {
|
|
174
|
+
await executeTeamRun({ manifest: executionManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, skillOverride, signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, onJsonEvent: ctx.onJsonEvent });
|
|
175
|
+
} finally {
|
|
176
|
+
unregisterActiveRun(updatedManifest.runId);
|
|
177
|
+
}
|
|
140
178
|
}, updatedManifest.runId);
|
|
141
179
|
const text = [
|
|
142
180
|
`Started foreground pi-crew run ${updatedManifest.runId}.`,
|
|
@@ -152,7 +190,12 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
152
190
|
].join("\n");
|
|
153
191
|
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
154
192
|
}
|
|
155
|
-
|
|
193
|
+
let executed: Awaited<ReturnType<typeof executeTeamRun>>;
|
|
194
|
+
try {
|
|
195
|
+
executed = await executeTeamRun({ manifest: executionManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, skillOverride, signal: ctx.signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, onJsonEvent: ctx.onJsonEvent });
|
|
196
|
+
} finally {
|
|
197
|
+
unregisterActiveRun(updatedManifest.runId);
|
|
198
|
+
}
|
|
156
199
|
const text = [
|
|
157
200
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
158
201
|
`Team: ${team.name}`,
|