@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,189 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RunStore } from '../store/run-store.js';
|
|
4
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
5
|
+
import { getActiveRuns, getCollisionRisk } from '../supervisor/collision.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get status of a single run.
|
|
8
|
+
*/
|
|
9
|
+
export async function statusCommand(options) {
|
|
10
|
+
const runStore = RunStore.init(options.runId, options.repo);
|
|
11
|
+
const state = runStore.readState();
|
|
12
|
+
console.log(JSON.stringify(state, null, 2));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get status of all runs in the repo.
|
|
16
|
+
* Displays a table sorted by: running runs first (most recent), then stopped runs (most recent).
|
|
17
|
+
*/
|
|
18
|
+
export async function statusAllCommand(options) {
|
|
19
|
+
const runsRoot = getRunsRoot(options.repo);
|
|
20
|
+
if (!fs.existsSync(runsRoot)) {
|
|
21
|
+
console.log('No runs found.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const runDirs = fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
25
|
+
.filter(d => d.isDirectory())
|
|
26
|
+
.map(d => d.name);
|
|
27
|
+
if (runDirs.length === 0) {
|
|
28
|
+
console.log('No runs found.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const summaries = [];
|
|
32
|
+
// Pre-compute active runs for collision detection
|
|
33
|
+
const allActiveRuns = getActiveRuns(options.repo);
|
|
34
|
+
for (const runId of runDirs) {
|
|
35
|
+
const statePath = path.join(runsRoot, runId, 'state.json');
|
|
36
|
+
if (!fs.existsSync(statePath)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const stateRaw = fs.readFileSync(statePath, 'utf-8');
|
|
41
|
+
const state = JSON.parse(stateRaw);
|
|
42
|
+
// Get last worker call info
|
|
43
|
+
const workerCallPath = path.join(runsRoot, runId, 'last_worker_call.json');
|
|
44
|
+
let workerCall = null;
|
|
45
|
+
if (fs.existsSync(workerCallPath)) {
|
|
46
|
+
try {
|
|
47
|
+
workerCall = JSON.parse(fs.readFileSync(workerCallPath, 'utf-8'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Ignore parse errors
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const isRunning = state.phase !== 'STOPPED';
|
|
54
|
+
const updatedAt = state.updated_at ? new Date(state.updated_at) : new Date(0);
|
|
55
|
+
const age = formatAge(updatedAt);
|
|
56
|
+
// In-flight worker info
|
|
57
|
+
let inFlight = '-';
|
|
58
|
+
if (isRunning && workerCall) {
|
|
59
|
+
const elapsed = Math.floor((Date.now() - new Date(workerCall.at).getTime()) / 1000);
|
|
60
|
+
inFlight = `${workerCall.worker}/${workerCall.stage} (${elapsed}s)`;
|
|
61
|
+
}
|
|
62
|
+
// Compute collision risk with other active runs (excluding this run)
|
|
63
|
+
// Only show risk for running runs; stopped runs show '-'
|
|
64
|
+
let collisionRisk = '-';
|
|
65
|
+
if (isRunning) {
|
|
66
|
+
collisionRisk = 'none';
|
|
67
|
+
const otherActiveRuns = allActiveRuns.filter(r => r.runId !== runId);
|
|
68
|
+
if (otherActiveRuns.length > 0) {
|
|
69
|
+
// Extract files_expected from milestones
|
|
70
|
+
const touchFiles = [];
|
|
71
|
+
for (const milestone of state.milestones) {
|
|
72
|
+
if (milestone.files_expected) {
|
|
73
|
+
touchFiles.push(...milestone.files_expected);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
collisionRisk = getCollisionRisk(state.scope_lock?.allowlist ?? [], touchFiles, otherActiveRuns);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
summaries.push({
|
|
80
|
+
runId,
|
|
81
|
+
status: isRunning ? 'running' : 'stopped',
|
|
82
|
+
phase: state.phase,
|
|
83
|
+
milestones: `${state.milestone_index + 1}/${state.milestones.length}`,
|
|
84
|
+
age,
|
|
85
|
+
stopReason: state.stop_reason ?? '-',
|
|
86
|
+
autoResumeCount: state.auto_resume_count ?? 0,
|
|
87
|
+
inFlight,
|
|
88
|
+
collisionRisk,
|
|
89
|
+
updatedAt
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Skip runs with invalid state
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (summaries.length === 0) {
|
|
97
|
+
console.log('No valid runs found.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Sort: running first (most recent), then stopped (most recent)
|
|
101
|
+
summaries.sort((a, b) => {
|
|
102
|
+
if (a.status !== b.status) {
|
|
103
|
+
return a.status === 'running' ? -1 : 1;
|
|
104
|
+
}
|
|
105
|
+
return b.updatedAt.getTime() - a.updatedAt.getTime();
|
|
106
|
+
});
|
|
107
|
+
// Print table
|
|
108
|
+
printTable(summaries);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Format age as human-readable string.
|
|
112
|
+
*/
|
|
113
|
+
function formatAge(date) {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const diffMs = now - date.getTime();
|
|
116
|
+
if (diffMs < 0)
|
|
117
|
+
return 'future';
|
|
118
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
119
|
+
if (seconds < 60)
|
|
120
|
+
return `${seconds}s`;
|
|
121
|
+
const minutes = Math.floor(seconds / 60);
|
|
122
|
+
if (minutes < 60)
|
|
123
|
+
return `${minutes}m`;
|
|
124
|
+
const hours = Math.floor(minutes / 60);
|
|
125
|
+
if (hours < 24)
|
|
126
|
+
return `${hours}h`;
|
|
127
|
+
const days = Math.floor(hours / 24);
|
|
128
|
+
return `${days}d`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Print formatted table of run summaries.
|
|
132
|
+
*/
|
|
133
|
+
function printTable(summaries) {
|
|
134
|
+
// Column headers
|
|
135
|
+
const headers = ['RUN ID', 'STATUS', 'PHASE', 'PROGRESS', 'AGE', 'STOP REASON', 'RESUMES', 'RISK', 'IN-FLIGHT'];
|
|
136
|
+
// Calculate column widths
|
|
137
|
+
const widths = headers.map((h, i) => {
|
|
138
|
+
const values = summaries.map(s => {
|
|
139
|
+
switch (i) {
|
|
140
|
+
case 0: return s.runId;
|
|
141
|
+
case 1: return s.status;
|
|
142
|
+
case 2: return s.phase;
|
|
143
|
+
case 3: return s.milestones;
|
|
144
|
+
case 4: return s.age;
|
|
145
|
+
case 5: return s.stopReason;
|
|
146
|
+
case 6: return String(s.autoResumeCount);
|
|
147
|
+
case 7: return s.collisionRisk;
|
|
148
|
+
case 8: return s.inFlight;
|
|
149
|
+
default: return '';
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return Math.max(h.length, ...values.map(v => v.length));
|
|
153
|
+
});
|
|
154
|
+
// Print header
|
|
155
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
|
|
156
|
+
console.log(headerLine);
|
|
157
|
+
console.log('-'.repeat(headerLine.length));
|
|
158
|
+
// Print rows
|
|
159
|
+
for (const s of summaries) {
|
|
160
|
+
const row = [
|
|
161
|
+
s.runId.padEnd(widths[0]),
|
|
162
|
+
s.status.padEnd(widths[1]),
|
|
163
|
+
s.phase.padEnd(widths[2]),
|
|
164
|
+
s.milestones.padEnd(widths[3]),
|
|
165
|
+
s.age.padEnd(widths[4]),
|
|
166
|
+
s.stopReason.padEnd(widths[5]),
|
|
167
|
+
String(s.autoResumeCount).padEnd(widths[6]),
|
|
168
|
+
s.collisionRisk.padEnd(widths[7]),
|
|
169
|
+
s.inFlight.padEnd(widths[8])
|
|
170
|
+
];
|
|
171
|
+
console.log(row.join(' '));
|
|
172
|
+
}
|
|
173
|
+
// Print summary
|
|
174
|
+
const running = summaries.filter(s => s.status === 'running').length;
|
|
175
|
+
const stopped = summaries.filter(s => s.status === 'stopped').length;
|
|
176
|
+
const allowlistRisk = summaries.filter(s => s.collisionRisk === 'allowlist').length;
|
|
177
|
+
const collisionRisk = summaries.filter(s => s.collisionRisk === 'collision').length;
|
|
178
|
+
console.log('');
|
|
179
|
+
let summaryLine = `Total: ${summaries.length} runs (${running} running, ${stopped} stopped)`;
|
|
180
|
+
if (allowlistRisk > 0 || collisionRisk > 0) {
|
|
181
|
+
const riskParts = [];
|
|
182
|
+
if (allowlistRisk > 0)
|
|
183
|
+
riskParts.push(`${allowlistRisk} allowlist`);
|
|
184
|
+
if (collisionRisk > 0)
|
|
185
|
+
riskParts.push(`${collisionRisk} collision`);
|
|
186
|
+
summaryLine += ` — risk: ${riskParts.join(', ')}`;
|
|
187
|
+
}
|
|
188
|
+
console.log(summaryLine);
|
|
189
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { RunStore } from '../store/run-store.js';
|
|
2
|
+
import { resolveRunId } from '../store/run-utils.js';
|
|
3
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
4
|
+
import { computeKpiFromEvents } from './report.js';
|
|
5
|
+
import { diagnoseStop, formatStopMarkdown } from '../diagnosis/index.js';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import readline from 'node:readline';
|
|
9
|
+
/**
|
|
10
|
+
* Generate a machine-readable JSON summary of a run.
|
|
11
|
+
* Outputs compact JSON to stdout for piping/parsing.
|
|
12
|
+
* Supports 'latest' as runId to resolve to most recent run.
|
|
13
|
+
*/
|
|
14
|
+
export async function summarizeCommand(options) {
|
|
15
|
+
// Resolve 'latest' to actual run ID and validate existence
|
|
16
|
+
const resolvedRunId = resolveRunId(options.runId, options.repo);
|
|
17
|
+
const runDir = path.join(getRunsRoot(options.repo), resolvedRunId);
|
|
18
|
+
const runStore = RunStore.init(resolvedRunId, options.repo);
|
|
19
|
+
const state = runStore.readState();
|
|
20
|
+
// Read timeline events for KPI computation
|
|
21
|
+
const timelinePath = path.join(runDir, 'timeline.jsonl');
|
|
22
|
+
const events = await readTimelineEvents(timelinePath);
|
|
23
|
+
const kpi = computeKpiFromEvents(events);
|
|
24
|
+
// Read config.snapshot.json for worktree_enabled and other config
|
|
25
|
+
const configSnapshot = readConfigSnapshot(runDir);
|
|
26
|
+
// Extract config flags from run_started event
|
|
27
|
+
const runStartedEvent = events.find((e) => e.type === 'run_started');
|
|
28
|
+
const configPayload = extractConfigPayload(runStartedEvent, configSnapshot);
|
|
29
|
+
// Get total milestones from state if available
|
|
30
|
+
const totalMilestones = state.milestones?.length ?? null;
|
|
31
|
+
// Extract ticks info from timeline events (sum across all sessions including resumes)
|
|
32
|
+
const ticksInfo = extractTicksInfo(events, state);
|
|
33
|
+
// Generate diagnosis for stopped runs (not for successful completions)
|
|
34
|
+
let diagnosisResult;
|
|
35
|
+
const isFailedStop = state.phase === 'STOPPED' && state.stop_reason !== 'complete';
|
|
36
|
+
if (isFailedStop) {
|
|
37
|
+
const diagnosisContext = {
|
|
38
|
+
runId: resolvedRunId,
|
|
39
|
+
runDir,
|
|
40
|
+
state: {
|
|
41
|
+
phase: state.phase,
|
|
42
|
+
stop_reason: state.stop_reason,
|
|
43
|
+
milestone_index: state.milestone_index,
|
|
44
|
+
milestones_total: state.milestones?.length ?? 0,
|
|
45
|
+
last_error: state.last_error
|
|
46
|
+
},
|
|
47
|
+
events,
|
|
48
|
+
configSnapshot: configSnapshot
|
|
49
|
+
};
|
|
50
|
+
diagnosisResult = diagnoseStop(diagnosisContext);
|
|
51
|
+
// Write diagnosis artifacts to handoffs directory
|
|
52
|
+
const handoffsDir = path.join(runDir, 'handoffs');
|
|
53
|
+
if (!fs.existsSync(handoffsDir)) {
|
|
54
|
+
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
// Write stop.json
|
|
57
|
+
const stopJsonPath = path.join(handoffsDir, 'stop.json');
|
|
58
|
+
fs.writeFileSync(stopJsonPath, JSON.stringify(diagnosisResult, null, 2) + '\n', 'utf-8');
|
|
59
|
+
// Write stop.md
|
|
60
|
+
const stopMdPath = path.join(handoffsDir, 'stop.md');
|
|
61
|
+
const stopMd = formatStopMarkdown(diagnosisResult);
|
|
62
|
+
fs.writeFileSync(stopMdPath, stopMd + '\n', 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
const summary = {
|
|
65
|
+
run_id: resolvedRunId,
|
|
66
|
+
outcome: kpi.outcome,
|
|
67
|
+
stop_reason: kpi.stop_reason,
|
|
68
|
+
duration_seconds: msToSeconds(kpi.total_duration_ms),
|
|
69
|
+
milestones: {
|
|
70
|
+
completed: kpi.milestones.completed,
|
|
71
|
+
total: totalMilestones
|
|
72
|
+
},
|
|
73
|
+
worker_calls: {
|
|
74
|
+
claude: kpi.workers.claude,
|
|
75
|
+
codex: kpi.workers.codex
|
|
76
|
+
},
|
|
77
|
+
verification: {
|
|
78
|
+
attempts: kpi.verify.attempts,
|
|
79
|
+
retries: kpi.verify.retries,
|
|
80
|
+
duration_seconds: msToSeconds(kpi.verify.total_duration_ms) ?? 0
|
|
81
|
+
},
|
|
82
|
+
reliability: {
|
|
83
|
+
infra_retries: kpi.reliability.infra_retries,
|
|
84
|
+
fallback_used: kpi.reliability.fallback_used,
|
|
85
|
+
fallback_count: kpi.reliability.fallback_count,
|
|
86
|
+
stalls_triggered: kpi.reliability.stalls_triggered,
|
|
87
|
+
late_results_ignored: kpi.reliability.late_results_ignored,
|
|
88
|
+
max_ticks_hit: ticksInfo.max_ticks_hit,
|
|
89
|
+
ticks_used: ticksInfo.ticks_used
|
|
90
|
+
},
|
|
91
|
+
config: configPayload,
|
|
92
|
+
timestamps: {
|
|
93
|
+
started_at: kpi.started_at,
|
|
94
|
+
ended_at: kpi.ended_at
|
|
95
|
+
},
|
|
96
|
+
diagnosis: diagnosisResult
|
|
97
|
+
? {
|
|
98
|
+
primary: diagnosisResult.primary_diagnosis,
|
|
99
|
+
confidence: diagnosisResult.confidence,
|
|
100
|
+
next_action: diagnosisResult.next_actions[0]?.command
|
|
101
|
+
}
|
|
102
|
+
: undefined
|
|
103
|
+
};
|
|
104
|
+
// Write summary.json to run directory (idempotent - overwrites if exists)
|
|
105
|
+
const summaryPath = path.join(runDir, 'summary.json');
|
|
106
|
+
const formattedJson = JSON.stringify(summary, null, 2);
|
|
107
|
+
fs.writeFileSync(summaryPath, formattedJson + '\n', 'utf-8');
|
|
108
|
+
console.log(`Summary written to ${summaryPath}`);
|
|
109
|
+
// Log diagnosis info if available
|
|
110
|
+
if (diagnosisResult) {
|
|
111
|
+
const handoffsDir = path.join(runDir, 'handoffs');
|
|
112
|
+
console.log(`Diagnosis written to ${path.join(handoffsDir, 'stop.json')} and stop.md`);
|
|
113
|
+
console.log(` Primary: ${diagnosisResult.primary_diagnosis} (${Math.round(diagnosisResult.confidence * 100)}%)`);
|
|
114
|
+
if (diagnosisResult.next_actions[0]) {
|
|
115
|
+
console.log(` Next: ${diagnosisResult.next_actions[0].title}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function readTimelineEvents(timelinePath) {
|
|
120
|
+
if (!fs.existsSync(timelinePath)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const events = [];
|
|
124
|
+
const stream = fs.createReadStream(timelinePath, { encoding: 'utf-8' });
|
|
125
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
126
|
+
for await (const line of rl) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed)
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
const event = JSON.parse(trimmed);
|
|
132
|
+
events.push(event);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Skip malformed lines
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return events;
|
|
139
|
+
}
|
|
140
|
+
function readConfigSnapshot(runDir) {
|
|
141
|
+
const snapshotPath = path.join(runDir, 'config.snapshot.json');
|
|
142
|
+
// Handle missing config.snapshot.json gracefully with defaults
|
|
143
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
144
|
+
return {
|
|
145
|
+
worktree_enabled: false
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const raw = fs.readFileSync(snapshotPath, 'utf-8');
|
|
150
|
+
const parsed = JSON.parse(raw);
|
|
151
|
+
// worktree_enabled is determined by presence of _worktree field
|
|
152
|
+
const worktreeEnabled = parsed._worktree !== undefined && parsed._worktree !== null;
|
|
153
|
+
return {
|
|
154
|
+
worktree_enabled: worktreeEnabled
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Handle parse errors gracefully with defaults
|
|
159
|
+
return {
|
|
160
|
+
worktree_enabled: false
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function extractConfigPayload(runStartedEvent, configSnapshot) {
|
|
165
|
+
const defaults = {
|
|
166
|
+
dry_run: null,
|
|
167
|
+
no_branch: null,
|
|
168
|
+
allow_dirty: null,
|
|
169
|
+
allow_deps: null,
|
|
170
|
+
worktree_enabled: configSnapshot?.worktree_enabled ?? false,
|
|
171
|
+
time_budget_minutes: null,
|
|
172
|
+
max_ticks: null
|
|
173
|
+
};
|
|
174
|
+
if (!runStartedEvent?.payload || typeof runStartedEvent.payload !== 'object') {
|
|
175
|
+
return defaults;
|
|
176
|
+
}
|
|
177
|
+
const payload = runStartedEvent.payload;
|
|
178
|
+
return {
|
|
179
|
+
dry_run: payload.dry_run ?? null,
|
|
180
|
+
no_branch: payload.no_branch ?? null,
|
|
181
|
+
allow_dirty: payload.allow_dirty ?? null,
|
|
182
|
+
allow_deps: payload.allow_deps ?? null,
|
|
183
|
+
worktree_enabled: configSnapshot?.worktree_enabled ?? false,
|
|
184
|
+
time_budget_minutes: payload.time_budget_minutes ?? null,
|
|
185
|
+
max_ticks: payload.max_ticks ?? null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function msToSeconds(ms) {
|
|
189
|
+
if (ms === null)
|
|
190
|
+
return null;
|
|
191
|
+
return Math.round(ms / 1000);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Extract ticks info from timeline events and state.
|
|
195
|
+
* Sums ticks_used across all sessions (including resumes).
|
|
196
|
+
*/
|
|
197
|
+
function extractTicksInfo(events, state) {
|
|
198
|
+
// Check if stop_reason indicates max_ticks_reached
|
|
199
|
+
const stopReason = state.stop_reason;
|
|
200
|
+
const maxTicksHit = stopReason === 'max_ticks_reached';
|
|
201
|
+
// Sum ticks_used from max_ticks_reached events and stop events with ticks_used
|
|
202
|
+
let totalTicks = 0;
|
|
203
|
+
for (const event of events) {
|
|
204
|
+
if (event.type === 'max_ticks_reached' || event.type === 'stop') {
|
|
205
|
+
const payload = event.payload;
|
|
206
|
+
const ticksUsed = payload?.ticks_used;
|
|
207
|
+
if (typeof ticksUsed === 'number') {
|
|
208
|
+
totalTicks += ticksUsed;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// If no ticks_used found in events, estimate from phase_start count
|
|
213
|
+
if (totalTicks === 0) {
|
|
214
|
+
totalTicks = events.filter((e) => e.type === 'phase_start').length;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
max_ticks_hit: maxTicksHit,
|
|
218
|
+
ticks_used: totalTicks
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version command.
|
|
3
|
+
*
|
|
4
|
+
* Outputs version information in JSON or human-readable format.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
// Get agent version from package.json
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
13
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
14
|
+
const AGENT_VERSION = packageJson.version;
|
|
15
|
+
/**
|
|
16
|
+
* Current artifact schema version.
|
|
17
|
+
* This should match the schema_version stamped into all artifacts.
|
|
18
|
+
*/
|
|
19
|
+
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
20
|
+
/**
|
|
21
|
+
* Get the current git commit hash (short form).
|
|
22
|
+
* Returns null if not in a git repo or git not available.
|
|
23
|
+
*/
|
|
24
|
+
function getGitCommit() {
|
|
25
|
+
// Check CI environment variables first
|
|
26
|
+
const ciCommit = process.env.GITHUB_SHA
|
|
27
|
+
|| process.env.CI_COMMIT_SHA
|
|
28
|
+
|| process.env.GIT_COMMIT;
|
|
29
|
+
if (ciCommit) {
|
|
30
|
+
return ciCommit.slice(0, 7);
|
|
31
|
+
}
|
|
32
|
+
// Try git command (best-effort, non-fatal)
|
|
33
|
+
try {
|
|
34
|
+
const commit = execSync('git rev-parse --short HEAD', {
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
37
|
+
}).trim();
|
|
38
|
+
return commit || null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build version output object.
|
|
46
|
+
*/
|
|
47
|
+
export function getVersionInfo() {
|
|
48
|
+
return {
|
|
49
|
+
schema_version: 1,
|
|
50
|
+
agent_version: AGENT_VERSION,
|
|
51
|
+
artifact_schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
52
|
+
node: process.version,
|
|
53
|
+
platform: process.platform,
|
|
54
|
+
commit: getGitCommit()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Format version for human-readable output.
|
|
59
|
+
*/
|
|
60
|
+
function formatVersion(info) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push(`Agent Runner v${info.agent_version}`);
|
|
63
|
+
lines.push(` Artifact Schema: v${info.artifact_schema_version}`);
|
|
64
|
+
lines.push(` Node: ${info.node}`);
|
|
65
|
+
lines.push(` Platform: ${info.platform}`);
|
|
66
|
+
if (info.commit) {
|
|
67
|
+
lines.push(` Commit: ${info.commit}`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run the version command.
|
|
73
|
+
*/
|
|
74
|
+
export async function versionCommand(options) {
|
|
75
|
+
const info = getVersionInfo();
|
|
76
|
+
if (options.json) {
|
|
77
|
+
console.log(JSON.stringify(info, null, 2));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(formatVersion(info));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent wait - Block until run reaches terminal state.
|
|
3
|
+
*
|
|
4
|
+
* Designed for meta-agent coordination. Returns machine-readable JSON
|
|
5
|
+
* with run outcome, suitable for scripting and automation.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
10
|
+
/**
|
|
11
|
+
* Current schema version for WaitResult.
|
|
12
|
+
* Increment when making breaking changes to the structure.
|
|
13
|
+
*/
|
|
14
|
+
export const WAIT_RESULT_SCHEMA_VERSION = 1;
|
|
15
|
+
const TERMINAL_PHASES = ['STOPPED', 'DONE'];
|
|
16
|
+
const POLL_INTERVAL_MS = 500;
|
|
17
|
+
const BACKOFF_MAX_MS = 2000;
|
|
18
|
+
function readState(statePath) {
|
|
19
|
+
if (!fs.existsSync(statePath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isTerminal(state) {
|
|
31
|
+
return TERMINAL_PHASES.includes(state.phase);
|
|
32
|
+
}
|
|
33
|
+
function matchesCondition(state, condition) {
|
|
34
|
+
if (!isTerminal(state))
|
|
35
|
+
return false;
|
|
36
|
+
switch (condition) {
|
|
37
|
+
case 'terminal':
|
|
38
|
+
return true;
|
|
39
|
+
case 'complete':
|
|
40
|
+
return state.stop_reason === 'complete';
|
|
41
|
+
case 'stop':
|
|
42
|
+
return state.stop_reason !== 'complete';
|
|
43
|
+
default:
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function buildResult(runId, runDir, repoRoot, state, elapsedMs, timedOut) {
|
|
48
|
+
const isComplete = state.stop_reason === 'complete';
|
|
49
|
+
const result = {
|
|
50
|
+
schema_version: WAIT_RESULT_SCHEMA_VERSION,
|
|
51
|
+
run_id: runId,
|
|
52
|
+
run_dir: runDir,
|
|
53
|
+
repo_root: repoRoot,
|
|
54
|
+
status: timedOut ? 'timeout' : isComplete ? 'complete' : 'stopped',
|
|
55
|
+
phase: state.phase,
|
|
56
|
+
progress: {
|
|
57
|
+
milestone: state.milestone_index + 1,
|
|
58
|
+
of: state.milestones.length
|
|
59
|
+
},
|
|
60
|
+
elapsed_ms: elapsedMs,
|
|
61
|
+
ts: new Date().toISOString()
|
|
62
|
+
};
|
|
63
|
+
if (state.stop_reason && state.stop_reason !== 'complete') {
|
|
64
|
+
result.stop_reason = state.stop_reason;
|
|
65
|
+
}
|
|
66
|
+
// Add resume command for non-complete stops
|
|
67
|
+
if (!isComplete && !timedOut) {
|
|
68
|
+
result.resume_command = `agent resume ${runId}`;
|
|
69
|
+
}
|
|
70
|
+
// Add collision info if relevant
|
|
71
|
+
if (state.stop_reason === 'parallel_file_collision') {
|
|
72
|
+
// Could extract from timeline, but for now just flag it
|
|
73
|
+
result.collision_info = {};
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
function sleep(ms) {
|
|
78
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
79
|
+
}
|
|
80
|
+
export async function waitCommand(options) {
|
|
81
|
+
const runDir = path.join(getRunsRoot(options.repo), options.runId);
|
|
82
|
+
const statePath = path.join(runDir, 'state.json');
|
|
83
|
+
if (!fs.existsSync(runDir)) {
|
|
84
|
+
if (options.json) {
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
error: 'run_not_found',
|
|
87
|
+
run_id: options.runId,
|
|
88
|
+
message: `Run directory not found: ${runDir}`
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error(`Run directory not found: ${runDir}`);
|
|
93
|
+
}
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const repoRoot = path.resolve(options.repo);
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
const timeoutMs = options.timeout ?? Infinity;
|
|
100
|
+
let pollInterval = POLL_INTERVAL_MS;
|
|
101
|
+
let lastState = null;
|
|
102
|
+
while (true) {
|
|
103
|
+
const elapsed = Date.now() - startTime;
|
|
104
|
+
// Check timeout
|
|
105
|
+
if (elapsed >= timeoutMs) {
|
|
106
|
+
const state = readState(statePath);
|
|
107
|
+
if (state) {
|
|
108
|
+
const result = buildResult(options.runId, runDir, repoRoot, state, elapsed, true);
|
|
109
|
+
if (options.json) {
|
|
110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(`Timeout after ${Math.round(elapsed / 1000)}s`);
|
|
114
|
+
console.log(`Current phase: ${state.phase}`);
|
|
115
|
+
console.log(`Progress: ${state.milestone_index + 1}/${state.milestones.length}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
process.exitCode = 124; // timeout exit code (like GNU timeout)
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Read current state
|
|
122
|
+
const state = readState(statePath);
|
|
123
|
+
if (!state) {
|
|
124
|
+
await sleep(pollInterval);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
lastState = state;
|
|
128
|
+
// Check if condition is met
|
|
129
|
+
if (matchesCondition(state, options.for)) {
|
|
130
|
+
const result = buildResult(options.runId, runDir, repoRoot, state, elapsed, false);
|
|
131
|
+
if (options.json) {
|
|
132
|
+
console.log(JSON.stringify(result, null, 2));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const statusWord = result.status === 'complete' ? 'Completed' : 'Stopped';
|
|
136
|
+
console.log(`${statusWord} after ${Math.round(elapsed / 1000)}s`);
|
|
137
|
+
console.log(`Phase: ${state.phase}`);
|
|
138
|
+
console.log(`Progress: ${result.progress.milestone}/${result.progress.of}`);
|
|
139
|
+
if (result.stop_reason) {
|
|
140
|
+
console.log(`Reason: ${result.stop_reason}`);
|
|
141
|
+
}
|
|
142
|
+
if (result.resume_command) {
|
|
143
|
+
console.log(`Resume: ${result.resume_command}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Exit code: 0 for complete, 1 for stop
|
|
147
|
+
process.exitCode = result.status === 'complete' ? 0 : 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Backoff polling interval
|
|
151
|
+
pollInterval = Math.min(pollInterval * 1.2, BACKOFF_MAX_MS);
|
|
152
|
+
await sleep(pollInterval);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Find the latest run ID for --latest flag.
|
|
157
|
+
*/
|
|
158
|
+
export function findLatestRunId(repoPath) {
|
|
159
|
+
const runsDir = getRunsRoot(repoPath);
|
|
160
|
+
if (!fs.existsSync(runsDir)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const runIds = fs
|
|
164
|
+
.readdirSync(runsDir, { withFileTypes: true })
|
|
165
|
+
.filter((e) => e.isDirectory() && /^\d{14}$/.test(e.name))
|
|
166
|
+
.map((e) => e.name)
|
|
167
|
+
.sort()
|
|
168
|
+
.reverse();
|
|
169
|
+
return runIds[0] ?? null;
|
|
170
|
+
}
|