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.
Files changed (103) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +5 -5
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/next-upgrade-roadmap.md +733 -0
  14. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  15. package/docs/research-oh-my-pi-distillation.md +322 -0
  16. package/docs/source-runtime-refactor-map.md +24 -0
  17. package/docs/usage.md +3 -3
  18. package/install.mjs +52 -8
  19. package/package.json +1 -1
  20. package/schema.json +2 -1
  21. package/skills/async-worker-recovery/SKILL.md +42 -0
  22. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  23. package/skills/delegation-patterns/SKILL.md +54 -0
  24. package/skills/mailbox-interactive/SKILL.md +40 -0
  25. package/skills/model-routing-context/SKILL.md +39 -0
  26. package/skills/multi-perspective-review/SKILL.md +58 -0
  27. package/skills/observability-reliability/SKILL.md +41 -0
  28. package/skills/ownership-session-security/SKILL.md +41 -0
  29. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  30. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  31. package/skills/resource-discovery-config/SKILL.md +41 -0
  32. package/skills/runtime-state-reader/SKILL.md +44 -0
  33. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  34. package/skills/state-mutation-locking/SKILL.md +42 -0
  35. package/skills/systematic-debugging/SKILL.md +67 -0
  36. package/skills/ui-render-performance/SKILL.md +39 -0
  37. package/skills/verification-before-done/SKILL.md +57 -0
  38. package/skills/worktree-isolation/SKILL.md +39 -0
  39. package/src/agents/discover-agents.ts +12 -11
  40. package/src/config/config.ts +48 -24
  41. package/src/config/defaults.ts +14 -0
  42. package/src/extension/project-init.ts +62 -2
  43. package/src/extension/register.ts +19 -10
  44. package/src/extension/registration/commands.ts +49 -26
  45. package/src/extension/registration/subagent-helpers.ts +8 -0
  46. package/src/extension/registration/subagent-tools.ts +2 -1
  47. package/src/extension/registration/team-tool.ts +28 -8
  48. package/src/extension/run-index.ts +13 -5
  49. package/src/extension/run-maintenance.ts +22 -3
  50. package/src/extension/team-tool/api.ts +25 -8
  51. package/src/extension/team-tool/cancel.ts +134 -102
  52. package/src/extension/team-tool/context.ts +6 -0
  53. package/src/extension/team-tool/lifecycle-actions.ts +17 -5
  54. package/src/extension/team-tool/respond.ts +103 -66
  55. package/src/extension/team-tool/run.ts +53 -10
  56. package/src/extension/team-tool/status.ts +12 -1
  57. package/src/extension/team-tool-types.ts +2 -0
  58. package/src/extension/team-tool.ts +32 -11
  59. package/src/observability/event-to-metric.ts +8 -1
  60. package/src/runtime/background-runner.ts +10 -4
  61. package/src/runtime/cancellation.ts +51 -0
  62. package/src/runtime/child-pi.ts +17 -4
  63. package/src/runtime/crash-recovery.ts +1 -0
  64. package/src/runtime/crew-agent-records.ts +41 -1
  65. package/src/runtime/deadletter.ts +1 -0
  66. package/src/runtime/delivery-coordinator.ts +174 -142
  67. package/src/runtime/effectiveness.ts +76 -0
  68. package/src/runtime/live-agent-control.ts +2 -1
  69. package/src/runtime/live-agent-manager.ts +20 -2
  70. package/src/runtime/live-control-realtime.ts +1 -1
  71. package/src/runtime/live-session-runtime.ts +5 -1
  72. package/src/runtime/manifest-cache.ts +17 -2
  73. package/src/runtime/model-fallback.ts +6 -4
  74. package/src/runtime/overflow-recovery.ts +175 -156
  75. package/src/runtime/pi-args.ts +18 -3
  76. package/src/runtime/process-status.ts +5 -1
  77. package/src/runtime/retry-executor.ts +26 -9
  78. package/src/runtime/runtime-resolver.ts +22 -6
  79. package/src/runtime/skill-instructions.ts +222 -0
  80. package/src/runtime/stale-reconciler.ts +189 -179
  81. package/src/runtime/subagent-manager.ts +3 -0
  82. package/src/runtime/task-runner/capabilities.ts +78 -0
  83. package/src/runtime/task-runner/live-executor.ts +4 -0
  84. package/src/runtime/task-runner/prompt-builder.ts +3 -1
  85. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  86. package/src/runtime/task-runner.ts +44 -5
  87. package/src/runtime/team-runner.ts +91 -19
  88. package/src/schema/config-schema.ts +1 -0
  89. package/src/schema/team-tool-schema.ts +3 -3
  90. package/src/state/active-run-registry.ts +165 -0
  91. package/src/state/contracts.ts +1 -1
  92. package/src/state/mailbox.ts +44 -4
  93. package/src/state/state-store.ts +51 -1
  94. package/src/state/types.ts +46 -2
  95. package/src/teams/team-config.ts +1 -0
  96. package/src/ui/crew-widget.ts +9 -4
  97. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  98. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  99. package/src/ui/powerbar-publisher.ts +1 -1
  100. package/src/ui/run-snapshot-cache.ts +66 -39
  101. package/src/ui/snapshot-types.ts +7 -0
  102. package/src/utils/paths.ts +4 -2
  103. 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 { logInternalError } from "../../utils/internal-error.ts";
