pi-crew 0.9.68 → 0.10.2
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.
- package/CHANGELOG.md +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- 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
|
-
};
|
package/src/runtime/run-drift.ts
DELETED
|
@@ -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
|
-
}
|