pi-crew 0.9.68 → 0.10.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 (217) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65838 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +4 -2
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +2 -1
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/settings-store.ts +219 -0
  111. package/src/runtime/spawn-policy.ts +217 -0
  112. package/src/runtime/stale-reconciler.ts +87 -6
  113. package/src/runtime/subagent-manager.ts +25 -1
  114. package/src/runtime/task-output-context.ts +230 -9
  115. package/src/runtime/task-packet.ts +23 -1
  116. package/src/runtime/task-runner/child-executor.ts +106 -7
  117. package/src/runtime/task-runner/post-execution.ts +125 -1
  118. package/src/runtime/task-runner/pre-execution.ts +39 -1
  119. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  120. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  121. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  122. package/src/runtime/task-runner/state-helpers.ts +26 -24
  123. package/src/runtime/task-runner.ts +11 -0
  124. package/src/runtime/team-runner.ts +129 -1671
  125. package/src/runtime/verification/spec-sandbox.ts +255 -0
  126. package/src/runtime/verification/verification-gates.ts +3 -2
  127. package/src/runtime/verification/verification-worktree.ts +2 -1
  128. package/src/runtime/workflow-phase-advance.ts +100 -0
  129. package/src/runtime/workspace-tree.ts +9 -0
  130. package/src/schema/config-schema.ts +65 -25
  131. package/src/schema/sensitive-config-paths.ts +64 -0
  132. package/src/schema/team-tool-schema.ts +13 -3
  133. package/src/state/README.md +4 -10
  134. package/src/state/atomic-write.ts +20 -3
  135. package/src/state/contracts.ts +38 -0
  136. package/src/state/coordination/mailbox.ts +12 -2
  137. package/src/state/event-log/cursor.ts +223 -0
  138. package/src/state/event-log/event-log-rotation.ts +12 -4
  139. package/src/state/event-log/event-log.ts +152 -369
  140. package/src/state/event-log/sequence-cache.ts +373 -0
  141. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  142. package/src/state/stores/active-run-registry.ts +3 -2
  143. package/src/state/stores/manifest-io.ts +237 -0
  144. package/src/state/stores/ownership-map.ts +162 -0
  145. package/src/state/stores/plan-store.ts +241 -0
  146. package/src/state/stores/run-cache.ts +0 -90
  147. package/src/state/stores/spec-store.ts +189 -0
  148. package/src/state/stores/state-store.ts +66 -229
  149. package/src/state/types.ts +197 -0
  150. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  151. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  152. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  153. package/src/ui/heartbeat-aggregator.ts +9 -1
  154. package/src/ui/keybinding-map.ts +54 -13
  155. package/src/ui/powerbar-publisher.ts +52 -1
  156. package/src/ui/run-dashboard.ts +31 -5
  157. package/src/ui/run-snapshot-cache.ts +57 -30
  158. package/src/ui/snapshot-types.ts +6 -1
  159. package/src/ui/widget/widget-renderer.ts +9 -1
  160. package/src/utils/file-coalescer.ts +0 -4
  161. package/src/utils/fs-errno.ts +66 -0
  162. package/src/utils/fs-watch.ts +1 -1
  163. package/src/utils/internal-error.ts +3 -1
  164. package/src/utils/paths.ts +11 -3
  165. package/src/utils/task-name-generator.ts +1 -8
  166. package/src/workflows/discover-workflows.ts +19 -2
  167. package/src/workflows/validate-workflow.ts +7 -1
  168. package/src/workflows/workflow-config.ts +10 -0
  169. package/src/workflows/workflow-serializer.ts +3 -0
  170. package/src/worktree/worktree-manager.ts +22 -0
  171. package/src/agents/agent-search.ts +0 -98
  172. package/src/benchmark/benchmark-runner.ts +0 -313
  173. package/src/benchmark/feedback-loop.ts +0 -73
  174. package/src/config/resilient-parser.ts +0 -117
  175. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  176. package/src/extension/result-watcher.ts +0 -139
  177. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  178. package/src/observability/metric-retention.ts +0 -64
  179. package/src/runtime/compaction/compaction-summary.ts +0 -278
  180. package/src/runtime/errors/crew-errors.ts +0 -162
  181. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  182. package/src/runtime/loop-gates.ts +0 -128
  183. package/src/runtime/metric-parser.ts +0 -36
  184. package/src/runtime/output/stream-preview.ts +0 -184
  185. package/src/runtime/output/tool-progress.ts +0 -278
  186. package/src/runtime/phase-tracker.ts +0 -385
  187. package/src/runtime/pipeline-runner.ts +0 -523
  188. package/src/runtime/process/process-lifecycle.ts +0 -491
  189. package/src/runtime/recovery/retry-runner.ts +0 -330
  190. package/src/runtime/run-drift.ts +0 -219
  191. package/src/runtime/task-quality.ts +0 -199
  192. package/src/runtime/task-runner/run-projection.ts +0 -128
  193. package/src/runtime/verification/post-checks.ts +0 -142
  194. package/src/state/coordination/schedule.ts +0 -166
  195. package/src/state/event-log/jsonl-writer.ts +0 -115
  196. package/src/state/hook-instinct-bridge.ts +0 -94
  197. package/src/state/hook-integrations.ts +0 -51
  198. package/src/state/session-state-map.ts +0 -51
  199. package/src/state/stores/blob-store.ts +0 -308
  200. package/src/state/stores/instinct-store.ts +0 -275
  201. package/src/state/stores/observation-store.ts +0 -176
  202. package/src/state/tiered-eval.ts +0 -480
  203. package/src/state/types-eval.ts +0 -58
  204. package/src/tools/safe-bash-extension.ts +0 -54
  205. package/src/tools/safe-bash.ts +0 -505
  206. package/src/ui/agent-management-overlay.ts +0 -160
  207. package/src/ui/crew-footer.ts +0 -102
  208. package/src/ui/crew-select-list.ts +0 -114
  209. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  210. package/src/ui/transcript-entries.ts +0 -256
  211. package/src/utils/conflict-detect.ts +0 -721
  212. package/src/utils/fingerprint.ts +0 -180
  213. package/src/utils/gh-protocol.ts +0 -556
  214. package/src/utils/project-detector.ts +0 -160
  215. package/src/utils/sse-parser.ts +0 -131
  216. package/src/workflows/cost-estimator.ts +0 -34
  217. package/src/workflows/intermediate-store.ts +0 -166
