@weldr/runr 0.3.1 → 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 +54 -0
- package/dist/cli.js +41 -0
- package/dist/commands/journal.js +167 -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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-01-03
|
|
11
|
+
|
|
12
|
+
**Case Files** — Every run leaves a machine-readable journal.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Case Files**: Auto-generated `journal.md` + `journal.json` for every run
|
|
17
|
+
- Schema v1.0 with immutable facts (timestamps, milestones, verification attempts)
|
|
18
|
+
- Living data (append-only notes)
|
|
19
|
+
- Secret redaction in error excerpts
|
|
20
|
+
- Warnings array captures all extraction issues
|
|
21
|
+
- **CLI Commands**:
|
|
22
|
+
- `runr journal [run_id]` — Generate and display journal (defaults to latest)
|
|
23
|
+
- `runr note <message> [--run-id]` — Add timestamped note (defaults to latest)
|
|
24
|
+
- `runr open [run_id]` — Open journal in $EDITOR (defaults to latest)
|
|
25
|
+
- **Auto-generation**: Journals written on run completion (stop or finish)
|
|
26
|
+
- **Non-interactive safety**: `runr open` fails cleanly in CI or when $EDITOR unset
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Package bloat**: Excluded test files from npm package (81 → 69 files)
|
|
31
|
+
- **Deprecation warnings**: Replaced deprecated `getRunsRoot()` with `getRunrPaths().runs_dir`
|
|
32
|
+
|
|
10
33
|
## [0.3.0] - 2026-01-01
|
|
11
34
|
|
|
12
35
|
**Renamed to Runr.** New identity, same reliability-first mission.
|
package/README.md
CHANGED
|
@@ -163,6 +163,9 @@ Available: `nextjs`, `react`, `drizzle`, `prisma`, `vitest`, `jest`, `playwright
|
|
|
163
163
|
| `runr status [id]` | Show run state |
|
|
164
164
|
| `runr follow [id]` | Tail run progress |
|
|
165
165
|
| `runr report <id>` | Generate run report (includes next_action) |
|
|
166
|
+
| `runr journal [id]` | Generate and display case file |
|
|
167
|
+
| `runr note <message>` | Add timestamped note to run |
|
|
168
|
+
| `runr open [id]` | Open journal in $EDITOR |
|
|
166
169
|
| `runr gc` | Clean up old runs |
|
|
167
170
|
| `runr doctor` | Check environment |
|
|
168
171
|
|
|
@@ -177,6 +180,57 @@ runr scry <id> # status
|
|
|
177
180
|
runr banish # gc
|
|
178
181
|
```
|
|
179
182
|
|
|
183
|
+
## Case Files
|
|
184
|
+
|
|
185
|
+
Every run automatically generates a **journal.md** case file in `.runr/runs/<run_id>/journal.md` containing:
|
|
186
|
+
|
|
187
|
+
- **Run metadata** (timestamps, duration, stop reason)
|
|
188
|
+
- **Task details** (goal, requirements, success criteria)
|
|
189
|
+
- **Milestone progress** (attempted, verified, checkpoints)
|
|
190
|
+
- **Verification history** (test attempts, pass/fail counts)
|
|
191
|
+
- **Code changes** (files changed, diff stats, top files)
|
|
192
|
+
- **Error excerpts** (last failure with redacted secrets)
|
|
193
|
+
- **Next action** (suggested command to continue)
|
|
194
|
+
- **Notes** (timestamped annotations)
|
|
195
|
+
|
|
196
|
+
### Commands
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Generate and display journal for latest run
|
|
200
|
+
runr journal
|
|
201
|
+
|
|
202
|
+
# Generate journal for specific run
|
|
203
|
+
runr journal <run_id>
|
|
204
|
+
|
|
205
|
+
# Force regeneration even if up to date
|
|
206
|
+
runr journal <run_id> --force
|
|
207
|
+
|
|
208
|
+
# Add a timestamped note to latest run
|
|
209
|
+
runr note "Debugging OAuth token refresh issue"
|
|
210
|
+
|
|
211
|
+
# Add note to specific run
|
|
212
|
+
runr note "Fixed token refresh" --run-id <run_id>
|
|
213
|
+
|
|
214
|
+
# Open journal in $EDITOR (defaults to latest run)
|
|
215
|
+
runr open
|
|
216
|
+
runr open <run_id>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Note**: If `<run_id>` is omitted, all commands default to the most recent run in the repository.
|
|
220
|
+
|
|
221
|
+
### Auto-Generation
|
|
222
|
+
|
|
223
|
+
Journals are automatically generated when runs complete (stop or finish). You can also:
|
|
224
|
+
- Manually regenerate with `runr journal <run_id> --force`
|
|
225
|
+
- Add timestamped notes during or after runs with `runr note` (stored in `.runr/runs/<run_id>/notes.jsonl`)
|
|
226
|
+
- Open in your editor with `runr open` (uses `$EDITOR` or `vim`)
|
|
227
|
+
|
|
228
|
+
**Use case**: Share run context with collaborators, document debugging sessions, track experiment results.
|
|
229
|
+
|
|
230
|
+
**Files generated:**
|
|
231
|
+
- `journal.md` - Human-readable case file
|
|
232
|
+
- `notes.jsonl` - Timestamped notes (one JSON object per line)
|
|
233
|
+
|
|
180
234
|
## Task Files
|
|
181
235
|
|
|
182
236
|
Tasks are markdown files:
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { metricsCommand } from './commands/metrics.js';
|
|
|
18
18
|
import { versionCommand } from './commands/version.js';
|
|
19
19
|
import { initCommand } from './commands/init.js';
|
|
20
20
|
import { watchCommand } from './commands/watch.js';
|
|
21
|
+
import { journalCommand, noteCommand, openCommand } from './commands/journal.js';
|
|
21
22
|
const program = new Command();
|
|
22
23
|
// Check if invoked as deprecated 'agent' command
|
|
23
24
|
const invokedAs = process.argv[1]?.split('/').pop() || 'runr';
|
|
@@ -517,4 +518,44 @@ program
|
|
|
517
518
|
olderThan: Number.parseInt(options.olderThan, 10)
|
|
518
519
|
});
|
|
519
520
|
});
|
|
521
|
+
// journal - Generate case file from run
|
|
522
|
+
program
|
|
523
|
+
.command('journal')
|
|
524
|
+
.description('Generate and display journal.md for a run')
|
|
525
|
+
.argument('[runId]', 'Run ID (defaults to latest)')
|
|
526
|
+
.option('--repo <path>', 'Target repo path', '.')
|
|
527
|
+
.option('--output <file>', 'Output file path (defaults to runs/<id>/journal.md)')
|
|
528
|
+
.option('--force', 'Force regeneration even if up to date', false)
|
|
529
|
+
.action(async (runId, options) => {
|
|
530
|
+
await journalCommand({
|
|
531
|
+
repo: options.repo,
|
|
532
|
+
runId,
|
|
533
|
+
output: options.output,
|
|
534
|
+
force: options.force
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
// note - Add timestamped note to run
|
|
538
|
+
program
|
|
539
|
+
.command('note <message>')
|
|
540
|
+
.description('Add a timestamped note to a run')
|
|
541
|
+
.option('--repo <path>', 'Target repo path', '.')
|
|
542
|
+
.option('--run-id <id>', 'Run ID (defaults to latest)')
|
|
543
|
+
.action(async (message, options) => {
|
|
544
|
+
await noteCommand(message, {
|
|
545
|
+
repo: options.repo,
|
|
546
|
+
runId: options.runId
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
// open - Open journal.md in editor
|
|
550
|
+
program
|
|
551
|
+
.command('open')
|
|
552
|
+
.description('Open journal.md in $EDITOR')
|
|
553
|
+
.argument('[runId]', 'Run ID (defaults to latest)')
|
|
554
|
+
.option('--repo <path>', 'Target repo path', '.')
|
|
555
|
+
.action(async (runId, options) => {
|
|
556
|
+
await openCommand({
|
|
557
|
+
repo: options.repo,
|
|
558
|
+
runId
|
|
559
|
+
});
|
|
560
|
+
});
|
|
520
561
|
program.parseAsync();
|
|
@@ -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
|
+
}
|