@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,176 @@
1
+ import fs from 'node:fs';
2
+ import fsPromises from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { renderTemplate } from './renderer.js';
5
+ /**
6
+ * Ensure .gitignore contains the specified entry.
7
+ * Idempotent: only adds if not already present.
8
+ */
9
+ async function ensureGitignoreEntry(action, context) {
10
+ const gitignorePath = path.join(context.repoPath, action.path);
11
+ const entry = action.line;
12
+ try {
13
+ let content = '';
14
+ try {
15
+ content = await fsPromises.readFile(gitignorePath, 'utf-8');
16
+ }
17
+ catch {
18
+ // File doesn't exist, will be created
19
+ }
20
+ // Check if entry already exists
21
+ const lines = content.split('\n');
22
+ const hasEntry = lines.some(line => line.trim() === entry.trim());
23
+ if (hasEntry) {
24
+ return {
25
+ action,
26
+ executed: false,
27
+ message: `Entry "${entry}" already in ${action.path}`
28
+ };
29
+ }
30
+ if (context.dryRun) {
31
+ return {
32
+ action,
33
+ executed: false,
34
+ message: `[DRY RUN] Would add "${entry}" to ${action.path}`
35
+ };
36
+ }
37
+ // Add entry
38
+ const newContent = content.endsWith('\n') || content === ''
39
+ ? `${content}${entry}\n`
40
+ : `${content}\n${entry}\n`;
41
+ await fsPromises.writeFile(gitignorePath, newContent);
42
+ return {
43
+ action,
44
+ executed: true,
45
+ message: `Added "${entry}" to ${action.path}`
46
+ };
47
+ }
48
+ catch (error) {
49
+ return {
50
+ action,
51
+ executed: false,
52
+ message: `Failed to update ${action.path}`,
53
+ error: error instanceof Error ? error.message : String(error)
54
+ };
55
+ }
56
+ }
57
+ /**
58
+ * Create a file from template if it doesn't already exist.
59
+ * Idempotent: only creates if file is missing.
60
+ */
61
+ async function createFileIfMissing(action, context) {
62
+ const targetPath = path.join(context.repoPath, action.path);
63
+ // Check "when" condition
64
+ if (action.when?.flag) {
65
+ const flagValue = context.flags[action.when.flag];
66
+ if (!flagValue) {
67
+ return {
68
+ action,
69
+ executed: false,
70
+ message: `Skipped ${action.path} (flag "${action.when.flag}" not set)`
71
+ };
72
+ }
73
+ }
74
+ // Check if file already exists
75
+ if (fs.existsSync(targetPath)) {
76
+ return {
77
+ action,
78
+ executed: false,
79
+ message: `File ${action.path} already exists`
80
+ };
81
+ }
82
+ try {
83
+ // Load template
84
+ const templateRelPath = context.templates[action.template];
85
+ if (!templateRelPath) {
86
+ return {
87
+ action,
88
+ executed: false,
89
+ message: `Template "${action.template}" not found in pack`,
90
+ error: 'Template not found in manifest'
91
+ };
92
+ }
93
+ const templatePath = path.join(context.packDir, templateRelPath);
94
+ // Security: Verify template path is within pack directory (prevent directory traversal)
95
+ const resolvedTemplatePath = path.resolve(templatePath);
96
+ const resolvedPackDir = path.resolve(context.packDir);
97
+ if (!resolvedTemplatePath.startsWith(resolvedPackDir + path.sep)) {
98
+ return {
99
+ action,
100
+ executed: false,
101
+ message: `Template path escapes pack directory: ${templateRelPath}`,
102
+ error: 'Invalid template path'
103
+ };
104
+ }
105
+ if (!fs.existsSync(templatePath)) {
106
+ return {
107
+ action,
108
+ executed: false,
109
+ message: `Template file not found: ${templateRelPath}`,
110
+ error: 'Template file missing'
111
+ };
112
+ }
113
+ if (context.dryRun) {
114
+ return {
115
+ action,
116
+ executed: false,
117
+ message: `[DRY RUN] Would create ${action.path} from template ${action.template}`
118
+ };
119
+ }
120
+ // Read and render template
121
+ const templateContent = await fsPromises.readFile(templatePath, 'utf-8');
122
+ const rendered = renderTemplate(templateContent, context.templateContext);
123
+ // Ensure parent directory exists
124
+ const parentDir = path.dirname(targetPath);
125
+ await fsPromises.mkdir(parentDir, { recursive: true });
126
+ // Write file
127
+ await fsPromises.writeFile(targetPath, rendered);
128
+ // Set permissions if specified
129
+ if (action.mode) {
130
+ const mode = parseInt(action.mode, 8);
131
+ await fsPromises.chmod(targetPath, mode);
132
+ }
133
+ return {
134
+ action,
135
+ executed: true,
136
+ message: `Created ${action.path} from template ${action.template}`
137
+ };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ action,
142
+ executed: false,
143
+ message: `Failed to create ${action.path}`,
144
+ error: error instanceof Error ? error.message : String(error)
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Execute a single init action
150
+ */
151
+ export async function executeAction(action, context) {
152
+ switch (action.type) {
153
+ case 'ensure_gitignore_entry':
154
+ return ensureGitignoreEntry(action, context);
155
+ case 'create_file_if_missing':
156
+ return createFileIfMissing(action, context);
157
+ default:
158
+ return {
159
+ action,
160
+ executed: false,
161
+ message: 'Unknown action type',
162
+ error: `Unknown action type: ${action.type}`
163
+ };
164
+ }
165
+ }
166
+ /**
167
+ * Execute all init actions from a pack
168
+ */
169
+ export async function executeActions(actions, context) {
170
+ const results = [];
171
+ for (const action of actions) {
172
+ const result = await executeAction(action, context);
173
+ results.push(result);
174
+ }
175
+ return results;
176
+ }
@@ -0,0 +1,200 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ /**
7
+ * Get the packs directory path (repo root / packs)
8
+ */
9
+ function getPacksDir() {
10
+ // Go up from src/packs/loader.ts to repo root
11
+ return path.resolve(__dirname, '../../packs');
12
+ }
13
+ /**
14
+ * Get the packs directory path (public API for debugging)
15
+ */
16
+ export function getPacksDirectory() {
17
+ return getPacksDir();
18
+ }
19
+ /**
20
+ * Validate a pack manifest
21
+ */
22
+ function validatePackManifest(manifest, packDir) {
23
+ const errors = [];
24
+ // Check required fields
25
+ if (typeof manifest !== 'object' || manifest === null) {
26
+ errors.push('Manifest must be an object');
27
+ return { valid: false, errors };
28
+ }
29
+ if (manifest.pack_version !== 1) {
30
+ errors.push(`pack_version must be 1, got: ${manifest.pack_version}`);
31
+ }
32
+ if (typeof manifest.name !== 'string' || !manifest.name.match(/^[a-z][a-z0-9-]*$/)) {
33
+ errors.push(`name must be lowercase alphanumeric with hyphens, got: ${manifest.name}`);
34
+ }
35
+ if (typeof manifest.display_name !== 'string' || manifest.display_name.length === 0) {
36
+ errors.push('display_name is required and must be a non-empty string');
37
+ }
38
+ if (typeof manifest.description !== 'string' || manifest.description.length === 0) {
39
+ errors.push('description is required and must be a non-empty string');
40
+ }
41
+ // Validate templates (if provided)
42
+ if (manifest.templates) {
43
+ if (typeof manifest.templates !== 'object') {
44
+ errors.push('templates must be an object');
45
+ }
46
+ else {
47
+ for (const [key, templatePath] of Object.entries(manifest.templates)) {
48
+ if (typeof templatePath !== 'string') {
49
+ errors.push(`Template path for "${key}" must be a string`);
50
+ continue;
51
+ }
52
+ const fullPath = path.join(packDir, templatePath);
53
+ if (!fs.existsSync(fullPath)) {
54
+ errors.push(`Template file not found: ${templatePath}`);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ // Validate init_actions (if provided)
60
+ if (manifest.init_actions) {
61
+ if (!Array.isArray(manifest.init_actions)) {
62
+ errors.push('init_actions must be an array');
63
+ }
64
+ else {
65
+ for (let i = 0; i < manifest.init_actions.length; i++) {
66
+ const action = manifest.init_actions[i];
67
+ if (!action.type) {
68
+ errors.push(`Action ${i}: missing type field`);
69
+ continue;
70
+ }
71
+ if (action.type === 'ensure_gitignore_entry') {
72
+ if (!action.path || typeof action.path !== 'string') {
73
+ errors.push(`Action ${i}: ensure_gitignore_entry requires path (string)`);
74
+ }
75
+ if (!action.line || typeof action.line !== 'string') {
76
+ errors.push(`Action ${i}: ensure_gitignore_entry requires line (string)`);
77
+ }
78
+ }
79
+ else if (action.type === 'create_file_if_missing') {
80
+ if (!action.path || typeof action.path !== 'string') {
81
+ errors.push(`Action ${i}: create_file_if_missing requires path (string)`);
82
+ }
83
+ if (!action.template || typeof action.template !== 'string') {
84
+ errors.push(`Action ${i}: create_file_if_missing requires template (string)`);
85
+ }
86
+ if (action.mode && !action.mode.match(/^0[0-7]{3}$/)) {
87
+ errors.push(`Action ${i}: mode must be octal string (e.g., "0644")`);
88
+ }
89
+ }
90
+ else {
91
+ errors.push(`Action ${i}: unknown action type "${action.type}"`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return {
97
+ valid: errors.length === 0,
98
+ errors
99
+ };
100
+ }
101
+ /**
102
+ * Load a single pack from a directory
103
+ */
104
+ function loadPack(packDir) {
105
+ const manifestPath = path.join(packDir, 'pack.json');
106
+ if (!fs.existsSync(manifestPath)) {
107
+ return null;
108
+ }
109
+ const packName = path.basename(packDir);
110
+ try {
111
+ const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
112
+ const manifest = JSON.parse(manifestContent);
113
+ const validation = validatePackManifest(manifest, packDir);
114
+ return {
115
+ name: packName,
116
+ packDir,
117
+ manifest,
118
+ validation
119
+ };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ name: packName,
124
+ packDir,
125
+ manifest: {},
126
+ validation: {
127
+ valid: false,
128
+ errors: [`Failed to parse pack.json: ${error instanceof Error ? error.message : String(error)}`]
129
+ }
130
+ };
131
+ }
132
+ }
133
+ /**
134
+ * Load all available packs
135
+ */
136
+ export function loadAllPacks() {
137
+ const packsDir = getPacksDir();
138
+ if (!fs.existsSync(packsDir)) {
139
+ return [];
140
+ }
141
+ const entries = fs.readdirSync(packsDir, { withFileTypes: true });
142
+ const packs = [];
143
+ for (const entry of entries) {
144
+ // Skip non-directories and special directories
145
+ if (!entry.isDirectory() || entry.name.startsWith('_')) {
146
+ continue;
147
+ }
148
+ const packDir = path.join(packsDir, entry.name);
149
+ const pack = loadPack(packDir);
150
+ if (pack) {
151
+ packs.push(pack);
152
+ }
153
+ }
154
+ return packs;
155
+ }
156
+ /**
157
+ * Sanitize pack name to prevent directory traversal
158
+ */
159
+ function sanitizePackName(name) {
160
+ // Only allow lowercase letters, numbers, and hyphens
161
+ // No dots, slashes, or other special characters
162
+ if (!name.match(/^[a-z][a-z0-9-]*$/)) {
163
+ return null;
164
+ }
165
+ // Additional check: ensure the sanitized name doesn't try to escape
166
+ const normalized = path.normalize(name);
167
+ if (normalized !== name || normalized.includes('..') || normalized.includes('/')) {
168
+ return null;
169
+ }
170
+ return name;
171
+ }
172
+ /**
173
+ * Load a specific pack by name
174
+ */
175
+ export function loadPackByName(name) {
176
+ // Sanitize pack name to prevent directory traversal
177
+ const sanitizedName = sanitizePackName(name);
178
+ if (!sanitizedName) {
179
+ return null;
180
+ }
181
+ const packsDir = getPacksDir();
182
+ const packDir = path.join(packsDir, sanitizedName);
183
+ // Verify the resolved path is actually within packsDir (defense in depth)
184
+ const resolvedPackDir = path.resolve(packDir);
185
+ const resolvedPacksDir = path.resolve(packsDir);
186
+ if (!resolvedPackDir.startsWith(resolvedPacksDir + path.sep)) {
187
+ return null;
188
+ }
189
+ if (!fs.existsSync(packDir)) {
190
+ return null;
191
+ }
192
+ return loadPack(packDir);
193
+ }
194
+ /**
195
+ * Get a list of valid pack names
196
+ */
197
+ export function getValidPackNames() {
198
+ const packs = loadAllPacks();
199
+ return packs.filter(p => p.validation.valid).map(p => p.name);
200
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Simple template renderer for pack templates.
3
+ * Supports basic variable substitution using {{variable}} syntax.
4
+ */
5
+ /**
6
+ * Render a template string with the provided context.
7
+ * Variables are denoted with {{variable_name}} syntax.
8
+ * Missing variables are replaced with empty string.
9
+ */
10
+ export function renderTemplate(template, context) {
11
+ return template.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
12
+ const value = context[varName];
13
+ return value !== undefined ? value : '';
14
+ });
15
+ }
16
+ /**
17
+ * Format verification commands as a bullet list for templates
18
+ */
19
+ export function formatVerificationCommands(verification) {
20
+ const lines = [];
21
+ if (verification.tier0 && verification.tier0.length > 0) {
22
+ lines.push('**Tier 0 (fast checks)**:');
23
+ for (const cmd of verification.tier0) {
24
+ lines.push(`- \`${cmd}\``);
25
+ }
26
+ lines.push('');
27
+ }
28
+ if (verification.tier1 && verification.tier1.length > 0) {
29
+ lines.push('**Tier 1 (build)**:');
30
+ for (const cmd of verification.tier1) {
31
+ lines.push(`- \`${cmd}\``);
32
+ }
33
+ lines.push('');
34
+ }
35
+ if (verification.tier2 && verification.tier2.length > 0) {
36
+ lines.push('**Tier 2 (tests)**:');
37
+ for (const cmd of verification.tier2) {
38
+ lines.push(`- \`${cmd}\``);
39
+ }
40
+ lines.push('');
41
+ }
42
+ if (lines.length === 0) {
43
+ return 'No verification commands configured yet. Edit `.runr/runr.config.json` to add them.';
44
+ }
45
+ return lines.join('\n').trim();
46
+ }