7
- import type { PiTeamsToolResult } from "../tool-result.ts";
8
- import { result, type TeamContext } from "./context.ts";
9
-
10
- export interface AbortOwnedResult {
11
- abortedIds: string[];
12
- missingIds: string[];
13
- foreignIds: string[];
14
- }
15
-
16
- /**
17
- * Classify task IDs by ownership.
18
- * - Tasks with status "queued" or "running" that belong to the current session → abortedIds
19
- * - Task IDs not found in the run → missingIds
20
- * - Tasks with status "queued" or "running" that belong to a different session → foreignIds
21
- * - Tasks already completed/failed/cancelled neither (not included in any list)
22
- *
23
- * Currently, task ownership is determined by the manifest's run-level ownership.
24
- * Since tasks in a single run are all owned by the session that created the run,
25
- * the ownerSessionId comes from the context. Foreign detection compares
26
- * the requesting session against the run's creating session.
27
- */
28
- export function abortOwned(
29
- runId: string,
30
- taskIds: string[] | undefined,
31
- ctx: TeamContext,
32
- ): AbortOwnedResult {
33
- const loaded = loadRunManifestById(ctx.cwd, runId);
34
- if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
35
-
36
- const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
37
- const taskMap = new Map(loaded.tasks.map((t) => [t.id, t] as const));
38
- const targetIds = taskIds ?? loaded.tasks.map((t) => t.id);
39
-
40
- for (const id of targetIds) {
41
- const task = taskMap.get(id);
42
- if (!task) {
43
- result.missingIds.push(id);
44
- continue;
45
- }
46
- if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") continue;
47
- // All tasks in a run are owned by the session that created the run.
48
- // Since cancel is always called within the session that created it,
49
- // all cancellable tasks are abortable.
50
- // Foreign detection is a placeholder for when tasks can be owned
51
- // by different sessions (e.g., shared runs with session-scoped tasks).
52
- result.abortedIds.push(id);
53
- }
54
-
55
- return result;
56
- }
57
-
58
- export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
59
- if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
60
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
61
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
62
- return withRunLockSync(loaded.manifest, () => {
63
- 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
-
65
- // Classify tasks for foreign-aware cancellation
66
- const abortResult = abortOwned(loaded.manifest.runId, undefined, ctx);
67
- const cancellableIds = new Set(abortResult.abortedIds);
68
-
69
- const tasks = loaded.tasks.map((task) => {
70
- if (cancellableIds.has(task.id) && (task.status === "queued" || task.status === "running" || task.status === "waiting")) {
71
- return { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." };
72
- }
73
- return task;
74
- });
75
- saveRunTasks(loaded.manifest, tasks);
76
- try {
77
- saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
78
- } catch (error) {
79
- logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
80
- }
81
- try {
82
- writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
83
- } catch (error) {
84
- logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
85
- }
86
- const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
87
-
88
- // Build descriptive message including foreign/missing info
89
- const parts = [`Cancelled run ${updated.runId}.`];
90
- if (abortResult.foreignIds.length > 0) parts.push(` ${abortResult.foreignIds.length} task(s) belong to another session and were not cancelled: ${abortResult.foreignIds.join(", ")}.`);
91
- if (abortResult.missingIds.length > 0) parts.push(` ${abortResult.missingIds.length} task ID(s) not found: ${abortResult.missingIds.join(", ")}.`);
92
-
93
- return result(parts.join(""), {
94
- action: "cancel",
95
- status: "ok",
96
- runId: updated.runId,
97
- artifactsRoot: updated.artifactsRoot,
98
- abortedIds: abortResult.abortedIds,
99
- missingIds: abortResult.missingIds,
100
- foreignIds: abortResult.foreignIds,
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 pruned = pruneFinishedRuns(ctx.cwd, keep);
56
- 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" });
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
- appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths } });
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 { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
5
- import { logInternalError } from "../../utils/internal-error.ts";
6
- import type { PiTeamsToolResult } from "../tool-result.ts";
7
- import { result, type TeamContext } from "./context.ts";
8
-
9
- /**
10
- * Handle `respond` action: send a message to a waiting (interactive) task.
11
- * The task must be in "waiting" status. The message is stored in the task's
12
- * mailbox and the task is transitioned back to "running".
13
- */
14
- export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
15
- if (!params.runId) return result("Respond requires runId.", { action: "respond", status: "error" }, true);
16
- if (!params.message && !params.taskId) return result("Respond requires taskId and/or message.", { action: "respond", status: "error" }, true);
17
-
18
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
19
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
20
-
21
- return withRunLockSync(loaded.manifest, () => {
22
- const taskId = params.taskId;
23
- const message = params.message ?? "";
24
-
25
- // Find the waiting task(s)
26
- const targetTasks = taskId
27
- ? loaded.tasks.filter((t) => t.id === taskId)
28
- : loaded.tasks.filter((t) => t.status === "waiting");
29
-
30
- if (targetTasks.length === 0) {
31
- return result(
32
- taskId ? `Task '${taskId}' not found or not in waiting state.` : `No waiting tasks in run ${loaded.manifest.runId}.`,
33
- { action: "respond", status: "error" },
34
- true,
35
- );
36
- }
37
-
38
- // Transition waiting tasks back to running
39
- const updatedTasks = loaded.tasks.map((task) => {
40
- if (task.status !== "waiting") return task;
41
- if (taskId && task.id !== taskId) return task;
42
- return {
43
- ...task,
44
- status: "running" as const,
45
- // Store the response in the task's adaptive field
46
- adaptive: {
47
- ...task.adaptive,
48
- phase: "resumed",
49
- task: message || task.adaptive?.task || "",
50
- },
51
- };
52
- });
53
-
54
- saveRunTasks(loaded.manifest, updatedTasks);
55
- try {
56
- saveCrewAgents(loaded.manifest, updatedTasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
57
- } catch (error) {
58
- logInternalError("team-tool.handleRespond.crewAgents", error, `runId=${loaded.manifest.runId}`);
59
- }
60
-
61
- const resumedIds = targetTasks.map((t) => t.id);
62
- return result(
63
- `Resumed ${resumedIds.length} task(s): ${resumedIds.join(", ")}. Message: ${message || "(no message)"}`,
64
- { action: "respond", status: "ok", runId: loaded.manifest.runId, resumedIds },
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
- const spawned = spawnBackgroundTeamRun(updatedManifest);
114
- const asyncManifest = { ...updatedManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
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(updatedManifest.eventsPath, { type: "async.spawned", runId: updatedManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
117
- scheduleBackgroundEarlyExitGuard(ctx.cwd, updatedManifest.runId, spawned.pid, spawned.logPath);
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
- const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
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
- await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, onJsonEvent: ctx.onJsonEvent });
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
- const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, onJsonEvent: ctx.onJsonEvent });
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}`,