pi-crew 0.1.51 → 0.2.1

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 (240) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +176 -781
  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 +70 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/actions-reference.md +595 -0
  14. package/docs/commands-reference.md +347 -0
  15. package/docs/runtime-flow.md +148 -148
  16. package/index.ts +6 -6
  17. package/package.json +99 -99
  18. package/skills/async-worker-recovery/SKILL.md +42 -42
  19. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  20. package/skills/delegation-patterns/SKILL.md +54 -54
  21. package/skills/mailbox-interactive/SKILL.md +40 -40
  22. package/skills/model-routing-context/SKILL.md +39 -39
  23. package/skills/multi-perspective-review/SKILL.md +58 -58
  24. package/skills/observability-reliability/SKILL.md +41 -41
  25. package/skills/orchestration/SKILL.md +157 -157
  26. package/skills/ownership-session-security/SKILL.md +41 -41
  27. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  28. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  29. package/skills/resource-discovery-config/SKILL.md +41 -41
  30. package/skills/runtime-state-reader/SKILL.md +44 -44
  31. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  32. package/skills/state-mutation-locking/SKILL.md +42 -42
  33. package/skills/systematic-debugging/SKILL.md +67 -67
  34. package/skills/ui-render-performance/SKILL.md +39 -39
  35. package/skills/verification-before-done/SKILL.md +57 -57
  36. package/skills/worktree-isolation/SKILL.md +39 -39
  37. package/src/adapters/claude-adapter.ts +25 -0
  38. package/src/adapters/codex-adapter.ts +21 -0
  39. package/src/adapters/cursor-adapter.ts +17 -0
  40. package/src/adapters/export-util.ts +137 -0
  41. package/src/adapters/index.ts +15 -0
  42. package/src/adapters/registry.ts +18 -0
  43. package/src/adapters/types.ts +23 -0
  44. package/src/agents/agent-config.ts +2 -0
  45. package/src/agents/agent-search.ts +98 -98
  46. package/src/agents/discover-agents.ts +2 -1
  47. package/src/config/config.ts +13 -1
  48. package/src/config/drift-detector.ts +211 -0
  49. package/src/config/markers.ts +327 -0
  50. package/src/config/resilient-parser.ts +108 -0
  51. package/src/config/suggestions.ts +74 -0
  52. package/src/extension/cross-extension-rpc.ts +103 -94
  53. package/src/extension/project-init.ts +21 -1
  54. package/src/extension/register.ts +45 -14
  55. package/src/extension/registration/commands.ts +77 -8
  56. package/src/extension/registration/subagent-tools.ts +10 -1
  57. package/src/extension/registration/team-tool.ts +10 -1
  58. package/src/extension/registration/viewers.ts +48 -34
  59. package/src/extension/run-bundle-schema.ts +89 -89
  60. package/src/extension/run-import.ts +25 -1
  61. package/src/extension/run-index.ts +5 -1
  62. package/src/extension/run-maintenance.ts +142 -68
  63. package/src/extension/team-manager-command.ts +10 -1
  64. package/src/extension/team-tool/api.ts +441 -441
  65. package/src/extension/team-tool/doctor.ts +28 -3
  66. package/src/extension/team-tool/handle-settings.ts +195 -188
  67. package/src/extension/team-tool/inspect.ts +41 -41
  68. package/src/extension/team-tool/intent-policy.ts +42 -42
  69. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  70. package/src/extension/team-tool/plan.ts +19 -19
  71. package/src/extension/team-tool/run.ts +12 -1
  72. package/src/extension/team-tool.ts +332 -322
  73. package/src/i18n.ts +184 -184
  74. package/src/observability/exporters/otlp-exporter.ts +92 -77
  75. package/src/prompt/prompt-runtime.ts +72 -72
  76. package/src/runtime/agent-memory.ts +72 -72
  77. package/src/runtime/agent-observability.ts +114 -114
  78. package/src/runtime/async-marker.ts +26 -26
  79. package/src/runtime/attention-events.ts +28 -28
  80. package/src/runtime/auto-resume.ts +100 -0
  81. package/src/runtime/background-runner.ts +11 -1
  82. package/src/runtime/cancellation-token.ts +89 -89
  83. package/src/runtime/cancellation.ts +61 -61
  84. package/src/runtime/capability-inventory.ts +116 -116
  85. package/src/runtime/child-pi.ts +7 -2
  86. package/src/runtime/compaction-summary.ts +271 -0
  87. package/src/runtime/completion-guard.ts +190 -190
  88. package/src/runtime/crash-recovery.ts +33 -1
  89. package/src/runtime/delta-conflict.ts +360 -0
  90. package/src/runtime/direct-run.ts +35 -35
  91. package/src/runtime/foreground-control.ts +82 -82
  92. package/src/runtime/green-contract.ts +46 -46
  93. package/src/runtime/group-join.ts +106 -106
  94. package/src/runtime/heartbeat-gradient.ts +28 -28
  95. package/src/runtime/heartbeat-watcher.ts +124 -124
  96. package/src/runtime/iteration-hooks.ts +264 -0
  97. package/src/runtime/live-agent-control.ts +88 -88
  98. package/src/runtime/live-control-realtime.ts +36 -36
  99. package/src/runtime/live-extension-bridge.ts +150 -150
  100. package/src/runtime/live-irc.ts +92 -92
  101. package/src/runtime/live-session-health.ts +100 -100
  102. package/src/runtime/loop-gates.ts +129 -0
  103. package/src/runtime/metric-parser.ts +40 -0
  104. package/src/runtime/notebook-helpers.ts +90 -90
  105. package/src/runtime/orphan-sentinel.ts +7 -7
  106. package/src/runtime/parallel-research.ts +44 -44
  107. package/src/runtime/phase-progress.ts +217 -0
  108. package/src/runtime/pi-args.ts +38 -11
  109. package/src/runtime/pi-json-output.ts +111 -111
  110. package/src/runtime/pi-spawn.ts +57 -7
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/post-checks.ts +122 -0
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/prose-compressor.ts +164 -164
  115. package/src/runtime/recovery-recipes.ts +74 -74
  116. package/src/runtime/result-extractor.ts +121 -121
  117. package/src/runtime/role-permission.ts +39 -39
  118. package/src/runtime/sensitive-paths.ts +2 -2
  119. package/src/runtime/session-resources.ts +25 -25
  120. package/src/runtime/session-snapshot.ts +59 -59
  121. package/src/runtime/session-usage.ts +79 -79
  122. package/src/runtime/sidechain-output.ts +29 -29
  123. package/src/runtime/stream-preview.ts +177 -177
  124. package/src/runtime/supervisor-contact.ts +59 -59
  125. package/src/runtime/task-display.ts +38 -38
  126. package/src/runtime/task-graph.ts +207 -0
  127. package/src/runtime/task-quality.ts +207 -0
  128. package/src/runtime/task-runner/capabilities.ts +78 -78
  129. package/src/runtime/task-runner/live-executor.ts +7 -1
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  132. package/src/runtime/task-runner/result-utils.ts +14 -14
  133. package/src/runtime/task-runner/run-projection.ts +103 -103
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/team-runner.ts +117 -7
  136. package/src/runtime/worker-heartbeat.ts +21 -21
  137. package/src/runtime/worker-startup.ts +57 -57
  138. package/src/runtime/workflow-state.ts +187 -0
  139. package/src/runtime/workspace-tree.ts +298 -298
  140. package/src/schema/config-schema.ts +11 -0
  141. package/src/schema/validation-types.ts +148 -0
  142. package/src/skills/skill-templates.ts +374 -0
  143. package/src/state/active-run-registry.ts +35 -11
  144. package/src/state/atomic-write.ts +33 -26
  145. package/src/state/contracts.ts +1 -0
  146. package/src/state/event-reconstructor.ts +217 -0
  147. package/src/state/locks.ts +2 -13
  148. package/src/state/mailbox.ts +4 -3
  149. package/src/state/state-store.ts +16 -6
  150. package/src/state/task-claims.ts +44 -44
  151. package/src/state/types.ts +9 -0
  152. package/src/state/usage.ts +29 -29
  153. package/src/subagents/async-entry.ts +1 -1
  154. package/src/subagents/index.ts +3 -3
  155. package/src/subagents/live/control.ts +1 -1
  156. package/src/subagents/live/manager.ts +1 -1
  157. package/src/subagents/live/realtime.ts +1 -1
  158. package/src/subagents/live/session-runtime.ts +1 -1
  159. package/src/subagents/manager.ts +1 -1
  160. package/src/subagents/spawn.ts +1 -1
  161. package/src/teams/team-serializer.ts +38 -38
  162. package/src/types/diff.d.ts +18 -18
  163. package/src/ui/crew-footer.ts +101 -101
  164. package/src/ui/crew-select-list.ts +111 -111
  165. package/src/ui/crew-widget.ts +5 -2
  166. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  167. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  168. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  169. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  170. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  171. package/src/ui/dynamic-border.ts +25 -25
  172. package/src/ui/layout-primitives.ts +106 -106
  173. package/src/ui/loaders.ts +158 -158
  174. package/src/ui/render-coalescer.ts +51 -51
  175. package/src/ui/render-diff.ts +119 -119
  176. package/src/ui/render-scheduler.ts +143 -143
  177. package/src/ui/run-action-dispatcher.ts +10 -1
  178. package/src/ui/spinner.ts +17 -17
  179. package/src/ui/status-colors.ts +58 -58
  180. package/src/ui/syntax-highlight.ts +116 -116
  181. package/src/ui/transcript-entries.ts +258 -258
  182. package/src/utils/completion-dedupe.ts +63 -63
  183. package/src/utils/frontmatter.ts +68 -68
  184. package/src/utils/git.ts +262 -262
  185. package/src/utils/ids.ts +17 -17
  186. package/src/utils/incremental-reader.ts +104 -104
  187. package/src/utils/names.ts +27 -27
  188. package/src/utils/redaction.ts +44 -44
  189. package/src/utils/safe-paths.ts +47 -47
  190. package/src/utils/scan-cache.ts +136 -136
  191. package/src/utils/sleep.ts +40 -26
  192. package/src/utils/task-name-generator.ts +337 -337
  193. package/src/workflows/validate-workflow.ts +40 -40
  194. package/src/worktree/branch-freshness.ts +45 -45
  195. package/teams/default.team.md +12 -12
  196. package/teams/fast-fix.team.md +11 -11
  197. package/teams/implementation.team.md +18 -18
  198. package/teams/parallel-research.team.md +14 -14
  199. package/teams/research.team.md +11 -11
  200. package/teams/review.team.md +12 -12
  201. package/workflows/default.workflow.md +30 -29
  202. package/workflows/fast-fix.workflow.md +23 -22
  203. package/workflows/implementation.workflow.md +43 -43
  204. package/workflows/parallel-research.workflow.md +46 -46
  205. package/workflows/research.workflow.md +22 -22
  206. package/workflows/review.workflow.md +30 -30
  207. package/docs/refactor-tasks-phase3.md +0 -394
  208. package/docs/refactor-tasks-phase4.md +0 -564
  209. package/docs/refactor-tasks-phase5.md +0 -402
  210. package/docs/refactor-tasks-phase6.md +0 -662
  211. package/docs/refactor-tasks.md +0 -1484
  212. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  213. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  214. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  215. package/docs/research/AUDIT_PI_CREW.md +0 -457
  216. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  217. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  218. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  219. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  220. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  221. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  222. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  223. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  224. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  225. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  226. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  227. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  228. package/docs/research-extension-examples.md +0 -297
  229. package/docs/research-extension-system.md +0 -324
  230. package/docs/research-oh-my-pi-distillation.md +0 -369
  231. package/docs/research-optimization-plan.md +0 -548
  232. package/docs/research-phase10-distillation.md +0 -199
  233. package/docs/research-phase11-distillation.md +0 -201
  234. package/docs/research-phase8-operator-experience-plan.md +0 -819
  235. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  236. package/docs/research-pi-coding-agent.md +0 -357
  237. package/docs/research-source-pi-crew-reference.md +0 -174
  238. package/docs/research-ui-optimization-plan.md +0 -480
  239. package/docs/source-runtime-refactor-map.md +0 -107
  240. package/src/utils/atomic-write.ts +0 -33
