@weldr/runr 0.4.0 → 0.7.2

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 (66) hide show
  1. package/CHANGELOG.md +127 -1
  2. package/README.md +124 -165
  3. package/dist/audit/classifier.js +331 -0
  4. package/dist/cli.js +570 -300
  5. package/dist/commands/audit.js +259 -0
  6. package/dist/commands/bundle.js +180 -0
  7. package/dist/commands/continue.js +276 -0
  8. package/dist/commands/doctor.js +430 -45
  9. package/dist/commands/hooks.js +352 -0
  10. package/dist/commands/init.js +368 -8
  11. package/dist/commands/intervene.js +109 -0
  12. package/dist/commands/meta.js +245 -0
  13. package/dist/commands/mode.js +157 -0
  14. package/dist/commands/orchestrate.js +29 -0
  15. package/dist/commands/packs.js +47 -0
  16. package/dist/commands/preflight.js +8 -5
  17. package/dist/commands/resume.js +421 -3
  18. package/dist/commands/run.js +63 -4
  19. package/dist/commands/status.js +47 -0
  20. package/dist/commands/submit.js +374 -0
  21. package/dist/config/schema.js +61 -1
  22. package/dist/diagnosis/analyzer.js +86 -1
  23. package/dist/diagnosis/formatter.js +3 -0
  24. package/dist/diagnosis/index.js +1 -0
  25. package/dist/diagnosis/stop-explainer.js +267 -0
  26. package/dist/diagnostics/stop-explainer.js +267 -0
  27. package/dist/guards/checkpoint.js +119 -0
  28. package/dist/journal/builder.js +36 -3
  29. package/dist/journal/renderer.js +19 -0
  30. package/dist/orchestrator/artifacts.js +17 -2
  31. package/dist/orchestrator/receipt.js +304 -0
  32. package/dist/output/stop-footer.js +185 -0
  33. package/dist/packs/actions.js +176 -0
  34. package/dist/packs/loader.js +200 -0
  35. package/dist/packs/renderer.js +46 -0
  36. package/dist/receipt/intervention.js +465 -0
  37. package/dist/receipt/writer.js +296 -0
  38. package/dist/redaction/redactor.js +95 -0
  39. package/dist/repo/context.js +147 -20
  40. package/dist/review/check-parser.js +211 -0
  41. package/dist/store/checkpoint-metadata.js +111 -0
  42. package/dist/store/run-store.js +21 -0
  43. package/dist/supervisor/runner.js +130 -10
  44. package/dist/tasks/task-metadata.js +74 -1
  45. package/dist/ux/brain.js +528 -0
  46. package/dist/ux/render.js +123 -0
  47. package/dist/ux/safe-commands.js +133 -0
  48. package/dist/ux/state.js +193 -0
  49. package/dist/ux/telemetry.js +110 -0
  50. package/package.json +3 -1
  51. package/packs/pr/pack.json +50 -0
  52. package/packs/pr/templates/AGENTS.md.tmpl +120 -0
  53. package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
  54. package/packs/pr/templates/bundle.md.tmpl +27 -0
  55. package/packs/solo/pack.json +82 -0
  56. package/packs/solo/templates/AGENTS.md.tmpl +80 -0
  57. package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
  58. package/packs/solo/templates/bundle.md.tmpl +27 -0
  59. package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
  60. package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
  61. package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
  62. package/packs/solo/templates/claude-skill.md.tmpl +96 -0
  63. package/packs/trunk/pack.json +50 -0
  64. package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
  65. package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
  66. package/packs/trunk/templates/bundle.md.tmpl +27 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Safe command parsing and validation for auto-fix execution.
