@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,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
|
+
}
|