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