@weldr/runr 0.3.0 → 0.4.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 +23 -0
- package/README.md +165 -47
- package/dist/cli.js +98 -1
- package/dist/commands/init.js +440 -0
- package/dist/commands/journal.js +167 -0
- package/dist/commands/next.js +25 -0
- package/dist/commands/report.js +55 -4
- package/dist/commands/watch.js +187 -0
- package/dist/journal/builder.js +464 -0
- package/dist/journal/redactor.js +68 -0
- package/dist/journal/renderer.js +201 -0
- package/dist/journal/types.js +7 -0
- package/dist/supervisor/runner.js +31 -0
- package/package.json +3 -1
- package/dist/commands/__tests__/report.test.js +0 -202
- package/dist/config/__tests__/presets.test.js +0 -104
- package/dist/context/__tests__/artifact.test.js +0 -130
- package/dist/context/__tests__/pack.test.js +0 -191
- package/dist/env/__tests__/fingerprint.test.js +0 -116
- package/dist/orchestrator/__tests__/policy.test.js +0 -185
- package/dist/orchestrator/__tests__/schema-version.test.js +0 -65
- package/dist/supervisor/__tests__/evidence-gate.test.js +0 -111
- package/dist/supervisor/__tests__/ownership.test.js +0 -103
- package/dist/supervisor/__tests__/state-machine.test.js +0 -290
- package/dist/workers/__tests__/claude.test.js +0 -88
- package/dist/workers/__tests__/codex.test.js +0 -81
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Detect Python project verification commands
|
|
5
|
+
*/
|
|
6
|
+
function detectPythonVerification(repoPath) {
|
|
7
|
+
const hasPyprojectToml = fs.existsSync(path.join(repoPath, 'pyproject.toml'));
|
|
8
|
+
const hasPytestIni = fs.existsSync(path.join(repoPath, 'pytest.ini'));
|
|
9
|
+
const hasPoetryLock = fs.existsSync(path.join(repoPath, 'poetry.lock'));
|
|
10
|
+
// If no Python markers, return null
|
|
11
|
+
if (!hasPyprojectToml && !hasPytestIni && !hasPoetryLock) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const verification = {
|
|
15
|
+
tier0: [],
|
|
16
|
+
tier1: [],
|
|
17
|
+
tier2: []
|
|
18
|
+
};
|
|
19
|
+
const presets = [];
|
|
20
|
+
// Parse pyproject.toml if it exists
|
|
21
|
+
let pyprojectContent = null;
|
|
22
|
+
if (hasPyprojectToml) {
|
|
23
|
+
try {
|
|
24
|
+
const pyprojectPath = path.join(repoPath, 'pyproject.toml');
|
|
25
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
26
|
+
// Simple TOML parsing for common sections
|
|
27
|
+
// Look for [tool.poetry], [tool.pytest], etc.
|
|
28
|
+
if (content.includes('[tool.poetry]') || hasPoetryLock) {
|
|
29
|
+
presets.push('poetry');
|
|
30
|
+
// Tier 1: Poetry install/check
|
|
31
|
+
verification.tier1.push('poetry check');
|
|
32
|
+
}
|
|
33
|
+
if (content.includes('[tool.pytest]') || hasPytestIni) {
|
|
34
|
+
presets.push('pytest');
|
|
35
|
+
// Tier 2: Run tests
|
|
36
|
+
verification.tier2.push('pytest');
|
|
37
|
+
}
|
|
38
|
+
// Check for mypy, black, ruff, etc.
|
|
39
|
+
if (content.includes('[tool.mypy]') || content.includes('mypy')) {
|
|
40
|
+
verification.tier0.push('mypy .');
|
|
41
|
+
}
|
|
42
|
+
if (content.includes('[tool.black]') || content.includes('black')) {
|
|
43
|
+
verification.tier0.push('black --check .');
|
|
44
|
+
}
|
|
45
|
+
if (content.includes('[tool.ruff]') || content.includes('ruff')) {
|
|
46
|
+
verification.tier0.push('ruff check .');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// If parsing fails, continue with basic detection
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// If pytest.ini exists but not already detected
|
|
54
|
+
if (hasPytestIni && !verification.tier2.includes('pytest')) {
|
|
55
|
+
presets.push('pytest');
|
|
56
|
+
verification.tier2.push('pytest');
|
|
57
|
+
}
|
|
58
|
+
// If nothing was detected, return null
|
|
59
|
+
if (verification.tier0.length === 0 && verification.tier1.length === 0 && verification.tier2.length === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
verification,
|
|
64
|
+
presets,
|
|
65
|
+
source: 'python'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Detect verification commands from package.json scripts
|
|
70
|
+
*/
|
|
71
|
+
function detectFromPackageJson(repoPath) {
|
|
72
|
+
const packageJsonPath = path.join(repoPath, 'package.json');
|
|
73
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
78
|
+
const scripts = packageJson.scripts || {};
|
|
79
|
+
const verification = {
|
|
80
|
+
tier0: [],
|
|
81
|
+
tier1: [],
|
|
82
|
+
tier2: []
|
|
83
|
+
};
|
|
84
|
+
const presets = [];
|
|
85
|
+
// Tier 0: fast checks (lint, typecheck)
|
|
86
|
+
if (scripts.typecheck) {
|
|
87
|
+
verification.tier0.push('npm run typecheck');
|
|
88
|
+
}
|
|
89
|
+
else if (scripts.tsc || scripts['type-check']) {
|
|
90
|
+
verification.tier0.push(`npm run ${scripts.tsc ? 'tsc' : 'type-check'}`);
|
|
91
|
+
}
|
|
92
|
+
if (scripts.lint) {
|
|
93
|
+
verification.tier0.push('npm run lint');
|
|
94
|
+
}
|
|
95
|
+
else if (scripts.eslint) {
|
|
96
|
+
verification.tier0.push('npm run eslint');
|
|
97
|
+
}
|
|
98
|
+
// Tier 1: build (slower, but catches integration issues)
|
|
99
|
+
if (scripts.build) {
|
|
100
|
+
verification.tier1.push('npm run build');
|
|
101
|
+
}
|
|
102
|
+
else if (scripts.compile) {
|
|
103
|
+
verification.tier1.push('npm run compile');
|
|
104
|
+
}
|
|
105
|
+
// Tier 2: tests (slowest, most comprehensive)
|
|
106
|
+
if (scripts.test) {
|
|
107
|
+
verification.tier2.push('npm run test');
|
|
108
|
+
}
|
|
109
|
+
else if (scripts.jest || scripts.vitest) {
|
|
110
|
+
verification.tier2.push(`npm run ${scripts.jest ? 'jest' : 'vitest'}`);
|
|
111
|
+
}
|
|
112
|
+
// Detect presets from dependencies
|
|
113
|
+
const allDeps = {
|
|
114
|
+
...packageJson.dependencies,
|
|
115
|
+
...packageJson.devDependencies
|
|
116
|
+
};
|
|
117
|
+
if (allDeps.typescript)
|
|
118
|
+
presets.push('typescript');
|
|
119
|
+
if (allDeps.vitest)
|
|
120
|
+
presets.push('vitest');
|
|
121
|
+
if (allDeps.jest)
|
|
122
|
+
presets.push('jest');
|
|
123
|
+
if (allDeps.next)
|
|
124
|
+
presets.push('nextjs');
|
|
125
|
+
if (allDeps.react && !allDeps.next)
|
|
126
|
+
presets.push('react');
|
|
127
|
+
if (allDeps.drizzle)
|
|
128
|
+
presets.push('drizzle');
|
|
129
|
+
if (allDeps.prisma || allDeps['@prisma/client'])
|
|
130
|
+
presets.push('prisma');
|
|
131
|
+
if (allDeps.playwright || allDeps['@playwright/test'])
|
|
132
|
+
presets.push('playwright');
|
|
133
|
+
if (allDeps.tailwindcss)
|
|
134
|
+
presets.push('tailwind');
|
|
135
|
+
if (allDeps.eslint)
|
|
136
|
+
presets.push('eslint');
|
|
137
|
+
// If nothing detected, return null
|
|
138
|
+
if (verification.tier0.length === 0 && verification.tier1.length === 0 && verification.tier2.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
verification,
|
|
143
|
+
presets,
|
|
144
|
+
source: 'package.json'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Generate default config when auto-detection fails
|
|
153
|
+
*/
|
|
154
|
+
function generateDefaultConfig(repoPath) {
|
|
155
|
+
const hasSrc = fs.existsSync(path.join(repoPath, 'src'));
|
|
156
|
+
const hasTests = fs.existsSync(path.join(repoPath, 'tests')) ||
|
|
157
|
+
fs.existsSync(path.join(repoPath, 'test'));
|
|
158
|
+
const presets = [];
|
|
159
|
+
// Check for common config files
|
|
160
|
+
if (fs.existsSync(path.join(repoPath, 'tsconfig.json'))) {
|
|
161
|
+
presets.push('typescript');
|
|
162
|
+
}
|
|
163
|
+
// Determine why we couldn't detect verification
|
|
164
|
+
let reason = 'no-package-json';
|
|
165
|
+
const packageJsonPath = path.join(repoPath, 'package.json');
|
|
166
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
167
|
+
try {
|
|
168
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
169
|
+
if (packageJson.scripts && Object.keys(packageJson.scripts).length > 0) {
|
|
170
|
+
reason = 'no-matching-scripts';
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
reason = 'empty-scripts';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
reason = 'invalid-package-json';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
verification: {
|
|
182
|
+
tier0: [],
|
|
183
|
+
tier1: [],
|
|
184
|
+
tier2: []
|
|
185
|
+
},
|
|
186
|
+
presets,
|
|
187
|
+
source: 'none',
|
|
188
|
+
reason
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Build config object from detection results
|
|
193
|
+
*/
|
|
194
|
+
function buildConfig(repoPath, detection) {
|
|
195
|
+
const hasSrc = fs.existsSync(path.join(repoPath, 'src'));
|
|
196
|
+
const hasTests = fs.existsSync(path.join(repoPath, 'tests')) ||
|
|
197
|
+
fs.existsSync(path.join(repoPath, 'test'));
|
|
198
|
+
// Build allowlist based on directory structure
|
|
199
|
+
const allowlist = [];
|
|
200
|
+
if (hasSrc)
|
|
201
|
+
allowlist.push('src/**');
|
|
202
|
+
if (hasTests) {
|
|
203
|
+
allowlist.push('tests/**');
|
|
204
|
+
allowlist.push('test/**');
|
|
205
|
+
}
|
|
206
|
+
if (allowlist.length === 0) {
|
|
207
|
+
// Default: allow everything except common excludes
|
|
208
|
+
allowlist.push('**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx');
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
agent: {
|
|
212
|
+
name: path.basename(repoPath),
|
|
213
|
+
version: '1'
|
|
214
|
+
},
|
|
215
|
+
scope: {
|
|
216
|
+
allowlist,
|
|
217
|
+
denylist: ['node_modules/**', '.env'],
|
|
218
|
+
lockfiles: ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'],
|
|
219
|
+
presets: detection.presets,
|
|
220
|
+
env_allowlist: [
|
|
221
|
+
'node_modules',
|
|
222
|
+
'node_modules/**',
|
|
223
|
+
'.next/**',
|
|
224
|
+
'dist/**',
|
|
225
|
+
'build/**',
|
|
226
|
+
'.turbo/**',
|
|
227
|
+
'.eslintcache',
|
|
228
|
+
'coverage/**'
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
verification: {
|
|
232
|
+
tier0: detection.verification.tier0,
|
|
233
|
+
tier1: detection.verification.tier1,
|
|
234
|
+
tier2: detection.verification.tier2,
|
|
235
|
+
risk_triggers: [],
|
|
236
|
+
max_verify_time_per_milestone: 600
|
|
237
|
+
},
|
|
238
|
+
repo: {},
|
|
239
|
+
workers: {
|
|
240
|
+
codex: {
|
|
241
|
+
bin: 'codex',
|
|
242
|
+
args: ['exec', '--full-auto', '--json'],
|
|
243
|
+
output: 'jsonl'
|
|
244
|
+
},
|
|
245
|
+
claude: {
|
|
246
|
+
bin: 'claude',
|
|
247
|
+
args: ['-p', '--output-format', 'json', '--dangerously-skip-permissions'],
|
|
248
|
+
output: 'json'
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
phases: {
|
|
252
|
+
plan: 'claude',
|
|
253
|
+
implement: 'codex',
|
|
254
|
+
review: 'claude'
|
|
255
|
+
},
|
|
256
|
+
resilience: {
|
|
257
|
+
auto_resume: false,
|
|
258
|
+
max_auto_resumes: 1,
|
|
259
|
+
auto_resume_delays_ms: [30000, 120000, 300000],
|
|
260
|
+
max_worker_call_minutes: 45,
|
|
261
|
+
max_review_rounds: 2
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Create example task files
|
|
267
|
+
*/
|
|
268
|
+
function createExampleTasks(runrDir) {
|
|
269
|
+
const tasksDir = path.join(runrDir, 'tasks');
|
|
270
|
+
fs.mkdirSync(tasksDir, { recursive: true });
|
|
271
|
+
const exampleBugfix = `# Fix Bug: [Description]
|
|
272
|
+
|
|
273
|
+
## Goal
|
|
274
|
+
Fix [specific bug] in [component/module]
|
|
275
|
+
|
|
276
|
+
## Requirements
|
|
277
|
+
- Identify root cause
|
|
278
|
+
- Implement fix
|
|
279
|
+
- Add test to prevent regression
|
|
280
|
+
|
|
281
|
+
## Success Criteria
|
|
282
|
+
- Bug is fixed (verified manually or with specific test)
|
|
283
|
+
- All existing tests still pass
|
|
284
|
+
- New test added covering the bug scenario
|
|
285
|
+
`;
|
|
286
|
+
const exampleFeature = `# Add Feature: [Description]
|
|
287
|
+
|
|
288
|
+
## Goal
|
|
289
|
+
Implement [feature] that allows users to [action]
|
|
290
|
+
|
|
291
|
+
## Requirements
|
|
292
|
+
- [Requirement 1]
|
|
293
|
+
- [Requirement 2]
|
|
294
|
+
- [Requirement 3]
|
|
295
|
+
|
|
296
|
+
## Success Criteria
|
|
297
|
+
- Feature works as described
|
|
298
|
+
- Tests added covering main use cases
|
|
299
|
+
- All verification checks pass (lint, typecheck, build, tests)
|
|
300
|
+
`;
|
|
301
|
+
const exampleDocs = `# Update Documentation
|
|
302
|
+
|
|
303
|
+
## Goal
|
|
304
|
+
Update documentation for [topic/module]
|
|
305
|
+
|
|
306
|
+
## Requirements
|
|
307
|
+
- Document new features/changes
|
|
308
|
+
- Update code examples if needed
|
|
309
|
+
- Fix any outdated information
|
|
310
|
+
|
|
311
|
+
## Success Criteria
|
|
312
|
+
- Documentation is accurate and clear
|
|
313
|
+
- Examples run without errors
|
|
314
|
+
- All verification checks pass
|
|
315
|
+
`;
|
|
316
|
+
fs.writeFileSync(path.join(tasksDir, 'example-bugfix.md'), exampleBugfix);
|
|
317
|
+
fs.writeFileSync(path.join(tasksDir, 'example-feature.md'), exampleFeature);
|
|
318
|
+
fs.writeFileSync(path.join(tasksDir, 'example-docs.md'), exampleDocs);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Initialize Runr configuration for a repository
|
|
322
|
+
*/
|
|
323
|
+
export async function initCommand(options) {
|
|
324
|
+
const repoPath = path.resolve(options.repo);
|
|
325
|
+
const runrDir = path.join(repoPath, '.runr');
|
|
326
|
+
const configPath = path.join(runrDir, 'runr.config.json');
|
|
327
|
+
// Handle --interactive flag
|
|
328
|
+
if (options.interactive) {
|
|
329
|
+
console.log('🚧 Interactive setup is planned for a future release');
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log('For now, use `runr init` without --interactive to generate config automatically,');
|
|
332
|
+
console.log('then edit .runr/runr.config.json to customize verification commands.');
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
// Check if config already exists
|
|
336
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
337
|
+
console.error('Error: .runr/runr.config.json already exists');
|
|
338
|
+
console.error('Use --force to overwrite');
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
// Detect verification commands - try Python first, then package.json, then default
|
|
342
|
+
const detection = detectPythonVerification(repoPath) ||
|
|
343
|
+
detectFromPackageJson(repoPath) ||
|
|
344
|
+
generateDefaultConfig(repoPath);
|
|
345
|
+
// Build config
|
|
346
|
+
const config = buildConfig(repoPath, detection);
|
|
347
|
+
// If --print mode, just output and exit
|
|
348
|
+
if (options.print) {
|
|
349
|
+
console.log(JSON.stringify(config, null, 2));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Create .runr directory
|
|
353
|
+
fs.mkdirSync(runrDir, { recursive: true });
|
|
354
|
+
// Write config
|
|
355
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
356
|
+
// Create example tasks
|
|
357
|
+
createExampleTasks(runrDir);
|
|
358
|
+
// Report results
|
|
359
|
+
console.log('✅ Runr initialized successfully!\n');
|
|
360
|
+
console.log(`Config written to: ${configPath}`);
|
|
361
|
+
console.log(`Example tasks created in: ${path.join(runrDir, 'tasks')}/\n`);
|
|
362
|
+
if (detection.source === 'package.json') {
|
|
363
|
+
console.log('Detected from package.json:');
|
|
364
|
+
if (detection.verification.tier0.length > 0) {
|
|
365
|
+
console.log(` tier0 (fast): ${detection.verification.tier0.join(', ')}`);
|
|
366
|
+
}
|
|
367
|
+
if (detection.verification.tier1.length > 0) {
|
|
368
|
+
console.log(` tier1 (build): ${detection.verification.tier1.join(', ')}`);
|
|
369
|
+
}
|
|
370
|
+
if (detection.verification.tier2.length > 0) {
|
|
371
|
+
console.log(` tier2 (tests): ${detection.verification.tier2.join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
if (detection.presets.length > 0) {
|
|
374
|
+
console.log(` presets: ${detection.presets.join(', ')}`);
|
|
375
|
+
}
|
|
376
|
+
console.log('');
|
|
377
|
+
}
|
|
378
|
+
else if (detection.source === 'python') {
|
|
379
|
+
console.log('Detected Python project:');
|
|
380
|
+
if (detection.verification.tier0.length > 0) {
|
|
381
|
+
console.log(` tier0 (fast): ${detection.verification.tier0.join(', ')}`);
|
|
382
|
+
}
|
|
383
|
+
if (detection.verification.tier1.length > 0) {
|
|
384
|
+
console.log(` tier1 (build): ${detection.verification.tier1.join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
if (detection.verification.tier2.length > 0) {
|
|
387
|
+
console.log(` tier2 (tests): ${detection.verification.tier2.join(', ')}`);
|
|
388
|
+
}
|
|
389
|
+
if (detection.presets.length > 0) {
|
|
390
|
+
console.log(` presets: ${detection.presets.join(', ')}`);
|
|
391
|
+
}
|
|
392
|
+
console.log('');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// No verification detected - provide detailed guidance
|
|
396
|
+
console.log('⚠️ No verification commands detected\n');
|
|
397
|
+
// Explain why based on the reason
|
|
398
|
+
const reason = detection.reason;
|
|
399
|
+
if (reason === 'no-package-json') {
|
|
400
|
+
console.log('No package.json found in this repository.');
|
|
401
|
+
console.log('For JavaScript/TypeScript projects, add a package.json with npm scripts.');
|
|
402
|
+
}
|
|
403
|
+
else if (reason === 'empty-scripts') {
|
|
404
|
+
console.log('Found package.json but it has no scripts defined.');
|
|
405
|
+
console.log('Add verification scripts like "test", "build", "lint", or "typecheck".');
|
|
406
|
+
}
|
|
407
|
+
else if (reason === 'no-matching-scripts') {
|
|
408
|
+
console.log('Found package.json with scripts, but none match common verification patterns.');
|
|
409
|
+
console.log('Expected scripts: test, build, lint, typecheck, tsc, eslint, jest, vitest.');
|
|
410
|
+
}
|
|
411
|
+
else if (reason === 'invalid-package-json') {
|
|
412
|
+
console.log('Found package.json but could not parse it (invalid JSON).');
|
|
413
|
+
}
|
|
414
|
+
console.log('');
|
|
415
|
+
console.log('📝 Next steps:');
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log('Option 1: Manual configuration');
|
|
418
|
+
console.log(` • Edit: ${configPath}`);
|
|
419
|
+
console.log(' • Add verification commands to tier0/tier1/tier2 arrays');
|
|
420
|
+
console.log(' • Example tier0: ["npm run lint", "npm run typecheck"]');
|
|
421
|
+
console.log(' • Example tier1: ["npm run build"]');
|
|
422
|
+
console.log(' • Example tier2: ["npm run test"]');
|
|
423
|
+
console.log('');
|
|
424
|
+
console.log('Option 2: Interactive setup');
|
|
425
|
+
console.log(' • Run: runr init --interactive --force');
|
|
426
|
+
console.log(' • Follow prompts to configure verification');
|
|
427
|
+
console.log('');
|
|
428
|
+
}
|
|
429
|
+
if (detection.source !== 'none') {
|
|
430
|
+
console.log('Next steps:');
|
|
431
|
+
console.log(' 1. Review/edit .runr/runr.config.json');
|
|
432
|
+
console.log(' 2. Create a task file in .runr/tasks/');
|
|
433
|
+
console.log(' 3. Run: runr run --task .runr/tasks/your-task.md --worktree');
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
console.log('After configuring verification:');
|
|
437
|
+
console.log(' 1. Create a task file in .runr/tasks/');
|
|
438
|
+
console.log(' 2. Run: runr run --task .runr/tasks/your-task.md --worktree');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal commands: journal, note, open
|
|
3
|
+
*
|
|
4
|
+
* Generate case files for agent runs with notes and markdown output.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { buildJournal } from '../journal/builder.js';
|
|
10
|
+
import { renderJournal } from '../journal/renderer.js';
|
|
11
|
+
import { getRunrPaths } from '../store/runs-root.js';
|
|
12
|
+
/**
|
|
13
|
+
* Journal command: Generate and optionally display journal.md
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* runr journal [run_id] [--repo <path>] [--output <file>] [--force]
|
|
17
|
+
*/
|
|
18
|
+
export async function journalCommand(options) {
|
|
19
|
+
const repo = options.repo || process.cwd();
|
|
20
|
+
const runId = options.runId || findLatestRunId(repo);
|
|
21
|
+
if (!runId) {
|
|
22
|
+
console.error('ERROR: No runs found. Specify --run-id or create a run first.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const runDir = path.join(getRunrPaths(repo).runs_dir, runId);
|
|
26
|
+
if (!fs.existsSync(runDir)) {
|
|
27
|
+
console.error(`ERROR: Run directory not found: ${runDir}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
// Build journal.json
|
|
32
|
+
const journal = await buildJournal(runId, repo);
|
|
33
|
+
// Render markdown
|
|
34
|
+
const markdown = renderJournal(journal);
|
|
35
|
+
// Determine output path
|
|
36
|
+
const outputPath = options.output || path.join(runDir, 'journal.md');
|
|
37
|
+
// Check if file exists and not forcing
|
|
38
|
+
if (fs.existsSync(outputPath) && !options.force) {
|
|
39
|
+
// Check if journal.json is newer than journal.md
|
|
40
|
+
const journalMtime = getMtime(path.join(runDir, 'journal.json'));
|
|
41
|
+
const markdownMtime = getMtime(outputPath);
|
|
42
|
+
if (journalMtime && markdownMtime && journalMtime <= markdownMtime) {
|
|
43
|
+
console.log(`✓ Journal is up to date: ${outputPath}`);
|
|
44
|
+
console.log(`\n${markdown}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Write markdown file
|
|
49
|
+
fs.writeFileSync(outputPath, markdown, 'utf-8');
|
|
50
|
+
console.log(`✓ Journal generated: ${outputPath}`);
|
|
51
|
+
console.log(`\n${markdown}`);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error('ERROR:', err.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Note command: Append timestamped note to notes.jsonl
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* runr note <message> [--run-id <id>] [--repo <path>]
|
|
63
|
+
*/
|
|
64
|
+
export async function noteCommand(message, options) {
|
|
65
|
+
const repo = options.repo || process.cwd();
|
|
66
|
+
const runId = options.runId || findLatestRunId(repo);
|
|
67
|
+
if (!runId) {
|
|
68
|
+
console.error('ERROR: No runs found. Specify --run-id or create a run first.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const runDir = path.join(getRunrPaths(repo).runs_dir, runId);
|
|
72
|
+
if (!fs.existsSync(runDir)) {
|
|
73
|
+
console.error(`ERROR: Run directory not found: ${runDir}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const notesPath = path.join(runDir, 'notes.jsonl');
|
|
77
|
+
// Append note
|
|
78
|
+
const note = {
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
message
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
fs.appendFileSync(notesPath, JSON.stringify(note) + '\n', 'utf-8');
|
|
84
|
+
console.log(`✓ Note added to run ${runId}`);
|
|
85
|
+
console.log(` "${message}"`);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error('ERROR:', err.message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Open command: Open journal.md in editor
|
|
94
|
+
*
|
|
95
|
+
* Usage:
|
|
96
|
+
* runr open [run_id] [--repo <path>]
|
|
97
|
+
*/
|
|
98
|
+
export async function openCommand(options) {
|
|
99
|
+
const repo = options.repo || process.cwd();
|
|
100
|
+
const runId = options.runId || findLatestRunId(repo);
|
|
101
|
+
if (!runId) {
|
|
102
|
+
console.error('ERROR: No runs found. Specify --run-id or create a run first.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const runDir = path.join(getRunrPaths(repo).runs_dir, runId);
|
|
106
|
+
const journalPath = path.join(runDir, 'journal.md');
|
|
107
|
+
if (!fs.existsSync(journalPath)) {
|
|
108
|
+
console.log(`Journal not found. Generating...`);
|
|
109
|
+
try {
|
|
110
|
+
const journal = await buildJournal(runId, repo);
|
|
111
|
+
const markdown = renderJournal(journal);
|
|
112
|
+
fs.writeFileSync(journalPath, markdown, 'utf-8');
|
|
113
|
+
console.log(`✓ Journal generated: ${journalPath}`);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error('ERROR:', err.message);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Open in editor (with non-interactive safety)
|
|
121
|
+
const editor = process.env.EDITOR;
|
|
122
|
+
// Check if running in non-interactive environment (CI, no TTY)
|
|
123
|
+
const isInteractive = process.stdout.isTTY && process.stdin.isTTY;
|
|
124
|
+
if (!isInteractive || !editor) {
|
|
125
|
+
// Non-interactive or no editor set: print path instead of hanging
|
|
126
|
+
console.log(`Journal: ${journalPath}`);
|
|
127
|
+
if (!editor) {
|
|
128
|
+
console.log('Tip: Set $EDITOR to open journals automatically');
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
execSync(`${editor} "${journalPath}"`, { stdio: 'inherit' });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`ERROR: Failed to open editor: ${err.message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find the latest run ID in the runs directory
|
|
142
|
+
*/
|
|
143
|
+
function findLatestRunId(repo) {
|
|
144
|
+
const runsRoot = getRunrPaths(repo).runs_dir;
|
|
145
|
+
if (!fs.existsSync(runsRoot)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const entries = fs.readdirSync(runsRoot, { withFileTypes: true });
|
|
149
|
+
const runDirs = entries
|
|
150
|
+
.filter((e) => e.isDirectory())
|
|
151
|
+
.map((e) => e.name)
|
|
152
|
+
.filter((name) => /^\d{14}$/.test(name)) // Format: YYYYMMDDHHMMSS
|
|
153
|
+
.sort()
|
|
154
|
+
.reverse();
|
|
155
|
+
return runDirs[0] || null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get file modification time (returns null if file doesn't exist)
|
|
159
|
+
*/
|
|
160
|
+
function getMtime(filePath) {
|
|
161
|
+
try {
|
|
162
|
+
return fs.statSync(filePath).mtimeMs;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Print the suggested next command from stop.json handoff
|
|
6
|
+
*/
|
|
7
|
+
export async function nextCommand(runId, options = {}) {
|
|
8
|
+
const repoPath = options.repo || process.cwd();
|
|
9
|
+
const runsRoot = getRunsRoot(repoPath);
|
|
10
|
+
// Read stop.json directly (no need to resolve - CLI already handles "latest")
|
|
11
|
+
const stopJsonPath = path.join(runsRoot, runId, 'handoffs', 'stop.json');
|
|
12
|
+
if (!fs.existsSync(stopJsonPath)) {
|
|
13
|
+
console.error(`No stop handoff found for run ${runId}`);
|
|
14
|
+
console.error(`Expected: ${stopJsonPath}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const stopData = JSON.parse(fs.readFileSync(stopJsonPath, 'utf-8'));
|
|
18
|
+
if (!stopData.next_actions || stopData.next_actions.length === 0) {
|
|
19
|
+
console.error(`No next actions available for run ${runId}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Print the first suggested command
|
|
23
|
+
const nextAction = stopData.next_actions[0];
|
|
24
|
+
console.log(nextAction.command);
|
|
25
|
+
}
|