@@ -1,330 +0,0 @@
1
- /**
2
- * RetryRunner - Execute tasks with retry support and summary accumulation.
3
- *
4
- * Based on pi-boomerang's --rethrow pattern:
5
- * - Retries failed tasks up to maxAttempts
6
- * - Generates handoffs between attempts
7
- * - Accumulates context from previous attempts
8
- * - Supports exponential backoff
9
- * - Limits handoff accumulation to prevent memory leaks
10
- */
11
-
12
- import type { HandoffManager, HandoffSummary, TaskPacket, TaskResult } from "../handoff-manager.ts";
13
-
14
- /**
15
- * Retry configuration.
16
- */
17
- export interface RetryConfig {
18
- /** Maximum number of retry attempts */
19
- maxAttempts: number;
20
- /** Generate summary between attempts for context accumulation */
21
- summaryBetweenAttempts?: boolean;
22
- /** Stop retrying if task succeeds */
23
- stopOnSuccess?: boolean;
24
- /** Base backoff delay in milliseconds (multiplied by attempt number) */
25
- backoffMs?: number;
26
- /** Backoff multiplier (default: 1) */
27
- backoffMultiplier?: number;
28
- /** Maximum backoff delay cap in milliseconds */
29
- maxBackoffMs?: number;
30
- /** Custom retry condition (return true to retry) */
31
- retryCondition?: (result: TaskResult, attempt: number) => boolean;
32
- /** Maximum handoffs to retain (default: 100) - prevents memory leaks */
33
- maxHandoffs?: number;
34
- }
35
-
36
- /**
37
- * Result of a single attempt.
38
- */
39
- export interface AttemptResult {
40
- attempt: number;
41
- result: TaskResult;
42
- summary?: HandoffSummary;
43
- duration: number;
44
- error?: string;
45
- timestamp: number;
46
- }
47
-
48
- /**
49
- * Final retry result.
50
- */
51
- export interface RetryResult {
52
- success: boolean;
53
- attempts: AttemptResult[];
54
- finalResult?: TaskResult;
55
- totalHandoffs: HandoffSummary[];
56
- totalDuration: number;
57
- }
58
-
59
- /**
60
- * Task runner interface (minimal for retry functionality).
61
- */
62
- export interface TaskRunnerLike {
63
- runTask(packet: TaskPacket): Promise<TaskResult>;
64
- }
65
-
66
- /**
67
- * RetryRunner handles task execution with automatic retry and summary accumulation.
68
- */
69
- export class RetryRunner {
70
- private _disposed = false;
71
-
72
- private taskRunner: TaskRunnerLike;
73
- private handoffManager: HandoffManager;
74
-
75
- constructor(taskRunner: TaskRunnerLike, handoffManager: HandoffManager) {
76
- this.taskRunner = taskRunner;
77
- this.handoffManager = handoffManager;
78
- }
79
-
80
- /**
81
- * Check if this runner has been disposed.
82
- */
83
- get isDisposed(): boolean {
84
- return this._disposed;
85
- }
86
-
87
- /**
88
- * Dispose of resources held by this runner.
89
- * Clears any accumulated state and prevents further operations.
90
- */
91
- dispose(): void {
92
- this._disposed = true;
93
- }
94
-
95
- /**
96
- * Clear accumulated handoffs to free memory.
97
- * Useful when you want to reset state without disposing.
98
- */
99
- clearHandoffs(): void {
100
- /* no-op: _handoffs field removed (was never read) */
101
- }
102
-
103
- /**
104
- * Get the effective max handoffs limit from config or default.
105
- */
106
- private getMaxHandoffs(config: RetryConfig): number {
107
- return config.maxHandoffs ?? 100;
108
- }
109
-
110
- /**
111
- * Trim handoffs array to max size, keeping most recent.
112
- */
113
- private trimHandoffs(handoffs: HandoffSummary[], maxSize: number): HandoffSummary[] {
114
- if (handoffs.length <= maxSize) {
115
- return handoffs;
116
- }
117
- // Keep the most recent handoffs (last maxSize items)
118
- return handoffs.slice(-maxSize);
119
- }
120
-
121
- /**
122
- * Execute task with retry support.
123
- * Summaries accumulate between attempts for better context.
124
- *
125
- * @param packet - Task packet to execute
126
- * @param config - Retry configuration (stopOnSuccess defaults to true)
127
- * @returns Final retry result with all attempts
128
- */
129
- async runWithRetry(packet: TaskPacket, config: RetryConfig): Promise<RetryResult> {
130
- if (this._disposed) {
131
- throw new Error("RetryRunner has been disposed");
132
- }
133
-
134
- const attempts: AttemptResult[] = [];
135
- const handoffs: HandoffSummary[] = [];
136
- const startTime = Date.now();
137
- const maxHandoffs = this.getMaxHandoffs(config);
138
-
139
- for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
140
- if (this._disposed) {
141
- throw new Error("RetryRunner was disposed during retry");
142
- }
143
- const attemptStart = Date.now();
144
-
145
- try {
146
- // Inject accumulated handoffs into context
147
- const enrichedPacket = this.enrichPacketWithHandoffs(packet, handoffs, attempt);
148
-
149
- // Execute task
150
- const result = await this.taskRunner.runTask(enrichedPacket);
151
-
152
- const attemptResult: AttemptResult = {
153
- attempt,
154
- result,
155
- duration: Date.now() - attemptStart,
156
- timestamp: Date.now(),
157
- };
158
-
159
- // Generate summary between attempts
160
- if (config.summaryBetweenAttempts !== false) {
161
- const summary = await this.handoffManager.generateSummary(packet, result);
162
- attemptResult.summary = summary;
163
- handoffs.push(summary);
164
-
165
- // Trim handoffs to prevent memory leak
166
- if (handoffs.length > maxHandoffs) {
167
- handoffs.splice(0, handoffs.length - maxHandoffs);
168
- }
169
- }
170
-
171
- attempts.push(attemptResult);
172
-
173
- // Stop on success if configured (default to true when undefined)
174
- if ((config.stopOnSuccess ?? true) && result.outcome === "success") {
175
- return {
176
- success: true,
177
- attempts,
178
- finalResult: result,
179
- totalHandoffs: handoffs,
180
- totalDuration: Date.now() - startTime,
181
- };
182
- }
183
-
184
- // Check custom retry condition
185
- if (config.retryCondition && !config.retryCondition(result, attempt)) {
186
- break;
187
- }
188
-
189
- // Check default retry condition (retry on failure)
190
- if (result.outcome !== "success" && attempt < config.maxAttempts) {
191
- // Apply backoff before retry
192
- const backoffDelay = this.calculateBackoff(
193
- config.backoffMs ?? 1000,
194
- attempt,
195
- config.backoffMultiplier ?? 1,
196
- config.maxBackoffMs,
197
- );
198
- if (backoffDelay > 0) {
199
- await this.sleep(backoffDelay);
200
- }
201
- }
202
- } catch (error) {
203
- attempts.push({
204
- attempt,
205
- result: {
206
- outcome: "failure",
207
- error: error instanceof Error ? error.message : String(error),
208
- },
209
- duration: Date.now() - attemptStart,
210
- timestamp: Date.now(),
211
- error: error instanceof Error ? error.message : String(error),
212
- });
213
-
214
- // Apply backoff before retry on error
215
- if (config.backoffMs && attempt < config.maxAttempts) {
216
- const backoffDelay = this.calculateBackoff(
217
- config.backoffMs,
218
- attempt,
219
- config.backoffMultiplier ?? 1,
220
- config.maxBackoffMs,
221
- );
222
- if (backoffDelay > 0) {
223
- await this.sleep(backoffDelay);
224
- }
225
- }
226
- }
227
- }
228
-
229
- const finalAttempt = attempts[attempts.length - 1];
230
- return {
231
- success: finalAttempt?.result.outcome === "success",
232
- attempts,
233
- finalResult: finalAttempt?.result,
234
- totalHandoffs: this.trimHandoffs(handoffs, maxHandoffs),
235
- totalDuration: Date.now() - startTime,
236
- };
237
- }
238
-
239
- /**
240
- * Enrich packet with accumulated handoffs from previous attempts.
241
- */
242
- private enrichPacketWithHandoffs(packet: TaskPacket, handoffs: HandoffSummary[], attempt: number): TaskPacket {
243
- if (handoffs.length === 0) {
244
- return packet;
245
- }
246
-
247
- // Build accumulated context from previous attempts
248
- const accumulatedContext = handoffs
249
- .map(
250
- (h, index) =>
251
- `## Attempt ${index + 1}: ${h.task}\n` +
252
- `Outcome: ${h.outcome}\n` +
253
- `Files: created=${h.filesCreated.join(", ")}, modified=${h.filesModified.join(", ")}\n` +
254
- `Decisions: ${h.decisions.map((d) => d.rationale).join("; ")}\n` +
255
- `Blockers: ${h.blockers.join(", ")}\n` +
256
- `Next Steps: ${h.nextSteps.join(", ")}\n`,
257
- )
258
- .join("\n---\n");
259
-
260
- return {
261
- ...packet,
262
- context: {
263
- ...packet.context,
264
- __boomerangAttempts: attempt,
265
- __boomerangHandoffs: handoffs,
266
- __boomerangContext: `Previous attempts summary:\n${accumulatedContext}`,
267
- },
268
- };
269
- }
270
-
271
- /**
272
- * Calculate backoff delay with optional capping.
273
- */
274
- private calculateBackoff(baseMs: number, attempt: number, multiplier: number, maxBackoffMs?: number): number {
275
- let delay = baseMs * multiplier ** (attempt - 1);
276
- if (maxBackoffMs !== undefined) {
277
- delay = Math.min(delay, maxBackoffMs);
278
- }
279
- return delay;
280
- }
281
-
282
- /**
283
- * Sleep helper.
284
- */
285
- private sleep(ms: number): Promise<void> {
286
- return new Promise((resolve) => setTimeout(resolve, ms));
287
- }
288
- }
289
-
290
- /**
291
- * Create a RetryRunner with default dependencies.
292
- */
293
- export function createRetryRunner(taskRunner: TaskRunnerLike, handoffManager: HandoffManager): RetryRunner {
294
- return new RetryRunner(taskRunner, handoffManager);
295
- }
296
-
297
- /**
298
- * Default retry config for common scenarios.
299
- */
300
- export const DEFAULT_RETRY_CONFIG: RetryConfig = {
301
- maxAttempts: 3,
302
- summaryBetweenAttempts: true,
303
- stopOnSuccess: true,
304
- backoffMs: 1000,
305
- backoffMultiplier: 2,
306
- maxBackoffMs: 30000,
307
- };
308
-
309
- /**
310
- * Retry config for transient failures (quick retries).
311
- */
312
- export const TRANSIENT_FAILURE_RETRY_CONFIG: RetryConfig = {
313
- maxAttempts: 2,
314
- summaryBetweenAttempts: false,
315
- stopOnSuccess: true,
316
- backoffMs: 500,
317
- backoffMultiplier: 1,
318
- };
319
-
320
- /**
321
- * Retry config for persistent failures (exponential backoff).
322
- */
323
- export const PERSISTENT_FAILURE_RETRY_CONFIG: RetryConfig = {
324
- maxAttempts: 5,
325
- summaryBetweenAttempts: true,
326
- stopOnSuccess: true,
327
- backoffMs: 2000,
328
- backoffMultiplier: 2,
329
- maxBackoffMs: 60000,
330
- };
@@ -1,219 +0,0 @@
1
- /**
2
- * Runtime drift detectors — detect state anomalies in pi-crew runs.
3
- *
4
- * Pattern origin: GSD-2 ADR-017 drift detection & state reconciliation.
5
- * Each detector checks for a specific anomaly and returns a report.
6
- * Repair handlers are idempotent — safe to run multiple times.
7
- */
8
-
9
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
10
- import path from "node:path";
11
- import { logInternalError } from "../utils/internal-error.ts";
12
-
13
- // ── Types ────────────────────────────────────────────────────────────────
14
-
15
- export type DriftKind =
16
- | "stale-process" // Heartbeat timeout (existing)
17
- | "orphaned-claim" // Task claim without task
18
- | "orphaned-worktree" // Worktree dir without active run
19
- | "missing-timestamps" // State files without timestamps
20
- | "status-divergence" // Manifest status ≠ status file
21
- | "unregistered-run"; // State dir but no manifest
22
-
23
- export interface DriftReport {
24
- kind: DriftKind;
25
- runId: string;
26
- details: string;
27
- repaired: boolean;
28
- repairResult?: string;
29
- }
30
-
31
- export interface DriftContext {
32
- /** Root directory for crew state (.crew/) */
33
- crewRoot: string;
34
- /** Active run IDs (from registry) */
35
- activeRunIds: Set<string>;
36
- /** Manifest content if available */
37
- manifest?: {
38
- runId: string;
39
- status: string;
40
- cwd: string;
41
- [k: string]: unknown;
42
- };
43
- }
44
-
45
- // ── Detectors ────────────────────────────────────────────────────────────
46
-
47
- /**
48
- * Detect task claims that reference tasks not in the manifest.
49
- */
50
- export function detectOrphanedClaim(ctx: DriftContext): DriftReport | null {
51
- if (!ctx.manifest) return null;
52
- const claimsDir = path.join(ctx.crewRoot, "state", "task-claims");
53
- if (!existsSync(claimsDir)) return null;
54
-
55
- const claimFiles = readdirSync(claimsDir).filter((f) => f.endsWith(".json"));
56
- for (const file of claimFiles) {
57
- try {
58
- const claim = JSON.parse(readFileSync(path.join(claimsDir, file), "utf-8"));
59
- if (claim.runId === ctx.manifest.runId && claim.taskId) {
60
- // Check if task exists in manifest tasks array
61
- const tasks = (ctx.manifest as Record<string, unknown>).tasks;
62
- if (Array.isArray(tasks) && !tasks.some((t: Record<string, unknown>) => t.id === claim.taskId)) {
63
- return {
64
- kind: "orphaned-claim",
65
- runId: ctx.manifest.runId,
66
- details: `Task claim '${claim.taskId}' references non-existent task`,
67
- repaired: false,
68
- };
69
- }
70
- }
71
- } catch {
72
- // Malformed claim file — skip
73
- }
74
- }
75
- return null;
76
- }
77
-
78
- /**
79
- * Detect worktree directories that don't belong to any active run.
80
- */
81
- export function detectOrphanedWorktree(ctx: DriftContext): DriftReport | null {
82
- const worktreesDir = path.join(ctx.crewRoot, "worktrees");
83
- if (!existsSync(worktreesDir)) return null;
84
-
85
- const dirs = readdirSync(worktreesDir, { withFileTypes: true })
86
- .filter((d) => d.isDirectory())
87
- .map((d) => d.name);
88
-
89
- for (const dir of dirs) {
90
- // Extract run ID from worktree dir name (format: <runId>-<taskId> or <runId>)
91
- const runId = dir.split("-").slice(0, 5).join("-"); // heuristic: run IDs are timestamp-based
92
- if (!ctx.activeRunIds.has(runId) && !ctx.activeRunIds.has(dir)) {
93
- return {
94
- kind: "orphaned-worktree",
95
- runId: dir,
96
- details: `Worktree '${dir}' has no active run`,
97
- repaired: false,
98
- };
99
- }
100
- }
101
- return null;
102
- }
103
-
104
- /**
105
- * Detect state files missing required timestamps.
106
- */
107
- export function detectMissingTimestamps(ctx: DriftContext): DriftReport | null {
108
- if (!ctx.manifest) return null;
109
- const stateDir = path.join(ctx.crewRoot, "state");
110
- if (!existsSync(stateDir)) return null;
111
-
112
- // Check manifest has createdAt/updatedAt
113
- const m = ctx.manifest as Record<string, unknown>;
114
- if (!m.createdAt && !m.updatedAt) {
115
- return {
116
- kind: "missing-timestamps",
117
- runId: ctx.manifest.runId,
118
- details: "Manifest missing createdAt/updatedAt timestamps",
119
- repaired: false,
120
- };
121
- }
122
- return null;
123
- }
124
-
125
- /**
126
- * Detect divergence between manifest status and individual task status files.
127
- */
128
- export function detectStatusDivergence(ctx: DriftContext): DriftReport | null {
129
- if (!ctx.manifest) return null;
130
- const statusPath = path.join(ctx.crewRoot, "state", `${ctx.manifest.runId}.status`);
131
- if (!existsSync(statusPath)) return null;
132
-
133
- try {
134
- const status = readFileSync(statusPath, "utf-8").trim();
135
- if (status !== ctx.manifest.status) {
136
- return {
137
- kind: "status-divergence",
138
- runId: ctx.manifest.runId,
139
- details: `Manifest says '${ctx.manifest.status}' but status file says '${status}'`,
140
- repaired: false,
141
- };
142
- }
143
- } catch {
144
- // Can't read status file — not drift, might be permissions
145
- }
146
- return null;
147
- }
148
-
149
- /**
150
- * Detect state directories that have no corresponding manifest.
151
- */
152
- export function detectUnregisteredRun(ctx: DriftContext): DriftReport | null {
153
- const runsDir = path.join(ctx.crewRoot, "runs");
154
- if (!existsSync(runsDir)) return null;
155
-
156
- const runDirs = readdirSync(runsDir, { withFileTypes: true })
157
- .filter((d) => d.isDirectory())
158
- .map((d) => d.name);
159
-
160
- for (const runId of runDirs) {
161
- if (!ctx.activeRunIds.has(runId)) {
162
- // Check if it has state files (manifest exists)
163
- const manifestPath = path.join(runsDir, runId, "manifest.json");
164
- if (existsSync(manifestPath)) {
165
- try {
166
- const stat = statSync(manifestPath);
167
- const ageMs = Date.now() - stat.mtimeMs;
168
- // Only flag if older than 1 hour (might be in-progress)
169
- if (ageMs > 60 * 60 * 1000) {
170
- return {
171
- kind: "unregistered-run",
172
- runId,
173
- details: `Run '${runId}' has manifest but is not in active registry (age: ${Math.round(ageMs / 60000)}m)`,
174
- repaired: false,
175
- };
176
- }
177
- } catch {
178
- // Can't stat — skip
179
- }
180
- }
181
- }
182
- }
183
- return null;
184
- }
185
-
186
- // ── Reconciliation Loop ─────────────────────────────────────────────────
187
-
188
- const ALL_DETECTORS = [detectOrphanedClaim, detectOrphanedWorktree, detectMissingTimestamps, detectStatusDivergence, detectUnregisteredRun];
189
-
190
- /**
191
- * Run all drift detectors and collect reports.
192
- * Capped at maxPasses repair attempts.
193
- *
194
- * Pattern origin: GSD-2 ADR-017 — capped at 2 retry passes.
195
- */
196
- export function runDriftDetection(ctx: DriftContext, maxPasses = 2): DriftReport[] {
197
- const reports: DriftReport[] = [];
198
-
199
- for (let pass = 0; pass < maxPasses; pass++) {
200
- let newFindings = 0;
201
-
202
- for (const detector of ALL_DETECTORS) {
203
- try {
204
- const report = detector(ctx);
205
- if (report) {
206
- reports.push(report);
207
- newFindings++;
208
- }
209
- } catch (error) {
210
- logInternalError("run-drift", error, `detector=${detector.name} runId=${ctx.manifest?.runId}`);
211
- }
212
- }
213
-
214
- // If no new findings, stop early
215
- if (newFindings === 0) break;
216
- }
217
-
218
- return reports;
219
- }