pi-crew 0.1.51 → 0.2.0

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 (239) 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/doctor.ts +28 -3
  65. package/src/extension/team-tool/handle-settings.ts +195 -188
  66. package/src/extension/team-tool/inspect.ts +41 -41
  67. package/src/extension/team-tool/intent-policy.ts +42 -42
  68. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  69. package/src/extension/team-tool/plan.ts +19 -19
  70. package/src/extension/team-tool/run.ts +12 -1
  71. package/src/extension/team-tool.ts +11 -1
  72. package/src/i18n.ts +184 -184
  73. package/src/observability/exporters/otlp-exporter.ts +92 -77
  74. package/src/prompt/prompt-runtime.ts +72 -72
  75. package/src/runtime/agent-memory.ts +72 -72
  76. package/src/runtime/agent-observability.ts +114 -114
  77. package/src/runtime/async-marker.ts +26 -26
  78. package/src/runtime/attention-events.ts +28 -28
  79. package/src/runtime/auto-resume.ts +100 -0
  80. package/src/runtime/background-runner.ts +11 -1
  81. package/src/runtime/cancellation-token.ts +89 -89
  82. package/src/runtime/cancellation.ts +61 -61
  83. package/src/runtime/capability-inventory.ts +116 -116
  84. package/src/runtime/child-pi.ts +7 -2
  85. package/src/runtime/compaction-summary.ts +271 -0
  86. package/src/runtime/completion-guard.ts +190 -190
  87. package/src/runtime/crash-recovery.ts +33 -0
  88. package/src/runtime/delta-conflict.ts +360 -0
  89. package/src/runtime/direct-run.ts +35 -35
  90. package/src/runtime/foreground-control.ts +82 -82
  91. package/src/runtime/green-contract.ts +46 -46
  92. package/src/runtime/group-join.ts +106 -106
  93. package/src/runtime/heartbeat-gradient.ts +28 -28
  94. package/src/runtime/heartbeat-watcher.ts +124 -124
  95. package/src/runtime/iteration-hooks.ts +262 -0
  96. package/src/runtime/live-agent-control.ts +88 -88
  97. package/src/runtime/live-control-realtime.ts +36 -36
  98. package/src/runtime/live-extension-bridge.ts +150 -150
  99. package/src/runtime/live-irc.ts +92 -92
  100. package/src/runtime/live-session-health.ts +100 -100
  101. package/src/runtime/loop-gates.ts +129 -0
  102. package/src/runtime/metric-parser.ts +40 -0
  103. package/src/runtime/notebook-helpers.ts +90 -90
  104. package/src/runtime/orphan-sentinel.ts +7 -7
  105. package/src/runtime/parallel-research.ts +44 -44
  106. package/src/runtime/phase-progress.ts +217 -0
  107. package/src/runtime/pi-args.ts +38 -11
  108. package/src/runtime/pi-json-output.ts +111 -111
  109. package/src/runtime/pi-spawn.ts +57 -7
  110. package/src/runtime/policy-engine.ts +79 -79
  111. package/src/runtime/post-checks.ts +122 -0
  112. package/src/runtime/progress-event-coalescer.ts +43 -43
  113. package/src/runtime/prose-compressor.ts +164 -164
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/result-extractor.ts +121 -121
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/sensitive-paths.ts +2 -2
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/stream-preview.ts +177 -177
  123. package/src/runtime/supervisor-contact.ts +59 -59
  124. package/src/runtime/task-display.ts +38 -38
  125. package/src/runtime/task-graph.ts +207 -0
  126. package/src/runtime/task-quality.ts +207 -0
  127. package/src/runtime/task-runner/capabilities.ts +78 -78
  128. package/src/runtime/task-runner/live-executor.ts +7 -1
  129. package/src/runtime/task-runner/progress.ts +119 -119
  130. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  131. package/src/runtime/task-runner/result-utils.ts +14 -14
  132. package/src/runtime/task-runner/run-projection.ts +103 -103
  133. package/src/runtime/task-runner/state-helpers.ts +22 -22
  134. package/src/runtime/team-runner.ts +117 -7
  135. package/src/runtime/worker-heartbeat.ts +21 -21
  136. package/src/runtime/worker-startup.ts +57 -57
  137. package/src/runtime/workflow-state.ts +187 -0
  138. package/src/runtime/workspace-tree.ts +298 -298
  139. package/src/schema/config-schema.ts +11 -0
  140. package/src/schema/validation-types.ts +148 -0
  141. package/src/skills/skill-templates.ts +374 -0
  142. package/src/state/active-run-registry.ts +35 -11
  143. package/src/state/atomic-write.ts +33 -26
  144. package/src/state/contracts.ts +1 -0
  145. package/src/state/event-reconstructor.ts +217 -0
  146. package/src/state/locks.ts +2 -13
  147. package/src/state/mailbox.ts +4 -3
  148. package/src/state/state-store.ts +32 -14
  149. package/src/state/task-claims.ts +44 -44
  150. package/src/state/types.ts +9 -0
  151. package/src/state/usage.ts +29 -29
  152. package/src/subagents/async-entry.ts +1 -1
  153. package/src/subagents/index.ts +3 -3
  154. package/src/subagents/live/control.ts +1 -1
  155. package/src/subagents/live/manager.ts +1 -1
  156. package/src/subagents/live/realtime.ts +1 -1
  157. package/src/subagents/live/session-runtime.ts +1 -1
  158. package/src/subagents/manager.ts +1 -1
  159. package/src/subagents/spawn.ts +1 -1
  160. package/src/teams/team-serializer.ts +38 -38
  161. package/src/types/diff.d.ts +18 -18
  162. package/src/ui/crew-footer.ts +101 -101
  163. package/src/ui/crew-select-list.ts +111 -111
  164. package/src/ui/crew-widget.ts +5 -2
  165. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  166. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  167. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  168. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  169. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  170. package/src/ui/dynamic-border.ts +25 -25
  171. package/src/ui/layout-primitives.ts +106 -106
  172. package/src/ui/loaders.ts +158 -158
  173. package/src/ui/render-coalescer.ts +51 -51
  174. package/src/ui/render-diff.ts +119 -119
  175. package/src/ui/render-scheduler.ts +143 -143
  176. package/src/ui/run-action-dispatcher.ts +10 -1
  177. package/src/ui/spinner.ts +17 -17
  178. package/src/ui/status-colors.ts +58 -58
  179. package/src/ui/syntax-highlight.ts +116 -116
  180. package/src/ui/transcript-entries.ts +258 -258
  181. package/src/utils/completion-dedupe.ts +63 -63
  182. package/src/utils/frontmatter.ts +68 -68
  183. package/src/utils/git.ts +262 -262
  184. package/src/utils/ids.ts +17 -17
  185. package/src/utils/incremental-reader.ts +104 -104
  186. package/src/utils/names.ts +27 -27
  187. package/src/utils/redaction.ts +44 -44
  188. package/src/utils/safe-paths.ts +47 -47
  189. package/src/utils/scan-cache.ts +136 -136
  190. package/src/utils/sleep.ts +40 -26
  191. package/src/utils/task-name-generator.ts +337 -337
  192. package/src/workflows/validate-workflow.ts +40 -40
  193. package/src/worktree/branch-freshness.ts +45 -45
  194. package/teams/default.team.md +12 -12
  195. package/teams/fast-fix.team.md +11 -11
  196. package/teams/implementation.team.md +18 -18
  197. package/teams/parallel-research.team.md +14 -14
  198. package/teams/research.team.md +11 -11
  199. package/teams/review.team.md +12 -12
  200. package/workflows/default.workflow.md +30 -29
  201. package/workflows/fast-fix.workflow.md +23 -22
  202. package/workflows/implementation.workflow.md +43 -43
  203. package/workflows/parallel-research.workflow.md +46 -46
  204. package/workflows/research.workflow.md +22 -22
  205. package/workflows/review.workflow.md +30 -30
  206. package/docs/refactor-tasks-phase3.md +0 -394
  207. package/docs/refactor-tasks-phase4.md +0 -564
  208. package/docs/refactor-tasks-phase5.md +0 -402
  209. package/docs/refactor-tasks-phase6.md +0 -662
  210. package/docs/refactor-tasks.md +0 -1484
  211. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  212. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  213. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  214. package/docs/research/AUDIT_PI_CREW.md +0 -457
  215. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  216. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  217. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  218. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  219. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  220. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  221. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  222. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  223. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  224. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  225. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  226. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  227. package/docs/research-extension-examples.md +0 -297
  228. package/docs/research-extension-system.md +0 -324
  229. package/docs/research-oh-my-pi-distillation.md +0 -369
  230. package/docs/research-optimization-plan.md +0 -548
  231. package/docs/research-phase10-distillation.md +0 -199
  232. package/docs/research-phase11-distillation.md +0 -201
  233. package/docs/research-phase8-operator-experience-plan.md +0 -819
  234. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  235. package/docs/research-pi-coding-agent.md +0 -357
  236. package/docs/research-source-pi-crew-reference.md +0 -174
  237. package/docs/research-ui-optimization-plan.md +0 -480
  238. package/docs/source-runtime-refactor-map.md +0 -107
  239. package/src/utils/atomic-write.ts +0 -33
@@ -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
+ }