@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +4 -0
  4. package/README.md +200 -0
  5. package/dist/cli.js +464 -0
  6. package/dist/commands/__tests__/report.test.js +202 -0
  7. package/dist/commands/compare.js +168 -0
  8. package/dist/commands/doctor.js +124 -0
  9. package/dist/commands/follow.js +251 -0
  10. package/dist/commands/gc.js +161 -0
  11. package/dist/commands/guards-only.js +89 -0
  12. package/dist/commands/metrics.js +441 -0
  13. package/dist/commands/orchestrate.js +800 -0
  14. package/dist/commands/paths.js +31 -0
  15. package/dist/commands/preflight.js +152 -0
  16. package/dist/commands/report.js +478 -0
  17. package/dist/commands/resume.js +149 -0
  18. package/dist/commands/run.js +538 -0
  19. package/dist/commands/status.js +189 -0
  20. package/dist/commands/summarize.js +220 -0
  21. package/dist/commands/version.js +82 -0
  22. package/dist/commands/wait.js +170 -0
  23. package/dist/config/__tests__/presets.test.js +104 -0
  24. package/dist/config/load.js +66 -0
  25. package/dist/config/schema.js +160 -0
  26. package/dist/context/__tests__/artifact.test.js +130 -0
  27. package/dist/context/__tests__/pack.test.js +191 -0
  28. package/dist/context/artifact.js +67 -0
  29. package/dist/context/index.js +2 -0
  30. package/dist/context/pack.js +273 -0
  31. package/dist/diagnosis/analyzer.js +678 -0
  32. package/dist/diagnosis/formatter.js +136 -0
  33. package/dist/diagnosis/index.js +6 -0
  34. package/dist/diagnosis/types.js +7 -0
  35. package/dist/env/__tests__/fingerprint.test.js +116 -0
  36. package/dist/env/fingerprint.js +111 -0
  37. package/dist/orchestrator/__tests__/policy.test.js +185 -0
  38. package/dist/orchestrator/__tests__/schema-version.test.js +65 -0
  39. package/dist/orchestrator/artifacts.js +405 -0
  40. package/dist/orchestrator/state-machine.js +646 -0
  41. package/dist/orchestrator/types.js +88 -0
  42. package/dist/ownership/normalize.js +45 -0
  43. package/dist/repo/context.js +90 -0
  44. package/dist/repo/git.js +13 -0
  45. package/dist/repo/worktree.js +239 -0
  46. package/dist/store/run-store.js +107 -0
  47. package/dist/store/run-utils.js +69 -0
  48. package/dist/store/runs-root.js +126 -0
  49. package/dist/supervisor/__tests__/evidence-gate.test.js +111 -0
  50. package/dist/supervisor/__tests__/ownership.test.js +103 -0
  51. package/dist/supervisor/__tests__/state-machine.test.js +290 -0
  52. package/dist/supervisor/collision.js +240 -0
  53. package/dist/supervisor/evidence-gate.js +98 -0
  54. package/dist/supervisor/planner.js +18 -0
  55. package/dist/supervisor/runner.js +1562 -0
  56. package/dist/supervisor/scope-guard.js +55 -0
  57. package/dist/supervisor/state-machine.js +121 -0
  58. package/dist/supervisor/verification-policy.js +64 -0
  59. package/dist/tasks/task-metadata.js +72 -0
  60. package/dist/types/schemas.js +1 -0
  61. package/dist/verification/engine.js +49 -0
  62. package/dist/workers/__tests__/claude.test.js +88 -0
  63. package/dist/workers/__tests__/codex.test.js +81 -0
  64. package/dist/workers/claude.js +119 -0
  65. package/dist/workers/codex.js +162 -0
  66. package/dist/workers/json.js +22 -0
  67. package/dist/workers/mock.js +193 -0
  68. package/dist/workers/prompts.js +98 -0
  69. package/dist/workers/schemas.js +39 -0
  70. package/package.json +47 -0
  71. package/templates/prompts/implementer.md +70 -0
  72. package/templates/prompts/planner.md +62 -0
  73. 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
+ }