@@ -11,6 +11,8 @@ import { checkProcessLiveness } from "./process-status.ts";
11
11
  import { reconcileStaleRun, type ReconcileResult } from "./stale-reconciler.ts";
12
12
  import { executeHook, appendHookEvent } from "../hooks/registry.ts";
13
13
  import { activeRunEntries, unregisterActiveRun, readActiveRunRegistry } from "../state/active-run-registry.ts";
14
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
15
+ import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
14
16
 
15
17
  export interface RecoveryPlan {
16
18
  runId: string;
@@ -168,6 +170,32 @@ export function cancelOrphanedRuns(
168
170
  * This is the **global** cleanup that cancelOrphanedRuns (project-scoped)
169
171
  * cannot reach.
170
172
  */
173
+ /**
174
+ * Best-effort removal of stateRoot and artifactsRoot directories for a purged run.
175
+ * Uses resolveRealContainedPath to ensure we only delete paths that are safely
176
+ * contained within a known crew root (project or user level).
177
+ */
178
+ function tryRemoveRunDirectories(entry: { stateRoot: string; cwd: string }): void {
179
+ const roots = [projectCrewRoot(entry.cwd), userCrewRoot()];
180
+ for (const root of roots) {
181
+ try {
182
+ resolveRealContainedPath(root, entry.stateRoot);
183
+ // If we get here, stateRoot is safely contained — remove it
184
+ fs.rmSync(entry.stateRoot, { recursive: true, force: true });
185
+ break;
186
+ } catch {
187
+ // Not contained in this root, try next
188
+ }
189
+ }
190
+ // NOTE: artifactsRoot is shared across runs and cleaned up by pruneFinishedRuns/pruneUserLevelRuns — not deleted here.
191
+ }
192
+
193
+ /**
194
+ * Purge the global active-run-index of entries whose manifest is no longer active.
195
+ *
196
+ * Note: This function only cleans user-level active run entries.
197
+ * Project-level stale runs are handled by session_start auto-prune triggered during run creation.
198
+ */
171
199
  export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.now()): { purged: string[]; kept: string[] } {
172
200
  const purged: string[] = [];
173
201
  const kept: string[] = [];
@@ -177,6 +205,7 @@ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.
177
205
  // 1. Manifest file gone → definitely stale
178
206
  if (!fs.existsSync(entry.manifestPath)) {
179
207
  unregisterActiveRun(entry.runId);
208
+ tryRemoveRunDirectories(entry);
180
209
  purged.push(entry.runId);
181
210
  continue;
182
211
  }
@@ -184,6 +213,7 @@ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.
184
213
  // 2. CWD gone → temp dir cleaned up
185
214
  if (!fs.existsSync(entry.cwd)) {
186
215
  unregisterActiveRun(entry.runId);
216
+ tryRemoveRunDirectories(entry);
187
217
  purged.push(entry.runId);
188
218
  continue;
189
219
  }
@@ -194,11 +224,12 @@ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.
194
224
  manifest = JSON.parse(fs.readFileSync(entry.manifestPath, "utf-8"));
195
225
  } catch {
196
226
  unregisterActiveRun(entry.runId);
227
+ tryRemoveRunDirectories(entry);
197
228
  purged.push(entry.runId);
198
229
  continue;
199
230
  }
200
231
 
201
- // 4. Terminal status → no longer active
232
+ // 4. Terminal status → no longer active (just unregister, don't delete files)
202
233
  const terminalStatuses = new Set(["completed", "failed", "cancelled", "blocked"]);
203
234
  if (manifest && terminalStatuses.has(manifest.status ?? "")) {
204
235
  unregisterActiveRun(entry.runId);
@@ -231,6 +262,7 @@ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.
231
262
  // Best-effort manifest cleanup
232
263
  }
233
264
  unregisterActiveRun(entry.runId);
265
+ tryRemoveRunDirectories(entry);
234
266
  purged.push(entry.runId);
235
267
  continue;
236
268
  }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Delta conflict detection for pi-crew import and resume operations.
