@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,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Registry — Runtime V2 agent lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* File-backed registry that replaces legacy session discovery as the
|
|
5
|
+
* authoritative source of truth for agent liveness, identity, and
|
|
6
|
+
* attribution.
|
|
7
|
+
*
|
|
8
|
+
* Key design rules:
|
|
9
|
+
* 1. Parent writes manifest BEFORE child is considered visible.
|
|
10
|
+
* 2. Parent updates manifest on every status transition.
|
|
11
|
+
* 3. Operator tools read the registry, not terminal-session probes.
|
|
12
|
+
* 4. Resume/cleanup validates pid + startedAt for orphan detection.
|
|
13
|
+
*
|
|
14
|
+
* File locations:
|
|
15
|
+
* .pi/runtime/{batchId}/registry.json — batch-level snapshot
|
|
16
|
+
* .pi/runtime/{batchId}/agents/{agentId}/manifest.json — per-agent
|
|
17
|
+
*
|
|
18
|
+
* @module orchid/process-registry
|
|
19
|
+
* @since TP-104
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
readdirSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
appendFileSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
} from "fs";
|
|
32
|
+
import { join, dirname } from "path";
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
TERMINAL_AGENT_STATUSES,
|
|
36
|
+
runtimeRoot,
|
|
37
|
+
runtimeAgentDir,
|
|
38
|
+
runtimeManifestPath,
|
|
39
|
+
runtimeRegistryPath,
|
|
40
|
+
runtimeAgentEventsPath,
|
|
41
|
+
runtimeLaneSnapshotPath,
|
|
42
|
+
runtimeMergeSnapshotPath,
|
|
43
|
+
validateAgentManifest,
|
|
44
|
+
type RuntimeAgentId,
|
|
45
|
+
type RuntimeAgentManifest,
|
|
46
|
+
type RuntimeAgentRole,
|
|
47
|
+
type RuntimeAgentStatus,
|
|
48
|
+
type RuntimeRegistry,
|
|
49
|
+
type RuntimeMergeSnapshot,
|
|
50
|
+
type PacketPaths,
|
|
51
|
+
} from "./types.ts";
|
|
52
|
+
|
|
53
|
+
// TP-195: Re-export RuntimeRegistry so dynamic-import references in
|
|
54
|
+
// execution.ts (`import("./process-registry.ts").RuntimeRegistry`) resolve
|
|
55
|
+
// without each call site having to import directly from types.ts. Pure
|
|
56
|
+
// re-export — no runtime impact.
|
|
57
|
+
export type { RuntimeRegistry };
|
|
58
|
+
|
|
59
|
+
// ── Manifest Lifecycle ───────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Write or update an agent manifest atomically.
|
|
63
|
+
*
|
|
64
|
+
* Uses write-to-temp + rename for crash safety. Creates parent
|
|
65
|
+
* directories if they don't exist.
|
|
66
|
+
*
|
|
67
|
+
* @since TP-104
|
|
68
|
+
*/
|
|
69
|
+
export function writeManifest(stateRoot: string, manifest: RuntimeAgentManifest): void {
|
|
70
|
+
const dir = runtimeAgentDir(stateRoot, manifest.batchId, manifest.agentId);
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
const path = runtimeManifestPath(stateRoot, manifest.batchId, manifest.agentId);
|
|
73
|
+
const tmpPath = path + ".tmp";
|
|
74
|
+
writeFileSync(tmpPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
75
|
+
// Atomic rename (same directory = safe on all platforms)
|
|
76
|
+
renameSync(tmpPath, path);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read an agent manifest. Returns null if not found or malformed.
|
|
81
|
+
*
|
|
82
|
+
* @since TP-104
|
|
83
|
+
*/
|
|
84
|
+
export function readManifest(
|
|
85
|
+
stateRoot: string,
|
|
86
|
+
batchId: string,
|
|
87
|
+
agentId: RuntimeAgentId,
|
|
88
|
+
): RuntimeAgentManifest | null {
|
|
89
|
+
const path = runtimeManifestPath(stateRoot, batchId, agentId);
|
|
90
|
+
if (!existsSync(path)) return null;
|
|
91
|
+
try {
|
|
92
|
+
const raw = readFileSync(path, "utf-8");
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
const errors = validateAgentManifest(parsed);
|
|
95
|
+
if (errors.length > 0) {
|
|
96
|
+
console.error(`[process-registry] invalid manifest ${agentId}: ${errors.join(", ")}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return parsed as RuntimeAgentManifest;
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
console.error(`[process-registry] failed to read manifest ${agentId}: ${err?.message}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update an agent's status in its manifest.
|
|
108
|
+
*
|
|
109
|
+
* Reads the current manifest, updates the status field, and writes
|
|
110
|
+
* it back atomically. No-op if manifest doesn't exist.
|
|
111
|
+
*
|
|
112
|
+
* @since TP-104
|
|
113
|
+
*/
|
|
114
|
+
export function updateManifestStatus(
|
|
115
|
+
stateRoot: string,
|
|
116
|
+
batchId: string,
|
|
117
|
+
agentId: RuntimeAgentId,
|
|
118
|
+
status: RuntimeAgentStatus,
|
|
119
|
+
): void {
|
|
120
|
+
const manifest = readManifest(stateRoot, batchId, agentId);
|
|
121
|
+
if (!manifest) return;
|
|
122
|
+
manifest.status = status;
|
|
123
|
+
writeManifest(stateRoot, manifest);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a fresh RuntimeAgentManifest with required fields.
|
|
128
|
+
*
|
|
129
|
+
* @since TP-104
|
|
130
|
+
*/
|
|
131
|
+
export function createManifest(opts: {
|
|
132
|
+
batchId: string;
|
|
133
|
+
agentId: RuntimeAgentId;
|
|
134
|
+
role: RuntimeAgentRole;
|
|
135
|
+
laneNumber: number | null;
|
|
136
|
+
taskId: string | null;
|
|
137
|
+
repoId: string;
|
|
138
|
+
pid: number;
|
|
139
|
+
parentPid: number;
|
|
140
|
+
cwd: string;
|
|
141
|
+
packet: PacketPaths | null;
|
|
142
|
+
}): RuntimeAgentManifest {
|
|
143
|
+
return {
|
|
144
|
+
batchId: opts.batchId,
|
|
145
|
+
agentId: opts.agentId,
|
|
146
|
+
role: opts.role,
|
|
147
|
+
laneNumber: opts.laneNumber,
|
|
148
|
+
taskId: opts.taskId,
|
|
149
|
+
repoId: opts.repoId,
|
|
150
|
+
pid: opts.pid,
|
|
151
|
+
parentPid: opts.parentPid,
|
|
152
|
+
startedAt: Date.now(),
|
|
153
|
+
status: "spawning",
|
|
154
|
+
cwd: opts.cwd,
|
|
155
|
+
packet: opts.packet,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Registry Snapshot ────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a registry snapshot from all agent manifests in a batch.
|
|
163
|
+
*
|
|
164
|
+
* Scans the agents/ directory under the runtime root and reads all
|
|
165
|
+
* valid manifests.
|
|
166
|
+
*
|
|
167
|
+
* @since TP-104
|
|
168
|
+
*/
|
|
169
|
+
export function buildRegistrySnapshot(stateRoot: string, batchId: string): RuntimeRegistry {
|
|
170
|
+
const agentsDir = join(runtimeRoot(stateRoot, batchId), "agents");
|
|
171
|
+
const agents: Record<RuntimeAgentId, RuntimeAgentManifest> = {};
|
|
172
|
+
|
|
173
|
+
if (existsSync(agentsDir)) {
|
|
174
|
+
try {
|
|
175
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (!entry.isDirectory()) continue;
|
|
178
|
+
const agentId = entry.name;
|
|
179
|
+
const manifest = readManifest(stateRoot, batchId, agentId);
|
|
180
|
+
if (manifest) {
|
|
181
|
+
agents[agentId] = manifest;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
console.error(`[process-registry] failed to scan agents dir: ${err?.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
batchId,
|
|
191
|
+
updatedAt: Date.now(),
|
|
192
|
+
agents,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Write the registry snapshot to disk.
|
|
198
|
+
*
|
|
199
|
+
* @since TP-104
|
|
200
|
+
*/
|
|
201
|
+
export function writeRegistrySnapshot(stateRoot: string, registry: RuntimeRegistry): void {
|
|
202
|
+
const path = runtimeRegistryPath(stateRoot, registry.batchId);
|
|
203
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
204
|
+
const tmpPath = path + ".tmp";
|
|
205
|
+
writeFileSync(tmpPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
206
|
+
renameSync(tmpPath, path);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Read the registry snapshot from disk. Returns null if not found.
|
|
211
|
+
*
|
|
212
|
+
* @since TP-104
|
|
213
|
+
*/
|
|
214
|
+
export function readRegistrySnapshot(stateRoot: string, batchId: string): RuntimeRegistry | null {
|
|
215
|
+
const path = runtimeRegistryPath(stateRoot, batchId);
|
|
216
|
+
if (!existsSync(path)) return null;
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Liveness Checks ──────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check whether a process with the given PID is still alive.
|
|
228
|
+
*
|
|
229
|
+
* Uses `process.kill(pid, 0)` which sends no signal but checks existence.
|
|
230
|
+
* Returns false for PID 0, negative PIDs, and dead processes.
|
|
231
|
+
*
|
|
232
|
+
* @since TP-104
|
|
233
|
+
*/
|
|
234
|
+
export function isProcessAlive(pid: number): boolean {
|
|
235
|
+
if (!pid || pid <= 0 || !Number.isFinite(pid)) return false;
|
|
236
|
+
try {
|
|
237
|
+
process.kill(pid, 0);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Determine if an agent is in a terminal (non-alive) state.
|
|
246
|
+
*
|
|
247
|
+
* @since TP-104
|
|
248
|
+
*/
|
|
249
|
+
export function isTerminalStatus(status: RuntimeAgentStatus): boolean {
|
|
250
|
+
return TERMINAL_AGENT_STATUSES.has(status);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get all live (non-terminal) agents from a registry snapshot.
|
|
255
|
+
*
|
|
256
|
+
* @since TP-104
|
|
257
|
+
*/
|
|
258
|
+
export function getLiveAgents(registry: RuntimeRegistry): RuntimeAgentManifest[] {
|
|
259
|
+
return Object.values(registry.agents).filter((m) => !isTerminalStatus(m.status));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get all agents matching a specific role from a registry snapshot.
|
|
264
|
+
*
|
|
265
|
+
* @since TP-104
|
|
266
|
+
*/
|
|
267
|
+
export function getAgentsByRole(
|
|
268
|
+
registry: RuntimeRegistry,
|
|
269
|
+
role: RuntimeAgentRole,
|
|
270
|
+
): RuntimeAgentManifest[] {
|
|
271
|
+
return Object.values(registry.agents).filter((m) => m.role === role);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Orphan Detection ─────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Detect orphaned agents — manifests that claim to be running but whose
|
|
278
|
+
* process is no longer alive.
|
|
279
|
+
*
|
|
280
|
+
* Returns agent IDs of orphans. Caller decides whether to terminate,
|
|
281
|
+
* update manifest status, or log.
|
|
282
|
+
*
|
|
283
|
+
* @since TP-104
|
|
284
|
+
*/
|
|
285
|
+
export function detectOrphans(registry: RuntimeRegistry): RuntimeAgentId[] {
|
|
286
|
+
const orphans: RuntimeAgentId[] = [];
|
|
287
|
+
for (const manifest of Object.values(registry.agents)) {
|
|
288
|
+
if (isTerminalStatus(manifest.status)) continue;
|
|
289
|
+
if (!isProcessAlive(manifest.pid)) {
|
|
290
|
+
orphans.push(manifest.agentId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return orphans;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Mark detected orphans as crashed in their manifests.
|
|
298
|
+
*
|
|
299
|
+
* @since TP-104
|
|
300
|
+
*/
|
|
301
|
+
export function markOrphansCrashed(
|
|
302
|
+
stateRoot: string,
|
|
303
|
+
batchId: string,
|
|
304
|
+
orphanIds: RuntimeAgentId[],
|
|
305
|
+
): void {
|
|
306
|
+
for (const agentId of orphanIds) {
|
|
307
|
+
updateManifestStatus(stateRoot, batchId, agentId, "crashed");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Cleanup ──────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Remove all runtime artifacts for a batch.
|
|
315
|
+
*
|
|
316
|
+
* Best-effort: logs errors but doesn't throw.
|
|
317
|
+
*
|
|
318
|
+
* @since TP-104
|
|
319
|
+
*/
|
|
320
|
+
export function cleanupBatchRuntime(
|
|
321
|
+
stateRoot: string,
|
|
322
|
+
batchId: string,
|
|
323
|
+
): { removed: boolean; error?: string } {
|
|
324
|
+
const root = runtimeRoot(stateRoot, batchId);
|
|
325
|
+
if (!existsSync(root)) return { removed: false };
|
|
326
|
+
try {
|
|
327
|
+
rmSync(root, { recursive: true, force: true });
|
|
328
|
+
return { removed: true };
|
|
329
|
+
} catch (err: any) {
|
|
330
|
+
console.error(`[process-registry] failed to cleanup batch runtime: ${err?.message}`);
|
|
331
|
+
return { removed: false, error: err?.message };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Normalized Event Helpers ─────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Append a normalized event to an agent's event log.
|
|
339
|
+
*
|
|
340
|
+
* Creates the events file and parent directories if they don't exist.
|
|
341
|
+
* Best-effort: logs errors but doesn't throw.
|
|
342
|
+
*
|
|
343
|
+
* @since TP-104
|
|
344
|
+
*/
|
|
345
|
+
export function appendAgentEvent(
|
|
346
|
+
stateRoot: string,
|
|
347
|
+
batchId: string,
|
|
348
|
+
agentId: RuntimeAgentId,
|
|
349
|
+
event: Record<string, unknown>,
|
|
350
|
+
): void {
|
|
351
|
+
const path = runtimeAgentEventsPath(stateRoot, batchId, agentId);
|
|
352
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
353
|
+
try {
|
|
354
|
+
appendFileSync(path, JSON.stringify(event) + "\n", "utf-8");
|
|
355
|
+
} catch (err: any) {
|
|
356
|
+
console.error(`[process-registry] failed to append event for ${agentId}: ${err?.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Write a lane snapshot to disk.
|
|
362
|
+
*
|
|
363
|
+
* @since TP-104
|
|
364
|
+
*/
|
|
365
|
+
export function writeLaneSnapshot(
|
|
366
|
+
stateRoot: string,
|
|
367
|
+
batchId: string,
|
|
368
|
+
laneNumber: number,
|
|
369
|
+
snapshot: Record<string, unknown>,
|
|
370
|
+
): void {
|
|
371
|
+
const path = runtimeLaneSnapshotPath(stateRoot, batchId, laneNumber);
|
|
372
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
373
|
+
const tmpPath = path + ".tmp";
|
|
374
|
+
writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
375
|
+
renameSync(tmpPath, path);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Read a V2 lane snapshot from disk.
|
|
380
|
+
* Returns null if the file doesn't exist or is unreadable.
|
|
381
|
+
* @since TP-115
|
|
382
|
+
*/
|
|
383
|
+
export function readLaneSnapshot(
|
|
384
|
+
stateRoot: string,
|
|
385
|
+
batchId: string,
|
|
386
|
+
laneNumber: number,
|
|
387
|
+
): { taskId?: string | null; status: string; updatedAt?: number } | null {
|
|
388
|
+
try {
|
|
389
|
+
const p = runtimeLaneSnapshotPath(stateRoot, batchId, laneNumber);
|
|
390
|
+
if (!existsSync(p)) return null;
|
|
391
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Write a V2 merge agent snapshot to disk (atomic rename).
|
|
399
|
+
*
|
|
400
|
+
* Stored in the `lanes/` directory alongside lane snapshots so the dashboard
|
|
401
|
+
* server picks it up with the same scan that reads lane-N.json files.
|
|
402
|
+
*
|
|
403
|
+
* Filename includes BOTH waveIndex and mergeNumber so wave-N+1's merges
|
|
404
|
+
* cannot overwrite wave-N's snapshots before the dashboard polls them (#509).
|
|
405
|
+
*
|
|
406
|
+
* @param stateRoot - Repository root (where `.pi/` lives)
|
|
407
|
+
* @param batchId - Current batch identifier
|
|
408
|
+
* @param waveIndex - 0-based wave index for the merge
|
|
409
|
+
* @param mergeNumber - 1-indexed merge agent number
|
|
410
|
+
* @param snapshot - Snapshot data to persist
|
|
411
|
+
*
|
|
412
|
+
* @since TP-164 (waveIndex parameter added in #509 remediation)
|
|
413
|
+
*/
|
|
414
|
+
export function writeMergeSnapshot(
|
|
415
|
+
stateRoot: string,
|
|
416
|
+
batchId: string,
|
|
417
|
+
waveIndex: number,
|
|
418
|
+
mergeNumber: number,
|
|
419
|
+
snapshot: RuntimeMergeSnapshot,
|
|
420
|
+
): void {
|
|
421
|
+
const path = runtimeMergeSnapshotPath(stateRoot, batchId, waveIndex, mergeNumber);
|
|
422
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
423
|
+
const tmpPath = path + ".tmp";
|
|
424
|
+
writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
425
|
+
renameSync(tmpPath, path);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Read a V2 merge agent snapshot from disk.
|
|
430
|
+
* Returns null if the file does not exist or is unreadable.
|
|
431
|
+
*
|
|
432
|
+
* @param stateRoot - Repository root (where `.pi/` lives)
|
|
433
|
+
* @param batchId - Current batch identifier
|
|
434
|
+
* @param waveIndex - 0-based wave index for the merge
|
|
435
|
+
* @param mergeNumber - 1-indexed merge agent number
|
|
436
|
+
*
|
|
437
|
+
* @since TP-164 (waveIndex parameter added in #509 remediation)
|
|
438
|
+
*/
|
|
439
|
+
export function readMergeSnapshot(
|
|
440
|
+
stateRoot: string,
|
|
441
|
+
batchId: string,
|
|
442
|
+
waveIndex: number,
|
|
443
|
+
mergeNumber: number,
|
|
444
|
+
): RuntimeMergeSnapshot | null {
|
|
445
|
+
try {
|
|
446
|
+
const p = runtimeMergeSnapshotPath(stateRoot, batchId, waveIndex, mergeNumber);
|
|
447
|
+
if (!existsSync(p)) return null;
|
|
448
|
+
return JSON.parse(readFileSync(p, "utf-8")) as RuntimeMergeSnapshot;
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|