@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.
- package/CHANGELOG.md +127 -1
- package/README.md +124 -165
- package/dist/audit/classifier.js +331 -0
- package/dist/cli.js +570 -300
- package/dist/commands/audit.js +259 -0
- package/dist/commands/bundle.js +180 -0
- package/dist/commands/continue.js +276 -0
- package/dist/commands/doctor.js +430 -45
- package/dist/commands/hooks.js +352 -0
- package/dist/commands/init.js +368 -8
- package/dist/commands/intervene.js +109 -0
- package/dist/commands/meta.js +245 -0
- package/dist/commands/mode.js +157 -0
- package/dist/commands/orchestrate.js +29 -0
- package/dist/commands/packs.js +47 -0
- package/dist/commands/preflight.js +8 -5
- package/dist/commands/resume.js +421 -3
- package/dist/commands/run.js +63 -4
- package/dist/commands/status.js +47 -0
- package/dist/commands/submit.js +374 -0
- package/dist/config/schema.js +61 -1
- package/dist/diagnosis/analyzer.js +86 -1
- package/dist/diagnosis/formatter.js +3 -0
- package/dist/diagnosis/index.js +1 -0
- package/dist/diagnosis/stop-explainer.js +267 -0
- package/dist/diagnostics/stop-explainer.js +267 -0
- package/dist/guards/checkpoint.js +119 -0
- package/dist/journal/builder.js +36 -3
- package/dist/journal/renderer.js +19 -0
- package/dist/orchestrator/artifacts.js +17 -2
- package/dist/orchestrator/receipt.js +304 -0
- package/dist/output/stop-footer.js +185 -0
- package/dist/packs/actions.js +176 -0
- package/dist/packs/loader.js +200 -0
- package/dist/packs/renderer.js +46 -0
- package/dist/receipt/intervention.js +465 -0
- package/dist/receipt/writer.js +296 -0
- package/dist/redaction/redactor.js +95 -0
- package/dist/repo/context.js +147 -20
- package/dist/review/check-parser.js +211 -0
- package/dist/store/checkpoint-metadata.js +111 -0
- package/dist/store/run-store.js +21 -0
- package/dist/supervisor/runner.js +130 -10
- package/dist/tasks/task-metadata.js +74 -1
- package/dist/ux/brain.js +528 -0
- package/dist/ux/render.js +123 -0
- package/dist/ux/safe-commands.js +133 -0
- package/dist/ux/state.js +193 -0
- package/dist/ux/telemetry.js +110 -0
- package/package.json +3 -1
- package/packs/pr/pack.json +50 -0
- package/packs/pr/templates/AGENTS.md.tmpl +120 -0
- package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
- package/packs/pr/templates/bundle.md.tmpl +27 -0
- package/packs/solo/pack.json +82 -0
- package/packs/solo/templates/AGENTS.md.tmpl +80 -0
- package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
- package/packs/solo/templates/bundle.md.tmpl +27 -0
- package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
- package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
- package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
- package/packs/solo/templates/claude-skill.md.tmpl +96 -0
- package/packs/trunk/pack.json +50 -0
- package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
- package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
- 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
|
+
}
|
package/dist/ux/state.js
ADDED
|
@@ -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.
|
|
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.
|