3
+ *
4
+ * Compares incoming bundles against existing state to surface conflicts
5
+ * (file overwrites, status mismatches, schema drift, deleted resources)
6
+ * without blocking the operation — only reporting for user awareness.
7
+ */
8
+
9
+ // ── Types ──────────────────────────────────────────────────────────────
10
+
11
+ export type ConflictKind = "file_overwrite" | "state_mismatch" | "schema_drift" | "resource_deleted";
12
+
13
+ export interface Conflict {
14
+ kind: ConflictKind;
15
+ /** File or resource path that is in conflict. */
16
+ path: string;
17
+ /** Current value or summary (optional). */
18
+ existing?: string;
19
+ /** Incoming value or summary (optional). */
20
+ incoming?: string;
21
+ severity: "error" | "warning";
22
+ autoResolvable: boolean;
23
+ }
24
+
25
+ export interface ConflictReport {
26
+ hasConflicts: boolean;
27
+ conflicts: Conflict[];
28
+ summary: { errors: number; warnings: number; autoResolvable: number };
29
+ }
30
+
31
+ export type ConflictStrategy = "skip" | "overwrite" | "merge";
32
+
33
+ export interface ConflictResolution {
34
+ resolved: boolean;
35
+ action: string;
36
+ }
37
+
38
+ // ── Helpers ────────────────────────────────────────────────────────────
39
+
40
+ interface TaskLike {
41
+ id: string;
42
+ status: string;
43
+ role?: string;
44
+ agent?: string;
45
+ }
46
+
47
+ function buildReport(conflicts: Conflict[]): ConflictReport {
48
+ const errors = conflicts.filter((c) => c.severity === "error").length;
49
+ const warnings = conflicts.filter((c) => c.severity === "warning").length;
50
+ const autoResolvable = conflicts.filter((c) => c.autoResolvable).length;
51
+ return {
52
+ hasConflicts: conflicts.length > 0,
53
+ conflicts,
54
+ summary: { errors, warnings, autoResolvable },
55
+ };
56
+ }
57
+
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
60
+ }
61
+
62
+ /**
63
+ * Extract task-like objects from an unknown array, filtering out non-records.
64
+ */
65
+ function extractTaskLikes(tasks: unknown[]): TaskLike[] {
66
+ return tasks
67
+ .filter(isRecord)
68
+ .map((t) => ({
69
+ id: typeof t.id === "string" ? t.id : "",
70
+ status: typeof t.status === "string" ? t.status : "",
71
+ role: typeof t.role === "string" ? t.role : undefined,
72
+ agent: typeof t.agent === "string" ? t.agent : undefined,
73
+ }))
74
+ .filter((t) => t.id !== "");
75
+ }
76
+
77
+ // ── Import Conflict Detection ──────────────────────────────────────────
78
+
79
+ export interface ImportBundle {
80
+ manifest: Record<string, unknown>;
81
+ tasks: unknown[];
82
+ events?: unknown[];
83
+ }
84
+
85
+ export interface ExistingState {
86
+ manifest?: Record<string, unknown>;
87
+ tasks?: unknown[];
88
+ }
89
+
90
+ /**
91
+ * Detect conflicts between an import bundle and existing run state.
92
+ *
93
+ * Checks:
94
+ * - **schema_drift**: manifest `schemaVersion` differs between import and existing.
95
+ * - **file_overwrite**: artifact paths in the import bundle that already exist
96
+ * in the current manifest's artifact list.
97
+ * - **state_mismatch**: task statuses differ between import and existing tasks.
98
+ * - **resource_deleted**: referenced agent/team/workflow in import does not exist
99
+ * in the current state.
100
+ */
101
+ export function detectImportConflicts(
102
+ importBundle: ImportBundle,
103
+ existingState: ExistingState,
104
+ ): ConflictReport {
105
+ const conflicts: Conflict[] = [];
106
+ const { manifest: incoming, tasks: incomingTasks } = importBundle;
107
+ const { manifest: current, tasks: currentTasks } = existingState;
108
+
109
+ // ── Schema drift ─────────────────────────────────────────────────
110
+ if (current) {
111
+ const incomingVersion = incoming.schemaVersion;
112
+ const currentVersion = current.schemaVersion;
113
+ if (
114
+ typeof incomingVersion !== "undefined" &&
115
+ typeof currentVersion !== "undefined" &&
116
+ incomingVersion !== currentVersion
117
+ ) {
118
+ conflicts.push({
119
+ kind: "schema_drift",
120
+ path: "manifest.schemaVersion",
121
+ existing: String(currentVersion),
122
+ incoming: String(incomingVersion),
123
+ severity: "warning",
124
+ autoResolvable: true,
125
+ });
126
+ }
127
+ }
128
+
129
+ // ── File overwrite (artifact collision) ──────────────────────────
130
+ const incomingArtifacts = Array.isArray(incoming.artifacts) ? incoming.artifacts : [];
131
+ const currentArtifacts = Array.isArray(current?.artifacts) ? current?.artifacts : [];
132
+
133
+ if (current && currentArtifacts.length > 0) {
134
+ const currentPaths = new Set(
135
+ currentArtifacts
136
+ .filter(isRecord)
137
+ .map((a) => (typeof a.path === "string" ? a.path : ""))
138
+ .filter((p) => p !== ""),
139
+ );
140
+
141
+ for (const artifact of incomingArtifacts) {
142
+ if (!isRecord(artifact)) continue;
143
+ const artifactPath = typeof artifact.path === "string" ? artifact.path : "";
144
+ if (artifactPath !== "" && currentPaths.has(artifactPath)) {
145
+ conflicts.push({
146
+ kind: "file_overwrite",
147
+ path: artifactPath,
148
+ existing: "present in current run",
149
+ incoming: "present in import bundle",
150
+ severity: "warning",
151
+ autoResolvable: true,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ // ── State mismatch (task status differences) ─────────────────────
158
+ if (currentTasks && currentTasks.length > 0) {
159
+ const currentTaskMap = new Map(
160
+ extractTaskLikes(currentTasks).map((t) => [t.id, t]),
161
+ );
162
+ const incomingTaskList = extractTaskLikes(incomingTasks);
163
+
164
+ for (const inTask of incomingTaskList) {
165
+ const curTask = currentTaskMap.get(inTask.id);
166
+ if (curTask && curTask.status !== inTask.status) {
167
+ conflicts.push({
168
+ kind: "state_mismatch",
169
+ path: `tasks/${inTask.id}`,
170
+ existing: curTask.status,
171
+ incoming: inTask.status,
172
+ severity: "error",
173
+ autoResolvable: false,
174
+ });
175
+ }
176
+ }
177
+ }
178
+
179
+ // ── Resource deleted (agent/team/workflow no longer in current) ──
180
+ if (current) {
181
+ const currentTeam = typeof current.team === "string" ? current.team : undefined;
182
+ const currentWorkflow = typeof current.workflow === "string" ? current.workflow : undefined;
183
+ const incomingTeam = typeof incoming.team === "string" ? incoming.team : undefined;
184
+ const incomingWorkflow = typeof incoming.workflow === "string" ? incoming.workflow : undefined;
185
+
186
+ if (incomingTeam && currentTeam && incomingTeam !== currentTeam) {
187
+ conflicts.push({
188
+ kind: "resource_deleted",
189
+ path: `team/${incomingTeam}`,
190
+ existing: currentTeam,
191
+ incoming: incomingTeam,
192
+ severity: "warning",
193
+ autoResolvable: true,
194
+ });
195
+ }
196
+
197
+ if (incomingWorkflow && currentWorkflow && incomingWorkflow !== currentWorkflow) {
198
+ conflicts.push({
199
+ kind: "resource_deleted",
200
+ path: `workflow/${incomingWorkflow}`,
201
+ existing: currentWorkflow,
202
+ incoming: incomingWorkflow,
203
+ severity: "warning",
204
+ autoResolvable: true,
205
+ });
206
+ }
207
+
208
+ // Check agent references from tasks vs current tasks
209
+ if (currentTasks && currentTasks.length > 0) {
210
+ const currentAgents = new Set(
211
+ extractTaskLikes(currentTasks)
212
+ .map((t) => t.agent)
213
+ .filter((a): a is string => a !== undefined),
214
+ );
215
+ const incomingTaskList = extractTaskLikes(incomingTasks);
216
+ for (const inTask of incomingTaskList) {
217
+ if (
218
+ inTask.agent &&
219
+ inTask.id !== "" &&
220
+ currentAgents.size > 0 &&
221
+ !currentAgents.has(inTask.agent) &&
222
+ // Only flag if there's a matching task id (agent was reassigned)
223
+ extractTaskLikes(currentTasks).some((ct) => ct.id === inTask.id)
224
+ ) {
225
+ conflicts.push({
226
+ kind: "resource_deleted",
227
+ path: `agent/${inTask.agent}`,
228
+ existing: "not in current run",
229
+ incoming: inTask.agent,
230
+ severity: "warning",
231
+ autoResolvable: true,
232
+ });
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ return buildReport(conflicts);
239
+ }
240
+
241
+ // ── Resume Conflict Detection ──────────────────────────────────────────
242
+
243
+ export interface SuspendedState {
244
+ tasks: unknown[];
245
+ artifacts: string[];
246
+ }
247
+
248
+ export interface CurrentState {
249
+ changedFiles: string[];
250
+ taskStatuses: Record<string, string>;
251
+ }
252
+
253
+ /**
254
+ * Detect conflicts when resuming a suspended run against current filesystem state.
255
+ *
256
+ * Checks:
257
+ * - **file_overwrite**: files changed since suspension.
258
+ * - **state_mismatch**: task statuses changed externally.
259
+ */
260
+ export function detectResumeConflicts(
261
+ suspendedState: SuspendedState,
262
+ currentState: CurrentState,
263
+ ): ConflictReport {
264
+ const conflicts: Conflict[] = [];
265
+ const { tasks: suspendedTasks, artifacts: suspendedArtifacts } = suspendedState;
266
+ const { changedFiles, taskStatuses } = currentState;
267
+
268
+ // ── File overwrite ───────────────────────────────────────────────
269
+ const changedSet = new Set(changedFiles);
270
+ for (const artifactPath of suspendedArtifacts) {
271
+ if (changedSet.has(artifactPath)) {
272
+ conflicts.push({
273
+ kind: "file_overwrite",
274
+ path: artifactPath,
275
+ existing: "modified since suspension",
276
+ incoming: "expected unchanged",
277
+ severity: "error",
278
+ autoResolvable: false,
279
+ });
280
+ }
281
+ }
282
+
283
+ // ── State mismatch ───────────────────────────────────────────────
284
+ const suspendedTaskList = extractTaskLikes(suspendedTasks);
285
+ for (const task of suspendedTaskList) {
286
+ const currentStatus = taskStatuses[task.id];
287
+ if (currentStatus !== undefined && currentStatus !== task.status) {
288
+ conflicts.push({
289
+ kind: "state_mismatch",
290
+ path: `tasks/${task.id}`,
291
+ existing: currentStatus,
292
+ incoming: task.status,
293
+ severity: "error",
294
+ autoResolvable: false,
295
+ });
296
+ }
297
+ }
298
+
299
+ return buildReport(conflicts);
300
+ }
301
+
302
+ // ── Conflict Resolution ────────────────────────────────────────────────
303
+
304
+ /**
305
+ * Apply a resolution strategy to a conflict.
306
+ *
307
+ * - **skip**: skip the conflicting item (always resolves).
308
+ * - **overwrite**: replace existing with incoming (resolves for `file_overwrite`
309
+ * and `schema_drift`; does not resolve `state_mismatch` or `resource_deleted`).
310
+ * - **merge**: attempt merge — resolves `state_mismatch` with merged status;
311
+ * resolves others conditionally.
312
+ */
313
+ export function resolveConflict(
314
+ conflict: Conflict,
315
+ strategy: ConflictStrategy,
316
+ ): ConflictResolution {
317
+ switch (strategy) {
318
+ case "skip":
319
+ return { resolved: true, action: `Skipped ${conflict.path}` };
320
+
321
+ case "overwrite":
322
+ if (conflict.kind === "file_overwrite" || conflict.kind === "schema_drift") {
323
+ return { resolved: true, action: `Overwritten ${conflict.path} with incoming value` };
324
+ }
325
+ return {
326
+ resolved: false,
327
+ action: `Cannot overwrite ${conflict.kind} at ${conflict.path}; manual resolution required`,
328
+ };
329
+
330
+ case "merge":
331
+ if (conflict.kind === "state_mismatch") {
332
+ return {
333
+ resolved: true,
334
+ action: `Merged ${conflict.path}: kept existing=${conflict.existing ?? "?"}, incoming=${conflict.incoming ?? "?"}`,
335
+ };
336
+ }
337
+ if (conflict.kind === "resource_deleted") {
338
+ return {
339
+ resolved: true,
340
+ action: `Merged ${conflict.path}: using incoming resource reference`,
341
+ };
342
+ }
343
+ if (conflict.kind === "file_overwrite") {
344
+ return {
345
+ resolved: true,
346
+ action: `Merged ${conflict.path}: kept both versions`,
347
+ };
348
+ }
349
+ if (conflict.kind === "schema_drift") {
350
+ return {
351
+ resolved: true,
352
+ action: `Merged ${conflict.path}: using incoming schema version`,
353
+ };
354
+ }
355
+ return {
356
+ resolved: false,
357
+ action: `Cannot merge ${conflict.kind} at ${conflict.path}`,
358
+ };
359
+ }
360
+ }
@@ -1,35 +1,35 @@
1
- import type { AgentConfig } from "../agents/agent-config.ts";
2
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
- import type { TeamConfig } from "../teams/team-config.ts";
4
- import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
-
6
- export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
- return manifest.workflow === "direct-agent";
8
- }
9
-
10
- export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
- if (!isDirectRun(manifest)) return undefined;
12
- const firstTask = tasks[0];
13
- const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
- const agent = agents.find((candidate) => candidate.name === agentName);
15
- const role = firstTask?.role ?? "agent";
16
- const stepId = firstTask?.stepId ?? "01_agent";
17
- return {
18
- team: {
19
- name: manifest.team,
20
- description: `Direct subagent run for ${agentName}`,
21
- source: "builtin",
22
- filePath: "<generated>",
23
- roles: [{ name: role, agent: agentName, description: agent?.description }],
24
- defaultWorkflow: "direct-agent",
25
- workspaceMode: manifest.workspaceMode,
26
- },
27
- workflow: {
28
- name: manifest.workflow ?? "direct-agent",
29
- description: `Direct task for ${agentName}`,
30
- source: "builtin",
31
- filePath: "<generated>",
32
- steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
- },
34
- };
35
- }
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import type { TeamConfig } from "../teams/team-config.ts";
4
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
+
6
+ export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
+ return manifest.workflow === "direct-agent";
8
+ }
9
+
10
+ export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
+ if (!isDirectRun(manifest)) return undefined;
12
+ const firstTask = tasks[0];
13
+ const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
+ const agent = agents.find((candidate) => candidate.name === agentName);
15
+ const role = firstTask?.role ?? "agent";
16
+ const stepId = firstTask?.stepId ?? "01_agent";
17
+ return {
18
+ team: {
19
+ name: manifest.team,
20
+ description: `Direct subagent run for ${agentName}`,
21
+ source: "builtin",
22
+ filePath: "<generated>",
23
+ roles: [{ name: role, agent: agentName, description: agent?.description }],
24
+ defaultWorkflow: "direct-agent",
25
+ workspaceMode: manifest.workspaceMode,
26
+ },
27
+ workflow: {
28
+ name: manifest.workflow ?? "direct-agent",
29
+ description: `Direct task for ${agentName}`,
30
+ source: "builtin",
31
+ filePath: "<generated>",
32
+ steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
+ },
34
+ };
35
+ }
@@ -1,82 +1,82 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { appendEvent } from "../state/event-log.ts";
4
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
- import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
- import { readCrewAgents } from "./crew-agent-records.ts";
7
-
8
- export type ForegroundControlRequestType = "interrupt" | "status";
9
-
10
- export interface ForegroundControlStatus {
11
- runId: string;
12
- status: TeamRunManifest["status"];
13
- active: boolean;
14
- asyncPid?: number;
15
- asyncAlive?: boolean;
16
- runningTasks: string[];
17
- runningAgents: string[];
18
- controlPath: string;
19
- lastRequest?: ForegroundControlRequest;
20
- }
21
-
22
- export interface ForegroundControlRequest {
23
- id: string;
24
- type: ForegroundControlRequestType;
25
- createdAt: string;
26
- reason: string;
27
- acknowledged: boolean;
28
- }
29
-
30
- export function foregroundControlPath(manifest: TeamRunManifest): string {
31
- return path.join(manifest.stateRoot, "foreground-control.json");
32
- }
33
-
34
- function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
- if (!fs.existsSync(controlPath)) return undefined;
36
- try {
37
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
- return parsed.requests?.at(-1);
39
- } catch {
40
- return undefined;
41
- }
42
- }
43
-
44
- export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
- const controlPath = foregroundControlPath(manifest);
46
- const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
- return {
48
- runId: manifest.runId,
49
- status: manifest.status,
50
- active: isActiveRunStatus(manifest.status),
51
- asyncPid: manifest.async?.pid,
52
- asyncAlive,
53
- runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
- runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
- controlPath,
56
- lastRequest: readLastRequest(controlPath),
57
- };
58
- }
59
-
60
- export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
- const controlPath = foregroundControlPath(manifest);
62
- let requests: ForegroundControlRequest[] = [];
63
- if (fs.existsSync(controlPath)) {
64
- try {
65
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
- requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
- } catch {
68
- requests = [];
69
- }
70
- }
71
- const request: ForegroundControlRequest = {
72
- id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
- type: "interrupt",
74
- createdAt: new Date().toISOString(),
75
- reason,
76
- acknowledged: false,
77
- };
78
- fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
- fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
- appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
- return request;
82
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { appendEvent } from "../state/event-log.ts";
4
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
+ import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
+ import { readCrewAgents } from "./crew-agent-records.ts";
7
+
8
+ export type ForegroundControlRequestType = "interrupt" | "status";
9
+
10
+ export interface ForegroundControlStatus {
11
+ runId: string;
12
+ status: TeamRunManifest["status"];
13
+ active: boolean;
14
+ asyncPid?: number;
15
+ asyncAlive?: boolean;
16
+ runningTasks: string[];
17
+ runningAgents: string[];
18
+ controlPath: string;
19
+ lastRequest?: ForegroundControlRequest;
20
+ }
21
+
22
+ export interface ForegroundControlRequest {
23
+ id: string;
24
+ type: ForegroundControlRequestType;
25
+ createdAt: string;
26
+ reason: string;
27
+ acknowledged: boolean;
28
+ }
29
+
30
+ export function foregroundControlPath(manifest: TeamRunManifest): string {
31
+ return path.join(manifest.stateRoot, "foreground-control.json");
32
+ }
33
+
34
+ function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
+ if (!fs.existsSync(controlPath)) return undefined;
36
+ try {
37
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
+ return parsed.requests?.at(-1);
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
+ const controlPath = foregroundControlPath(manifest);
46
+ const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
+ return {
48
+ runId: manifest.runId,
49
+ status: manifest.status,
50
+ active: isActiveRunStatus(manifest.status),
51
+ asyncPid: manifest.async?.pid,
52
+ asyncAlive,
53
+ runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
+ runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
+ controlPath,
56
+ lastRequest: readLastRequest(controlPath),
57
+ };
58
+ }
59
+
60
+ export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
+ const controlPath = foregroundControlPath(manifest);
62
+ let requests: ForegroundControlRequest[] = [];
63
+ if (fs.existsSync(controlPath)) {
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
+ requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
+ } catch {
68
+ requests = [];
69
+ }
70
+ }
71
+ const request: ForegroundControlRequest = {
72
+ id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
+ type: "interrupt",
74
+ createdAt: new Date().toISOString(),
75
+ reason,
76
+ acknowledged: false,
77
+ };
78
+ fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
+ fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
+ appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
+ return request;
82
+ }