@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/agents/AGENTS-MANIFEST.md +42 -0
  5. package/agents/brain.md +42 -0
  6. package/agents/context-builder.md +46 -0
  7. package/agents/delegate.md +12 -0
  8. package/agents/dev-1.md +42 -0
  9. package/agents/oracle.md +73 -0
  10. package/agents/planner.md +55 -0
  11. package/agents/researcher.md +52 -0
  12. package/agents/reviewer.md +79 -0
  13. package/agents/scout.md +50 -0
  14. package/agents/tester.md +45 -0
  15. package/agents/worker.md +55 -0
  16. package/extensions/ralph.ts +1 -0
  17. package/extensions/reviewer-extension.ts +125 -0
  18. package/extensions/task-orchestrator.ts +28 -0
  19. package/package.json +63 -0
  20. package/prompts/gather-context-and-clarify.md +13 -0
  21. package/prompts/parallel-cleanup.md +59 -0
  22. package/prompts/parallel-context-build.md +53 -0
  23. package/prompts/parallel-handoff-plan.md +59 -0
  24. package/prompts/parallel-research.md +50 -0
  25. package/prompts/parallel-review.md +54 -0
  26. package/prompts/review-loop.md +41 -0
  27. package/skills/orchid/SKILL.md +214 -0
  28. package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
  29. package/skills/orchid/orchid-converge/SKILL.md +124 -0
  30. package/skills/orchid/orchid-decompose/SKILL.md +201 -0
  31. package/skills/orchid/orchid-doctor/SKILL.md +162 -0
  32. package/skills/orchid/orchid-investigate/SKILL.md +102 -0
  33. package/skills/orchid/orchid-launch/SKILL.md +147 -0
  34. package/skills/ralph/SKILL.md +73 -0
  35. package/skills/subagents/pi-subagents/SKILL.md +813 -0
  36. package/src/index.ts +7 -0
  37. package/src/orchestrator/abort.ts +534 -0
  38. package/src/orchestrator/agent-bridge-extension.ts +1020 -0
  39. package/src/orchestrator/agent-host.ts +954 -0
  40. package/src/orchestrator/cleanup.ts +776 -0
  41. package/src/orchestrator/config-loader.ts +1412 -0
  42. package/src/orchestrator/config-schema.ts +690 -0
  43. package/src/orchestrator/config.ts +81 -0
  44. package/src/orchestrator/context-window.ts +66 -0
  45. package/src/orchestrator/diagnostic-reports.ts +475 -0
  46. package/src/orchestrator/diagnostics.ts +394 -0
  47. package/src/orchestrator/discovery.ts +1833 -0
  48. package/src/orchestrator/engine-worker.ts +415 -0
  49. package/src/orchestrator/engine.ts +5940 -0
  50. package/src/orchestrator/execution.ts +3104 -0
  51. package/src/orchestrator/extension.ts +5934 -0
  52. package/src/orchestrator/formatting.ts +785 -0
  53. package/src/orchestrator/git.ts +88 -0
  54. package/src/orchestrator/index.ts +28 -0
  55. package/src/orchestrator/lane-runner.ts +1787 -0
  56. package/src/orchestrator/mailbox.ts +780 -0
  57. package/src/orchestrator/merge.ts +3414 -0
  58. package/src/orchestrator/messages.ts +1062 -0
  59. package/src/orchestrator/migrations.ts +278 -0
  60. package/src/orchestrator/naming.ts +117 -0
  61. package/src/orchestrator/path-resolver.ts +275 -0
  62. package/src/orchestrator/persistence.ts +2625 -0
  63. package/src/orchestrator/process-registry.ts +452 -0
  64. package/src/orchestrator/quality-gate.ts +1085 -0
  65. package/src/orchestrator/resume.ts +3488 -0
  66. package/src/orchestrator/sessions.ts +57 -0
  67. package/src/orchestrator/settings-loader.ts +136 -0
  68. package/src/orchestrator/settings-tui.ts +2208 -0
  69. package/src/orchestrator/sidecar-telemetry.ts +267 -0
  70. package/src/orchestrator/supervisor.ts +4548 -0
  71. package/src/orchestrator/task-executor-core.ts +675 -0
  72. package/src/orchestrator/tmux-compat.ts +37 -0
  73. package/src/orchestrator/tool-allowlist-constants.ts +37 -0
  74. package/src/orchestrator/types.ts +4465 -0
  75. package/src/orchestrator/verification.ts +547 -0
  76. package/src/orchestrator/waves.ts +1564 -0
  77. package/src/orchestrator/workspace.ts +707 -0
  78. package/src/orchestrator/worktree.ts +2725 -0
  79. package/src/ralph/index.ts +825 -0
  80. package/src/subagents/agents/agent-management.ts +648 -0
  81. package/src/subagents/agents/agent-scope.ts +6 -0
  82. package/src/subagents/agents/agent-selection.ts +23 -0
  83. package/src/subagents/agents/agent-serializer.ts +86 -0
  84. package/src/subagents/agents/agents.ts +832 -0
  85. package/src/subagents/agents/chain-serializer.ts +137 -0
  86. package/src/subagents/agents/frontmatter.ts +29 -0
  87. package/src/subagents/agents/identity.ts +30 -0
  88. package/src/subagents/agents/skills.ts +632 -0
  89. package/src/subagents/extension/config.ts +16 -0
  90. package/src/subagents/extension/control-notices.ts +92 -0
  91. package/src/subagents/extension/doctor.ts +199 -0
  92. package/src/subagents/extension/fanout-child.ts +170 -0
  93. package/src/subagents/extension/index.ts +573 -0
  94. package/src/subagents/extension/schemas.ts +168 -0
  95. package/src/subagents/intercom/intercom-bridge.ts +379 -0
  96. package/src/subagents/intercom/result-intercom.ts +377 -0
  97. package/src/subagents/runs/background/async-execution.ts +712 -0
  98. package/src/subagents/runs/background/async-job-tracker.ts +310 -0
  99. package/src/subagents/runs/background/async-resume.ts +345 -0
  100. package/src/subagents/runs/background/async-status.ts +325 -0
  101. package/src/subagents/runs/background/completion-dedupe.ts +63 -0
  102. package/src/subagents/runs/background/notify.ts +108 -0
  103. package/src/subagents/runs/background/parallel-groups.ts +45 -0
  104. package/src/subagents/runs/background/result-watcher.ts +307 -0
  105. package/src/subagents/runs/background/run-id-resolver.ts +83 -0
  106. package/src/subagents/runs/background/run-status.ts +269 -0
  107. package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
  108. package/src/subagents/runs/background/subagent-runner.ts +1808 -0
  109. package/src/subagents/runs/background/top-level-async.ts +13 -0
  110. package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
  111. package/src/subagents/runs/foreground/chain-execution.ts +938 -0
  112. package/src/subagents/runs/foreground/execution.ts +918 -0
  113. package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
  114. package/src/subagents/runs/shared/completion-guard.ts +147 -0
  115. package/src/subagents/runs/shared/long-running-guard.ts +175 -0
  116. package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  117. package/src/subagents/runs/shared/model-fallback.ts +103 -0
  118. package/src/subagents/runs/shared/nested-events.ts +819 -0
  119. package/src/subagents/runs/shared/nested-path.ts +52 -0
  120. package/src/subagents/runs/shared/nested-render.ts +115 -0
  121. package/src/subagents/runs/shared/parallel-utils.ts +109 -0
  122. package/src/subagents/runs/shared/pi-args.ts +220 -0
  123. package/src/subagents/runs/shared/pi-spawn.ts +115 -0
  124. package/src/subagents/runs/shared/run-history.ts +60 -0
  125. package/src/subagents/runs/shared/single-output.ts +164 -0
  126. package/src/subagents/runs/shared/subagent-control.ts +226 -0
  127. package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
  128. package/src/subagents/runs/shared/worktree.ts +577 -0
  129. package/src/subagents/shared/artifacts.ts +98 -0
  130. package/src/subagents/shared/atomic-json.ts +16 -0
  131. package/src/subagents/shared/file-coalescer.ts +40 -0
  132. package/src/subagents/shared/fork-context.ts +76 -0
  133. package/src/subagents/shared/formatters.ts +133 -0
  134. package/src/subagents/shared/jsonl-writer.ts +81 -0
  135. package/src/subagents/shared/model-info.ts +78 -0
  136. package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
  137. package/src/subagents/shared/session-identity.ts +10 -0
  138. package/src/subagents/shared/session-tokens.ts +44 -0
  139. package/src/subagents/shared/settings.ts +397 -0
  140. package/src/subagents/shared/status-format.ts +49 -0
  141. package/src/subagents/shared/types.ts +822 -0
  142. package/src/subagents/shared/utils.ts +450 -0
  143. package/src/subagents/slash/prompt-template-bridge.ts +397 -0
  144. package/src/subagents/slash/slash-bridge.ts +174 -0
  145. package/src/subagents/slash/slash-commands.ts +528 -0
  146. package/src/subagents/slash/slash-live-state.ts +292 -0
  147. package/src/subagents/tui/render-helpers.ts +80 -0
  148. package/src/subagents/tui/render.ts +1358 -0
  149. package/templates/agents/local/supervisor.md +33 -0
  150. package/templates/agents/local/task-merger.md +27 -0
  151. package/templates/agents/local/task-reviewer.md +30 -0
  152. package/templates/agents/local/task-worker.md +34 -0
  153. package/templates/agents/supervisor-routing.md +92 -0
  154. package/templates/agents/supervisor.md +229 -0
  155. package/templates/agents/task-merger.md +214 -0
  156. package/templates/agents/task-reviewer.md +260 -0
  157. package/templates/agents/task-worker-segment.md +44 -0
  158. package/templates/agents/task-worker.md +557 -0
  159. package/templates/tasks/CONTEXT.md +30 -0
  160. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
  161. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
  162. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
  163. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * OrchID — Unified orchestration meta-skill