3
+ *
4
+ * This module ensures that only "boringly safe" commands are auto-executed
5
+ * by the continue command. It rejects shell pipelines, redirects, and
6
+ * anything that could be an injection surface.
7
+ */
8
+ /**
9
+ * Dangerous patterns that indicate shell injection or unsafe behavior.
10
+ * If any of these are present, the command is rejected.
11
+ */
12
+ const DANGEROUS_PATTERNS = [
13
+ '|', // pipe
14
+ '>', // redirect stdout
15
+ '<', // redirect stdin
16
+ '&&', // chain commands
17
+ ';', // command separator
18
+ '$(', // command substitution
19
+ '`', // backtick command substitution
20
+ '"', // double quotes (could hide injection)
21
+ "'", // single quotes (could hide injection)
22
+ '\n', // newlines (multiple commands)
23
+ '\\', // escape sequences
24
+ ];
25
+ const SAFE_PATTERNS = [
26
+ // Node package managers - test/typecheck/lint only
27
+ { binary: 'npm', allowedSubcommands: ['test', 'run'], allowedScripts: ['test', 'typecheck', 'lint', 'type-check', 'types'] },
28
+ { binary: 'pnpm', allowedSubcommands: ['test', 'run'], allowedScripts: ['test', 'typecheck', 'lint', 'type-check', 'types'] },
29
+ { binary: 'yarn', allowedSubcommands: ['test', 'run'], allowedScripts: ['test', 'typecheck', 'lint', 'type-check', 'types'] },
30
+ // Direct TypeScript/linting tools
31
+ { binary: 'tsc' },
32
+ { binary: 'eslint' },
33
+ { binary: 'prettier', allowedSubcommands: ['--check'] },
34
+ // Python tools
35
+ { binary: 'pytest' },
36
+ { binary: 'python', allowedSubcommands: ['-m'], allowedScripts: ['pytest', 'mypy', 'ruff'] },
37
+ { binary: 'ruff' },
38
+ { binary: 'mypy' },
39
+ // Go tools
40
+ { binary: 'go', allowedSubcommands: ['test'] },
41
+ // Rust tools
42
+ { binary: 'cargo', allowedSubcommands: ['test', 'check', 'clippy'] },
43
+ ];
44
+ /**
45
+ * Parse a raw command string into a canonical form.
46
+ * Returns null if the command contains dangerous patterns or is unparseable.
47
+ */
48
+ export function canonicalizeCommand(raw) {
49
+ // Trim whitespace
50
+ const trimmed = raw.trim();
51
+ if (!trimmed) {
52
+ return null;
53
+ }
54
+ // Check for dangerous patterns
55
+ for (const pattern of DANGEROUS_PATTERNS) {
56
+ if (trimmed.includes(pattern)) {
57
+ return null;
58
+ }
59
+ }
60
+ // Split on whitespace (simple tokenization)
61
+ const parts = trimmed.split(/\s+/);
62
+ if (parts.length === 0 || !parts[0]) {
63
+ return null;
64
+ }
65
+ const binary = parts[0];
66
+ const args = parts.slice(1);
67
+ return {
68
+ binary,
69
+ args,
70
+ raw: trimmed,
71
+ };
72
+ }
73
+ /**
74
+ * Check if a canonical command is allowed for auto-fix execution.
75
+ */
76
+ export function isAutoFixCommandAllowed(cmd) {
77
+ const pattern = SAFE_PATTERNS.find(p => p.binary === cmd.binary);
78
+ if (!pattern) {
79
+ return false;
80
+ }
81
+ // If no subcommand restrictions, binary alone is safe
82
+ if (!pattern.allowedSubcommands) {
83
+ return true;
84
+ }
85
+ // Check first arg against allowed subcommands
86
+ const firstArg = cmd.args[0];
87
+ if (!firstArg || !pattern.allowedSubcommands.includes(firstArg)) {
88
+ return false;
89
+ }
90
+ // Special handling for 'run' subcommand - check script name
91
+ if (firstArg === 'run' && pattern.allowedScripts) {
92
+ const scriptName = cmd.args[1];
93
+ if (!scriptName || !pattern.allowedScripts.includes(scriptName)) {
94
+ return false;
95
+ }
96
+ }
97
+ // Special handling for python -m
98
+ if (cmd.binary === 'python' && firstArg === '-m' && pattern.allowedScripts) {
99
+ const moduleName = cmd.args[1];
100
+ if (!moduleName || !pattern.allowedScripts.includes(moduleName)) {
101
+ return false;
102
+ }
103
+ }
104
+ return true;
105
+ }
106
+ /**
107
+ * Parse a raw command and check if it's allowed in one step.
108
+ * Returns the canonical command if allowed, null otherwise.
109
+ */
110
+ export function parseAndValidateCommand(raw) {
111
+ const cmd = canonicalizeCommand(raw);
112
+ if (!cmd) {
113
+ return null;
114
+ }
115
+ if (!isAutoFixCommandAllowed(cmd)) {
116
+ return null;
117
+ }
118
+ return cmd;
119
+ }
120
+ /**
121
+ * Extract safe commands from a list of suggested commands.
122
+ * Filters out any commands that are not in the safe allowlist.
123
+ */
124
+ export function filterSafeCommands(commands) {
125
+ const result = [];
126
+ for (const raw of commands) {
127
+ const cmd = parseAndValidateCommand(raw);
128
+ if (cmd) {
129
+ result.push(cmd);
130
+ }
131
+ }
132
+ return result;
133
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Repo state resolution for the UX layer.
3
+ *
4
+ * This module gathers signals from the filesystem to determine the current
5
+ * state of the repository: what's running, what's stopped, what orchestration
6
+ * is in progress, etc.
7
+ *
8
+ * All functions here do I/O. The brain module is pure and consumes this data.
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { getRunsRoot } from '../store/runs-root.js';
13
+ import { findLatestRunId, listRecentRunIds } from '../store/run-utils.js';
14
+ import { getCurrentMode } from '../commands/mode.js';
15
+ import { findLatestOrchestrationId, loadOrchestratorState } from '../orchestrator/state-machine.js';
16
+ import { git } from '../repo/git.js';
17
+ /**
18
+ * Read minimal run info from state.json.
19
+ * Returns null if file doesn't exist or is unparseable.
20
+ */
21
+ function readRunInfo(runDir, runId) {
22
+ const statePath = path.join(runDir, 'state.json');
23
+ if (!fs.existsSync(statePath)) {
24
+ return null;
25
+ }
26
+ try {
27
+ const content = fs.readFileSync(statePath, 'utf-8');
28
+ const state = JSON.parse(content);
29
+ return {
30
+ runId,
31
+ phase: state.phase ?? 'unknown',
32
+ stopReason: state.stop_reason ?? null,
33
+ taskPath: null, // Could read from config.snapshot.json if needed
34
+ startedAt: state.started_at ?? null,
35
+ updatedAt: state.updated_at ?? null,
36
+ };
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Read task path from config snapshot.
44
+ */
45
+ function readTaskPath(runDir) {
46
+ const snapshotPath = path.join(runDir, 'config.snapshot.json');
47
+ if (!fs.existsSync(snapshotPath)) {
48
+ return null;
49
+ }
50
+ try {
51
+ const content = fs.readFileSync(snapshotPath, 'utf-8');
52
+ const snapshot = JSON.parse(content);
53
+ return snapshot.task_path ?? snapshot.taskPath ?? null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Find the most recent stopped run.
61
+ * Scans runs newest→oldest, returns first with phase=STOPPED.
62
+ */
63
+ export function findLatestStoppedRun(repoPath) {
64
+ const runsRoot = getRunsRoot(repoPath);
65
+ const runIds = listRecentRunIds(repoPath, 20); // Check last 20 runs
66
+ for (const runId of runIds) {
67
+ const runDir = path.join(runsRoot, runId);
68
+ const info = readRunInfo(runDir, runId);
69
+ if (info && info.phase === 'STOPPED' && info.stopReason) {
70
+ // Check for stop.json
71
+ const stopJsonPath = path.join(runDir, 'handoffs', 'stop.json');
72
+ const diagnosticsPath = path.join(runDir, 'stop_diagnostics.json');
73
+ return {
74
+ ...info,
75
+ stopReason: info.stopReason,
76
+ taskPath: readTaskPath(runDir),
77
+ stopJsonPath: fs.existsSync(stopJsonPath) ? stopJsonPath : null,
78
+ diagnosticsPath: fs.existsSync(diagnosticsPath) ? diagnosticsPath : null,
79
+ };
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Find any currently running run.
86
+ * A run is "running" if phase is not STOPPED.
87
+ */
88
+ export function findActiveRun(repoPath) {
89
+ const runsRoot = getRunsRoot(repoPath);
90
+ const runIds = listRecentRunIds(repoPath, 10); // Check last 10 runs
91
+ for (const runId of runIds) {
92
+ const runDir = path.join(runsRoot, runId);
93
+ const info = readRunInfo(runDir, runId);
94
+ if (info && info.phase !== 'STOPPED') {
95
+ return {
96
+ ...info,
97
+ taskPath: readTaskPath(runDir),
98
+ };
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Get orchestration cursor if one exists and is not complete.
105
+ */
106
+ export function getOrchestrationCursor(repoPath) {
107
+ const orchId = findLatestOrchestrationId(repoPath);
108
+ if (!orchId) {
109
+ return null;
110
+ }
111
+ const state = loadOrchestratorState(orchId, repoPath);
112
+ if (!state) {
113
+ return null;
114
+ }
115
+ // Only return cursor if orchestration is still running or has stopped tasks
116
+ if (state.status === 'complete') {
117
+ return null;
118
+ }
119
+ const complete = state.tracks.filter(t => t.status === 'complete').length;
120
+ const stopped = state.tracks.filter(t => t.status === 'stopped' || t.status === 'failed').length;
121
+ return {
122
+ orchestratorId: orchId,
123
+ status: state.status,
124
+ tracksTotal: state.tracks.length,
125
+ tracksComplete: complete,
126
+ tracksStopped: stopped,
127
+ configPath: null, // Could be read from state if stored
128
+ };
129
+ }
130
+ /**
131
+ * Check if working tree is clean.
132
+ */
133
+ export async function getTreeStatus(repoPath) {
134
+ try {
135
+ const result = await git(['status', '--porcelain'], repoPath);
136
+ const lines = result.stdout.trim().split('\n').filter(l => l.trim());
137
+ return lines.length === 0 ? 'clean' : 'dirty';
138
+ }
139
+ catch {
140
+ // If git fails, assume clean (conservative)
141
+ return 'clean';
142
+ }
143
+ }
144
+ /**
145
+ * Resolve complete repo state.
146
+ * This is the main entry point for the UX layer.
147
+ */
148
+ export async function resolveRepoState(repoPath = process.cwd()) {
149
+ // Find active run first (takes priority)
150
+ const activeRun = findActiveRun(repoPath);
151
+ // Find latest run (any state)
152
+ const latestRunId = findLatestRunId(repoPath);
153
+ let latestRun = null;
154
+ if (latestRunId) {
155
+ const runDir = path.join(getRunsRoot(repoPath), latestRunId);
156
+ latestRun = readRunInfo(runDir, latestRunId);
157
+ if (latestRun) {
158
+ latestRun.taskPath = readTaskPath(runDir);
159
+ }
160
+ }
161
+ // Find latest stopped run (for continue)
162
+ const latestStopped = findLatestStoppedRun(repoPath);
163
+ // Get orchestration cursor
164
+ const orchestration = getOrchestrationCursor(repoPath);
165
+ // Get tree status
166
+ const treeStatus = await getTreeStatus(repoPath);
167
+ // Get workflow mode
168
+ const mode = getCurrentMode(repoPath);
169
+ return {
170
+ activeRun,
171
+ latestRun,
172
+ latestStopped,
173
+ orchestration,
174
+ treeStatus,
175
+ mode,
176
+ repoPath,
177
+ };
178
+ }
179
+ /**
180
+ * Derive display status from repo state.
181
+ */
182
+ export function deriveDisplayStatus(state) {
183
+ if (state.activeRun) {
184
+ return 'running';
185
+ }
186
+ if (state.latestStopped) {
187
+ return 'stopped';
188
+ }
189
+ if (state.orchestration && state.orchestration.status !== 'complete') {
190
+ return 'orch_ready';
191
+ }
192
+ return 'clean';
193
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * UX Telemetry - Breadcrumb tracking for debugging UX flows.
3
+ *
4
+ * Writes events to:
5
+ * - Run's timeline.jsonl if a run is in-scope
6
+ * - .runr/ux-breadcrumbs.jsonl otherwise
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { getRunsRoot } from '../store/runs-root.js';
11
+ /**
12
+ * Write a UX breadcrumb event.
13
+ *
14
+ * If runId is provided, writes to the run's timeline.
15
+ * Otherwise writes to a general breadcrumb file.
16
+ */
17
+ export function writeBreadcrumb(repoPath, event) {
18
+ const fullEvent = {
19
+ ...event,
20
+ timestamp: new Date().toISOString(),
21
+ };
22
+ try {
23
+ if (event.runId) {
24
+ // Write to run's timeline
25
+ const runsRoot = getRunsRoot(repoPath);
26
+ const timelinePath = path.join(runsRoot, event.runId, 'timeline.jsonl');
27
+ if (fs.existsSync(path.dirname(timelinePath))) {
28
+ fs.appendFileSync(timelinePath, JSON.stringify(fullEvent) + '\n');
29
+ return;
30
+ }
31
+ }
32
+ // Fallback: write to general breadcrumb file
33
+ const breadcrumbPath = path.join(getRunsRoot(repoPath), 'ux-breadcrumbs.jsonl');
34
+ fs.appendFileSync(breadcrumbPath, JSON.stringify(fullEvent) + '\n');
35
+ }
36
+ catch {
37
+ // Silent fail - telemetry should never break the workflow
38
+ }
39
+ }
40
+ /**
41
+ * Record front door display.
42
+ */
43
+ export function recordFrontDoor(repoPath, runId) {
44
+ writeBreadcrumb(repoPath, {
45
+ type: 'front_door_shown',
46
+ runId,
47
+ });
48
+ }
49
+ /**
50
+ * Record continue command attempt.
51
+ * Includes analysis fields for debugging "why did continue not run?" scenarios.
52
+ */
53
+ export function recordContinueAttempt(repoPath, runId, strategy, analysis) {
54
+ writeBreadcrumb(repoPath, {
55
+ type: 'continue_attempted',
56
+ runId,
57
+ payload: {
58
+ strategyType: strategy.type,
59
+ ...(strategy.type === 'auto_fix' && { commandCount: strategy.commands.length }),
60
+ ...(strategy.type === 'manual' && { blockedReason: strategy.blockedReason }),
61
+ ...(strategy.type === 'continue_orch' && { orchestratorId: strategy.orchestratorId }),
62
+ // Include analysis fields for debugging
63
+ ...(analysis && {
64
+ autoFixAvailable: analysis.autoFixAvailable,
65
+ autoFixPermitted: analysis.autoFixPermitted,
66
+ treeDirty: analysis.treeDirty,
67
+ mode: analysis.mode,
68
+ blockReason: analysis.blockReason,
69
+ }),
70
+ },
71
+ });
72
+ }
73
+ /**
74
+ * Record auto-fix step execution.
75
+ */
76
+ export function recordAutoFixStep(repoPath, runId, stepIndex, command, exitCode) {
77
+ writeBreadcrumb(repoPath, {
78
+ type: 'continue_auto_fix_step',
79
+ runId,
80
+ payload: {
81
+ stepIndex,
82
+ command,
83
+ exitCode,
84
+ success: exitCode === 0,
85
+ },
86
+ });
87
+ }
88
+ /**
89
+ * Record continue command failure.
90
+ */
91
+ export function recordContinueFailed(repoPath, runId, reason, stepIndex) {
92
+ writeBreadcrumb(repoPath, {
93
+ type: 'continue_failed',
94
+ runId,
95
+ payload: {
96
+ reason,
97
+ ...(stepIndex !== undefined && { failedAtStep: stepIndex }),
98
+ },
99
+ });
100
+ }
101
+ /**
102
+ * Record continue command success.
103
+ */
104
+ export function recordContinueSuccess(repoPath, runId, strategyType) {
105
+ writeBreadcrumb(repoPath, {
106
+ type: 'continue_success',
107
+ runId,
108
+ payload: { strategyType },
109
+ });
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weldr/runr",
3
- "version": "0.4.0",
3
+ "version": "0.7.2",
4
4
  "description": "Phase-gated orchestration for agent tasks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,8 @@
11
11
  "dist/",
12
12
  "!dist/**/__tests__/",
13
13
  "!dist/**/*.test.js",
14
+ "packs/",
15
+ "!packs/_schema/",
14
16
  "templates/prompts/",
15
17
  "README.md",
16
18
  "LICENSE",
@@ -0,0 +1,50 @@
1
+ {
2
+ "pack_version": 1,
3
+ "name": "pr",
4
+ "display_name": "Pull Request Workflow (feature → main via PR)",
5
+ "description": "Feature branch workflow with verified checkpoints, reviewable proof packets, and optional PR integration.",
6
+ "defaults": {
7
+ "profile": "pr",
8
+ "integration_branch": "main",
9
+ "release_branch": "main",
10
+ "submit_strategy": "cherry-pick",
11
+ "require_clean_tree": true,
12
+ "require_verification": true,
13
+ "protected_branches": ["main"]
14
+ },
15
+ "templates": {
16
+ "AGENTS.md": "templates/AGENTS.md.tmpl",
17
+ "CLAUDE.md": "templates/CLAUDE.md.tmpl",
18
+ "bundle.md": "templates/bundle.md.tmpl"
19
+ },
20
+ "init_actions": [
21
+ {
22
+ "type": "ensure_gitignore_entry",
23
+ "path": ".gitignore",
24
+ "line": ".runr/runs/"
25
+ },
26
+ {
27
+ "type": "ensure_gitignore_entry",
28
+ "path": ".gitignore",
29
+ "line": ".runr-worktrees/"
30
+ },
31
+ {
32
+ "type": "ensure_gitignore_entry",
33
+ "path": ".gitignore",
34
+ "line": ".runr/orchestrations/"
35
+ },
36
+ {
37
+ "type": "create_file_if_missing",
38
+ "path": "AGENTS.md",
39
+ "template": "AGENTS.md",
40
+ "mode": "0644"
41
+ },
42
+ {
43
+ "type": "create_file_if_missing",
44
+ "path": "CLAUDE.md",
45
+ "template": "CLAUDE.md",
46
+ "mode": "0644",
47
+ "when": { "flag": "with_claude" }
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,120 @@
1
+ # Agent Guidelines
2
+
3
+ This repo uses **Runr** for reliable agent-driven development with PR workflow.
4
+
5
+ ## Workflow: Feature Branches + Pull Requests
6
+
7
+ ### How it works
8
+
9
+ 1. **Create feature branch** before starting work
10
+ ```bash
11
+ git checkout -b feature/your-feature-name
12
+ ```
13
+
14
+ 2. **Run verified task** with Runr
15
+ ```bash
16
+ runr run --task task.md
17
+ ```
18
+
19
+ 3. **Runr creates checkpoints** as it progresses
20
+ - Each checkpoint is a git commit with full audit trail
21
+ - Only verified checkpoints are marked as "good"
22
+
23
+ 4. **Generate proof packet** when verified
24
+ ```bash
25
+ runr bundle <run_id>
26
+ ```
27
+ This creates a reviewable evidence packet showing:
28
+ - What changed (diff)
29
+ - Why it changed (task + plan)
30
+ - How it was verified (test results, checks)
31
+
32
+ 5. **Create PR** with proof packet
33
+ ```bash
34
+ # Push your feature branch
35
+ git push -u origin feature/your-feature-name
36
+
37
+ # Create PR (manual or via gh cli)
38
+ gh pr create --title "Feature: X" --body-file .runr/runs/<run_id>/bundle.md
39
+ ```
40
+
41
+ 6. **Review proof packet** instead of reading all code
42
+ - Reviewer sees: task → plan → implementation → verification
43
+ - Focus on "did it solve the right problem correctly?"
44
+
45
+ 7. **Merge when approved**
46
+ - PR merges feature branch to main
47
+ - Clean, verified commits only
48
+
49
+ ### Why this workflow?
50
+
51
+ **Social artifacts:** PRs provide:
52
+ - Review ritual (even for solo developers)
53
+ - Discussion thread attached to change
54
+ - Historical context ("why did we do this?")
55
+
56
+ **Proof packets:** Bundle provides:
57
+ - Complete audit trail in one document
58
+ - Verification evidence upfront
59
+ - Faster review (don't need to reconstruct context)
60
+
61
+ **Branch isolation:** Feature branches keep:
62
+ - Experimental work separate from main
63
+ - Multiple tasks in parallel
64
+ - Easy to abandon failed experiments
65
+
66
+ ## Key Commands
67
+
68
+ ```bash
69
+ # Start new task on feature branch
70
+ git checkout -b feature/add-x
71
+ runr run --task task.md
72
+
73
+ # Generate reviewable proof packet
74
+ runr bundle <run_id>
75
+
76
+ # Push + create PR with proof packet
77
+ git push -u origin feature/add-x
78
+ gh pr create --title "Add X" --body-file .runr/runs/<run_id>/bundle.md
79
+
80
+ # Resume if interrupted
81
+ runr run --resume
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ See `runr.config.json`:
87
+ - `integration_branch`: `main` (where PRs merge to)
88
+ - `release_branch`: `main` (same as integration for PR workflow)
89
+ - `require_verification`: `true` (only verified checkpoints are "good")
90
+ - `protected_branches`: `["main"]` (prevent accidental direct pushes)
91
+
92
+ ## Directory Structure
93
+
94
+ ```
95
+ .runr/
96
+ ├── runs/
97
+ │ └── <run_id>/
98
+ │ ├── bundle.md # Proof packet for PR body
99
+ │ ├── final_patch.diff # Complete change
100
+ │ └── timeline.jsonl # Full audit trail
101
+ └── checkpoints/
102
+ └── <sha>.json # Checkpoint metadata
103
+ ```
104
+
105
+ ## Tips
106
+
107
+ 1. **One feature per branch** - Keep PRs focused and reviewable
108
+ 2. **Use bundle as PR description** - Reviewers get full context upfront
109
+ 3. **Verify before pushing** - Only push verified checkpoints
110
+ 4. **Small, frequent PRs** - Easier to review than giant changes
111
+ 5. **Archive proof packets** - `.runr/runs/` is your audit trail
112
+
113
+ ## Future: PR Integration (v2)
114
+
115
+ Later versions may add:
116
+ - `runr submit --pr` to auto-create PR with proof packet
117
+ - PR status updates when verification completes
118
+ - Auto-merge when verified + approved
119
+
120
+ For now: manual PR creation with bundle.md as description works great.