pi-crew 0.1.41 → 0.1.44

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 (191) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  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/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-phase10-distillation.md +199 -0
  21. package/docs/research-phase11-distillation.md +201 -0
  22. package/docs/research-pi-coding-agent.md +357 -357
  23. package/docs/research-source-pi-crew-reference.md +174 -174
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/src/agents/agent-serializer.ts +34 -34
  29. package/src/agents/discover-agents.ts +5 -4
  30. package/src/config/config.ts +28 -4
  31. package/src/extension/cross-extension-rpc.ts +82 -82
  32. package/src/extension/management.ts +37 -8
  33. package/src/extension/notification-router.ts +2 -2
  34. package/src/extension/register.ts +130 -8
  35. package/src/extension/registration/commands.ts +11 -9
  36. package/src/extension/registration/compaction-guard.ts +125 -125
  37. package/src/extension/registration/subagent-tools.ts +28 -19
  38. package/src/extension/registration/team-tool.ts +2 -1
  39. package/src/extension/result-watcher.ts +4 -4
  40. package/src/extension/run-bundle-schema.ts +8 -4
  41. package/src/extension/run-import.ts +4 -0
  42. package/src/extension/run-index.ts +23 -1
  43. package/src/extension/run-maintenance.ts +43 -24
  44. package/src/extension/team-tool/api.ts +2 -2
  45. package/src/extension/team-tool/cancel.ts +76 -4
  46. package/src/extension/team-tool/context.ts +1 -0
  47. package/src/extension/team-tool/doctor.ts +8 -1
  48. package/src/extension/team-tool/handle-settings.ts +188 -0
  49. package/src/extension/team-tool/inspect.ts +41 -41
  50. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  51. package/src/extension/team-tool/plan.ts +19 -19
  52. package/src/extension/team-tool/respond.ts +67 -0
  53. package/src/extension/team-tool/run.ts +6 -4
  54. package/src/extension/team-tool/status.ts +99 -93
  55. package/src/extension/team-tool-types.ts +4 -0
  56. package/src/extension/team-tool.ts +5 -1
  57. package/src/i18n.ts +184 -0
  58. package/src/observability/correlation.ts +2 -2
  59. package/src/observability/event-to-metric.ts +10 -3
  60. package/src/observability/exporters/adapter.ts +7 -1
  61. package/src/observability/exporters/otlp-exporter.ts +14 -2
  62. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  63. package/src/observability/metric-registry.ts +18 -3
  64. package/src/observability/metric-retention.ts +11 -3
  65. package/src/observability/metric-sink.ts +9 -4
  66. package/src/observability/metrics-primitives.ts +4 -3
  67. package/src/prompt/prompt-runtime.ts +72 -68
  68. package/src/runtime/agent-control.ts +63 -63
  69. package/src/runtime/agent-memory.ts +72 -72
  70. package/src/runtime/agent-observability.ts +114 -114
  71. package/src/runtime/async-marker.ts +26 -26
  72. package/src/runtime/attention-events.ts +28 -23
  73. package/src/runtime/background-runner.ts +53 -53
  74. package/src/runtime/child-pi.ts +4 -4
  75. package/src/runtime/completion-guard.ts +95 -4
  76. package/src/runtime/concurrency.ts +1 -1
  77. package/src/runtime/crash-recovery.ts +32 -1
  78. package/src/runtime/crew-agent-runtime.ts +59 -58
  79. package/src/runtime/deadletter.ts +14 -4
  80. package/src/runtime/delivery-coordinator.ts +143 -0
  81. package/src/runtime/direct-run.ts +35 -35
  82. package/src/runtime/foreground-control.ts +82 -82
  83. package/src/runtime/green-contract.ts +46 -46
  84. package/src/runtime/group-join.ts +106 -106
  85. package/src/runtime/heartbeat-gradient.ts +28 -28
  86. package/src/runtime/heartbeat-watcher.ts +48 -4
  87. package/src/runtime/live-agent-control.ts +87 -87
  88. package/src/runtime/live-agent-manager.ts +85 -85
  89. package/src/runtime/live-control-realtime.ts +36 -36
  90. package/src/runtime/live-session-runtime.ts +305 -305
  91. package/src/runtime/manifest-cache.ts +2 -2
  92. package/src/runtime/model-fallback.ts +272 -261
  93. package/src/runtime/overflow-recovery.ts +157 -0
  94. package/src/runtime/parallel-research.ts +44 -44
  95. package/src/runtime/parallel-utils.ts +1 -1
  96. package/src/runtime/pi-json-output.ts +111 -111
  97. package/src/runtime/policy-engine.ts +79 -78
  98. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  99. package/src/runtime/process-status.ts +56 -56
  100. package/src/runtime/progress-event-coalescer.ts +43 -43
  101. package/src/runtime/recovery-recipes.ts +74 -74
  102. package/src/runtime/retry-executor.ts +5 -0
  103. package/src/runtime/role-permission.ts +39 -39
  104. package/src/runtime/runtime-resolver.ts +1 -1
  105. package/src/runtime/session-resources.ts +25 -0
  106. package/src/runtime/session-snapshot.ts +59 -0
  107. package/src/runtime/session-usage.ts +79 -79
  108. package/src/runtime/sidechain-output.ts +29 -29
  109. package/src/runtime/stale-reconciler.ts +179 -0
  110. package/src/runtime/subagent-manager.ts +3 -3
  111. package/src/runtime/supervisor-contact.ts +59 -0
  112. package/src/runtime/task-display.ts +38 -38
  113. package/src/runtime/task-output-context.ts +127 -127
  114. package/src/runtime/task-runner/live-executor.ts +101 -101
  115. package/src/runtime/task-runner/progress.ts +119 -111
  116. package/src/runtime/task-runner/result-utils.ts +14 -14
  117. package/src/runtime/task-runner/state-helpers.ts +22 -22
  118. package/src/runtime/task-runner.ts +14 -0
  119. package/src/runtime/team-runner.ts +9 -10
  120. package/src/runtime/worker-heartbeat.ts +21 -21
  121. package/src/runtime/worker-startup.ts +57 -57
  122. package/src/schema/config-schema.ts +2 -1
  123. package/src/schema/team-tool-schema.ts +115 -109
  124. package/src/state/artifact-store.ts +4 -2
  125. package/src/state/atomic-write.ts +12 -4
  126. package/src/state/contracts.ts +109 -105
  127. package/src/state/event-log.ts +3 -4
  128. package/src/state/jsonl-writer.ts +4 -1
  129. package/src/state/locks.ts +9 -1
  130. package/src/state/task-claims.ts +44 -42
  131. package/src/state/usage.ts +29 -29
  132. package/src/subagents/async-entry.ts +1 -1
  133. package/src/subagents/index.ts +3 -3
  134. package/src/subagents/live/control.ts +1 -1
  135. package/src/subagents/live/manager.ts +1 -1
  136. package/src/subagents/live/realtime.ts +1 -1
  137. package/src/subagents/live/session-runtime.ts +1 -1
  138. package/src/subagents/manager.ts +1 -1
  139. package/src/subagents/spawn.ts +1 -1
  140. package/src/teams/discover-teams.ts +2 -2
  141. package/src/teams/team-serializer.ts +38 -38
  142. package/src/types/diff.d.ts +18 -18
  143. package/src/ui/crew-footer.ts +101 -101
  144. package/src/ui/crew-select-list.ts +111 -111
  145. package/src/ui/crew-widget.ts +5 -4
  146. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  147. package/src/ui/dynamic-border.ts +25 -25
  148. package/src/ui/layout-primitives.ts +106 -106
  149. package/src/ui/live-run-sidebar.ts +1 -1
  150. package/src/ui/loaders.ts +158 -158
  151. package/src/ui/mascot.ts +3 -2
  152. package/src/ui/powerbar-publisher.ts +7 -6
  153. package/src/ui/render-diff.ts +119 -119
  154. package/src/ui/render-scheduler.ts +54 -14
  155. package/src/ui/run-dashboard.ts +39 -11
  156. package/src/ui/run-snapshot-cache.ts +336 -36
  157. package/src/ui/spinner.ts +17 -17
  158. package/src/ui/status-colors.ts +58 -54
  159. package/src/ui/syntax-highlight.ts +116 -116
  160. package/src/ui/theme-adapter.ts +1 -1
  161. package/src/ui/transcript-viewer.ts +7 -2
  162. package/src/utils/atomic-write.ts +33 -0
  163. package/src/utils/completion-dedupe.ts +63 -63
  164. package/src/utils/file-coalescer.ts +5 -3
  165. package/src/utils/frontmatter.ts +68 -36
  166. package/src/utils/git.ts +262 -262
  167. package/src/utils/ids.ts +12 -12
  168. package/src/utils/internal-error.ts +1 -1
  169. package/src/utils/names.ts +27 -26
  170. package/src/utils/paths.ts +1 -1
  171. package/src/utils/redaction.ts +44 -41
  172. package/src/utils/safe-paths.ts +47 -34
  173. package/src/utils/sleep.ts +2 -2
  174. package/src/utils/timings.ts +2 -0
  175. package/src/utils/visual.ts +9 -1
  176. package/src/workflows/discover-workflows.ts +4 -1
  177. package/src/workflows/validate-workflow.ts +40 -40
  178. package/src/worktree/branch-freshness.ts +45 -45
  179. package/src/worktree/worktree-manager.ts +6 -1
  180. package/teams/default.team.md +12 -12
  181. package/teams/fast-fix.team.md +11 -11
  182. package/teams/implementation.team.md +18 -18
  183. package/teams/parallel-research.team.md +14 -14
  184. package/teams/research.team.md +11 -11
  185. package/teams/review.team.md +12 -12
  186. package/workflows/default.workflow.md +29 -29
  187. package/workflows/fast-fix.workflow.md +22 -22
  188. package/workflows/implementation.workflow.md +38 -38
  189. package/workflows/parallel-research.workflow.md +46 -46
  190. package/workflows/research.workflow.md +22 -22
  191. package/workflows/review.workflow.md +30 -30
