@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,149 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { RunStore } from '../store/run-store.js';
4
+ import { agentConfigSchema } from '../config/schema.js';
5
+ import { loadConfig, resolveConfigPath } from '../config/load.js';
6
+ import { runSupervisorLoop } from '../supervisor/runner.js';
7
+ import { prepareForResume } from '../supervisor/state-machine.js';
8
+ import { captureFingerprint, compareFingerprints } from '../env/fingerprint.js';
9
+ import { recreateWorktree } from '../repo/worktree.js';
10
+ /**
11
+ * Format effective configuration for display at resume.
12
+ */
13
+ function formatResumeConfig(options) {
14
+ const parts = [
15
+ `run_id=${options.runId}`,
16
+ `time=${options.time}min`,
17
+ `ticks=${options.maxTicks}`,
18
+ `auto_resume=${options.autoResume ? 'on' : 'off'}`,
19
+ `allow_deps=${options.allowDeps ? 'yes' : 'no'}`,
20
+ `force=${options.force ? 'yes' : 'no'}`
21
+ ];
22
+ return `Resume: ${parts.join(' | ')}`;
23
+ }
24
+ function readConfigSnapshot(runDir) {
25
+ const snapshotPath = path.join(runDir, 'config.snapshot.json');
26
+ if (!fs.existsSync(snapshotPath)) {
27
+ return { config: null, worktree: null };
28
+ }
29
+ const raw = fs.readFileSync(snapshotPath, 'utf-8');
30
+ const parsed = JSON.parse(raw);
31
+ // Extract worktree info before parsing config
32
+ const worktree = parsed._worktree ?? null;
33
+ delete parsed._worktree;
34
+ // Parse the config without worktree field
35
+ const config = agentConfigSchema.parse(parsed);
36
+ return { config, worktree };
37
+ }
38
+ function readTaskArtifact(runDir) {
39
+ const taskPath = path.join(runDir, 'artifacts', 'task.md');
40
+ if (!fs.existsSync(taskPath)) {
41
+ throw new Error(`Task artifact not found: ${taskPath}`);
42
+ }
43
+ return fs.readFileSync(taskPath, 'utf-8');
44
+ }
45
+ export async function resumeCommand(options) {
46
+ // Log effective configuration for transparency
47
+ console.log(formatResumeConfig(options));
48
+ const runStore = RunStore.init(options.runId, options.repo);
49
+ let state;
50
+ try {
51
+ state = runStore.readState();
52
+ }
53
+ catch {
54
+ throw new Error(`Run state not found for ${options.runId}`);
55
+ }
56
+ const { config: configSnapshot, worktree: worktreeInfo } = readConfigSnapshot(runStore.path);
57
+ const config = configSnapshot ??
58
+ loadConfig(resolveConfigPath(state.repo_path, options.config));
59
+ const taskText = readTaskArtifact(runStore.path);
60
+ // Handle worktree reattachment if this run used a worktree
61
+ let effectiveRepoPath = state.repo_path;
62
+ if (worktreeInfo?.worktree_enabled) {
63
+ try {
64
+ const result = await recreateWorktree(worktreeInfo, options.force);
65
+ if (result.recreated) {
66
+ console.log(`Worktree recreated: ${worktreeInfo.effective_repo_path}`);
67
+ runStore.appendEvent({
68
+ type: 'worktree_recreated',
69
+ source: 'cli',
70
+ payload: {
71
+ worktree_path: worktreeInfo.effective_repo_path,
72
+ base_sha: worktreeInfo.base_sha
73
+ }
74
+ });
75
+ }
76
+ if (result.branchMismatch) {
77
+ runStore.appendEvent({
78
+ type: 'worktree_branch_mismatch',
79
+ source: 'cli',
80
+ payload: {
81
+ expected_branch: worktreeInfo.run_branch,
82
+ force_used: true
83
+ }
84
+ });
85
+ }
86
+ if (result.nodeModulesSymlinked) {
87
+ runStore.appendEvent({
88
+ type: 'node_modules_symlinked',
89
+ source: 'cli',
90
+ payload: {
91
+ worktree_path: worktreeInfo.effective_repo_path
92
+ }
93
+ });
94
+ }
95
+ effectiveRepoPath = result.info.effective_repo_path;
96
+ console.log(`Using worktree: ${effectiveRepoPath}`);
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : 'Unknown error';
100
+ console.error(`Failed to recreate worktree: ${message}`);
101
+ console.error('Run with --force to override, or start fresh with: node dist/cli.js run --worktree ...');
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ }
106
+ // Check environment fingerprint
107
+ const originalFingerprint = runStore.readFingerprint();
108
+ if (originalFingerprint) {
109
+ const currentFingerprint = await captureFingerprint(config, effectiveRepoPath);
110
+ const diffs = compareFingerprints(originalFingerprint, currentFingerprint);
111
+ if (diffs.length > 0) {
112
+ console.warn('Environment fingerprint mismatch:');
113
+ for (const diff of diffs) {
114
+ console.warn(` ${diff.field}: ${diff.original ?? 'null'} -> ${diff.current ?? 'null'}`);
115
+ }
116
+ if (!options.force) {
117
+ console.error('\nRun with --force to resume despite fingerprint mismatch.');
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+ console.warn('\nWARNING: Forcing resume despite environment mismatch (--force)\n');
122
+ }
123
+ }
124
+ // Use shared helper to prepare state for resume
125
+ const updated = prepareForResume(state, { resumeToken: options.runId });
126
+ runStore.writeState(updated);
127
+ runStore.appendEvent({
128
+ type: 'run_resumed',
129
+ source: 'cli',
130
+ payload: {
131
+ run_id: options.runId,
132
+ max_ticks: options.maxTicks,
133
+ time: options.time,
134
+ allow_deps: options.allowDeps,
135
+ auto_resume: options.autoResume,
136
+ resume_phase: updated.phase
137
+ }
138
+ });
139
+ await runSupervisorLoop({
140
+ runStore,
141
+ repoPath: effectiveRepoPath,
142
+ taskText,
143
+ config,
144
+ timeBudgetMinutes: options.time,
145
+ maxTicks: options.maxTicks,
146
+ allowDeps: options.allowDeps,
147
+ autoResume: options.autoResume
148
+ });
149
+ }
@@ -0,0 +1,538 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig, resolveConfigPath } from '../config/load.js';
4
+ import { RunStore } from '../store/run-store.js';
5
+ import { getRunsRoot, getWorktreesRoot, getAgentPaths } from '../store/runs-root.js';
6
+ import { git, gitOptional } from '../repo/git.js';
7
+ import { createWorktree, ensureRepoInfoExclude } from '../repo/worktree.js';
8
+ import { buildMilestonesFromTask } from '../supervisor/planner.js';
9
+ import { createInitialState, stopRun, updatePhase } from '../supervisor/state-machine.js';
10
+ import { runPreflight } from './preflight.js';
11
+ import { runSupervisorLoop } from '../supervisor/runner.js';
12
+ import { runDoctorChecks } from './doctor.js';
13
+ import { captureFingerprint } from '../env/fingerprint.js';
14
+ import { loadTaskMetadata } from '../tasks/task-metadata.js';
15
+ import { getActiveRuns, checkAllowlistOverlaps, formatAllowlistWarning } from '../supervisor/collision.js';
16
+ function makeRunId() {
17
+ const now = new Date();
18
+ const parts = [
19
+ now.getUTCFullYear(),
20
+ String(now.getUTCMonth() + 1).padStart(2, '0'),
21
+ String(now.getUTCDate()).padStart(2, '0'),
22
+ String(now.getUTCHours()).padStart(2, '0'),
23
+ String(now.getUTCMinutes()).padStart(2, '0'),
24
+ String(now.getUTCSeconds()).padStart(2, '0')
25
+ ];
26
+ return parts.join('');
27
+ }
28
+ function slugFromTask(taskPath) {
29
+ const base = path.basename(taskPath, path.extname(taskPath));
30
+ return base.toLowerCase().replace(/[^a-z0-9-]/g, '-');
31
+ }
32
+ async function ensureRunBranch(gitRoot, runBranch, defaultBranch) {
33
+ const existing = await gitOptional(['branch', '--list', runBranch], gitRoot);
34
+ if (existing?.stdout?.trim()) {
35
+ await git(['checkout', runBranch], gitRoot);
36
+ return;
37
+ }
38
+ await git(['checkout', '-b', runBranch, defaultBranch], gitRoot);
39
+ }
40
+ function formatSummaryLine(input) {
41
+ return [
42
+ `run_id=${input.runId}`,
43
+ `run_dir=${input.runDir}`,
44
+ `repo_root=${input.repoRoot}`,
45
+ `current_branch=${input.currentBranch}`,
46
+ `planned_run_branch=${input.plannedRunBranch}`,
47
+ `guard=${input.guardOk ? 'pass' : 'fail'}`,
48
+ `tiers=${input.tiers.join('|')}`,
49
+ `tier_reasons=${input.tierReasons.join('|') || 'none'}`,
50
+ `no_write=${input.noWrite ? 'true' : 'false'}`
51
+ ].join(' ');
52
+ }
53
+ function normalizePath(input) {
54
+ return input.replace(/\\/g, '/').replace(/\/+$/, '');
55
+ }
56
+ /**
57
+ * Format effective configuration for display at run start.
58
+ * Shows key settings to eliminate "is it broken?" confusion.
59
+ */
60
+ function formatEffectiveConfig(options) {
61
+ const contextPack = process.env.CONTEXT_PACK === '1' ? 'on' : 'off';
62
+ const parts = [
63
+ `time=${options.time}min`,
64
+ `ticks=${options.maxTicks}`,
65
+ `worktree=${options.worktree ? 'on' : 'off'}`,
66
+ `fast=${options.fast ? 'on' : 'off'}`,
67
+ `auto_resume=${options.autoResume ? 'on' : 'off'}`,
68
+ `context_pack=${contextPack}`,
69
+ `allow_deps=${options.allowDeps ? 'yes' : 'no'}`
70
+ ];
71
+ return `Config: ${parts.join(' | ')}`;
72
+ }
73
+ /**
74
+ * Format paths summary for debugging.
75
+ * Shows where runs and worktrees are stored.
76
+ */
77
+ function formatPathsSummary(repoPath, worktreeEnabled, worktreePath) {
78
+ // Import is at the top of the file, use directly
79
+ const paths = getAgentPaths(repoPath);
80
+ const worktreesOverride = process.env.AGENT_WORKTREES_DIR;
81
+ const parts = [
82
+ `repo=${paths.repo_root}`,
83
+ `runs=${paths.runs_dir}`,
84
+ `worktrees=${paths.worktrees_dir}${worktreesOverride ? ' (env override)' : ''}`
85
+ ];
86
+ if (worktreeEnabled && worktreePath) {
87
+ parts.push(`current_worktree=${worktreePath}`);
88
+ }
89
+ return `Paths: ${parts.join(' | ')}`;
90
+ }
91
+ /**
92
+ * Check for legacy worktree locations and print a warning if found.
93
+ * Legacy paths:
94
+ * - v2: .agent/worktrees/<runId>/
95
+ * - v1: .agent/runs/<runId>/worktree/
96
+ */
97
+ function checkLegacyWorktrees(repoPath) {
98
+ const legacyPaths = [];
99
+ // Legacy v2: .agent/worktrees/
100
+ const legacyV2 = path.join(repoPath, '.agent', 'worktrees');
101
+ if (fs.existsSync(legacyV2) && fs.statSync(legacyV2).isDirectory()) {
102
+ const entries = fs.readdirSync(legacyV2);
103
+ if (entries.length > 0) {
104
+ legacyPaths.push(legacyV2);
105
+ }
106
+ }
107
+ // Legacy v1: .agent/runs/<runId>/worktree/
108
+ const runsDir = path.join(repoPath, '.agent', 'runs');
109
+ if (fs.existsSync(runsDir) && fs.statSync(runsDir).isDirectory()) {
110
+ const runDirs = fs.readdirSync(runsDir);
111
+ for (const runId of runDirs) {
112
+ const worktreePath = path.join(runsDir, runId, 'worktree');
113
+ if (fs.existsSync(worktreePath) && fs.statSync(worktreePath).isDirectory()) {
114
+ legacyPaths.push(worktreePath);
115
+ break; // One example is enough
116
+ }
117
+ }
118
+ }
119
+ if (legacyPaths.length > 0) {
120
+ console.warn('');
121
+ console.warn('⚠️ Legacy worktree layout detected:');
122
+ for (const p of legacyPaths) {
123
+ console.warn(` ${p}`);
124
+ }
125
+ console.warn('');
126
+ console.warn(' This version uses `.agent-worktrees/` instead.');
127
+ console.warn(' Run `agent gc` to clean up old worktrees, or delete them manually.');
128
+ console.warn('');
129
+ }
130
+ }
131
+ function basePathFromAllowlist(pattern, repoPath) {
132
+ const globIndex = pattern.search(/[*?[\]]/);
133
+ const withoutGlob = globIndex === -1 ? pattern : pattern.slice(0, globIndex);
134
+ const trimmed = normalizePath(withoutGlob);
135
+ if (!trimmed)
136
+ return null;
137
+ const abs = path.resolve(repoPath, trimmed);
138
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
139
+ return trimmed;
140
+ }
141
+ if (pattern.endsWith('/**') || pattern.endsWith('/*') || pattern.endsWith('/')) {
142
+ return trimmed;
143
+ }
144
+ const dir = normalizePath(path.posix.dirname(trimmed));
145
+ return dir && dir !== '.' ? dir : null;
146
+ }
147
+ function commonPathPrefix(paths) {
148
+ if (paths.length === 0)
149
+ return null;
150
+ const segments = paths.map((p) => normalizePath(p).split('/').filter(Boolean));
151
+ const max = Math.min(...segments.map((parts) => parts.length));
152
+ const common = [];
153
+ for (let i = 0; i < max; i += 1) {
154
+ const segment = segments[0][i];
155
+ if (segments.every((parts) => parts[i] === segment)) {
156
+ common.push(segment);
157
+ }
158
+ else {
159
+ break;
160
+ }
161
+ }
162
+ return common.length ? common.join('/') : null;
163
+ }
164
+ function resolveTargetRoot(repoPath, allowlist) {
165
+ const roots = allowlist
166
+ .map((pattern) => basePathFromAllowlist(pattern, repoPath))
167
+ .filter((value) => Boolean(value));
168
+ const root = commonPathPrefix(roots);
169
+ if (!root)
170
+ return null;
171
+ const repoAbs = path.resolve(repoPath);
172
+ const targetAbs = path.resolve(repoPath, root);
173
+ if (!targetAbs.startsWith(`${repoAbs}${path.sep}`))
174
+ return null;
175
+ if (targetAbs === repoAbs)
176
+ return null;
177
+ return root;
178
+ }
179
+ async function freshenTargetRoot(repoPath, allowlist) {
180
+ const targetRoot = resolveTargetRoot(repoPath, allowlist);
181
+ if (!targetRoot) {
182
+ throw new Error('Unable to resolve safe target root from allowlist.');
183
+ }
184
+ await gitOptional(['checkout', '--', targetRoot], repoPath);
185
+ await git(['clean', '-fd', targetRoot], repoPath);
186
+ return targetRoot;
187
+ }
188
+ export async function runCommand(options) {
189
+ const repoPath = path.resolve(options.repo);
190
+ const taskPath = path.resolve(options.task);
191
+ const configPath = resolveConfigPath(repoPath, options.config);
192
+ const config = loadConfig(configPath);
193
+ const taskMetadata = loadTaskMetadata(taskPath);
194
+ const taskText = taskMetadata.body;
195
+ const ownsRaw = taskMetadata.owns_raw;
196
+ const ownsNormalized = taskMetadata.owns_normalized;
197
+ // Auto-inject git excludes for agent artifacts BEFORE any git status checks.
198
+ // This prevents .agent/ and .agent-worktrees/ from appearing as dirty on fresh repos.
199
+ ensureRepoInfoExclude(repoPath, [
200
+ '.agent',
201
+ '.agent/',
202
+ '.agent-worktrees',
203
+ '.agent-worktrees/',
204
+ ]);
205
+ // Warn about legacy worktree locations (helps users clean up after upgrade)
206
+ if (!options.json) {
207
+ checkLegacyWorktrees(repoPath);
208
+ }
209
+ // Log effective configuration and paths for transparency (skip in JSON mode)
210
+ if (!options.json) {
211
+ console.log(formatEffectiveConfig(options));
212
+ console.log(formatPathsSummary(repoPath, options.worktree));
213
+ }
214
+ // Run doctor checks unless skipped (via flag or env var)
215
+ const skipDoctor = options.skipDoctor || process.env.AGENT_SKIP_DOCTOR === '1';
216
+ if (skipDoctor) {
217
+ console.warn('WARNING: Skipping worker health checks (--skip-doctor)');
218
+ }
219
+ else {
220
+ const doctorChecks = await runDoctorChecks(config, repoPath);
221
+ const failedChecks = doctorChecks.filter((c) => c.error);
222
+ if (failedChecks.length > 0) {
223
+ console.error('Doctor checks failed:');
224
+ for (const check of failedChecks) {
225
+ console.error(` ${check.name}: ${check.error}`);
226
+ }
227
+ console.error('\nRun with --skip-doctor to bypass worker health checks.');
228
+ process.exitCode = 1;
229
+ return;
230
+ }
231
+ }
232
+ // Stage 1: Pre-PLAN collision check (allowlist overlap warning)
233
+ if (!options.forceParallel) {
234
+ const activeRuns = getActiveRuns(repoPath);
235
+ if (activeRuns.length > 0) {
236
+ const overlaps = checkAllowlistOverlaps(config.scope.allowlist, activeRuns);
237
+ if (overlaps.length > 0) {
238
+ console.warn('');
239
+ console.warn(formatAllowlistWarning(overlaps));
240
+ console.warn('');
241
+ }
242
+ }
243
+ }
244
+ let freshTargetRoot = null;
245
+ if (options.freshTarget) {
246
+ try {
247
+ freshTargetRoot = await freshenTargetRoot(repoPath, config.scope.allowlist);
248
+ console.log(`Fresh target: cleaned ${freshTargetRoot}`);
249
+ }
250
+ catch (error) {
251
+ const message = error instanceof Error ? error.message : 'Unknown error';
252
+ console.error(`Fresh target failed: ${message}`);
253
+ process.exitCode = 1;
254
+ return;
255
+ }
256
+ }
257
+ const runId = makeRunId();
258
+ const slug = slugFromTask(taskPath);
259
+ const runDir = path.join(getRunsRoot(repoPath), runId);
260
+ const milestones = buildMilestonesFromTask(taskText);
261
+ const milestoneRiskLevel = milestones[0]?.risk_level ?? 'medium';
262
+ // Create worktree for isolated execution if enabled
263
+ let effectiveRepoPath = repoPath;
264
+ let worktreeInfo = null;
265
+ if (options.worktree) {
266
+ const worktreePath = path.join(getWorktreesRoot(repoPath), runId);
267
+ const runBranch = options.noBranch
268
+ ? undefined
269
+ : `agent/${runId}/${slug}`;
270
+ try {
271
+ worktreeInfo = await createWorktree(repoPath, worktreePath, runBranch);
272
+ effectiveRepoPath = worktreeInfo.effective_repo_path;
273
+ console.log(`Worktree created: ${worktreePath}`);
274
+ }
275
+ catch (error) {
276
+ const message = error instanceof Error ? error.message : 'Unknown error';
277
+ console.error(`Failed to create worktree: ${message}`);
278
+ process.exitCode = 1;
279
+ return;
280
+ }
281
+ }
282
+ // Always run ping for quick auth check (catches OAuth failures early)
283
+ // Doctor is more thorough but ping is faster for auth validation
284
+ const preflight = await runPreflight({
285
+ repoPath: effectiveRepoPath,
286
+ runId,
287
+ slug,
288
+ config,
289
+ allowDeps: options.allowDeps,
290
+ allowDirty: options.allowDirty,
291
+ milestoneRiskLevel,
292
+ skipPing: false
293
+ });
294
+ const runStore = options.noWrite ? null : RunStore.init(runId, repoPath);
295
+ if (runStore) {
296
+ // Write config snapshot with worktree info if enabled
297
+ const configWithWorktree = worktreeInfo
298
+ ? { ...config, _worktree: worktreeInfo }
299
+ : config;
300
+ runStore.writeConfigSnapshot(configWithWorktree);
301
+ runStore.writeArtifact('task.md', taskText);
302
+ runStore.writeArtifact('task.meta.json', JSON.stringify({
303
+ task_path: taskPath,
304
+ owns_raw: ownsRaw,
305
+ owns_normalized: ownsNormalized
306
+ }, null, 2));
307
+ const fingerprint = await captureFingerprint(config, effectiveRepoPath);
308
+ runStore.writeFingerprint(fingerprint);
309
+ if (worktreeInfo) {
310
+ runStore.appendEvent({
311
+ type: 'worktree_created',
312
+ source: 'cli',
313
+ payload: {
314
+ worktree_path: worktreeInfo.effective_repo_path,
315
+ base_sha: worktreeInfo.base_sha,
316
+ run_branch: worktreeInfo.run_branch
317
+ }
318
+ });
319
+ }
320
+ if (freshTargetRoot) {
321
+ runStore.appendEvent({
322
+ type: 'fresh_target',
323
+ source: 'cli',
324
+ payload: { target_root: freshTargetRoot }
325
+ });
326
+ }
327
+ runStore.appendEvent({
328
+ type: 'run_started',
329
+ source: 'cli',
330
+ payload: {
331
+ repo: preflight.repo_context,
332
+ task: taskPath,
333
+ time_budget_minutes: options.time,
334
+ allow_deps: options.allowDeps,
335
+ allow_dirty: options.allowDirty,
336
+ web: options.web,
337
+ no_branch: options.noBranch,
338
+ dry_run: options.dryRun,
339
+ max_ticks: options.maxTicks
340
+ }
341
+ });
342
+ runStore.appendEvent({
343
+ type: 'preflight',
344
+ source: 'cli',
345
+ payload: {
346
+ guard: preflight.guard,
347
+ binary: preflight.binary,
348
+ ping: preflight.ping,
349
+ tiers: preflight.tiers,
350
+ tier_reasons: preflight.tier_reasons
351
+ }
352
+ });
353
+ }
354
+ const summaryLine = formatSummaryLine({
355
+ runId,
356
+ runDir,
357
+ repoRoot: preflight.repo_context.git_root,
358
+ currentBranch: preflight.repo_context.current_branch,
359
+ plannedRunBranch: preflight.repo_context.run_branch,
360
+ guardOk: preflight.guard.ok,
361
+ tiers: preflight.tiers,
362
+ tierReasons: preflight.tier_reasons,
363
+ noWrite: options.noWrite
364
+ });
365
+ if (!preflight.guard.ok) {
366
+ // Build detailed guard diagnostics (always, not just when runStore exists)
367
+ const binaryLines = preflight.binary.results.map(r => r.ok
368
+ ? `- ${r.worker}: ${r.version}`
369
+ : `- ${r.worker}: FAIL - ${r.error}`);
370
+ const pingLines = preflight.ping.skipped
371
+ ? ['- Skipped']
372
+ : preflight.ping.results.map(r => r.ok
373
+ ? `- ${r.worker}: OK (${r.ms}ms)`
374
+ : `- ${r.worker}: FAIL - ${r.category} (${r.message})`);
375
+ const guardSummary = [
376
+ 'Guard Failure Details:',
377
+ '',
378
+ 'Reasons:',
379
+ preflight.guard.reasons.length
380
+ ? preflight.guard.reasons.map(r => ` - ${r}`).join('\n')
381
+ : ' - None',
382
+ '',
383
+ 'Scope violations:',
384
+ preflight.guard.scope_violations.length
385
+ ? preflight.guard.scope_violations.map(f => ` - ${f}`).join('\n')
386
+ : ' - None',
387
+ '',
388
+ 'Lockfile violations:',
389
+ preflight.guard.lockfile_violations.length
390
+ ? preflight.guard.lockfile_violations.map(f => ` - ${f}`).join('\n')
391
+ : ' - None',
392
+ '',
393
+ 'Dirty files (env noise excluded):',
394
+ preflight.guard.dirty_files.length
395
+ ? preflight.guard.dirty_files.map(f => ` - ${f}`).join('\n')
396
+ : ' - None',
397
+ '',
398
+ 'Binary checks:',
399
+ binaryLines.length ? binaryLines.join('\n') : ' - None',
400
+ '',
401
+ 'Ping results:',
402
+ pingLines.map(l => ` ${l}`).join('\n')
403
+ ].join('\n');
404
+ if (runStore) {
405
+ let state = createInitialState({
406
+ run_id: runId,
407
+ repo_path: effectiveRepoPath,
408
+ task_text: taskText,
409
+ owned_paths: {
410
+ raw: ownsRaw,
411
+ normalized: ownsNormalized
412
+ },
413
+ allowlist: config.scope.allowlist,
414
+ denylist: config.scope.denylist
415
+ });
416
+ state.current_branch = preflight.repo_context.current_branch;
417
+ state.planned_run_branch = preflight.repo_context.run_branch;
418
+ state.tier_reasons = preflight.tier_reasons;
419
+ state = stopRun(state, 'guard_violation');
420
+ runStore.writeState(state);
421
+ runStore.appendEvent({
422
+ type: 'guard_violation',
423
+ source: 'cli',
424
+ payload: {
425
+ guard: preflight.guard,
426
+ binary: preflight.binary,
427
+ ping: preflight.ping
428
+ }
429
+ });
430
+ // Write markdown summary to run store
431
+ const summaryMd = [
432
+ '# Summary',
433
+ '',
434
+ 'Run stopped due to guard violations.',
435
+ '',
436
+ guardSummary
437
+ ].join('\n');
438
+ runStore.writeSummary(summaryMd);
439
+ }
440
+ if (options.json) {
441
+ const jsonOutput = {
442
+ run_id: runId,
443
+ run_dir: runDir,
444
+ repo_root: preflight.repo_context.git_root,
445
+ status: 'guard_failed',
446
+ guard_ok: false,
447
+ tiers: preflight.tiers
448
+ };
449
+ console.log(JSON.stringify(jsonOutput));
450
+ }
451
+ else {
452
+ // Print detailed diagnostics to console (not just the one-liner)
453
+ console.log(summaryLine);
454
+ console.log('');
455
+ console.log(guardSummary);
456
+ }
457
+ return;
458
+ }
459
+ const noBranchEffective = options.noBranch || options.dryRun || options.noWrite;
460
+ if (!noBranchEffective) {
461
+ await ensureRunBranch(preflight.repo_context.git_root, preflight.repo_context.run_branch, preflight.repo_context.current_branch);
462
+ }
463
+ let state = createInitialState({
464
+ run_id: runId,
465
+ repo_path: effectiveRepoPath,
466
+ task_text: taskText,
467
+ owned_paths: {
468
+ raw: ownsRaw,
469
+ normalized: ownsNormalized
470
+ },
471
+ allowlist: config.scope.allowlist,
472
+ denylist: config.scope.denylist
473
+ });
474
+ state.current_branch = preflight.repo_context.current_branch;
475
+ state.planned_run_branch = preflight.repo_context.run_branch;
476
+ state.tier_reasons = preflight.tier_reasons;
477
+ // Fast path: skip PLAN, go directly to IMPLEMENT
478
+ state = updatePhase(state, options.fast ? 'IMPLEMENT' : 'PLAN');
479
+ if (runStore) {
480
+ runStore.writeState(state);
481
+ }
482
+ if (options.dryRun) {
483
+ if (runStore) {
484
+ runStore.appendEvent({
485
+ type: 'run_dry_stop',
486
+ source: 'cli',
487
+ payload: { reason: 'dry_run' }
488
+ });
489
+ runStore.writeSummary('# Summary\n\nRun initialized in dry-run mode.');
490
+ }
491
+ if (options.json) {
492
+ const jsonOutput = {
493
+ run_id: runId,
494
+ run_dir: runDir,
495
+ repo_root: preflight.repo_context.git_root,
496
+ status: 'dry_run',
497
+ guard_ok: true,
498
+ tiers: preflight.tiers
499
+ };
500
+ console.log(JSON.stringify(jsonOutput));
501
+ }
502
+ else {
503
+ console.log(summaryLine);
504
+ }
505
+ return;
506
+ }
507
+ // Output JSON early for orchestrator consumption (run_id available immediately)
508
+ if (options.json) {
509
+ const jsonOutput = {
510
+ run_id: runId,
511
+ run_dir: runDir,
512
+ repo_root: preflight.repo_context.git_root,
513
+ status: 'started',
514
+ guard_ok: true,
515
+ tiers: preflight.tiers
516
+ };
517
+ console.log(JSON.stringify(jsonOutput));
518
+ }
519
+ if (runStore) {
520
+ runStore.writeSummary('# Summary\n\nRun initialized. Supervisor loop not yet executed.');
521
+ await runSupervisorLoop({
522
+ runStore,
523
+ repoPath: effectiveRepoPath,
524
+ taskText,
525
+ config,
526
+ timeBudgetMinutes: options.time,
527
+ maxTicks: options.maxTicks,
528
+ allowDeps: options.allowDeps,
529
+ fast: options.fast,
530
+ autoResume: options.autoResume,
531
+ forceParallel: options.forceParallel,
532
+ ownedPaths: ownsNormalized
533
+ });
534
+ }
535
+ if (!options.json) {
536
+ console.log(summaryLine);
537
+ }
538
+ }