@pi-agents/orchid 0.1.0-beta.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.
- package/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/agents/AGENTS-MANIFEST.md +42 -0
- package/agents/brain.md +42 -0
- package/agents/context-builder.md +46 -0
- package/agents/delegate.md +12 -0
- package/agents/dev-1.md +42 -0
- package/agents/oracle.md +73 -0
- package/agents/planner.md +55 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/tester.md +45 -0
- package/agents/worker.md +55 -0
- package/extensions/ralph.ts +1 -0
- package/extensions/reviewer-extension.ts +125 -0
- package/extensions/task-orchestrator.ts +28 -0
- package/package.json +63 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-context-build.md +53 -0
- package/prompts/parallel-handoff-plan.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +41 -0
- package/skills/orchid/SKILL.md +214 -0
- package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
- package/skills/orchid/orchid-converge/SKILL.md +124 -0
- package/skills/orchid/orchid-decompose/SKILL.md +201 -0
- package/skills/orchid/orchid-doctor/SKILL.md +162 -0
- package/skills/orchid/orchid-investigate/SKILL.md +102 -0
- package/skills/orchid/orchid-launch/SKILL.md +147 -0
- package/skills/ralph/SKILL.md +73 -0
- package/skills/subagents/pi-subagents/SKILL.md +813 -0
- package/src/index.ts +7 -0
- package/src/orchestrator/abort.ts +534 -0
- package/src/orchestrator/agent-bridge-extension.ts +1020 -0
- package/src/orchestrator/agent-host.ts +954 -0
- package/src/orchestrator/cleanup.ts +776 -0
- package/src/orchestrator/config-loader.ts +1412 -0
- package/src/orchestrator/config-schema.ts +690 -0
- package/src/orchestrator/config.ts +81 -0
- package/src/orchestrator/context-window.ts +66 -0
- package/src/orchestrator/diagnostic-reports.ts +475 -0
- package/src/orchestrator/diagnostics.ts +394 -0
- package/src/orchestrator/discovery.ts +1833 -0
- package/src/orchestrator/engine-worker.ts +415 -0
- package/src/orchestrator/engine.ts +5940 -0
- package/src/orchestrator/execution.ts +3104 -0
- package/src/orchestrator/extension.ts +5934 -0
- package/src/orchestrator/formatting.ts +785 -0
- package/src/orchestrator/git.ts +88 -0
- package/src/orchestrator/index.ts +28 -0
- package/src/orchestrator/lane-runner.ts +1787 -0
- package/src/orchestrator/mailbox.ts +780 -0
- package/src/orchestrator/merge.ts +3414 -0
- package/src/orchestrator/messages.ts +1062 -0
- package/src/orchestrator/migrations.ts +278 -0
- package/src/orchestrator/naming.ts +117 -0
- package/src/orchestrator/path-resolver.ts +275 -0
- package/src/orchestrator/persistence.ts +2625 -0
- package/src/orchestrator/process-registry.ts +452 -0
- package/src/orchestrator/quality-gate.ts +1085 -0
- package/src/orchestrator/resume.ts +3488 -0
- package/src/orchestrator/sessions.ts +57 -0
- package/src/orchestrator/settings-loader.ts +136 -0
- package/src/orchestrator/settings-tui.ts +2208 -0
- package/src/orchestrator/sidecar-telemetry.ts +267 -0
- package/src/orchestrator/supervisor.ts +4548 -0
- package/src/orchestrator/task-executor-core.ts +675 -0
- package/src/orchestrator/tmux-compat.ts +37 -0
- package/src/orchestrator/tool-allowlist-constants.ts +37 -0
- package/src/orchestrator/types.ts +4465 -0
- package/src/orchestrator/verification.ts +547 -0
- package/src/orchestrator/waves.ts +1564 -0
- package/src/orchestrator/workspace.ts +707 -0
- package/src/orchestrator/worktree.ts +2725 -0
- package/src/ralph/index.ts +825 -0
- package/src/subagents/agents/agent-management.ts +648 -0
- package/src/subagents/agents/agent-scope.ts +6 -0
- package/src/subagents/agents/agent-selection.ts +23 -0
- package/src/subagents/agents/agent-serializer.ts +86 -0
- package/src/subagents/agents/agents.ts +832 -0
- package/src/subagents/agents/chain-serializer.ts +137 -0
- package/src/subagents/agents/frontmatter.ts +29 -0
- package/src/subagents/agents/identity.ts +30 -0
- package/src/subagents/agents/skills.ts +632 -0
- package/src/subagents/extension/config.ts +16 -0
- package/src/subagents/extension/control-notices.ts +92 -0
- package/src/subagents/extension/doctor.ts +199 -0
- package/src/subagents/extension/fanout-child.ts +170 -0
- package/src/subagents/extension/index.ts +573 -0
- package/src/subagents/extension/schemas.ts +168 -0
- package/src/subagents/intercom/intercom-bridge.ts +379 -0
- package/src/subagents/intercom/result-intercom.ts +377 -0
- package/src/subagents/runs/background/async-execution.ts +712 -0
- package/src/subagents/runs/background/async-job-tracker.ts +310 -0
- package/src/subagents/runs/background/async-resume.ts +345 -0
- package/src/subagents/runs/background/async-status.ts +325 -0
- package/src/subagents/runs/background/completion-dedupe.ts +63 -0
- package/src/subagents/runs/background/notify.ts +108 -0
- package/src/subagents/runs/background/parallel-groups.ts +45 -0
- package/src/subagents/runs/background/result-watcher.ts +307 -0
- package/src/subagents/runs/background/run-id-resolver.ts +83 -0
- package/src/subagents/runs/background/run-status.ts +269 -0
- package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
- package/src/subagents/runs/background/subagent-runner.ts +1808 -0
- package/src/subagents/runs/background/top-level-async.ts +13 -0
- package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
- package/src/subagents/runs/foreground/chain-execution.ts +938 -0
- package/src/subagents/runs/foreground/execution.ts +918 -0
- package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
- package/src/subagents/runs/shared/completion-guard.ts +147 -0
- package/src/subagents/runs/shared/long-running-guard.ts +175 -0
- package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/subagents/runs/shared/model-fallback.ts +103 -0
- package/src/subagents/runs/shared/nested-events.ts +819 -0
- package/src/subagents/runs/shared/nested-path.ts +52 -0
- package/src/subagents/runs/shared/nested-render.ts +115 -0
- package/src/subagents/runs/shared/parallel-utils.ts +109 -0
- package/src/subagents/runs/shared/pi-args.ts +220 -0
- package/src/subagents/runs/shared/pi-spawn.ts +115 -0
- package/src/subagents/runs/shared/run-history.ts +60 -0
- package/src/subagents/runs/shared/single-output.ts +164 -0
- package/src/subagents/runs/shared/subagent-control.ts +226 -0
- package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
- package/src/subagents/runs/shared/worktree.ts +577 -0
- package/src/subagents/shared/artifacts.ts +98 -0
- package/src/subagents/shared/atomic-json.ts +16 -0
- package/src/subagents/shared/file-coalescer.ts +40 -0
- package/src/subagents/shared/fork-context.ts +76 -0
- package/src/subagents/shared/formatters.ts +133 -0
- package/src/subagents/shared/jsonl-writer.ts +81 -0
- package/src/subagents/shared/model-info.ts +78 -0
- package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
- package/src/subagents/shared/session-identity.ts +10 -0
- package/src/subagents/shared/session-tokens.ts +44 -0
- package/src/subagents/shared/settings.ts +397 -0
- package/src/subagents/shared/status-format.ts +49 -0
- package/src/subagents/shared/types.ts +822 -0
- package/src/subagents/shared/utils.ts +450 -0
- package/src/subagents/slash/prompt-template-bridge.ts +397 -0
- package/src/subagents/slash/slash-bridge.ts +174 -0
- package/src/subagents/slash/slash-commands.ts +528 -0
- package/src/subagents/slash/slash-live-state.ts +292 -0
- package/src/subagents/tui/render-helpers.ts +80 -0
- package/src/subagents/tui/render.ts +1358 -0
- package/templates/agents/local/supervisor.md +33 -0
- package/templates/agents/local/task-merger.md +27 -0
- package/templates/agents/local/task-reviewer.md +30 -0
- package/templates/agents/local/task-worker.md +34 -0
- package/templates/agents/supervisor-routing.md +92 -0
- package/templates/agents/supervisor.md +229 -0
- package/templates/agents/task-merger.md +214 -0
- package/templates/agents/task-reviewer.md +260 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +557 -0
- package/templates/tasks/CONTEXT.md +30 -0
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
- package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing message templates (ORCH_MESSAGES)
|
|
3
|
+
* @module orch/messages
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
AbortMode,
|
|
7
|
+
MergeFailureClassification,
|
|
8
|
+
MergeRetryCallbacks,
|
|
9
|
+
MergeRetryDecision,
|
|
10
|
+
MergeRetryLoopOutcome,
|
|
11
|
+
MergeRetryPolicy,
|
|
12
|
+
MergeWaveResult,
|
|
13
|
+
OrchestratorConfig,
|
|
14
|
+
RepoMergeOutcome,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
import { MERGE_RETRY_POLICY_MATRIX } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
// โโ Message Templates โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deterministic message templates for user-facing /orch commands.
|
|
22
|
+
* Ensures consistent UX across invocations.
|
|
23
|
+
*/
|
|
24
|
+
export const ORCH_MESSAGES = {
|
|
25
|
+
// /orch
|
|
26
|
+
orchStarting: (batchId: string, waves: number, tasks: number) =>
|
|
27
|
+
`๐ Starting batch ${batchId}: ${waves} wave(s), ${tasks} task(s)`,
|
|
28
|
+
orchWaveStart: (waveNum: number, totalWaves: number, tasks: number, lanes: number) =>
|
|
29
|
+
`\n๐ Wave ${waveNum}/${totalWaves}: ${tasks} task(s) across ${lanes} lane(s)`,
|
|
30
|
+
orchWaveComplete: (
|
|
31
|
+
waveNum: number,
|
|
32
|
+
succeeded: number,
|
|
33
|
+
failed: number,
|
|
34
|
+
skipped: number,
|
|
35
|
+
elapsedSec: number,
|
|
36
|
+
) =>
|
|
37
|
+
`โ
Wave ${waveNum} complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped (${elapsedSec}s)`,
|
|
38
|
+
orchMergeStart: (waveNum: number, laneCount: number) =>
|
|
39
|
+
`๐ [Wave ${waveNum}] Merging ${laneCount} lane(s) into target branch...`,
|
|
40
|
+
orchMergeLaneSuccess: (laneNum: number, commit: string, durationSec: number) =>
|
|
41
|
+
` โ
Lane ${laneNum} merged (${commit.slice(0, 8)}, ${durationSec}s)`,
|
|
42
|
+
orchMergeLaneConflictResolved: (laneNum: number, conflictCount: number, durationSec: number) =>
|
|
43
|
+
` โก Lane ${laneNum} merged with ${conflictCount} auto-resolved conflict(s) (${durationSec}s)`,
|
|
44
|
+
orchMergeLaneFailed: (laneNum: number, reason: string) =>
|
|
45
|
+
` โ Lane ${laneNum} merge failed: ${reason}`,
|
|
46
|
+
orchMergeComplete: (waveNum: number, mergedCount: number, totalSec: number) =>
|
|
47
|
+
`๐ [Wave ${waveNum}] Merge complete: ${mergedCount} lane(s) merged (${totalSec}s)`,
|
|
48
|
+
orchMergeFailed: (waveNum: number, laneNum: number, reason: string) =>
|
|
49
|
+
`โ [Wave ${waveNum}] Merge failed at lane ${laneNum}: ${reason}`,
|
|
50
|
+
orchMergeSkipped: (waveNum: number) => `๐ [Wave ${waveNum}] No successful lanes to merge`,
|
|
51
|
+
orchMergePlaceholder: (waveNum: number) =>
|
|
52
|
+
`๐ [Wave ${waveNum}] Merge: placeholder โ Step 3 (TS-008) will replace with mergeWave()`,
|
|
53
|
+
orchWorktreeReset: (waveNum: number, lanes: number) =>
|
|
54
|
+
`๐ Resetting ${lanes} worktree(s) to target branch HEAD after wave ${waveNum}`,
|
|
55
|
+
orchBatchComplete: (
|
|
56
|
+
batchId: string,
|
|
57
|
+
succeeded: number,
|
|
58
|
+
failed: number,
|
|
59
|
+
skipped: number,
|
|
60
|
+
blocked: number,
|
|
61
|
+
elapsedSec: number,
|
|
62
|
+
orchBranch?: string,
|
|
63
|
+
baseBranch?: string,
|
|
64
|
+
) => {
|
|
65
|
+
const lines = [
|
|
66
|
+
`\n๐ Batch ${batchId} complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped, ${blocked} blocked (${elapsedSec}s)`,
|
|
67
|
+
];
|
|
68
|
+
if (failed > 0 || blocked > 0) {
|
|
69
|
+
lines.push("");
|
|
70
|
+
if (blocked > 0) {
|
|
71
|
+
lines.push(` ${blocked} task(s) were blocked because upstream tasks failed.`);
|
|
72
|
+
}
|
|
73
|
+
lines.push(" Next steps:");
|
|
74
|
+
lines.push(" โข /orch-status โ review what failed and why");
|
|
75
|
+
lines.push(" โข /orch-resume โ retry from the failed wave");
|
|
76
|
+
lines.push(" โข /orch-abort โ clean up and start fresh");
|
|
77
|
+
}
|
|
78
|
+
if (orchBranch && succeeded > 0) {
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
81
|
+
lines.push(` โ Your changes are on branch: ${orchBranch}`);
|
|
82
|
+
lines.push(` โ Your ${baseBranch || "working"} branch was not modified.`);
|
|
83
|
+
if (baseBranch) {
|
|
84
|
+
lines.push(` โ Preview: git log ${baseBranch}..${orchBranch}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push(" โ");
|
|
87
|
+
lines.push(" โ ๐ To bring changes into your working branch:");
|
|
88
|
+
lines.push(" โ");
|
|
89
|
+
lines.push(" โ /orch-integrate โ merge directly (recommended)");
|
|
90
|
+
lines.push(" โ /orch-integrate --pr โ create a pull request");
|
|
91
|
+
lines.push(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
},
|
|
95
|
+
orchBatchFailed: (batchId: string, reason: string) => `\nโ Batch ${batchId} failed: ${reason}`,
|
|
96
|
+
orchBatchStopped: (batchId: string, policy: string) =>
|
|
97
|
+
`\nโ Batch ${batchId} stopped by ${policy} policy`,
|
|
98
|
+
|
|
99
|
+
// /orch-pause
|
|
100
|
+
pauseNoBatch: () => "No active batch is running. Use /orch <areas|all> to start.",
|
|
101
|
+
pauseAlreadyPaused: (batchId: string) => `Batch ${batchId} is already paused.`,
|
|
102
|
+
pauseActivated: (batchId: string) =>
|
|
103
|
+
`โธ๏ธ Pausing batch ${batchId}... lanes will stop after their current tasks complete.`,
|
|
104
|
+
|
|
105
|
+
// /orch-sessions
|
|
106
|
+
sessionsNone: () => "No active orchestrator sessions found.",
|
|
107
|
+
sessionsHeader: (count: number) => `๐ฅ๏ธ ${count} orchestrator session(s):`,
|
|
108
|
+
|
|
109
|
+
// /orch orphan detection
|
|
110
|
+
orphanDetectionResume: (batchId: string, sessionCount: number) =>
|
|
111
|
+
`๐ Found ${sessionCount} running orchestrator session(s) from batch ${batchId}.\n` +
|
|
112
|
+
` Use /orch-resume to continue, or /orch-abort to clean up.`,
|
|
113
|
+
orphanDetectionAbort: (sessionCount: number) =>
|
|
114
|
+
`โ ๏ธ Found ${sessionCount} orphan orchestrator session(s) without usable state.\n` +
|
|
115
|
+
` Use /orch-abort to clean up before starting a new batch.`,
|
|
116
|
+
orphanDetectionCleanup: () => `๐งน Cleaned up stale batch state file. Starting fresh.`,
|
|
117
|
+
|
|
118
|
+
// /orch-resume
|
|
119
|
+
resumeStarting: (batchId: string, phase: string) =>
|
|
120
|
+
`๐ Resuming batch ${batchId} (was: ${phase})...`,
|
|
121
|
+
resumeReconciled: (
|
|
122
|
+
batchId: string,
|
|
123
|
+
completed: number,
|
|
124
|
+
pending: number,
|
|
125
|
+
failed: number,
|
|
126
|
+
reconnecting: number,
|
|
127
|
+
reExecuting: number = 0,
|
|
128
|
+
) =>
|
|
129
|
+
`๐ Batch ${batchId} reconciliation: ${completed} completed, ${pending} pending, ${failed} failed, ${reconnecting} reconnecting` +
|
|
130
|
+
(reExecuting > 0 ? `, ${reExecuting} re-executing` : ""),
|
|
131
|
+
resumeSkippedWaves: (skippedCount: number) => `โญ๏ธ Skipping ${skippedCount} completed wave(s)`,
|
|
132
|
+
resumeReconnecting: (sessionCount: number) =>
|
|
133
|
+
`๐ Reconnecting to ${sessionCount} alive session(s)...`,
|
|
134
|
+
resumeNoState: () =>
|
|
135
|
+
`โ No batch to resume. No batch-state.json file found.\n` +
|
|
136
|
+
` Use /orch <areas|all> to start a new batch.`,
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* TP-187 (#539): Successful reconstruction from .pi/runtime/<batchId>/
|
|
140
|
+
* runtime artifacts during force-resume after `orch_abort()`.
|
|
141
|
+
*/
|
|
142
|
+
resumeReconstructed: (batchId: string, selectionNote: string) =>
|
|
143
|
+
`๐จ Reconstructed batch ${batchId} from .pi/runtime/ artifacts (${selectionNote}).\n` +
|
|
144
|
+
` Force-resume will proceed with a fresh wave-zero pass; the existing\n` +
|
|
145
|
+
` reconciliation logic will re-detect succeeded tasks via .DONE markers.`,
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* TP-187 (#539): Fail-loud message when force-resume can't reconstruct
|
|
149
|
+
* after `orch_abort()` because required runtime artifacts are missing.
|
|
150
|
+
*/
|
|
151
|
+
resumeNoStateAfterAbort: (missingArtifact: string, batchId: string | null) =>
|
|
152
|
+
`โ Cannot resume after abort: ${missingArtifact}.\n` +
|
|
153
|
+
(batchId ? ` Last known batch: ${batchId}.\n` : "") +
|
|
154
|
+
` To start fresh from the preserved worktree state, run\n` +
|
|
155
|
+
` \`orch_start <PROMPT.md>\` (or \`/orch <areas|all>\`).`,
|
|
156
|
+
resumeInvalidState: (error: string) =>
|
|
157
|
+
`โ Cannot resume: batch state file is invalid.\n` +
|
|
158
|
+
` Error: ${error}\n` +
|
|
159
|
+
` Delete .pi/batch-state.json and start a new batch.`,
|
|
160
|
+
resumePhaseNotResumable: (batchId: string, phase: string, reason: string) =>
|
|
161
|
+
`โ Cannot resume batch ${batchId} (phase: ${phase}).\n` + ` ${reason}`,
|
|
162
|
+
resumeComplete: (
|
|
163
|
+
batchId: string,
|
|
164
|
+
succeeded: number,
|
|
165
|
+
failed: number,
|
|
166
|
+
skipped: number,
|
|
167
|
+
blocked: number,
|
|
168
|
+
elapsedSec: number,
|
|
169
|
+
) =>
|
|
170
|
+
`\n๐ Resumed batch ${batchId} complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped, ${blocked} blocked (${elapsedSec}s total)`,
|
|
171
|
+
|
|
172
|
+
// /orch-resume --force
|
|
173
|
+
forceResumeStarting: (batchId: string, phase: string) =>
|
|
174
|
+
`โ ๏ธ Force-resuming batch ${batchId} from ${phase} state. Running pre-resume diagnostics...`,
|
|
175
|
+
forceResumeDiagnosticsFailed: (batchId: string) =>
|
|
176
|
+
`โ Cannot force-resume batch ${batchId}: pre-resume diagnostics failed.\n` +
|
|
177
|
+
` Fix the issues above, then retry /orch-resume --force.`,
|
|
178
|
+
|
|
179
|
+
// /orch-abort
|
|
180
|
+
abortGracefulStarting: (batchId: string, sessionCount: number) =>
|
|
181
|
+
`โณ Graceful abort of batch ${batchId}: signaling ${sessionCount} session(s) to checkpoint and exit...`,
|
|
182
|
+
abortGracefulWaiting: (batchId: string, graceSec: number) =>
|
|
183
|
+
`โณ Waiting up to ${graceSec}s for sessions to checkpoint and exit...`,
|
|
184
|
+
abortGracefulForceKill: (count: number) =>
|
|
185
|
+
`โ ๏ธ Force-killing ${count} session(s) that did not exit within timeout`,
|
|
186
|
+
abortGracefulComplete: (
|
|
187
|
+
batchId: string,
|
|
188
|
+
graceful: number,
|
|
189
|
+
forceKilled: number,
|
|
190
|
+
durationSec: number,
|
|
191
|
+
) =>
|
|
192
|
+
`โ
Graceful abort complete for batch ${batchId}: ${graceful} exited gracefully, ${forceKilled} force-killed (${durationSec}s)`,
|
|
193
|
+
abortHardStarting: (batchId: string, sessionCount: number) =>
|
|
194
|
+
`โก Hard abort of batch ${batchId}: killing ${sessionCount} session(s) immediately...`,
|
|
195
|
+
abortHardComplete: (batchId: string, killed: number, durationSec: number) =>
|
|
196
|
+
`โ
Hard abort complete for batch ${batchId}: ${killed} session(s) killed (${durationSec}s)`,
|
|
197
|
+
abortPartialFailure: (failureCount: number) =>
|
|
198
|
+
`โ ๏ธ ${failureCount} error(s) during abort (see details above)`,
|
|
199
|
+
abortNoBatch: () => `No active batch to abort. Use /orch <areas|all> to start a batch.`,
|
|
200
|
+
abortComplete: (mode: AbortMode, sessionsKilled: number) =>
|
|
201
|
+
`๐ Abort (${mode}) complete: ${sessionsKilled} session(s) terminated. Worktrees and branches preserved.`,
|
|
202
|
+
// /orch merge โ repo-scoped partial summary (TP-005 Step 1)
|
|
203
|
+
orchMergePartialRepoSummary: (waveNum: number, repoLines: string[]) =>
|
|
204
|
+
`โ ๏ธ [Wave ${waveNum}] Merge partially succeeded โ repo outcomes diverged:\n${repoLines.join("\n")}`,
|
|
205
|
+
|
|
206
|
+
// /orch integration โ post-batch integration guidance (TP-022 Step 4)
|
|
207
|
+
orchIntegrationAutoSuccess: (orchBranch: string, baseBranch: string) =>
|
|
208
|
+
`โ
Auto-integrated: ${baseBranch} fast-forwarded to ${orchBranch}.`,
|
|
209
|
+
orchIntegrationAutoFailed: (orchBranch: string, baseBranch: string, reason: string) =>
|
|
210
|
+
`โ ๏ธ Auto-integration skipped: ${reason}\n` +
|
|
211
|
+
` Orch branch ${orchBranch} preserved. Integrate manually:\n` +
|
|
212
|
+
` git log ${baseBranch}..${orchBranch}\n` +
|
|
213
|
+
` git merge ${orchBranch}`,
|
|
214
|
+
orchIntegrationManual: (orchBranch: string, baseBranch: string, mergedTaskCount: number) => {
|
|
215
|
+
const lines = [
|
|
216
|
+
`โน๏ธ Batch complete. Orch branch ${orchBranch} has ${mergedTaskCount} merged task(s).`,
|
|
217
|
+
` Review and integrate:`,
|
|
218
|
+
` git log ${baseBranch}..${orchBranch}`,
|
|
219
|
+
` git merge ${orchBranch}`,
|
|
220
|
+
];
|
|
221
|
+
return lines.join("\n");
|
|
222
|
+
},
|
|
223
|
+
} as const;
|
|
224
|
+
|
|
225
|
+
// โโ Repo-Scoped Merge Summary (TP-005) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Status emoji for repo merge outcome.
|
|
229
|
+
*/
|
|
230
|
+
function repoStatusIcon(status: RepoMergeOutcome["status"]): string {
|
|
231
|
+
switch (status) {
|
|
232
|
+
case "succeeded":
|
|
233
|
+
return "โ
";
|
|
234
|
+
case "partial":
|
|
235
|
+
return "โ ๏ธ";
|
|
236
|
+
case "failed":
|
|
237
|
+
return "โ";
|
|
238
|
+
default:
|
|
239
|
+
return "โ";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format a repo-divergence summary for a partial merge wave result.
|
|
245
|
+
*
|
|
246
|
+
* Returns null if:
|
|
247
|
+
* - repoResults is empty or undefined (mono-repo mode)
|
|
248
|
+
* - all repos have the same status (no divergence)
|
|
249
|
+
* - there is only one repo group (divergence is meaningless)
|
|
250
|
+
*
|
|
251
|
+
* When the partial result is caused by mixed-outcome lanes within
|
|
252
|
+
* a single repo (not repo divergence), this returns null to avoid
|
|
253
|
+
* misleading "cross-repo divergence" messaging.
|
|
254
|
+
*
|
|
255
|
+
* The returned string is a complete, ready-to-emit message.
|
|
256
|
+
*
|
|
257
|
+
* @param mergeResult - The MergeWaveResult with status "partial"
|
|
258
|
+
* @returns Formatted summary string, or null if no repo-divergence summary applies
|
|
259
|
+
*/
|
|
260
|
+
export function formatRepoMergeSummary(mergeResult: MergeWaveResult): string | null {
|
|
261
|
+
const repoResults = mergeResult.repoResults;
|
|
262
|
+
|
|
263
|
+
// No repo attribution โ mono-repo mode, no summary
|
|
264
|
+
if (!repoResults || repoResults.length === 0) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Single repo group โ divergence is meaningless (partial is lane-level)
|
|
269
|
+
if (repoResults.length < 2) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for actual divergence: are there different statuses across repos?
|
|
274
|
+
const statuses = new Set(repoResults.map((r) => r.status));
|
|
275
|
+
if (statuses.size < 2) {
|
|
276
|
+
// All repos have the same status (e.g., all "partial") โ
|
|
277
|
+
// the partial is from within-repo lane failures, not cross-repo divergence
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Build per-repo summary lines (sorted by repoId, which repoResults already is)
|
|
282
|
+
const repoLines = repoResults.map((r) => {
|
|
283
|
+
const repoLabel = r.repoId ?? "(default)";
|
|
284
|
+
const icon = repoStatusIcon(r.status);
|
|
285
|
+
// TP-032 R006-3: Exclude verification_new_failure lanes from success count
|
|
286
|
+
const mergedCount = r.laneResults.filter(
|
|
287
|
+
(lr) =>
|
|
288
|
+
!lr.error && (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED"),
|
|
289
|
+
).length;
|
|
290
|
+
const totalCount = r.laneResults.length;
|
|
291
|
+
let detail = `${mergedCount}/${totalCount} lane(s) merged`;
|
|
292
|
+
if (r.failureReason) {
|
|
293
|
+
detail += ` โ ${r.failureReason.slice(0, 150)}`;
|
|
294
|
+
}
|
|
295
|
+
return ` ${icon} ${repoLabel}: ${detail}`;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return ORCH_MESSAGES.orchMergePartialRepoSummary(mergeResult.waveIndex, repoLines);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// โโ Merge Failure Policy Application (TP-005 Step 2) โโโโโโโโโโโโโโโโโ
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Result of applying the merge failure policy.
|
|
305
|
+
*
|
|
306
|
+
* Pure function output โ callers use this to perform state mutations
|
|
307
|
+
* and notifications consistently. Ensures engine.ts and resume.ts
|
|
308
|
+
* apply identical pause/abort transitions.
|
|
309
|
+
*/
|
|
310
|
+
export interface MergeFailurePolicyResult {
|
|
311
|
+
/** The applied policy: "pause" or "abort". */
|
|
312
|
+
policy: "pause" | "abort";
|
|
313
|
+
/** Target phase for batchState.phase. */
|
|
314
|
+
targetPhase: "paused" | "stopped";
|
|
315
|
+
/** Error message to push to batchState.errors. */
|
|
316
|
+
errorMessage: string;
|
|
317
|
+
/** Persistence trigger label. */
|
|
318
|
+
persistTrigger: "merge-failure-pause" | "merge-failure-abort";
|
|
319
|
+
/** User-facing notification message. */
|
|
320
|
+
notifyMessage: string;
|
|
321
|
+
/** Notification level for onNotify. */
|
|
322
|
+
notifyLevel: "error";
|
|
323
|
+
/** Comma-separated failed lane identifiers for logging. */
|
|
324
|
+
failedLaneIds: string;
|
|
325
|
+
/** Structured log details for execLog. */
|
|
326
|
+
logDetails: {
|
|
327
|
+
failedLane: number;
|
|
328
|
+
failedLaneIds: string;
|
|
329
|
+
reason: string;
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Compute the merge failure policy application result.
|
|
335
|
+
*
|
|
336
|
+
* This is a **pure function** โ it computes all outputs deterministically
|
|
337
|
+
* from the merge result and config, without performing any side effects.
|
|
338
|
+
*
|
|
339
|
+
* Both engine.ts and resume.ts MUST use this function to guarantee
|
|
340
|
+
* identical failure attribution, phase transitions, error messages,
|
|
341
|
+
* and notifications on repo-scoped merge failures.
|
|
342
|
+
*
|
|
343
|
+
* Failure attribution rules (priority chain):
|
|
344
|
+
* 1. Lane-level: lanes with CONFLICT_UNRESOLVED, BUILD_FAILURE, or error
|
|
345
|
+
* โ formatted as `lane-<N>` (comma-separated).
|
|
346
|
+
* 2. Fallback: if no lane-level failures but `mergeResult.failedLane`
|
|
347
|
+
* is non-null, uses `lane-<N>` as the identifier.
|
|
348
|
+
* 3. Repo-level: if no lane-level failures and failedLane is null
|
|
349
|
+
* (repo setup failure), uses `repo:<repoId>` from repoResults
|
|
350
|
+
* entries with non-succeeded status. Sorted deterministically.
|
|
351
|
+
* - The failure reason is truncated to 200 chars for notifications and
|
|
352
|
+
* logged in full in batchState.errors.
|
|
353
|
+
*
|
|
354
|
+
* @param mergeResult - The merge wave result with status "failed" or "partial"
|
|
355
|
+
* @param waveIndex - 0-based wave index (displayed as 1-indexed)
|
|
356
|
+
* @param config - Orchestrator configuration (for on_merge_failure policy)
|
|
357
|
+
* @returns Policy result object for callers to apply
|
|
358
|
+
*/
|
|
359
|
+
export function computeMergeFailurePolicy(
|
|
360
|
+
mergeResult: MergeWaveResult,
|
|
361
|
+
waveIndex: number,
|
|
362
|
+
config: OrchestratorConfig,
|
|
363
|
+
): MergeFailurePolicyResult {
|
|
364
|
+
const waveNum = waveIndex + 1;
|
|
365
|
+
const mergeFailurePolicy = config.failure.on_merge_failure;
|
|
366
|
+
|
|
367
|
+
// Build failed lane identifiers from lane results.
|
|
368
|
+
// Priority chain:
|
|
369
|
+
// 1. Lane-level: lanes with CONFLICT_UNRESOLVED, BUILD_FAILURE, or error
|
|
370
|
+
// 2. Fallback: failedLane from mergeResult (single lane ID)
|
|
371
|
+
// 3. Repo-level: repos with non-succeeded status from repoResults
|
|
372
|
+
// (catches setup failures where failedLane=null and no lane results)
|
|
373
|
+
let failedLaneIds = mergeResult.laneResults
|
|
374
|
+
.filter(
|
|
375
|
+
(r) =>
|
|
376
|
+
r.result?.status === "CONFLICT_UNRESOLVED" || r.result?.status === "BUILD_FAILURE" || r.error,
|
|
377
|
+
)
|
|
378
|
+
.map((r) => `lane-${r.laneNumber}`)
|
|
379
|
+
.join(", ");
|
|
380
|
+
if (!failedLaneIds && mergeResult.failedLane !== null) {
|
|
381
|
+
failedLaneIds = `lane-${mergeResult.failedLane}`;
|
|
382
|
+
}
|
|
383
|
+
if (!failedLaneIds && mergeResult.repoResults && mergeResult.repoResults.length > 0) {
|
|
384
|
+
// Repo-level fallback for setup failures (no lane results, failedLane=null).
|
|
385
|
+
// Uses sorted repoResults order for determinism.
|
|
386
|
+
failedLaneIds = mergeResult.repoResults
|
|
387
|
+
.filter((r) => r.status !== "succeeded")
|
|
388
|
+
.map((r) => `repo:${r.repoId ?? "default"}`)
|
|
389
|
+
.join(", ");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const reason = mergeResult.failureReason || "unknown";
|
|
393
|
+
const reasonTruncated = reason.slice(0, 200);
|
|
394
|
+
|
|
395
|
+
const logDetails = {
|
|
396
|
+
failedLane: mergeResult.failedLane ?? 0,
|
|
397
|
+
failedLaneIds,
|
|
398
|
+
reason: reasonTruncated,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const errorMessage =
|
|
402
|
+
`Merge failed at wave ${waveNum}: ${reason}. ` +
|
|
403
|
+
(mergeFailurePolicy === "pause"
|
|
404
|
+
? `Batch paused. Resolve conflicts and use /orch-resume to continue.`
|
|
405
|
+
: `Batch aborted by on_merge_failure policy.`);
|
|
406
|
+
|
|
407
|
+
const laneDetail = failedLaneIds ? ` (${failedLaneIds})` : "";
|
|
408
|
+
|
|
409
|
+
let notifyMessage: string;
|
|
410
|
+
if (mergeFailurePolicy === "pause") {
|
|
411
|
+
notifyMessage =
|
|
412
|
+
`โธ๏ธ Batch paused due to merge failure at wave ${waveNum}${laneDetail}. ` +
|
|
413
|
+
`Reason: ${reasonTruncated}. ` +
|
|
414
|
+
`Resolve conflicts and resume.`;
|
|
415
|
+
} else {
|
|
416
|
+
notifyMessage =
|
|
417
|
+
`โ Batch aborted due to merge failure at wave ${waveNum}${laneDetail}. ` +
|
|
418
|
+
`Reason: ${reasonTruncated}.`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
policy: mergeFailurePolicy,
|
|
423
|
+
targetPhase: mergeFailurePolicy === "pause" ? "paused" : "stopped",
|
|
424
|
+
errorMessage,
|
|
425
|
+
persistTrigger: mergeFailurePolicy === "pause" ? "merge-failure-pause" : "merge-failure-abort",
|
|
426
|
+
notifyMessage,
|
|
427
|
+
notifyLevel: "error",
|
|
428
|
+
failedLaneIds,
|
|
429
|
+
logDetails,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// โโ Cleanup Gate Policy (TP-029 Step 2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Per-repo cleanup failure detail.
|
|
437
|
+
* Collected during post-merge inter-wave verification.
|
|
438
|
+
*/
|
|
439
|
+
export interface CleanupGateRepoFailure {
|
|
440
|
+
/** Repo root path that has stale worktrees */
|
|
441
|
+
repoRoot: string;
|
|
442
|
+
/** Repo ID (undefined for primary/repo-mode) */
|
|
443
|
+
repoId: string | undefined;
|
|
444
|
+
/** Paths of stale worktrees still registered after cleanup */
|
|
445
|
+
staleWorktrees: string[];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Result of applying the cleanup gate policy.
|
|
450
|
+
*
|
|
451
|
+
* Pure function output โ callers use this to perform state mutations
|
|
452
|
+
* and notifications consistently. Ensures engine.ts and resume.ts
|
|
453
|
+
* apply identical pause transitions on cleanup failure.
|
|
454
|
+
*/
|
|
455
|
+
export interface CleanupGatePolicyResult {
|
|
456
|
+
/** Always "pause" โ cleanup failures block next wave but preserve merged work */
|
|
457
|
+
policy: "pause";
|
|
458
|
+
/** Target phase for batchState.phase */
|
|
459
|
+
targetPhase: "paused";
|
|
460
|
+
/** Error message to push to batchState.errors */
|
|
461
|
+
errorMessage: string;
|
|
462
|
+
/** Persistence trigger label โ matches spec classification naming */
|
|
463
|
+
persistTrigger: "cleanup_post_merge_failed";
|
|
464
|
+
/** User-facing notification message */
|
|
465
|
+
notifyMessage: string;
|
|
466
|
+
/** Notification level for onNotify */
|
|
467
|
+
notifyLevel: "error";
|
|
468
|
+
/** Structured log details for execLog */
|
|
469
|
+
logDetails: {
|
|
470
|
+
waveNumber: number;
|
|
471
|
+
failedRepoCount: number;
|
|
472
|
+
totalStaleWorktrees: number;
|
|
473
|
+
repos: Array<{ repoId: string; staleCount: number }>;
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Compute the cleanup gate policy result for post-merge verification failure.
|
|
479
|
+
*
|
|
480
|
+
* This is a **pure function** โ it computes all outputs deterministically
|
|
481
|
+
* from the wave index and per-repo failure details, without performing any
|
|
482
|
+
* side effects.
|
|
483
|
+
*
|
|
484
|
+
* Both engine.ts and resume.ts MUST use this function to guarantee
|
|
485
|
+
* identical failure attribution, phase transitions, error messages,
|
|
486
|
+
* and notifications when post-merge cleanup leaves stale worktrees.
|
|
487
|
+
*
|
|
488
|
+
* The cleanup gate always pauses (never aborts) because:
|
|
489
|
+
* - Merged commits are already on the orch branch and must not be lost
|
|
490
|
+
* - The operator can manually remove stale worktrees and `/orch-resume`
|
|
491
|
+
*
|
|
492
|
+
* @param waveIndex - 0-based wave index (displayed as 1-indexed)
|
|
493
|
+
* @param failures - Per-repo cleanup failure details
|
|
494
|
+
* @returns Policy result object for callers to apply
|
|
495
|
+
*/
|
|
496
|
+
export function computeCleanupGatePolicy(
|
|
497
|
+
waveIndex: number,
|
|
498
|
+
failures: CleanupGateRepoFailure[],
|
|
499
|
+
): CleanupGatePolicyResult {
|
|
500
|
+
const waveNum = waveIndex + 1;
|
|
501
|
+
const failedRepoCount = failures.length;
|
|
502
|
+
const totalStaleWorktrees = failures.reduce((sum, f) => sum + f.staleWorktrees.length, 0);
|
|
503
|
+
|
|
504
|
+
const repos = failures.map((f) => ({
|
|
505
|
+
repoId: f.repoId ?? "(default)",
|
|
506
|
+
staleCount: f.staleWorktrees.length,
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
const repoDetail = repos.map((r) => `${r.repoId} (${r.staleCount} stale)`).join(", ");
|
|
510
|
+
|
|
511
|
+
const errorMessage =
|
|
512
|
+
`Post-merge cleanup failed at wave ${waveNum}: ${totalStaleWorktrees} stale worktree(s) ` +
|
|
513
|
+
`in ${failedRepoCount} repo(s) [${repoDetail}]. ` +
|
|
514
|
+
`Batch paused. Remove stale worktrees manually and use /orch-resume to continue.`;
|
|
515
|
+
|
|
516
|
+
// Build recovery commands for each failed repo
|
|
517
|
+
const recoveryLines: string[] = [];
|
|
518
|
+
for (const f of failures) {
|
|
519
|
+
const label = f.repoId ?? "default";
|
|
520
|
+
for (const wt of f.staleWorktrees) {
|
|
521
|
+
recoveryLines.push(` git worktree remove --force "${wt}" # repo: ${label}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const notifyMessage =
|
|
526
|
+
`โธ๏ธ Batch paused: post-merge cleanup failed at wave ${waveNum}.\n` +
|
|
527
|
+
` ${totalStaleWorktrees} stale worktree(s) in ${failedRepoCount} repo(s): ${repoDetail}\n` +
|
|
528
|
+
` Manual recovery:\n` +
|
|
529
|
+
recoveryLines.join("\n") +
|
|
530
|
+
"\n" +
|
|
531
|
+
` Then: /orch-resume`;
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
policy: "pause",
|
|
535
|
+
targetPhase: "paused",
|
|
536
|
+
errorMessage,
|
|
537
|
+
persistTrigger: "cleanup_post_merge_failed",
|
|
538
|
+
notifyMessage,
|
|
539
|
+
notifyLevel: "error",
|
|
540
|
+
logDetails: {
|
|
541
|
+
waveNumber: waveNum,
|
|
542
|
+
failedRepoCount,
|
|
543
|
+
totalStaleWorktrees,
|
|
544
|
+
repos,
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// โโ Merge Retry Policy (TP-033 Step 2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Classify a merge failure into a MergeFailureClassification.
|
|
553
|
+
*
|
|
554
|
+
* Inspects the MergeWaveResult โ lane errors, failure reasons, and merge
|
|
555
|
+
* result statuses โ to determine which retry policy class applies.
|
|
556
|
+
*
|
|
557
|
+
* Classification priority (first match wins):
|
|
558
|
+
* 1. `verification_new_failure` โ any lane error starts with "verification_new_failure"
|
|
559
|
+
* 2. `merge_conflict_unresolved` โ any lane result has CONFLICT_UNRESOLVED status
|
|
560
|
+
* 3. `cleanup_post_merge_failed` โ failure reason contains "cleanup" or "stale worktree"
|
|
561
|
+
* 4. `git_lock_file` โ failure reason contains "lock" or ".lock"
|
|
562
|
+
* 5. `git_worktree_dirty` โ failure reason contains "dirty" or "worktree"
|
|
563
|
+
* 6. `null` โ unclassifiable (treated as non-retriable by callers)
|
|
564
|
+
*
|
|
565
|
+
* This is a **pure function** โ no side effects.
|
|
566
|
+
*
|
|
567
|
+
* @param mergeResult - The failed MergeWaveResult to classify
|
|
568
|
+
* @returns Classification or null if no merge-retry class matches
|
|
569
|
+
* @since TP-033
|
|
570
|
+
*/
|
|
571
|
+
export function classifyMergeFailure(
|
|
572
|
+
mergeResult: MergeWaveResult,
|
|
573
|
+
): MergeFailureClassification | null {
|
|
574
|
+
// Check lane-level errors first (most specific)
|
|
575
|
+
for (const lr of mergeResult.laneResults) {
|
|
576
|
+
if (lr.error && lr.error.startsWith("verification_new_failure")) {
|
|
577
|
+
return "verification_new_failure";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Check lane result statuses
|
|
582
|
+
for (const lr of mergeResult.laneResults) {
|
|
583
|
+
if (lr.result?.status === "CONFLICT_UNRESOLVED") {
|
|
584
|
+
return "merge_conflict_unresolved";
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Check failure reason string patterns
|
|
589
|
+
const reason = (mergeResult.failureReason || "").toLowerCase();
|
|
590
|
+
|
|
591
|
+
// Lock file detection: git operations fail with "Unable to create '.../.git/index.lock': File exists"
|
|
592
|
+
if (reason.includes("lock") || reason.includes(".lock")) {
|
|
593
|
+
return "git_lock_file";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Cleanup failures: stale worktrees or cleanup errors
|
|
597
|
+
if (reason.includes("cleanup") || reason.includes("stale worktree")) {
|
|
598
|
+
return "cleanup_post_merge_failed";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Dirty worktree: git operations fail due to uncommitted changes
|
|
602
|
+
if (reason.includes("dirty") || reason.includes("worktree")) {
|
|
603
|
+
return "git_worktree_dirty";
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Compute the retry decision for a merge failure.
|
|
611
|
+
*
|
|
612
|
+
* Given the failure classification and the current retry count for the
|
|
613
|
+
* relevant scope, returns a decision indicating whether to retry, the
|
|
614
|
+
* cooldown to wait, or the exhaustion action to take.
|
|
615
|
+
*
|
|
616
|
+
* This is a **pure function** โ both engine.ts and resume.ts MUST use
|
|
617
|
+
* this function to guarantee identical retry behavior.
|
|
618
|
+
*
|
|
619
|
+
* @param classification - The classified merge failure (null = unclassifiable)
|
|
620
|
+
* @param currentRetryCount - Current retry attempts for this scope (0 = first failure)
|
|
621
|
+
* @returns Retry decision with all fields populated
|
|
622
|
+
* @since TP-033
|
|
623
|
+
*/
|
|
624
|
+
export function computeMergeRetryDecision(
|
|
625
|
+
classification: MergeFailureClassification | null,
|
|
626
|
+
currentRetryCount: number,
|
|
627
|
+
): MergeRetryDecision {
|
|
628
|
+
// Unclassifiable failures are never retried
|
|
629
|
+
if (classification === null) {
|
|
630
|
+
return {
|
|
631
|
+
shouldRetry: false,
|
|
632
|
+
cooldownMs: 0,
|
|
633
|
+
reason: "Unclassifiable merge failure โ no retry policy available",
|
|
634
|
+
currentAttempt: currentRetryCount,
|
|
635
|
+
maxAttempts: 0,
|
|
636
|
+
classification: "merge_conflict_unresolved", // placeholder for type safety
|
|
637
|
+
exhaustionAction: "pause",
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const policy: MergeRetryPolicy = MERGE_RETRY_POLICY_MATRIX[classification];
|
|
642
|
+
|
|
643
|
+
if (!policy.retriable) {
|
|
644
|
+
return {
|
|
645
|
+
shouldRetry: false,
|
|
646
|
+
cooldownMs: 0,
|
|
647
|
+
reason: `${classification} is not retriable โ immediate ${policy.exhaustionAction}`,
|
|
648
|
+
currentAttempt: currentRetryCount,
|
|
649
|
+
maxAttempts: 0,
|
|
650
|
+
classification,
|
|
651
|
+
exhaustionAction: policy.exhaustionAction,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (currentRetryCount >= policy.maxAttempts) {
|
|
656
|
+
return {
|
|
657
|
+
shouldRetry: false,
|
|
658
|
+
cooldownMs: 0,
|
|
659
|
+
reason: `${classification} retry exhausted (${currentRetryCount}/${policy.maxAttempts}) โ ${policy.exhaustionAction}`,
|
|
660
|
+
currentAttempt: currentRetryCount,
|
|
661
|
+
maxAttempts: policy.maxAttempts,
|
|
662
|
+
classification,
|
|
663
|
+
exhaustionAction: policy.exhaustionAction,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
shouldRetry: true,
|
|
669
|
+
cooldownMs: policy.cooldownMs,
|
|
670
|
+
reason:
|
|
671
|
+
`${classification} retry ${currentRetryCount + 1}/${policy.maxAttempts}` +
|
|
672
|
+
(policy.cooldownMs > 0 ? ` (cooldown: ${policy.cooldownMs}ms)` : ""),
|
|
673
|
+
currentAttempt: currentRetryCount + 1,
|
|
674
|
+
maxAttempts: policy.maxAttempts,
|
|
675
|
+
classification,
|
|
676
|
+
exhaustionAction: policy.exhaustionAction,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Build the merge retry scope key for persisted retry counters.
|
|
682
|
+
*
|
|
683
|
+
* Format: `{repoId}:w{waveIndex}:l{laneNumber}`
|
|
684
|
+
* - In workspace mode: uses the repo ID (e.g., "api:w0:l1")
|
|
685
|
+
* - In repo mode (repoId undefined/null): uses "default" (e.g., "default:w0:l1")
|
|
686
|
+
*
|
|
687
|
+
* NOTE: This is a different key format from the task-scoped format in v3 types
|
|
688
|
+
* (`{taskId}:w{waveIndex}:l{laneNumber}`). The merge retry scope is intentionally
|
|
689
|
+
* repo-scoped because merge failures are per-repo, not per-task. Both formats
|
|
690
|
+
* coexist in `resilience.retryCountByScope` โ the prefix disambiguates them.
|
|
691
|
+
*
|
|
692
|
+
* @param repoId - Repo ID (undefined/null in repo mode)
|
|
693
|
+
* @param waveIndex - 0-based wave index
|
|
694
|
+
* @param laneNumber - Lane number
|
|
695
|
+
* @returns Scope key string
|
|
696
|
+
* @since TP-033
|
|
697
|
+
*/
|
|
698
|
+
export function buildMergeRetryScopeKey(
|
|
699
|
+
repoId: string | undefined | null,
|
|
700
|
+
waveIndex: number,
|
|
701
|
+
laneNumber: number,
|
|
702
|
+
): string {
|
|
703
|
+
const repo = repoId ?? "default";
|
|
704
|
+
return `${repo}:w${waveIndex}:l${laneNumber}`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Extract the repo ID for a failed merge from the MergeWaveResult.
|
|
709
|
+
*
|
|
710
|
+
* Priority:
|
|
711
|
+
* 1. Lane-level: find the failed lane result and use its repoId
|
|
712
|
+
* 2. Repo-level: when failedLane is null (setup failure), check repoResults
|
|
713
|
+
* for the first failed repo group
|
|
714
|
+
* 3. Fallback: undefined (will become "default" in scope key)
|
|
715
|
+
*
|
|
716
|
+
* This ensures workspace-mode setup failures (e.g., worktree dirty before
|
|
717
|
+
* any lane starts) still get repo-scoped counters rather than all collapsing
|
|
718
|
+
* into "default:w{N}:l0".
|
|
719
|
+
*
|
|
720
|
+
* @param mergeResult - The failed MergeWaveResult
|
|
721
|
+
* @returns Repo ID or undefined if not determinable
|
|
722
|
+
* @since TP-033 R006
|
|
723
|
+
*/
|
|
724
|
+
export function extractFailedRepoId(mergeResult: MergeWaveResult): string | undefined {
|
|
725
|
+
const failedLaneNum = mergeResult.failedLane;
|
|
726
|
+
|
|
727
|
+
// 1. Try lane-level extraction
|
|
728
|
+
if (failedLaneNum !== null && failedLaneNum !== undefined) {
|
|
729
|
+
const failedLaneResult = mergeResult.laneResults.find(
|
|
730
|
+
(lr) =>
|
|
731
|
+
lr.laneNumber === failedLaneNum &&
|
|
732
|
+
(lr.error ||
|
|
733
|
+
lr.result?.status === "CONFLICT_UNRESOLVED" ||
|
|
734
|
+
lr.result?.status === "BUILD_FAILURE"),
|
|
735
|
+
);
|
|
736
|
+
if (failedLaneResult?.repoId) return failedLaneResult.repoId;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// 2. Repo-level fallback for setup failures (failedLane === null)
|
|
740
|
+
if (mergeResult.repoResults && mergeResult.repoResults.length > 0) {
|
|
741
|
+
const failedRepo = mergeResult.repoResults.find(
|
|
742
|
+
(rr) => rr.status === "failed" || rr.status === "partial",
|
|
743
|
+
);
|
|
744
|
+
if (failedRepo?.repoId) return failedRepo.repoId;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 3. If failureReason mentions a specific repo path, we could parse it,
|
|
748
|
+
// but that's fragile. Return undefined โ "default" in scope key.
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Shared merge retry loop used by both engine.ts and resume.ts.
|
|
754
|
+
*
|
|
755
|
+
* Wraps the retry cycle in a loop: after each failed retry, re-classifies
|
|
756
|
+
* the latest mergeResult, recomputes the retry decision using the persisted
|
|
757
|
+
* counter, and continues until success, safe-stop, or exhaustion/non-retriable.
|
|
758
|
+
*
|
|
759
|
+
* This is the **single implementation** of retry loop semantics.
|
|
760
|
+
* Engine.ts and resume.ts provide callbacks for their specific side effects
|
|
761
|
+
* (persistence, merge invocation, notification) to guarantee parity.
|
|
762
|
+
*
|
|
763
|
+
* **Important:** On retry exhaustion, this returns `kind: "exhausted"` which
|
|
764
|
+
* the caller MUST handle by forcing `paused` phase regardless of
|
|
765
|
+
* `on_merge_failure` config. The exhaustion action from the matrix takes
|
|
766
|
+
* precedence over config policy.
|
|
767
|
+
*
|
|
768
|
+
* @param mergeResult - The initial failed merge result
|
|
769
|
+
* @param waveIdx - 0-based wave index (for logging)
|
|
770
|
+
* @param retryCountByScope - Mutable reference to persisted retry counters
|
|
771
|
+
* @param callbacks - Side-effect callbacks for persistence/merge/logging
|
|
772
|
+
* @returns Outcome describing what happened during the retry cycle
|
|
773
|
+
* @since TP-033 R006
|
|
774
|
+
*/
|
|
775
|
+
export async function applyMergeRetryLoop(
|
|
776
|
+
mergeResult: MergeWaveResult,
|
|
777
|
+
waveIdx: number,
|
|
778
|
+
retryCountByScope: Record<string, number>,
|
|
779
|
+
callbacks: MergeRetryCallbacks,
|
|
780
|
+
): Promise<MergeRetryLoopOutcome> {
|
|
781
|
+
let currentResult = mergeResult;
|
|
782
|
+
|
|
783
|
+
// Classify the initial failure
|
|
784
|
+
let classification = classifyMergeFailure(currentResult);
|
|
785
|
+
const failedRepoId = extractFailedRepoId(currentResult);
|
|
786
|
+
const failedLaneNum = currentResult.failedLane ?? 0;
|
|
787
|
+
const scopeKey = buildMergeRetryScopeKey(failedRepoId, waveIdx, failedLaneNum);
|
|
788
|
+
const currentRetryCount = retryCountByScope[scopeKey] ?? 0;
|
|
789
|
+
|
|
790
|
+
// Check if any retry is possible at all
|
|
791
|
+
const initialDecision = computeMergeRetryDecision(classification, currentRetryCount);
|
|
792
|
+
|
|
793
|
+
if (!initialDecision.shouldRetry) {
|
|
794
|
+
// Non-retriable or already exhausted before we start
|
|
795
|
+
if (classification !== null && initialDecision.currentAttempt > 0) {
|
|
796
|
+
// Previously had retries โ this is exhaustion
|
|
797
|
+
return {
|
|
798
|
+
kind: "exhausted",
|
|
799
|
+
mergeResult: currentResult,
|
|
800
|
+
classification,
|
|
801
|
+
scopeKey,
|
|
802
|
+
lastDecision: initialDecision,
|
|
803
|
+
errorMessage: `Merge retry exhausted at wave ${waveIdx + 1}: ${initialDecision.reason}`,
|
|
804
|
+
notifyMessage: `โธ๏ธ Merge retry exhausted at wave ${waveIdx + 1}. ${initialDecision.reason}`,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
// No retry was ever possible
|
|
808
|
+
return {
|
|
809
|
+
kind: "no_retry",
|
|
810
|
+
mergeResult: currentResult,
|
|
811
|
+
classification,
|
|
812
|
+
scopeKey,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Enter retry loop
|
|
817
|
+
let lastDecision = initialDecision;
|
|
818
|
+
|
|
819
|
+
while (lastDecision.shouldRetry) {
|
|
820
|
+
// Increment counter in persisted state
|
|
821
|
+
retryCountByScope[scopeKey] = lastDecision.currentAttempt;
|
|
822
|
+
|
|
823
|
+
callbacks.log(`merge retry: ${lastDecision.reason}`, {
|
|
824
|
+
classification,
|
|
825
|
+
scopeKey,
|
|
826
|
+
attempt: lastDecision.currentAttempt,
|
|
827
|
+
maxAttempts: lastDecision.maxAttempts,
|
|
828
|
+
cooldownMs: lastDecision.cooldownMs,
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
callbacks.persist("merge-retry-increment");
|
|
832
|
+
|
|
833
|
+
// Emit Tier 0 attempt event via callback (TP-039 R004: emit only when retry is scheduled)
|
|
834
|
+
callbacks.onRetryAttempt?.(lastDecision);
|
|
835
|
+
|
|
836
|
+
callbacks.notify(
|
|
837
|
+
`๐ Merge retry (${lastDecision.reason}) at wave ${waveIdx + 1}. ` +
|
|
838
|
+
(lastDecision.cooldownMs > 0
|
|
839
|
+
? `Waiting ${lastDecision.cooldownMs}ms before retry...`
|
|
840
|
+
: "Retrying immediately..."),
|
|
841
|
+
"warning",
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
if (lastDecision.cooldownMs > 0) {
|
|
845
|
+
await callbacks.sleep(lastDecision.cooldownMs);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Re-invoke merge
|
|
849
|
+
callbacks.persist("merge-retry-start");
|
|
850
|
+
currentResult = await callbacks.performMerge();
|
|
851
|
+
callbacks.updateMergeResult(currentResult);
|
|
852
|
+
callbacks.persist("merge-retry-complete");
|
|
853
|
+
|
|
854
|
+
// Check outcome
|
|
855
|
+
if (currentResult.status === "succeeded") {
|
|
856
|
+
callbacks.notify(`โ
Merge retry succeeded at wave ${waveIdx + 1}.`, "info");
|
|
857
|
+
return {
|
|
858
|
+
kind: "retry_succeeded",
|
|
859
|
+
mergeResult: currentResult,
|
|
860
|
+
classification,
|
|
861
|
+
scopeKey,
|
|
862
|
+
lastDecision,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (currentResult.rollbackFailed) {
|
|
867
|
+
// Safe-stop takes priority
|
|
868
|
+
const hasPersistErrors =
|
|
869
|
+
currentResult.persistenceErrors && currentResult.persistenceErrors.length > 0;
|
|
870
|
+
const persistWarning = hasPersistErrors
|
|
871
|
+
? ` WARNING: ${currentResult.persistenceErrors!.length} transaction record(s) failed to persist.`
|
|
872
|
+
: "";
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
kind: "safe_stop",
|
|
876
|
+
mergeResult: currentResult,
|
|
877
|
+
classification,
|
|
878
|
+
scopeKey,
|
|
879
|
+
lastDecision,
|
|
880
|
+
errorMessage:
|
|
881
|
+
`Safe-stop at wave ${waveIdx + 1}: verification rollback failed after retry. ` +
|
|
882
|
+
`Merge worktree and temp branch preserved for recovery.` +
|
|
883
|
+
persistWarning,
|
|
884
|
+
notifyMessage:
|
|
885
|
+
`๐ Safe-stop: verification rollback failed at wave ${waveIdx + 1} after retry. ` +
|
|
886
|
+
`Batch force-paused.` +
|
|
887
|
+
persistWarning,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Retry failed โ re-classify and check if we can retry again
|
|
892
|
+
classification = classifyMergeFailure(currentResult);
|
|
893
|
+
const updatedCount = retryCountByScope[scopeKey] ?? 0;
|
|
894
|
+
lastDecision = computeMergeRetryDecision(classification, updatedCount);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Loop ended: exhaustion
|
|
898
|
+
return {
|
|
899
|
+
kind: "exhausted",
|
|
900
|
+
mergeResult: currentResult,
|
|
901
|
+
classification,
|
|
902
|
+
scopeKey,
|
|
903
|
+
lastDecision,
|
|
904
|
+
errorMessage: `Merge retry exhausted at wave ${waveIdx + 1}: ${lastDecision.reason}`,
|
|
905
|
+
notifyMessage: `โธ๏ธ Merge retry exhausted at wave ${waveIdx + 1}. ${lastDecision.reason}`,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// โโ Integrate Cleanup Acceptance (TP-029 Step 3) โโโโโโโโโโโโโโโโโโโโโ
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Per-repo acceptance check findings after /orch-integrate.
|
|
913
|
+
* Collected by scanning all workspace repos (not just repos that had the orch branch).
|
|
914
|
+
*/
|
|
915
|
+
export interface IntegrateCleanupRepoFindings {
|
|
916
|
+
/** Repo root path */
|
|
917
|
+
repoRoot: string;
|
|
918
|
+
/** Repo ID (undefined for repo-mode / primary) */
|
|
919
|
+
repoId: string | undefined;
|
|
920
|
+
/** Stale lane worktrees still registered (git worktree list matches) */
|
|
921
|
+
staleWorktrees: string[];
|
|
922
|
+
/** Stale lane branches (task/{opId}-lane-*) */
|
|
923
|
+
staleLaneBranches: string[];
|
|
924
|
+
/** Stale orch branches (orch/{opId}-{batchId}) */
|
|
925
|
+
staleOrchBranches: string[];
|
|
926
|
+
/** Batch-scoped autostash entries still present */
|
|
927
|
+
staleAutostashEntries: string[];
|
|
928
|
+
/** Non-empty .worktrees/ containers */
|
|
929
|
+
nonEmptyWorktreeContainers: string[];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Result of the /orch-integrate cleanup acceptance check.
|
|
934
|
+
* Pure function output โ callers use this to format the summary notification.
|
|
935
|
+
*/
|
|
936
|
+
export interface IntegrateCleanupResult {
|
|
937
|
+
/** True if all repos pass all acceptance criteria */
|
|
938
|
+
clean: boolean;
|
|
939
|
+
/** Notification severity level: "info" when clean, "warning" when dirty */
|
|
940
|
+
notifyLevel: "info" | "warning";
|
|
941
|
+
/** Per-repo findings (only repos with at least one finding) */
|
|
942
|
+
dirtyRepos: IntegrateCleanupRepoFindings[];
|
|
943
|
+
/** User-facing cleanup report (appended to integrate summary) */
|
|
944
|
+
report: string;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Compute the integrate cleanup result from per-repo acceptance findings.
|
|
949
|
+
*
|
|
950
|
+
* This is a **pure function** โ computes all outputs deterministically
|
|
951
|
+
* from the per-repo findings without side effects.
|
|
952
|
+
*
|
|
953
|
+
* The acceptance criteria (roadmap 2d) are:
|
|
954
|
+
* 1. No registered lane worktrees remain in any workspace repo
|
|
955
|
+
* 2. No lane branches remain (task/{opId}-lane-*)
|
|
956
|
+
* 3. No orch branches remain (orch/{opId}-{batchId})
|
|
957
|
+
* 4. No stale autostash from current batch remains
|
|
958
|
+
* 5. No non-empty .worktrees/ containers remain
|
|
959
|
+
*
|
|
960
|
+
* @param repoFindings - Per-repo findings from scanning all workspace repos
|
|
961
|
+
* @returns Cleanup result with pass/fail verdict and human-readable report
|
|
962
|
+
*/
|
|
963
|
+
export function computeIntegrateCleanupResult(
|
|
964
|
+
repoFindings: IntegrateCleanupRepoFindings[],
|
|
965
|
+
): IntegrateCleanupResult {
|
|
966
|
+
// Filter to repos that have at least one issue
|
|
967
|
+
const dirtyRepos = repoFindings.filter(
|
|
968
|
+
(r) =>
|
|
969
|
+
r.staleWorktrees.length > 0 ||
|
|
970
|
+
r.staleLaneBranches.length > 0 ||
|
|
971
|
+
r.staleOrchBranches.length > 0 ||
|
|
972
|
+
r.staleAutostashEntries.length > 0 ||
|
|
973
|
+
r.nonEmptyWorktreeContainers.length > 0,
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
if (dirtyRepos.length === 0) {
|
|
977
|
+
return {
|
|
978
|
+
clean: true,
|
|
979
|
+
notifyLevel: "info",
|
|
980
|
+
dirtyRepos: [],
|
|
981
|
+
report: "๐งน Cleanup verified: no stale worktrees, branches, or autostash entries remain.",
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Build per-repo detail lines
|
|
986
|
+
const details: string[] = [];
|
|
987
|
+
for (const repo of dirtyRepos) {
|
|
988
|
+
const label = repo.repoId ?? "(default)";
|
|
989
|
+
const issues: string[] = [];
|
|
990
|
+
if (repo.staleWorktrees.length > 0) {
|
|
991
|
+
issues.push(`${repo.staleWorktrees.length} stale worktree(s)`);
|
|
992
|
+
}
|
|
993
|
+
if (repo.staleLaneBranches.length > 0) {
|
|
994
|
+
issues.push(`${repo.staleLaneBranches.length} lane branch(es)`);
|
|
995
|
+
}
|
|
996
|
+
if (repo.staleOrchBranches.length > 0) {
|
|
997
|
+
issues.push(`${repo.staleOrchBranches.length} orch branch(es)`);
|
|
998
|
+
}
|
|
999
|
+
if (repo.staleAutostashEntries.length > 0) {
|
|
1000
|
+
issues.push(`${repo.staleAutostashEntries.length} autostash entr(ies)`);
|
|
1001
|
+
}
|
|
1002
|
+
if (repo.nonEmptyWorktreeContainers.length > 0) {
|
|
1003
|
+
issues.push(`${repo.nonEmptyWorktreeContainers.length} non-empty .worktrees/ container(s)`);
|
|
1004
|
+
}
|
|
1005
|
+
details.push(` ${label}: ${issues.join(", ")}`);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Build recovery commands
|
|
1009
|
+
const recovery: string[] = [];
|
|
1010
|
+
for (const repo of dirtyRepos) {
|
|
1011
|
+
const label = repo.repoId ?? "default";
|
|
1012
|
+
for (const wt of repo.staleWorktrees) {
|
|
1013
|
+
recovery.push(` git worktree remove --force "${wt}" # repo: ${label}`);
|
|
1014
|
+
}
|
|
1015
|
+
for (const br of repo.staleLaneBranches) {
|
|
1016
|
+
recovery.push(` git branch -D "${br}" # repo: ${label}`);
|
|
1017
|
+
}
|
|
1018
|
+
for (const br of repo.staleOrchBranches) {
|
|
1019
|
+
recovery.push(` git branch -D "${br}" # repo: ${label}`);
|
|
1020
|
+
}
|
|
1021
|
+
for (const entry of repo.staleAutostashEntries) {
|
|
1022
|
+
recovery.push(` git stash drop "${entry}" # repo: ${label}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const report =
|
|
1027
|
+
`โ ๏ธ Cleanup incomplete โ residual artifacts found:\n` +
|
|
1028
|
+
details.join("\n") +
|
|
1029
|
+
(recovery.length > 0 ? `\n Manual cleanup:\n${recovery.join("\n")}` : "");
|
|
1030
|
+
|
|
1031
|
+
return {
|
|
1032
|
+
clean: false,
|
|
1033
|
+
notifyLevel: "warning",
|
|
1034
|
+
dirtyRepos,
|
|
1035
|
+
report,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// โโ Resume ORCH_MESSAGES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1040
|
+
|
|
1041
|
+
// Note: These are added via extension to the ORCH_MESSAGES object below.
|
|
1042
|
+
|
|
1043
|
+
// โโ Resume Orchestration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Resume an interrupted batch from persisted state.
|
|
1047
|
+
*
|
|
1048
|
+
* Flow:
|
|
1049
|
+
* 1. Load and validate batch-state.json
|
|
1050
|
+
* 2. Check phase eligibility (paused/executing/merging only)
|
|
1051
|
+
* 3. Check for alive TMUX sessions and .DONE files
|
|
1052
|
+
* 4. Reconcile persisted state against live signals
|
|
1053
|
+
* 5. Compute resume point (which wave to start from)
|
|
1054
|
+
* 6. Reconstruct runtime state and continue execution
|
|
1055
|
+
*
|
|
1056
|
+
* @param orchConfig - Orchestrator configuration
|
|
1057
|
+
* @param runnerConfig - Task runner configuration
|
|
1058
|
+
* @param cwd - Repository root
|
|
1059
|
+
* @param batchState - Mutable batch state (will be populated from persisted state)
|
|
1060
|
+
* @param onNotify - Callback for user-facing messages
|
|
1061
|
+
* @param onMonitorUpdate - Optional callback for dashboard updates
|
|
1062
|
+
*/
|