@@ -7,13 +7,71 @@ import { logInternalError } from "../../utils/internal-error.ts";
7
7
  import type { PiTeamsToolResult } from "../tool-result.ts";
8
8
  import { result, type TeamContext } from "./context.ts";
9
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
+
10
58
  export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
11
59
  if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
12
60
  const loaded = loadRunManifestById(ctx.cwd, params.runId);
13
61
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
14
62
  return withRunLockSync(loaded.manifest, () => {
15
- if (loaded.manifest.status === "completed" && !params.force) return result(`Run ${loaded.manifest.runId} is already completed; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
16
- const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
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
+ });
17
75
  saveRunTasks(loaded.manifest, tasks);
18
76
  try {
19
77
  saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
@@ -26,6 +84,20 @@ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiT
26
84
  logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
27
85
  }
28
86
  const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
29
- return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
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
+ });
30
102
  });
31
- }
103
+ }
@@ -11,6 +11,7 @@ export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<Extension
11
11
  signal?: AbortSignal;
12
12
  startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
13
13
  onRunStarted?: (runId: string) => void;
14
+ onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
14
15
  };
15
16
 
16
17
  export function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
@@ -52,7 +52,14 @@ function checkWritableDir(dir: string): { ok: boolean; detail: string } {
52
52
  try {
53
53
  if (!fs.existsSync(dir)) return { ok: false, detail: `${dir}: missing` };
54
54
  if (!fs.statSync(dir).isDirectory()) return { ok: false, detail: `${dir}: not a directory` };
55
- fs.accessSync(dir, fs.constants.W_OK);
55
+ // fs.accessSync(W_OK) is unreliable on Windows; verify by writing a temp file.
56
+ const probePath = `${dir}/.pi-crew-write-test`;
57
+ try {
58
+ fs.writeFileSync(probePath, "ok", "utf-8");
59
+ fs.rmSync(probePath, { force: true });
60
+ } catch {
61
+ return { ok: false, detail: `${dir}: not writable (write test failed)` };
62
+ }
56
63
  return { ok: true, detail: dir };
57
64
  } catch (error) {
58
65
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,188 @@
1
+ import type { TeamContext } from "../team-tool/context.ts";
2
+ import { loadConfig, updateConfig } from "../../config/config.ts";
3
+ import { configPatchFromConfig } from "../team-tool/config-patch.ts";
4
+ import { result } from "../team-tool/context.ts";
5
+ import type { PiTeamsToolResult } from "../tool-result.ts";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function setNested(obj: Record<string, unknown>, path: string, value: unknown): void {
12
+ const keys = path.split(".");
13
+ let target: Record<string, unknown> = obj;
14
+ for (let i = 0; i < keys.length - 1; i++) {
15
+ if (!target[keys[i]] || typeof target[keys[i]] !== "object") {
16
+ target[keys[i]] = {};
17
+ }
18
+ target = target[keys[i]] as Record<string, unknown>;
19
+ }
20
+ target[keys[keys.length - 1]] = value;
21
+ }
22
+
23
+ function getNested(obj: Record<string, unknown>, path: string): unknown {
24
+ const keys = path.split(".");
25
+ let current: unknown = obj;
26
+ for (const key of keys) {
27
+ if (!current || typeof current !== "object") return undefined;
28
+ current = (current as Record<string, unknown>)[key];
29
+ }
30
+ return current;
31
+ }
32
+
33
+ function formatValue(value: unknown): string {
34
+ if (value === undefined) return "<not set>";
35
+ if (typeof value === "object") return JSON.stringify(value);
36
+ return String(value);
37
+ }
38
+
39
+ function parseValue(raw: string): unknown {
40
+ // JSON handles strings (quoted), numbers, booleans, null, arrays, objects.
41
+ try { return JSON.parse(raw); } catch { /* keep as string */ }
42
+ return raw;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Known config keys — mirrors config-schema.ts + config.ts.
47
+ // When adding new config fields, add the dotted path here so team-settings
48
+ // can discover and display them.
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const KNOWN_KEYS = new Set([
52
+ // top-level
53
+ "asyncByDefault",
54
+ "executeWorkers",
55
+ "notifierIntervalMs",
56
+ "requireCleanWorktreeLeader",
57
+ // runtime
58
+ "runtime.mode",
59
+ "runtime.preferLiveSession",
60
+ "runtime.allowChildProcessFallback",
61
+ "runtime.maxTurns",
62
+ "runtime.graceTurns",
63
+ "runtime.inheritContext",
64
+ "runtime.promptMode",
65
+ "runtime.groupJoin",
66
+ "runtime.groupJoinAckTimeoutMs",
67
+ "runtime.requirePlanApproval",
68
+ "runtime.completionMutationGuard",
69
+ // limits
70
+ "limits.maxConcurrentWorkers",
71
+ "limits.allowUnboundedConcurrency",
72
+ "limits.maxTaskDepth",
73
+ "limits.maxChildrenPerTask",
74
+ "limits.maxRunMinutes",
75
+ "limits.maxRetriesPerTask",
76
+ "limits.maxTasksPerRun",
77
+ "limits.heartbeatStaleMs",
78
+ // control
79
+ "control.enabled",
80
+ "control.needsAttentionAfterMs",
81
+ // autonomous
82
+ "autonomous.profile",
83
+ "autonomous.enabled",
84
+ "autonomous.injectPolicy",
85
+ "autonomous.preferAsyncForLongTasks",
86
+ "autonomous.allowWorktreeSuggestion",
87
+ // tools
88
+ "tools.enableClaudeStyleAliases",
89
+ "tools.enableSteer",
90
+ "tools.terminateOnForeground",
91
+ // agents
92
+ "agents.disableBuiltins",
93
+ // observability
94
+ "observability.prometheus.enabled",
95
+ "observability.otlp.enabled",
96
+ // worktree
97
+ "worktree.enabled",
98
+ ]);
99
+
100
+ const KNOWN_SORTED = [...KNOWN_KEYS].sort();
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Detail objects – all require { action, status } from TeamToolDetails.
104
+ // Extras (count, key, value, path) are passed as never to bypass the narrow
105
+ // TeamToolDetails interface (consistent with the rest of the codebase).
106
+ // ---------------------------------------------------------------------------
107
+
108
+ const OK = { action: "settings", status: "ok" as const };
109
+ const ERR = { action: "settings", status: "error" as const };
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Main handler
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export function handleSettings(params: { config?: Record<string, unknown> }, ctx: TeamContext): PiTeamsToolResult {
116
+ const cfg = (params.config ?? {}) as Record<string, unknown>;
117
+ const args = typeof cfg.args === "string" ? cfg.args.trim() : "";
118
+ const scope = cfg.scope === "project" ? "project" : "user";
119
+ const loaded = loadConfig(ctx.cwd);
120
+ const effective = loaded.config as Record<string, unknown>;
121
+
122
+ // team-settings list
123
+ if (!args || args === "list") {
124
+ const lines = ["pi-crew settings:", `Path: ${loaded.path}`, ""];
125
+ for (const key of KNOWN_SORTED) {
126
+ const value = getNested(effective, key);
127
+ lines.push(` ${key} = ${formatValue(value)}`);
128
+ }
129
+ lines.push("", "Usage: team-settings [list|get <key>|set <key> <value>|unset <key>|path|scope]");
130
+ return result(lines.join("\n"), { ...OK, count: KNOWN_KEYS.size } as never);
131
+ }
132
+
133
+ // team-settings path
134
+ if (args === "path") {
135
+ return result(`pi-crew config path: ${loaded.path}`, { ...OK, path: loaded.path } as never);
136
+ }
137
+
138
+ // team-settings scope
139
+ if (args === "scope") {
140
+ return result(`Current scope: ${scope}\nConfig at: ${loaded.path}`, { ...OK, scope } as never);
141
+ }
142
+
143
+ // team-settings get <key>
144
+ if (args.startsWith("get ")) {
145
+ const key = args.slice(4).trim();
146
+ if (!key) return result("Usage: team-settings get <key>", { ...ERR }, true);
147
+ const value = getNested(effective, key);
148
+ const note = KNOWN_KEYS.has(key) ? "" : " (unknown key — may not take effect)";
149
+ return result(`${key} = ${formatValue(value)}${note}`, { ...OK, key, value } as never);
150
+ }
151
+
152
+ // team-settings unset <key>
153
+ if (args.startsWith("unset ")) {
154
+ const key = args.slice(6).trim();
155
+ if (!key) return result("Usage: team-settings unset <key>", { ...ERR }, true);
156
+ try {
157
+ const saved = updateConfig({}, { cwd: ctx.cwd, scope, unsetPaths: [key] });
158
+ return result(`Unset ${key}\nPath: ${saved.path}`, { ...OK, key } as never);
159
+ } catch (error) {
160
+ return result(error instanceof Error ? error.message : String(error), { ...ERR }, true);
161
+ }
162
+ }
163
+
164
+ // team-settings set <key> <value>
165
+ if (args.startsWith("set ")) {
166
+ const rest = args.slice(4).trim();
167
+ const spaceIdx = rest.indexOf(" ");
168
+ if (spaceIdx === -1) return result("Usage: team-settings set <key> <value>", { ...ERR }, true);
169
+ const key = rest.slice(0, spaceIdx);
170
+ const rawValue = rest.slice(spaceIdx + 1).trim();
171
+ if (!key) return result("Usage: team-settings set <key> <value>", { ...ERR }, true);
172
+
173
+ const value = parseValue(rawValue);
174
+ const patch = {};
175
+ setNested(patch as Record<string, unknown>, key, value);
176
+
177
+ try {
178
+ const converted = configPatchFromConfig({ config: patch as Record<string, unknown> });
179
+ const saved = updateConfig(converted, { cwd: ctx.cwd, scope });
180
+ const warning = KNOWN_KEYS.has(key) ? "" : "\nWarning: unknown key — verify it exists in config schema.";
181
+ return result(`Set ${key} = ${formatValue(value)}\nPath: ${saved.path}${warning}`, { ...OK, key, value } as never);
182
+ } catch (error) {
183
+ return result(error instanceof Error ? error.message : String(error), { ...ERR }, true);
184
+ }
185
+ }
186
+
187
+ return result("Unknown subcommand. Usage: team-settings [list|get <key>|set <key> <value>|unset <key>|path|scope]", { ...ERR }, true);
188
+ }
@@ -1,41 +1,41 @@
1
- import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
2
- import { readEvents } from "../../state/event-log.ts";
3
- import { loadRunManifestById } from "../../state/state-store.ts";
4
- import { aggregateUsage, formatUsage } from "../../state/usage.ts";
5
- import type { PiTeamsToolResult } from "../tool-result.ts";
6
- import { result, type TeamContext } from "./context.ts";
7
-
8
- export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
- if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
10
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
11
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
12
- const events = readEvents(loaded.manifest.eventsPath);
13
- const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
14
- return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
15
- }
16
-
17
- export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
18
- if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
19
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
20
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
21
- const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
22
- return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
23
- }
24
-
25
- export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
26
- if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
27
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
28
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
29
- const usage = aggregateUsage(loaded.tasks);
30
- const lines = [
31
- `Summary for ${loaded.manifest.runId}`,
32
- `Status: ${loaded.manifest.status}`,
33
- `Team: ${loaded.manifest.team}`,
34
- `Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
35
- `Goal: ${loaded.manifest.goal}`,
36
- `Usage: ${formatUsage(usage)}`,
37
- "Tasks:",
38
- ...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
39
- ];
40
- return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
41
- }
1
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
2
+ import { readEvents } from "../../state/event-log.ts";
3
+ import { loadRunManifestById } from "../../state/state-store.ts";
4
+ import { aggregateUsage, formatUsage } from "../../state/usage.ts";
5
+ import type { PiTeamsToolResult } from "../tool-result.ts";
6
+ import { result, type TeamContext } from "./context.ts";
7
+
8
+ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
+ if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
10
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
11
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
12
+ const events = readEvents(loaded.manifest.eventsPath);
13
+ const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
14
+ return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
15
+ }
16
+
17
+ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
18
+ if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
19
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
20
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
21
+ const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
22
+ return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
23
+ }
24
+
25
+ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
26
+ if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
27
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
28
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
29
+ const usage = aggregateUsage(loaded.tasks);
30
+ const lines = [
31
+ `Summary for ${loaded.manifest.runId}`,
32
+ `Status: ${loaded.manifest.status}`,
33
+ `Team: ${loaded.manifest.team}`,
34
+ `Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
35
+ `Goal: ${loaded.manifest.goal}`,
36
+ `Usage: ${formatUsage(usage)}`,
37
+ "Tasks:",
38
+ ...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
39
+ ];
40
+ return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
41
+ }
@@ -1,79 +1,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
-
13
- export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
14
- if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
15
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
16
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
17
- const withWorktrees = loaded.tasks.filter((task) => task.worktree);
18
- 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)"])];
19
- return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
20
- }
21
-
22
- export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
23
- const imports = listImportedRuns(ctx.cwd);
24
- 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)"])];
25
- return result(lines.join("\n"), { action: "imports", status: "ok" });
26
- }
27
-
28
- export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
29
- const cfg = configRecord(params.config);
30
- const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
31
- if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
32
- const scope = cfg.scope === "user" ? "user" : "project";
33
- try {
34
- const imported = importRunBundle(ctx.cwd, bundlePath, scope);
35
- return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
36
- } catch (error) {
37
- const message = error instanceof Error ? error.message : String(error);
38
- return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
39
- }
40
- }
41
-
42
- export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
43
- if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
44
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
45
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
46
- const exported = exportRunBundle(loaded.manifest, loaded.tasks);
47
- appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
48
- 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 });
49
- }
50
-
51
- export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
52
- const keep = params.keep ?? 20;
53
- if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
54
- 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" });
57
- }
58
-
59
- export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
60
- if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
61
- if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
62
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
63
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
64
- const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
65
- 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);
66
- fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
67
- 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 });
69
- }
70
-
71
- export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
72
- if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
73
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
74
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
75
- 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 } });
77
- 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 });
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
+
13
+ export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
14
+ if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
15
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
16
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
17
+ const withWorktrees = loaded.tasks.filter((task) => task.worktree);
18
+ 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)"])];
19
+ return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
20
+ }
21
+
22
+ export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
23
+ const imports = listImportedRuns(ctx.cwd);
24
+ 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)"])];
25
+ return result(lines.join("\n"), { action: "imports", status: "ok" });
26
+ }
27
+
28
+ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
29
+ const cfg = configRecord(params.config);
30
+ const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
31
+ if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
32
+ const scope = cfg.scope === "user" ? "user" : "project";
33
+ try {
34
+ const imported = importRunBundle(ctx.cwd, bundlePath, scope);
35
+ return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
39
+ }
40
+ }
41
+
42
+ export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
43
+ if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
44
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
45
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
46
+ const exported = exportRunBundle(loaded.manifest, loaded.tasks);
47
+ appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
48
+ 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 });
49
+ }
50
+
51
+ export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
52
+ const keep = params.keep ?? 20;
53
+ if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
54
+ 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" });
57
+ }
58
+
59
+ export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
60
+ if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
61
+ if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
62
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
63
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
64
+ const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
65
+ 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);
66
+ fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
67
+ 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 });
69
+ }
70
+
71
+ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
72
+ if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
73
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
74
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
75
+ 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 } });
77
+ 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 });
79
+ }
@@ -1,19 +1,19 @@
1
- import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
2
- import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
3
- import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
4
- import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
- import type { PiTeamsToolResult } from "../tool-result.ts";
6
- import { result, type TeamContext } from "./context.ts";
7
-
8
- export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
- const teamName = params.team ?? "default";
10
- const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === teamName);
11
- if (!team) return result(`Team '${teamName}' not found.`, { action: "plan", status: "error" }, true);
12
- const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
13
- const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === workflowName);
14
- if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "plan", status: "error" }, true);
15
- const errors = validateWorkflowForTeam(workflow, team);
16
- if (errors.length > 0) return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...errors.map((error) => `- ${error}`)].join("\n"), { action: "plan", status: "error" }, true);
17
- const lines = [`Team plan: ${team.name}`, `Workflow: ${workflow.name}`, `Goal: ${params.goal ?? params.task ?? "(not provided)"}`, "", "Steps:", ...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`)];
18
- return result(lines.join("\n"), { action: "plan", status: "ok" });
19
- }
1
+ import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
2
+ import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
3
+ import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
4
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
+ import type { PiTeamsToolResult } from "../tool-result.ts";
6
+ import { result, type TeamContext } from "./context.ts";
7
+
8
+ export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
+ const teamName = params.team ?? "default";
10
+ const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === teamName);
11
+ if (!team) return result(`Team '${teamName}' not found.`, { action: "plan", status: "error" }, true);
12
+ const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
13
+ const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === workflowName);
14
+ if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "plan", status: "error" }, true);
15
+ const errors = validateWorkflowForTeam(workflow, team);
16
+ if (errors.length > 0) return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...errors.map((error) => `- ${error}`)].join("\n"), { action: "plan", status: "error" }, true);
17
+ const lines = [`Team plan: ${team.name}`, `Workflow: ${workflow.name}`, `Goal: ${params.goal ?? params.task ?? "(not provided)"}`, "", "Steps:", ...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`)];
18
+ return result(lines.join("\n"), { action: "plan", status: "ok" });
19
+ }