@weldr/runr 0.3.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 +216 -0
- package/LICENSE +190 -0
- package/NOTICE +4 -0
- package/README.md +200 -0
- package/dist/cli.js +464 -0
- package/dist/commands/__tests__/report.test.js +202 -0
- package/dist/commands/compare.js +168 -0
- package/dist/commands/doctor.js +124 -0
- package/dist/commands/follow.js +251 -0
- package/dist/commands/gc.js +161 -0
- package/dist/commands/guards-only.js +89 -0
- package/dist/commands/metrics.js +441 -0
- package/dist/commands/orchestrate.js +800 -0
- package/dist/commands/paths.js +31 -0
- package/dist/commands/preflight.js +152 -0
- package/dist/commands/report.js +478 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run.js +538 -0
- package/dist/commands/status.js +189 -0
- package/dist/commands/summarize.js +220 -0
- package/dist/commands/version.js +82 -0
- package/dist/commands/wait.js +170 -0
- package/dist/config/__tests__/presets.test.js +104 -0
- package/dist/config/load.js +66 -0
- package/dist/config/schema.js +160 -0
- package/dist/context/__tests__/artifact.test.js +130 -0
- package/dist/context/__tests__/pack.test.js +191 -0
- package/dist/context/artifact.js +67 -0
- package/dist/context/index.js +2 -0
- package/dist/context/pack.js +273 -0
- package/dist/diagnosis/analyzer.js +678 -0
- package/dist/diagnosis/formatter.js +136 -0
- package/dist/diagnosis/index.js +6 -0
- package/dist/diagnosis/types.js +7 -0
- package/dist/env/__tests__/fingerprint.test.js +116 -0
- package/dist/env/fingerprint.js +111 -0
- package/dist/orchestrator/__tests__/policy.test.js +185 -0
- package/dist/orchestrator/__tests__/schema-version.test.js +65 -0
- package/dist/orchestrator/artifacts.js +405 -0
- package/dist/orchestrator/state-machine.js +646 -0
- package/dist/orchestrator/types.js +88 -0
- package/dist/ownership/normalize.js +45 -0
- package/dist/repo/context.js +90 -0
- package/dist/repo/git.js +13 -0
- package/dist/repo/worktree.js +239 -0
- package/dist/store/run-store.js +107 -0
- package/dist/store/run-utils.js +69 -0
- package/dist/store/runs-root.js +126 -0
- package/dist/supervisor/__tests__/evidence-gate.test.js +111 -0
- package/dist/supervisor/__tests__/ownership.test.js +103 -0
- package/dist/supervisor/__tests__/state-machine.test.js +290 -0
- package/dist/supervisor/collision.js +240 -0
- package/dist/supervisor/evidence-gate.js +98 -0
- package/dist/supervisor/planner.js +18 -0
- package/dist/supervisor/runner.js +1562 -0
- package/dist/supervisor/scope-guard.js +55 -0
- package/dist/supervisor/state-machine.js +121 -0
- package/dist/supervisor/verification-policy.js +64 -0
- package/dist/tasks/task-metadata.js +72 -0
- package/dist/types/schemas.js +1 -0
- package/dist/verification/engine.js +49 -0
- package/dist/workers/__tests__/claude.test.js +88 -0
- package/dist/workers/__tests__/codex.test.js +81 -0
- package/dist/workers/claude.js +119 -0
- package/dist/workers/codex.js +162 -0
- package/dist/workers/json.js +22 -0
- package/dist/workers/mock.js +193 -0
- package/dist/workers/prompts.js +98 -0
- package/dist/workers/schemas.js +39 -0
- package/package.json +47 -0
- package/templates/prompts/implementer.md +70 -0
- package/templates/prompts/planner.md +62 -0
- package/templates/prompts/reviewer.md +77 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator terminal artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Handles writing complete.json, stop.json, summary.json, and orchestration.md
|
|
5
|
+
* with proper ordering (summary first, terminal marker last).
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION } from './types.js';
|
|
10
|
+
import { getOrchestrationsRoot, getLegacyOrchestrationsRoot } from '../store/runs-root.js';
|
|
11
|
+
/**
|
|
12
|
+
* Get the orchestration directory for a given orchestrator ID.
|
|
13
|
+
* Uses new canonical path: .agent/orchestrations/<orchId>
|
|
14
|
+
*/
|
|
15
|
+
export function getOrchestrationDir(repoPath, orchestratorId) {
|
|
16
|
+
return path.join(getOrchestrationsRoot(repoPath), orchestratorId);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the legacy orchestration directory (for migration).
|
|
20
|
+
* Old path was: .agent/runs/orchestrations/<orchId>
|
|
21
|
+
*/
|
|
22
|
+
export function getLegacyOrchestrationDir(repoPath, orchestratorId) {
|
|
23
|
+
return path.join(getLegacyOrchestrationsRoot(repoPath), orchestratorId);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Migrate orchestration from legacy path to new path if needed.
|
|
27
|
+
* Returns true if migration occurred.
|
|
28
|
+
*/
|
|
29
|
+
export function migrateOrchestrationIfNeeded(repoPath, orchestratorId) {
|
|
30
|
+
const newDir = getOrchestrationDir(repoPath, orchestratorId);
|
|
31
|
+
const legacyDir = getLegacyOrchestrationDir(repoPath, orchestratorId);
|
|
32
|
+
// Already at new location - nothing to do
|
|
33
|
+
if (fs.existsSync(newDir)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Check if exists at legacy location
|
|
37
|
+
if (!fs.existsSync(legacyDir)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Migrate: copy to new location, then remove old
|
|
41
|
+
console.log(`Migrating orchestration ${orchestratorId} to new path structure...`);
|
|
42
|
+
// Ensure parent directory exists
|
|
43
|
+
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
|
44
|
+
// Copy recursively
|
|
45
|
+
copyDirRecursive(legacyDir, newDir);
|
|
46
|
+
// Remove old directory
|
|
47
|
+
fs.rmSync(legacyDir, { recursive: true, force: true });
|
|
48
|
+
// Clean up empty legacy orchestrations dir if empty
|
|
49
|
+
const legacyRoot = getLegacyOrchestrationsRoot(repoPath);
|
|
50
|
+
try {
|
|
51
|
+
const remaining = fs.readdirSync(legacyRoot);
|
|
52
|
+
if (remaining.length === 0) {
|
|
53
|
+
fs.rmdirSync(legacyRoot);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore if can't clean up
|
|
58
|
+
}
|
|
59
|
+
console.log(` Migrated to: ${newDir}`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Find orchestration directory, checking both new and legacy paths.
|
|
64
|
+
* Automatically migrates if found at legacy location.
|
|
65
|
+
*/
|
|
66
|
+
export function findOrchestrationDir(repoPath, orchestratorId) {
|
|
67
|
+
const newDir = getOrchestrationDir(repoPath, orchestratorId);
|
|
68
|
+
// Check new location first
|
|
69
|
+
if (fs.existsSync(newDir)) {
|
|
70
|
+
return newDir;
|
|
71
|
+
}
|
|
72
|
+
// Check legacy location and migrate if found
|
|
73
|
+
const legacyDir = getLegacyOrchestrationDir(repoPath, orchestratorId);
|
|
74
|
+
if (fs.existsSync(legacyDir)) {
|
|
75
|
+
migrateOrchestrationIfNeeded(repoPath, orchestratorId);
|
|
76
|
+
return getOrchestrationDir(repoPath, orchestratorId);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Copy directory recursively.
|
|
82
|
+
*/
|
|
83
|
+
function copyDirRecursive(src, dest) {
|
|
84
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
85
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
86
|
+
const srcPath = path.join(src, entry.name);
|
|
87
|
+
const destPath = path.join(dest, entry.name);
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
copyDirRecursive(srcPath, destPath);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
fs.copyFileSync(srcPath, destPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get the handoffs directory for terminal artifacts.
|
|
98
|
+
*/
|
|
99
|
+
export function getHandoffsDir(repoPath, orchestratorId) {
|
|
100
|
+
return path.join(getOrchestrationDir(repoPath, orchestratorId), 'handoffs');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Ensure orchestration directories exist.
|
|
104
|
+
*/
|
|
105
|
+
export function ensureOrchestrationDirs(repoPath, orchestratorId) {
|
|
106
|
+
const orchDir = getOrchestrationDir(repoPath, orchestratorId);
|
|
107
|
+
const handoffsDir = getHandoffsDir(repoPath, orchestratorId);
|
|
108
|
+
fs.mkdirSync(orchDir, { recursive: true });
|
|
109
|
+
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Determine stop reason family from stop reason.
|
|
113
|
+
*/
|
|
114
|
+
function getStopReasonFamily(stopReason) {
|
|
115
|
+
if (!stopReason || stopReason === 'complete')
|
|
116
|
+
return undefined;
|
|
117
|
+
switch (stopReason) {
|
|
118
|
+
case 'orchestrator_timeout':
|
|
119
|
+
case 'orchestrator_max_ticks':
|
|
120
|
+
return 'budget';
|
|
121
|
+
case 'orchestrator_track_stopped':
|
|
122
|
+
case 'orchestrator_blocked_on_collision':
|
|
123
|
+
case 'orchestrator_internal_error':
|
|
124
|
+
return 'orchestrator';
|
|
125
|
+
default:
|
|
126
|
+
return 'orchestrator';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Find the first failed track for stop artifact context.
|
|
131
|
+
*/
|
|
132
|
+
function findFailedTrack(state) {
|
|
133
|
+
for (const track of state.tracks) {
|
|
134
|
+
if (track.status === 'stopped' || track.status === 'failed') {
|
|
135
|
+
const failedStep = track.steps.find(s => s.result?.status !== 'complete');
|
|
136
|
+
const stepIndex = failedStep ? track.steps.indexOf(failedStep) : track.current_step;
|
|
137
|
+
const step = track.steps[stepIndex];
|
|
138
|
+
return {
|
|
139
|
+
track_id: track.id,
|
|
140
|
+
step_index: stepIndex,
|
|
141
|
+
run_id: step?.run_id,
|
|
142
|
+
stop_reason: step?.result?.stop_reason
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Determine the orchestrator-level stop reason from state.
|
|
150
|
+
*/
|
|
151
|
+
export function determineStopReason(state) {
|
|
152
|
+
if (state.status === 'complete')
|
|
153
|
+
return 'complete';
|
|
154
|
+
// Check for specific failure modes
|
|
155
|
+
const failedTrack = state.tracks.find(t => t.status === 'failed' || t.status === 'stopped');
|
|
156
|
+
if (failedTrack) {
|
|
157
|
+
const failedStep = failedTrack.steps.find(s => s.result?.status !== 'complete');
|
|
158
|
+
if (failedStep?.result?.stop_reason?.includes('collision')) {
|
|
159
|
+
return 'orchestrator_blocked_on_collision';
|
|
160
|
+
}
|
|
161
|
+
return 'orchestrator_track_stopped';
|
|
162
|
+
}
|
|
163
|
+
return 'orchestrator_internal_error';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build OrchestratorWaitResult from state.
|
|
167
|
+
*/
|
|
168
|
+
export function buildWaitResult(state, repoPath) {
|
|
169
|
+
const orchDir = getOrchestrationDir(repoPath, state.orchestrator_id);
|
|
170
|
+
const startTime = new Date(state.started_at).getTime();
|
|
171
|
+
const endTime = state.ended_at ? new Date(state.ended_at).getTime() : Date.now();
|
|
172
|
+
const completedTracks = state.tracks.filter(t => t.status === 'complete').length;
|
|
173
|
+
const completedSteps = state.tracks.reduce((sum, t) => sum + t.steps.filter(s => s.result?.status === 'complete').length, 0);
|
|
174
|
+
const totalSteps = state.tracks.reduce((sum, t) => sum + t.steps.length, 0);
|
|
175
|
+
const isComplete = state.status === 'complete';
|
|
176
|
+
const stopReason = isComplete ? undefined : determineStopReason(state);
|
|
177
|
+
const result = {
|
|
178
|
+
schema_version: ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION,
|
|
179
|
+
orchestrator_id: state.orchestrator_id,
|
|
180
|
+
orchestrator_dir: orchDir,
|
|
181
|
+
repo_root: path.resolve(repoPath),
|
|
182
|
+
status: isComplete ? 'complete' : 'stopped',
|
|
183
|
+
tracks: {
|
|
184
|
+
completed: completedTracks,
|
|
185
|
+
total: state.tracks.length
|
|
186
|
+
},
|
|
187
|
+
steps: {
|
|
188
|
+
completed: completedSteps,
|
|
189
|
+
total: totalSteps
|
|
190
|
+
},
|
|
191
|
+
active_runs: state.active_runs,
|
|
192
|
+
elapsed_ms: endTime - startTime,
|
|
193
|
+
ts: new Date().toISOString()
|
|
194
|
+
};
|
|
195
|
+
if (!isComplete) {
|
|
196
|
+
result.stop_reason = stopReason;
|
|
197
|
+
result.stop_reason_family = getStopReasonFamily(stopReason);
|
|
198
|
+
result.resume_command = `agent orchestrate resume ${state.orchestrator_id} --repo ${repoPath}`;
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Build stop artifact with additional context.
|
|
204
|
+
*/
|
|
205
|
+
export function buildStopArtifact(state, repoPath) {
|
|
206
|
+
const waitResult = buildWaitResult(state, repoPath);
|
|
207
|
+
const lastFailedTrack = findFailedTrack(state);
|
|
208
|
+
return {
|
|
209
|
+
...waitResult,
|
|
210
|
+
last_failed_track: lastFailedTrack
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Build summary artifact for meta-agent consumption.
|
|
215
|
+
*/
|
|
216
|
+
export function buildSummaryArtifact(state, repoPath) {
|
|
217
|
+
const startTime = new Date(state.started_at).getTime();
|
|
218
|
+
const endTime = state.ended_at ? new Date(state.ended_at).getTime() : Date.now();
|
|
219
|
+
const isComplete = state.status === 'complete';
|
|
220
|
+
const tracks = state.tracks.map(track => ({
|
|
221
|
+
track_id: track.id,
|
|
222
|
+
name: track.name,
|
|
223
|
+
status: track.status,
|
|
224
|
+
steps: track.steps.map((step, idx) => ({
|
|
225
|
+
index: idx,
|
|
226
|
+
task: step.task_path,
|
|
227
|
+
run_id: step.run_id,
|
|
228
|
+
status: step.result?.status === 'complete'
|
|
229
|
+
? 'complete'
|
|
230
|
+
: step.result
|
|
231
|
+
? 'stopped'
|
|
232
|
+
: 'pending',
|
|
233
|
+
stop_reason: step.result?.stop_reason
|
|
234
|
+
}))
|
|
235
|
+
}));
|
|
236
|
+
// Determine next action
|
|
237
|
+
let nextAction;
|
|
238
|
+
if (isComplete) {
|
|
239
|
+
nextAction = { kind: 'none' };
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const failedTrack = findFailedTrack(state);
|
|
243
|
+
if (failedTrack?.run_id) {
|
|
244
|
+
nextAction = {
|
|
245
|
+
kind: 'fix_and_resume_run',
|
|
246
|
+
command: `agent resume ${failedTrack.run_id} --repo ${repoPath}`
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
nextAction = {
|
|
251
|
+
kind: 'resume_orchestrator',
|
|
252
|
+
command: `agent orchestrate resume ${state.orchestrator_id} --repo ${repoPath}`
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
schema_version: ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION,
|
|
258
|
+
orchestrator_id: state.orchestrator_id,
|
|
259
|
+
status: isComplete ? 'complete' : 'stopped',
|
|
260
|
+
repo_root: path.resolve(repoPath),
|
|
261
|
+
started_at: state.started_at,
|
|
262
|
+
ended_at: state.ended_at ?? new Date().toISOString(),
|
|
263
|
+
elapsed_ms: endTime - startTime,
|
|
264
|
+
policy: {
|
|
265
|
+
collision_policy: state.collision_policy,
|
|
266
|
+
time_budget_minutes: state.time_budget_minutes,
|
|
267
|
+
max_ticks: state.max_ticks
|
|
268
|
+
},
|
|
269
|
+
tracks,
|
|
270
|
+
collisions: [], // TODO: Track collisions during orchestration
|
|
271
|
+
next_action: nextAction
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Generate human-readable orchestration.md summary.
|
|
276
|
+
*/
|
|
277
|
+
export function generateOrchestrationMarkdown(state, summary) {
|
|
278
|
+
const lines = [];
|
|
279
|
+
// Header
|
|
280
|
+
const statusEmoji = summary.status === 'complete' ? '✓' : '✗';
|
|
281
|
+
lines.push(`# Orchestration ${statusEmoji} ${summary.status.toUpperCase()}`);
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push(`**ID:** ${summary.orchestrator_id}`);
|
|
284
|
+
lines.push(`**Repo:** ${summary.repo_root}`);
|
|
285
|
+
lines.push(`**Duration:** ${Math.round(summary.elapsed_ms / 1000)}s`);
|
|
286
|
+
lines.push('');
|
|
287
|
+
// Policy
|
|
288
|
+
lines.push('## Configuration');
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push(`- Collision policy: ${summary.policy.collision_policy}`);
|
|
291
|
+
lines.push(`- Run time limit: ${summary.policy.time_budget_minutes}min (each run)`);
|
|
292
|
+
lines.push(`- Run tick limit: ${summary.policy.max_ticks} (each run)`);
|
|
293
|
+
lines.push('');
|
|
294
|
+
// Tracks table
|
|
295
|
+
lines.push('## Tracks');
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push('| Track | Status | Steps | Run IDs |');
|
|
298
|
+
lines.push('|-------|--------|-------|---------|');
|
|
299
|
+
for (const track of summary.tracks) {
|
|
300
|
+
const completedSteps = track.steps.filter(s => s.status === 'complete').length;
|
|
301
|
+
const runIds = track.steps
|
|
302
|
+
.filter(s => s.run_id)
|
|
303
|
+
.map(s => s.run_id)
|
|
304
|
+
.join(', ') || '-';
|
|
305
|
+
lines.push(`| ${track.name} | ${track.status} | ${completedSteps}/${track.steps.length} | ${runIds} |`);
|
|
306
|
+
}
|
|
307
|
+
lines.push('');
|
|
308
|
+
// Step details
|
|
309
|
+
lines.push('## Step Details');
|
|
310
|
+
lines.push('');
|
|
311
|
+
for (const track of summary.tracks) {
|
|
312
|
+
lines.push(`### ${track.name}`);
|
|
313
|
+
lines.push('');
|
|
314
|
+
for (const step of track.steps) {
|
|
315
|
+
const statusMark = step.status === 'complete' ? '✓' : step.status === 'stopped' ? '✗' : '○';
|
|
316
|
+
const runInfo = step.run_id ? ` → ${step.run_id}` : '';
|
|
317
|
+
const stopInfo = step.stop_reason ? ` (${step.stop_reason})` : '';
|
|
318
|
+
lines.push(`${step.index + 1}. ${statusMark} ${step.task}${runInfo}${stopInfo}`);
|
|
319
|
+
}
|
|
320
|
+
lines.push('');
|
|
321
|
+
}
|
|
322
|
+
// Collisions
|
|
323
|
+
if (summary.collisions.length > 0) {
|
|
324
|
+
lines.push('## Collisions Encountered');
|
|
325
|
+
lines.push('');
|
|
326
|
+
for (const collision of summary.collisions) {
|
|
327
|
+
lines.push(`- Run ${collision.run_id} conflicted with ${collision.conflicts_with.join(', ')}`);
|
|
328
|
+
lines.push(` - Stage: ${collision.stage}, Files: ${collision.file_count}`);
|
|
329
|
+
}
|
|
330
|
+
lines.push('');
|
|
331
|
+
}
|
|
332
|
+
// Next action
|
|
333
|
+
if (summary.next_action.kind !== 'none') {
|
|
334
|
+
lines.push('## Next Action');
|
|
335
|
+
lines.push('');
|
|
336
|
+
if (summary.next_action.kind === 'resume_orchestrator') {
|
|
337
|
+
lines.push('Resume the orchestration:');
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
lines.push('Fix the failed run and resume:');
|
|
341
|
+
}
|
|
342
|
+
lines.push('');
|
|
343
|
+
lines.push('```bash');
|
|
344
|
+
lines.push(summary.next_action.command);
|
|
345
|
+
lines.push('```');
|
|
346
|
+
lines.push('');
|
|
347
|
+
}
|
|
348
|
+
return lines.join('\n');
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Write all terminal artifacts in correct order.
|
|
352
|
+
*
|
|
353
|
+
* Order is critical:
|
|
354
|
+
* 1. summary.json
|
|
355
|
+
* 2. orchestration.md
|
|
356
|
+
* 3. complete.json OR stop.json (LAST - signals terminal)
|
|
357
|
+
*/
|
|
358
|
+
export function writeTerminalArtifacts(state, repoPath) {
|
|
359
|
+
const handoffsDir = getHandoffsDir(repoPath, state.orchestrator_id);
|
|
360
|
+
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
361
|
+
const isComplete = state.status === 'complete';
|
|
362
|
+
// 1. Write summary.json
|
|
363
|
+
const summary = buildSummaryArtifact(state, repoPath);
|
|
364
|
+
fs.writeFileSync(path.join(handoffsDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
|
365
|
+
// 2. Write orchestration.md
|
|
366
|
+
const markdown = generateOrchestrationMarkdown(state, summary);
|
|
367
|
+
fs.writeFileSync(path.join(handoffsDir, 'orchestration.md'), markdown);
|
|
368
|
+
// 3. Write complete.json OR stop.json (LAST)
|
|
369
|
+
if (isComplete) {
|
|
370
|
+
const completeArtifact = buildWaitResult(state, repoPath);
|
|
371
|
+
fs.writeFileSync(path.join(handoffsDir, 'complete.json'), JSON.stringify(completeArtifact, null, 2));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const stopArtifact = buildStopArtifact(state, repoPath);
|
|
375
|
+
fs.writeFileSync(path.join(handoffsDir, 'stop.json'), JSON.stringify(stopArtifact, null, 2));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Check if orchestration has terminal artifacts (fast check).
|
|
380
|
+
*/
|
|
381
|
+
export function hasTerminalArtifact(repoPath, orchestratorId) {
|
|
382
|
+
const handoffsDir = getHandoffsDir(repoPath, orchestratorId);
|
|
383
|
+
if (fs.existsSync(path.join(handoffsDir, 'complete.json'))) {
|
|
384
|
+
return { terminal: true, status: 'complete' };
|
|
385
|
+
}
|
|
386
|
+
if (fs.existsSync(path.join(handoffsDir, 'stop.json'))) {
|
|
387
|
+
return { terminal: true, status: 'stopped' };
|
|
388
|
+
}
|
|
389
|
+
return { terminal: false };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Read terminal artifact if it exists.
|
|
393
|
+
*/
|
|
394
|
+
export function readTerminalArtifact(repoPath, orchestratorId) {
|
|
395
|
+
const handoffsDir = getHandoffsDir(repoPath, orchestratorId);
|
|
396
|
+
const completePath = path.join(handoffsDir, 'complete.json');
|
|
397
|
+
if (fs.existsSync(completePath)) {
|
|
398
|
+
return JSON.parse(fs.readFileSync(completePath, 'utf-8'));
|
|
399
|
+
}
|
|
400
|
+
const stopPath = path.join(handoffsDir, 'stop.json');
|
|
401
|
+
if (fs.existsSync(stopPath)) {
|
|
402
|
+
return JSON.parse(fs.readFileSync(stopPath, 'utf-8'));
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|