@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
@@ -0,0 +1,336 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
4
+ import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
5
+ import { normalizeParallelGroups } from "./parallel-groups.ts";
6
+ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
7
+
8
+ export type PidLiveness = "alive" | "dead" | "unknown";
9
+
10
+ type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
11
+
12
+ interface StartedRunMetadata {
13
+ runId: string;
14
+ pid?: number;
15
+ sessionId?: string;
16
+ mode?: SubagentRunMode;
17
+ agents?: string[];
18
+ chainStepCount?: number;
19
+ parallelGroups?: AsyncParallelGroupStatus[];
20
+ startedAt?: number;
21
+ sessionFile?: string;
22
+ }
23
+
24
+ interface ReconcileAsyncRunOptions {
25
+ resultsDir?: string;
26
+ kill?: KillFn;
27
+ now?: () => number;
28
+ startedRun?: StartedRunMetadata;
29
+ missingStatusGraceMs?: number;
30
+ staleAlivePidMs?: number;
31
+ }
32
+
33
+ interface ReconcileAsyncRunResult {
34
+ status: AsyncStatus | null;
35
+ repaired: boolean;
36
+ resultPath?: string;
37
+ message?: string;
38
+ }
39
+
40
+ function getErrorMessage(error: unknown): string {
41
+ return error instanceof Error ? error.message : String(error);
42
+ }
43
+
44
+ function isNotFoundError(error: unknown): boolean {
45
+ return typeof error === "object"
46
+ && error !== null
47
+ && "code" in error
48
+ && (error as NodeJS.ErrnoException).code === "ENOENT";
49
+ }
50
+
51
+ function appendJsonl(filePath: string, payload: object): void {
52
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
+ fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
54
+ }
55
+
56
+ function readStatusFile(asyncDir: string): AsyncStatus | null {
57
+ const statusPath = path.join(asyncDir, "status.json");
58
+ let content: string;
59
+ try {
60
+ content = fs.readFileSync(statusPath, "utf-8");
61
+ } catch (error) {
62
+ if (isNotFoundError(error)) return null;
63
+ throw new Error(`Failed to read async status file '${statusPath}': ${getErrorMessage(error)}`, {
64
+ cause: error instanceof Error ? error : undefined,
65
+ });
66
+ }
67
+ try {
68
+ return JSON.parse(content) as AsyncStatus;
69
+ } catch (error) {
70
+ throw new Error(`Failed to parse async status file '${statusPath}': ${getErrorMessage(error)}`, {
71
+ cause: error instanceof Error ? error : undefined,
72
+ });
73
+ }
74
+ }
75
+
76
+ interface ResultChildOutcome {
77
+ agent?: string;
78
+ success?: boolean;
79
+ error?: string;
80
+ sessionFile?: string;
81
+ model?: string;
82
+ attemptedModels?: string[];
83
+ modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
84
+ }
85
+
86
+ interface ResultRepairData {
87
+ state: "complete" | "failed" | "paused";
88
+ results?: ResultChildOutcome[];
89
+ }
90
+
91
+ function readResultRepairData(resultPath: string): ResultRepairData | undefined {
92
+ try {
93
+ const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; results?: ResultChildOutcome[] };
94
+ const state = data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
95
+ return { state, ...(Array.isArray(data.results) ? { results: data.results } : {}) };
96
+ } catch (error) {
97
+ if (isNotFoundError(error)) return undefined;
98
+ throw new Error(`Failed to read async result file '${resultPath}': ${getErrorMessage(error)}`, {
99
+ cause: error instanceof Error ? error : undefined,
100
+ });
101
+ }
102
+ }
103
+
104
+ function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): "complete" | "failed" | "paused" {
105
+ if (child?.success === true) return "complete";
106
+ if (child?.success === false) return "failed";
107
+ return overallState;
108
+ }
109
+
110
+ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now: number): AsyncStatus | undefined {
111
+ const repair = readResultRepairData(resultPath);
112
+ if (!repair) return undefined;
113
+ const steps = (status.steps ?? []).map((step, index) => {
114
+ if (step.status !== "running" && step.status !== "pending") return step;
115
+ const child = repair.results?.[index];
116
+ const state = childState(repair.state, child);
117
+ return {
118
+ ...step,
119
+ status: state === "complete" ? "complete" as const : state,
120
+ endedAt: step.endedAt ?? now,
121
+ durationMs: step.startedAt !== undefined && step.durationMs === undefined ? Math.max(0, now - step.startedAt) : step.durationMs,
122
+ exitCode: step.exitCode ?? (state === "complete" || state === "paused" ? 0 : 1),
123
+ error: state === "failed" ? step.error ?? child?.error : step.error,
124
+ sessionFile: step.sessionFile ?? child?.sessionFile,
125
+ model: step.model ?? child?.model,
126
+ attemptedModels: step.attemptedModels ?? child?.attemptedModels,
127
+ modelAttempts: step.modelAttempts ?? child?.modelAttempts,
128
+ };
129
+ });
130
+ return {
131
+ ...status,
132
+ state: repair.state,
133
+ activityState: undefined,
134
+ lastUpdate: now,
135
+ endedAt: status.endedAt ?? now,
136
+ steps,
137
+ };
138
+ }
139
+
140
+ function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, now: number): AsyncStatus {
141
+ const startedAt = startedRun.startedAt ?? now;
142
+ const agents = startedRun.agents?.length ? startedRun.agents : ["subagent"];
143
+ const chainStepCount = startedRun.chainStepCount;
144
+ const parallelGroups = chainStepCount !== undefined
145
+ ? normalizeParallelGroups(startedRun.parallelGroups, agents.length, chainStepCount)
146
+ : [];
147
+ return {
148
+ runId: startedRun.runId || path.basename(asyncDir),
149
+ ...(startedRun.sessionId ? { sessionId: startedRun.sessionId } : {}),
150
+ mode: startedRun.mode ?? "single",
151
+ state: "running",
152
+ pid: startedRun.pid,
153
+ startedAt,
154
+ lastUpdate: now,
155
+ currentStep: 0,
156
+ ...(chainStepCount !== undefined ? { chainStepCount } : {}),
157
+ ...(parallelGroups.length ? { parallelGroups } : {}),
158
+ steps: agents.map((agent) => ({
159
+ agent,
160
+ status: "running" as const,
161
+ startedAt,
162
+ })),
163
+ ...(startedRun.sessionFile ? { sessionFile: startedRun.sessionFile } : {}),
164
+ };
165
+ }
166
+
167
+ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result: object; message: string } {
168
+ const runId = status.runId || path.basename(asyncDir);
169
+ const pid = typeof status.pid === "number" ? status.pid : "unknown";
170
+ const message = reason ?? `Async runner process ${pid} exited or disappeared before writing a result. Marked run failed by stale-run reconciliation.`;
171
+ const steps = status.steps?.length ? status.steps : [{ agent: "subagent", status: "running" as const }];
172
+ const repairedSteps = steps.map((step) => step.status === "running" || step.status === "pending"
173
+ ? {
174
+ ...step,
175
+ status: "failed" as const,
176
+ activityState: undefined,
177
+ endedAt: step.endedAt ?? now,
178
+ durationMs: step.startedAt !== undefined && step.durationMs === undefined ? Math.max(0, now - step.startedAt) : step.durationMs,
179
+ exitCode: step.exitCode ?? 1,
180
+ error: step.error ?? message,
181
+ }
182
+ : step);
183
+ const repairedStatus: AsyncStatus = {
184
+ ...status,
185
+ state: "failed",
186
+ activityState: undefined,
187
+ lastUpdate: now,
188
+ endedAt: now,
189
+ steps: repairedSteps,
190
+ };
191
+ const resultAgent = repairedSteps[status.currentStep ?? 0]?.agent ?? repairedSteps[0]?.agent ?? "subagent";
192
+ return {
193
+ status: repairedStatus,
194
+ message,
195
+ result: {
196
+ id: runId,
197
+ agent: resultAgent,
198
+ mode: status.mode,
199
+ success: false,
200
+ state: "failed",
201
+ summary: message,
202
+ results: repairedSteps.map((step) => ({
203
+ agent: step.agent,
204
+ output: step.status === "complete" || step.status === "completed" ? "" : message,
205
+ error: step.status === "complete" || step.status === "completed" ? undefined : step.error ?? message,
206
+ success: step.status === "complete" || step.status === "completed",
207
+ model: step.model,
208
+ attemptedModels: step.attemptedModels,
209
+ modelAttempts: step.modelAttempts,
210
+ sessionFile: step.sessionFile,
211
+ })),
212
+ exitCode: 1,
213
+ timestamp: now,
214
+ durationMs: Math.max(0, now - status.startedAt),
215
+ asyncDir,
216
+ sessionId: status.sessionId,
217
+ sessionFile: status.sessionFile,
218
+ },
219
+ };
220
+ }
221
+
222
+ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: string, now: number, reason?: string): ReconcileAsyncRunResult {
223
+ const repair = buildFailedRepair(status, asyncDir, now, reason);
224
+ writeAtomicJson(resultPath, repair.result);
225
+ writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
226
+ appendJsonl(path.join(asyncDir, "events.jsonl"), {
227
+ type: "subagent.run.repaired_stale",
228
+ ts: now,
229
+ runId: repair.status.runId,
230
+ pid: status.pid,
231
+ resultPath,
232
+ message: repair.message,
233
+ });
234
+ return { status: repair.status, repaired: true, resultPath, message: repair.message };
235
+ }
236
+
237
+ function terminal(state: AsyncStatus["state"]): boolean {
238
+ return state === "complete" || state === "failed" || state === "paused";
239
+ }
240
+
241
+ function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
242
+ for (const child of children ?? []) {
243
+ yield child;
244
+ yield* nestedRuns(child.children);
245
+ yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
246
+ }
247
+ }
248
+
249
+ export function reconcileNestedAsyncDescendants(route: NestedRoute, options: ReconcileAsyncRunOptions = {}): void {
250
+ const registry = projectNestedEvents(route);
251
+ for (const run of nestedRuns(registry.children)) {
252
+ if (run.state !== "running" && run.state !== "queued") continue;
253
+ const asyncDir = resolveNestedAsyncDir(route.rootRunId, run);
254
+ if (!asyncDir) continue;
255
+ const result = reconcileAsyncRun(asyncDir, {
256
+ ...options,
257
+ resultsDir: path.join(options.resultsDir ?? RESULTS_DIR, "nested", route.rootRunId),
258
+ });
259
+ const status = result.status;
260
+ if (!status) continue;
261
+ if (!result.repaired && !terminal(status.state)) continue;
262
+ const ts = options.now?.() ?? Date.now();
263
+ writeNestedEvent(route, {
264
+ type: terminal(status.state) ? "subagent.nested.completed" : "subagent.nested.updated",
265
+ ts,
266
+ parentRunId: run.parentRunId,
267
+ parentStepIndex: run.parentStepIndex,
268
+ child: nestedSummaryFromAsyncStatus(status, asyncDir, {
269
+ id: run.id,
270
+ parentRunId: run.parentRunId,
271
+ parentStepIndex: run.parentStepIndex,
272
+ depth: run.depth,
273
+ path: run.path,
274
+ mode: run.mode,
275
+ ts,
276
+ }),
277
+ });
278
+ }
279
+ }
280
+
281
+ export function checkPidLiveness(pid: number, kill: KillFn = process.kill): PidLiveness {
282
+ try {
283
+ kill(pid, 0);
284
+ return "alive";
285
+ } catch (error) {
286
+ const code = typeof error === "object" && error !== null && "code" in error
287
+ ? (error as NodeJS.ErrnoException).code
288
+ : undefined;
289
+ if (code === "ESRCH") return "dead";
290
+ if (code === "EPERM") return "unknown";
291
+ return "unknown";
292
+ }
293
+ }
294
+
295
+ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOptions = {}): ReconcileAsyncRunResult {
296
+ const now = options.now?.() ?? Date.now();
297
+ const status = readStatusFile(asyncDir);
298
+ const startedStatus = !status && options.startedRun ? buildStartedStatus(asyncDir, options.startedRun, now) : undefined;
299
+ const effectiveStatus = status ?? startedStatus;
300
+ if (!effectiveStatus) return { status: null, repaired: false };
301
+
302
+ const runId = effectiveStatus.runId || path.basename(asyncDir);
303
+ const resultPath = path.join(options.resultsDir ?? RESULTS_DIR, `${runId}.json`);
304
+ if (fs.existsSync(resultPath)) {
305
+ const terminalStatus = effectiveStatus.state === "running" || effectiveStatus.state === "queued"
306
+ ? terminalStatusFromResult(effectiveStatus, resultPath, now)
307
+ : undefined;
308
+ if (terminalStatus) {
309
+ writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
310
+ return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
311
+ }
312
+ return { status: effectiveStatus, repaired: false, resultPath };
313
+ }
314
+
315
+ if (effectiveStatus.state !== "running" || typeof effectiveStatus.pid !== "number") {
316
+ return { status: status ?? null, repaired: false, resultPath };
317
+ }
318
+
319
+ if (!status) {
320
+ const startedAt = options.startedRun?.startedAt ?? effectiveStatus.startedAt;
321
+ if (now - startedAt < (options.missingStatusGraceMs ?? 1000)) {
322
+ return { status: null, repaired: false, resultPath };
323
+ }
324
+ }
325
+
326
+ const liveness = checkPidLiveness(effectiveStatus.pid, options.kill);
327
+ if (liveness !== "dead") {
328
+ const staleAfterMs = options.staleAlivePidMs ?? 24 * 60 * 60 * 1000;
329
+ const lastUpdate = effectiveStatus.lastUpdate ?? effectiveStatus.startedAt;
330
+ if (now - lastUpdate <= staleAfterMs) return { status: status ?? null, repaired: false, resultPath };
331
+ const message = `Async runner process ${effectiveStatus.pid} still has a live PID, but status has not updated for ${now - lastUpdate}ms. Marked run failed by stale-run reconciliation because PID ownership cannot be verified.`;
332
+ return writeFailedRepair(asyncDir, effectiveStatus, resultPath, now, message);
333
+ }
334
+
335
+ return writeFailedRepair(asyncDir, effectiveStatus, resultPath, now);
336
+ }