3
+ * Blends OrchID (formerly taskplane + ralph-wiggum + pi-subagents) into a single pi package.
4
+ */
5
+ export { default as orchestrator } from "./orchestrator/extension.js";
6
+ export { default as ralph } from "./ralph/index.js";
7
+ export { default as subagents } from "./subagents/extension/index.js";
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Abort logic (graceful and hard)
3
+ * @module orch/abort
4
+ */
5
+ import { writeFileSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+
8
+ import { execLog, killV2LaneAgents, resolveCanonicalTaskPaths } from "./execution.ts";
9
+ import { killMergeAgentV2, killAllMergeAgentsV2 } from "./merge.ts";
10
+ import { deleteBatchState, persistRuntimeState } from "./persistence.ts";
11
+ import type {
12
+ AbortActionStep,
13
+ AbortErrorCode,
14
+ AbortLaneResult,
15
+ AbortMode,
16
+ AbortResult,
17
+ AbortTargetSession,
18
+ AllocatedLane,
19
+ OrchBatchRuntimeState,
20
+ PersistedBatchState,
21
+ PersistedLaneRecord,
22
+ } from "./types.ts";
23
+
24
+ // ── Abort Pure Functions ─────────────────────────────────────────────
25
+
26
+ /**
27
+ * Select and enrich target sessions for abort.
28
+ *
29
+ * Filters sessions to only `orch-lane-*` and `orch-merge-*` patterns,
30
+ * then enriches with task folder and worktree info from persisted or
31
+ * runtime state.
32
+ *
33
+ * Pure function: no side effects.
34
+ *
35
+ * @param allSessionNames - All TMUX session names matching the prefix
36
+ * @param persistedState - Loaded persisted state (null if unavailable)
37
+ * @param runtimeLanes - Current in-memory lanes (from orchBatchState)
38
+ * @param repoRoot - Repository root path for task folder resolution
39
+ * @returns Filtered and enriched target sessions
40
+ */
41
+ export function selectAbortTargetSessions(
42
+ allSessionNames: string[],
43
+ persistedState: PersistedBatchState | null,
44
+ runtimeLanes: AllocatedLane[],
45
+ repoRoot: string,
46
+ prefix: string = "orch",
47
+ ): AbortTargetSession[] {
48
+ // Filter to only lane and merge sessions for the exact orchestrator prefix.
49
+ // Handles both repo-mode (`<prefix>-lane-<N>`) and workspace-mode
50
+ // (`<prefix>-<repoId>-lane-<N>`) session name formats.
51
+ const targetNames = allSessionNames.filter((name) => {
52
+ const prefixWithDash = `${prefix}-`;
53
+ if (!name.startsWith(prefixWithDash)) return false;
54
+ const suffix = name.slice(prefixWithDash.length);
55
+ // Repo mode: suffix starts with "lane-" or "merge-"
56
+ if (suffix.startsWith("lane-") || suffix.startsWith("merge-")) return true;
57
+ // Workspace mode: suffix is "<repoId>-lane-<N>" — contains "-lane-"
58
+ // Match any suffix that contains "-lane-" or "-merge-" followed by a number
59
+ if (/\-lane-\d/.test(suffix) || /\-merge-\d/.test(suffix)) return true;
60
+ return false;
61
+ });
62
+
63
+ // Build lookup from persisted lane records for workspace-aware laneId resolution.
64
+ // Keyed by lane session ID for direct session-to-lane mapping.
65
+ const persistedLaneLookup = new Map<string, PersistedLaneRecord>();
66
+ if (persistedState?.lanes) {
67
+ for (const lane of persistedState.lanes) {
68
+ persistedLaneLookup.set(lane.laneSessionId, lane);
69
+ }
70
+ }
71
+
72
+ // Build lookup from persisted state task records
73
+ const persistedLookup = new Map<string, { laneId: string; taskId: string; taskFolder: string }>();
74
+ if (persistedState) {
75
+ for (const task of persistedState.tasks) {
76
+ if (task.sessionName) {
77
+ // Source laneId from persisted lane records (workspace-aware)
78
+ // rather than reconstructing as `lane-${laneNumber}` which
79
+ // drops the repo dimension in workspace mode.
80
+ const laneRecord = persistedLaneLookup.get(task.sessionName);
81
+ const laneId = laneRecord?.laneId ?? `lane-${task.laneNumber}`;
82
+ persistedLookup.set(task.sessionName, {
83
+ laneId,
84
+ taskId: task.taskId,
85
+ taskFolder: task.taskFolder,
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ // Build lookup from runtime lanes
92
+ const runtimeLookup = new Map<
93
+ string,
94
+ { laneId: string; taskId: string | null; worktreePath: string; taskFolder: string | null }
95
+ >();
96
+ for (const lane of runtimeLanes) {
97
+ const currentTask = lane.tasks.length > 0 ? lane.tasks[0] : null;
98
+ runtimeLookup.set(lane.laneSessionId, {
99
+ laneId: lane.laneId,
100
+ taskId: currentTask?.taskId || null,
101
+ worktreePath: lane.worktreePath,
102
+ // TP-169: Guard against null task stubs from reconstructAllocatedLanes
103
+ taskFolder: currentTask?.task?.taskFolder || null,
104
+ });
105
+ }
106
+
107
+ return targetNames.map((sessionName) => {
108
+ const runtime = runtimeLookup.get(sessionName);
109
+ const persisted = persistedLookup.get(sessionName);
110
+
111
+ const laneId = runtime?.laneId || persisted?.laneId || "unknown";
112
+ const taskId = runtime?.taskId || persisted?.taskId || null;
113
+ const worktreePath = runtime?.worktreePath || null;
114
+ const taskFolder = runtime?.taskFolder || persisted?.taskFolder || null;
115
+
116
+ // Resolve task folder path using the canonical resolver.
117
+ // For repo-contained tasks: translates to worktree-relative path.
118
+ // For external tasks: uses the absolute canonical path directly.
119
+ let taskFolderInWorktree: string | null = null;
120
+ if (taskFolder && worktreePath && repoRoot) {
121
+ const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot);
122
+ taskFolderInWorktree = resolved.taskFolderResolved;
123
+ }
124
+
125
+ return {
126
+ sessionName,
127
+ laneId,
128
+ taskId,
129
+ taskFolderInWorktree,
130
+ worktreePath,
131
+ };
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Plan the ordered list of abort actions based on mode.
137
+ *
138
+ * Pure function: no side effects.
139
+ *
140
+ * @param mode - Abort mode (graceful or hard)
141
+ * @param gracePeriodMs - Grace period in ms (graceful only, default 60000)
142
+ * @param pollIntervalMs - Poll interval in ms (graceful only, default 2000)
143
+ * @returns Ordered list of abort action steps
144
+ */
145
+ export function planAbortActions(
146
+ mode: AbortMode,
147
+ gracePeriodMs: number = 60_000,
148
+ pollIntervalMs: number = 2_000,
149
+ ): AbortActionStep[] {
150
+ if (mode === "hard") {
151
+ return [{ type: "kill-all" }];
152
+ }
153
+ return [
154
+ { type: "write-wrapup" },
155
+ { type: "poll-wait", gracePeriodMs, pollIntervalMs },
156
+ { type: "kill-remaining" },
157
+ ];
158
+ }
159
+
160
+ /**
161
+ * Discover abort target session names from Runtime V2 state sources.
162
+ *
163
+ * Sources (deduped):
164
+ * - in-memory runtime lanes (`batchState.currentLanes`)
165
+ * - persisted lane records (`persistedState.lanes`)
166
+ * - persisted task records (`persistedState.tasks[].sessionName`)
167
+ */
168
+ export function discoverAbortSessionNames(
169
+ prefix: string,
170
+ persistedState: PersistedBatchState | null,
171
+ runtimeLanes: AllocatedLane[],
172
+ ): string[] {
173
+ const names = new Set<string>();
174
+ const prefixWithDash = `${prefix}-`;
175
+ const add = (name: string | null | undefined) => {
176
+ if (!name) return;
177
+ const trimmed = name.trim();
178
+ if (!trimmed || !trimmed.startsWith(prefixWithDash)) return;
179
+ names.add(trimmed);
180
+ };
181
+
182
+ for (const lane of runtimeLanes) {
183
+ add(lane.laneSessionId);
184
+ }
185
+
186
+ if (persistedState?.lanes) {
187
+ for (const lane of persistedState.lanes) {
188
+ add(lane.laneSessionId);
189
+ }
190
+ }
191
+
192
+ if (persistedState?.tasks) {
193
+ for (const task of persistedState.tasks) {
194
+ add(task.sessionName);
195
+ }
196
+ }
197
+
198
+ return [...names];
199
+ }
200
+
201
+ // ── Abort Orchestration Functions ────────────────────────────────────
202
+
203
+ /**
204
+ * Write wrap-up signal files to each lane's task folder.
205
+ *
206
+ * Writes `.task-wrap-up` signal file to each lane's task folder.
207
+ * Continues on partial failure — aggregates errors per lane.
208
+ *
209
+ * @param targets - Target sessions with resolved task folders
210
+ * @returns Updated target results with wrapUpWritten/wrapUpError
211
+ */
212
+ export function writeWrapUpFiles(
213
+ targets: AbortTargetSession[],
214
+ ): Array<{ sessionName: string; written: boolean; error: string | null }> {
215
+ const timestamp = new Date().toISOString();
216
+ const content = `Abort requested at ${timestamp}`;
217
+ const results: Array<{ sessionName: string; written: boolean; error: string | null }> = [];
218
+
219
+ for (const target of targets) {
220
+ if (!target.taskFolderInWorktree) {
221
+ // Skip child sessions (workers, reviewers) — only main lane sessions have task folders
222
+ // Also skip merge sessions (no task folder)
223
+ if (
224
+ target.sessionName.endsWith("-worker") ||
225
+ target.sessionName.endsWith("-reviewer") ||
226
+ target.sessionName.includes("merge")
227
+ ) {
228
+ results.push({ sessionName: target.sessionName, written: false, error: null });
229
+ } else {
230
+ results.push({
231
+ sessionName: target.sessionName,
232
+ written: false,
233
+ error: "No task folder resolved",
234
+ });
235
+ }
236
+ continue;
237
+ }
238
+
239
+ try {
240
+ const primaryPath = join(target.taskFolderInWorktree, ".task-wrap-up");
241
+
242
+ // Ensure directory exists
243
+ if (!existsSync(target.taskFolderInWorktree)) {
244
+ results.push({
245
+ sessionName: target.sessionName,
246
+ written: false,
247
+ error: `Task folder does not exist: ${target.taskFolderInWorktree}`,
248
+ });
249
+ continue;
250
+ }
251
+
252
+ writeFileSync(primaryPath, content, "utf-8");
253
+ results.push({ sessionName: target.sessionName, written: true, error: null });
254
+ } catch (err) {
255
+ results.push({
256
+ sessionName: target.sessionName,
257
+ written: false,
258
+ error: err instanceof Error ? err.message : String(err),
259
+ });
260
+ }
261
+ }
262
+
263
+ return results;
264
+ }
265
+
266
+ /**
267
+ * Wait for graceful shutdown window to elapse.
268
+ *
269
+ * Runtime V2 no longer relies on TMUX session liveness as an abort signal.
270
+ * We keep this grace window so workers can observe `.task-wrap-up` and exit
271
+ * naturally before forced cleanup.
272
+ *
273
+ * @param sessionNames - Session names being tracked for abort
274
+ * @param gracePeriodMs - Maximum time to wait
275
+ * @param pollIntervalMs - Polling cadence for the grace wait loop
276
+ * @returns Object with exited and remaining session names
277
+ */
278
+ export async function waitForSessionExit(
279
+ sessionNames: string[],
280
+ gracePeriodMs: number,
281
+ pollIntervalMs: number,
282
+ ): Promise<{ exited: string[]; remaining: string[] }> {
283
+ if (sessionNames.length === 0 || gracePeriodMs <= 0) {
284
+ return { exited: [], remaining: [...sessionNames] };
285
+ }
286
+
287
+ const deadline = Date.now() + gracePeriodMs;
288
+ while (Date.now() < deadline) {
289
+ const sleepMs = Math.max(1, Math.min(pollIntervalMs, deadline - Date.now()));
290
+ await new Promise((r) => setTimeout(r, sleepMs));
291
+ }
292
+
293
+ return { exited: [], remaining: [...sessionNames] };
294
+ }
295
+
296
+ /**
297
+ * Kill orchestrator Runtime V2 agents.
298
+ *
299
+ * Kills lane worker/reviewer agents and merge agents by process handle.
300
+ * Session names are normalized to base lane/merge IDs so child suffixes do
301
+ * not trigger duplicate cleanup attempts.
302
+ *
303
+ * @param sessionNames - Session names to kill
304
+ * @returns Per-session kill results
305
+ */
306
+ export function killOrchSessions(
307
+ sessionNames: string[],
308
+ options?: { stateRoot?: string; batchId?: string },
309
+ ): Array<{ sessionName: string; killed: boolean; error: string | null }> {
310
+ const results: Array<{ sessionName: string; killed: boolean; error: string | null }> = [];
311
+ const killedBaseSessions = new Set<string>();
312
+
313
+ for (const name of sessionNames) {
314
+ const baseSessionName = name.replace(/-(worker|reviewer)$/, "");
315
+ if (!killedBaseSessions.has(baseSessionName)) {
316
+ killV2LaneAgents(baseSessionName, {
317
+ stateRoot: options?.stateRoot,
318
+ batchId: options?.batchId,
319
+ logContext: "abort",
320
+ });
321
+ killMergeAgentV2(baseSessionName);
322
+ killedBaseSessions.add(baseSessionName);
323
+ }
324
+
325
+ results.push({
326
+ sessionName: name,
327
+ killed: true,
328
+ error: null,
329
+ });
330
+ }
331
+
332
+ return results;
333
+ }
334
+
335
+ /**
336
+ * Execute a full abort operation.
337
+ *
338
+ * Phase/state transition ordering:
339
+ * 1. Set phase to "stopped"
340
+ * 2. Persist runtime state (so state file reflects stopped phase)
341
+ * 3. Select target sessions
342
+ * 4. Execute mode-specific flow (graceful or hard)
343
+ * 5. Delete batch state file
344
+ * 6. Return AbortResult
345
+ *
346
+ * Non-goal: does NOT delete worktrees/branches (preserved for inspection).
347
+ *
348
+ * @param mode - Abort mode (graceful or hard)
349
+ * @param prefix - orchestrator session prefix (e.g., "orch")
350
+ * @param repoRoot - Repository root path
351
+ * @param batchState - Current batch runtime state (mutated: phase set to stopped)
352
+ * @param persistedState - Loaded persisted state (for session enrichment)
353
+ * @param gracePeriodMs - Grace period for graceful abort (default 60000)
354
+ * @param pollIntervalMs - Poll interval for graceful abort (default 2000)
355
+ * @returns AbortResult with per-lane details
356
+ */
357
+ export async function executeAbort(
358
+ mode: AbortMode,
359
+ prefix: string,
360
+ repoRoot: string,
361
+ batchState: OrchBatchRuntimeState,
362
+ persistedState: PersistedBatchState | null,
363
+ gracePeriodMs: number = 60_000,
364
+ pollIntervalMs: number = 2_000,
365
+ ): Promise<AbortResult> {
366
+ const startTime = Date.now();
367
+ const errors: Array<{ code: AbortErrorCode; message: string }> = [];
368
+
369
+ // Step 1: Set phase to stopped
370
+ batchState.phase = "stopped";
371
+ batchState.endedAt = Date.now();
372
+
373
+ // Step 2: Persist state (best-effort — abort must continue even if persist fails)
374
+ try {
375
+ persistRuntimeState(
376
+ `abort-${mode}`,
377
+ batchState,
378
+ [], // wavePlan not needed for abort persistence
379
+ batchState.currentLanes,
380
+ [], // taskOutcomes not needed
381
+ null, // discovery not needed
382
+ repoRoot,
383
+ );
384
+ } catch (err) {
385
+ execLog(
386
+ "abort",
387
+ batchState.batchId,
388
+ `Failed to persist state during abort: ${err instanceof Error ? err.message : String(err)}`,
389
+ );
390
+ }
391
+
392
+ // TP-108: Kill all V2 merge agents (process-owned, not TMUX)
393
+ // This catches V2 merge agents that have no TMUX session.
394
+ const v2MergeKilled = killAllMergeAgentsV2();
395
+ if (v2MergeKilled > 0) {
396
+ execLog("abort", batchState.batchId, `killed ${v2MergeKilled} V2 merge agent(s)`);
397
+ }
398
+
399
+ // Step 3: Discover target sessions from Runtime V2 state sources.
400
+ const allSessionNames = discoverAbortSessionNames(prefix, persistedState, batchState.currentLanes);
401
+ if (allSessionNames.length === 0) {
402
+ execLog(
403
+ "abort",
404
+ batchState.batchId,
405
+ `No abort targets discovered for prefix "${prefix}" from runtime/persisted state.`,
406
+ );
407
+ }
408
+
409
+ // Step 4: Select and enrich target sessions
410
+ const targets = selectAbortTargetSessions(
411
+ allSessionNames,
412
+ persistedState,
413
+ batchState.currentLanes,
414
+ repoRoot,
415
+ prefix,
416
+ );
417
+
418
+ const laneResults: AbortLaneResult[] = [];
419
+ let gracefulExits = 0;
420
+ let wrapUpFailures = 0;
421
+
422
+ if (mode === "graceful") {
423
+ // Step 5a: Write wrap-up files
424
+ const wrapUpResults = writeWrapUpFiles(targets);
425
+ for (const wr of wrapUpResults) {
426
+ if (wr.error) wrapUpFailures++;
427
+ }
428
+ if (wrapUpFailures > 0) {
429
+ errors.push({
430
+ code: "ABORT_WRAPUP_WRITE_FAILED",
431
+ message: `Failed to write wrap-up files for ${wrapUpFailures} session(s)`,
432
+ });
433
+ }
434
+
435
+ // Step 5b: Wait for sessions to exit
436
+ const allTargetNames = targets.map((t) => t.sessionName);
437
+ const waitResult = await waitForSessionExit(allTargetNames, gracePeriodMs, pollIntervalMs);
438
+ gracefulExits = waitResult.exited.length;
439
+
440
+ // Step 5c: Force-kill remaining sessions
441
+ const killResultBySession = new Map<string, { killed: boolean; error: string | null }>();
442
+ if (waitResult.remaining.length > 0) {
443
+ const killResults = killOrchSessions(waitResult.remaining, {
444
+ stateRoot: repoRoot,
445
+ batchId: batchState.batchId,
446
+ });
447
+ for (const kr of killResults) {
448
+ killResultBySession.set(kr.sessionName, { killed: kr.killed, error: kr.error });
449
+ }
450
+ const killFailures = killResults.filter((kr) => !kr.killed);
451
+ if (killFailures.length > 0) {
452
+ errors.push({
453
+ code: "ABORT_KILL_FAILED",
454
+ message: `Failed to kill ${killFailures.length} session(s)`,
455
+ });
456
+ }
457
+ }
458
+
459
+ // Build lane results
460
+ const exitedSet = new Set(waitResult.exited);
461
+ for (const target of targets) {
462
+ const wrapUp = wrapUpResults.find((wr) => wr.sessionName === target.sessionName);
463
+ const wasGraceful = exitedSet.has(target.sessionName);
464
+ const killResult = killResultBySession.get(target.sessionName);
465
+ const sessionKilled = wasGraceful || killResult?.killed === true;
466
+ laneResults.push({
467
+ sessionName: target.sessionName,
468
+ laneId: target.laneId,
469
+ taskId: target.taskId,
470
+ taskFolderInWorktree: target.taskFolderInWorktree,
471
+ wrapUpWritten: wrapUp?.written || false,
472
+ wrapUpError: wrapUp?.error || null,
473
+ sessionKilled,
474
+ exitedGracefully: wasGraceful,
475
+ });
476
+ }
477
+ } else {
478
+ // Hard mode: kill all immediately
479
+ const allTargetNames = targets.map((t) => t.sessionName);
480
+ const killResults = killOrchSessions(allTargetNames, {
481
+ stateRoot: repoRoot,
482
+ batchId: batchState.batchId,
483
+ });
484
+ const killResultBySession = new Map<string, { killed: boolean; error: string | null }>();
485
+ for (const kr of killResults) {
486
+ killResultBySession.set(kr.sessionName, { killed: kr.killed, error: kr.error });
487
+ }
488
+ const killFailures = killResults.filter((kr) => !kr.killed);
489
+ if (killFailures.length > 0) {
490
+ errors.push({
491
+ code: "ABORT_KILL_FAILED",
492
+ message: `Failed to kill ${killFailures.length} session(s)`,
493
+ });
494
+ }
495
+
496
+ for (const target of targets) {
497
+ const killResult = killResultBySession.get(target.sessionName);
498
+ laneResults.push({
499
+ sessionName: target.sessionName,
500
+ laneId: target.laneId,
501
+ taskId: target.taskId,
502
+ taskFolderInWorktree: target.taskFolderInWorktree,
503
+ wrapUpWritten: false,
504
+ wrapUpError: null,
505
+ sessionKilled: killResult?.killed === true,
506
+ exitedGracefully: false,
507
+ });
508
+ }
509
+ }
510
+
511
+ // Step 6: Delete batch state file
512
+ let stateDeleted = false;
513
+ try {
514
+ deleteBatchState(repoRoot);
515
+ stateDeleted = true;
516
+ } catch (err) {
517
+ errors.push({
518
+ code: "ABORT_STATE_DELETE_FAILED",
519
+ message: err instanceof Error ? err.message : String(err),
520
+ });
521
+ }
522
+
523
+ return {
524
+ mode,
525
+ sessionsFound: targets.length,
526
+ sessionsKilled: laneResults.filter((lr) => lr.sessionKilled).length,
527
+ gracefulExits,
528
+ laneResults,
529
+ wrapUpFailures,
530
+ stateDeleted,
531
+ errors,
532
+ durationMs: Date.now() - startTime,
533
+ };
534
+ }