@entelligentsia/forgecli 0.8.4 → 0.9.1
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 +61 -0
- package/README.md +165 -2
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +17 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/config.d.ts +69 -0
- package/dist/bin/config.js +315 -0
- package/dist/bin/config.js.map +1 -0
- package/dist/bin/doctor.d.ts +1 -0
- package/dist/bin/doctor.js +12 -0
- package/dist/bin/doctor.js.map +1 -1
- package/dist/bin/forge.js +7 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/config-command.js +66 -0
- package/dist/extensions/forgecli/config-command.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +38 -0
- package/dist/extensions/forgecli/config-layer.js +68 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
- package/dist/extensions/forgecli/config-tui/component.js +236 -0
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
- package/dist/extensions/forgecli/config-tui/handler.js +240 -0
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
- package/dist/extensions/forgecli/config-tui/index.js +5 -0
- package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
- package/dist/extensions/forgecli/config-tui/keys.js +33 -0
- package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens.js +78 -0
- package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
- package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
- package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
- package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
- package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state.js +11 -0
- package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
- package/dist/extensions/forgecli/config-tui/theme.js +88 -0
- package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
- package/dist/extensions/forgecli/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config-writer.js +63 -0
- package/dist/extensions/forgecli/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +85 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
- package/dist/extensions/forgecli/forge-commands.js +3 -8
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
- package/dist/extensions/forgecli/forge-subagent.js +19 -0
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/index.js +16 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/input-router.js +133 -0
- package/dist/extensions/forgecli/input-router.js.map +1 -0
- package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/model-resolver.js +65 -0
- package/dist/extensions/forgecli/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/model-validator.js +107 -0
- package/dist/extensions/forgecli/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +59 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +93 -1
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.js +5 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/whats-new-widget.js +5 -2
- package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
- package/package.json +11 -3
- package/dist/extensions/forgecli/review-command.d.ts +0 -2
- package/dist/extensions/forgecli/review-command.js +0 -184
- package/dist/extensions/forgecli/review-command.js.map +0 -1
- package/dist/forge-payload/.tools/banners.cjs +0 -435
- package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
- package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
- package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
- package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
- package/dist/forge-payload/.tools/collate.cjs +0 -1041
- package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
- package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
- package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
- package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
- package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
- package/dist/forge-payload/.tools/lib/result.js +0 -40
- package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
- package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
- package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
- package/dist/forge-payload/.tools/lib/validate.js +0 -141
- package/dist/forge-payload/.tools/manage-config.cjs +0 -340
- package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
- package/dist/forge-payload/.tools/package.json +0 -3
- package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
- package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
- package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
- package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
- package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
- package/dist/forge-payload/.tools/seed-store.cjs +0 -237
- package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
- package/dist/forge-payload/.tools/store-query.cjs +0 -319
- package/dist/forge-payload/.tools/store.cjs +0 -315
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
- package/dist/forge-payload/.tools/validate-store.cjs +0 -593
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// Forge tool: build-overlay
|
|
5
|
-
// Materializes a per-spawn PROJECT_OVERLAY from task/bug record + MASTER_INDEX slice.
|
|
6
|
-
// Usage: node build-overlay.cjs --task <TASK_ID> [--bug <BUG_ID>] [--format json|md]
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
|
|
11
|
-
const PHASE_ORDER = ['plan', 'review_plan', 'implementation', 'code_review', 'validation'];
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
main();
|
|
15
|
-
} catch (err) {
|
|
16
|
-
console.error('build-overlay error:', err.message);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function main() {
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
|
|
23
|
-
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
24
|
-
console.error('Usage: node build-overlay.cjs --task <TASK_ID> [--bug <BUG_ID>] [--format json|md]');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const taskId = argValue(args, '--task');
|
|
29
|
-
const bugId = argValue(args, '--bug');
|
|
30
|
-
const format = argValue(args, '--format') || 'json';
|
|
31
|
-
|
|
32
|
-
if (!taskId && !bugId) {
|
|
33
|
-
console.error('Error: --task <TASK_ID> or --bug <BUG_ID> required');
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const config = loadConfig();
|
|
38
|
-
const overlay = taskId
|
|
39
|
-
? buildTaskOverlay(taskId, config)
|
|
40
|
-
: buildBugOverlay(bugId, config);
|
|
41
|
-
|
|
42
|
-
validateOverlay(overlay);
|
|
43
|
-
|
|
44
|
-
if (format === 'md') {
|
|
45
|
-
process.stdout.write(renderMarkdown(overlay));
|
|
46
|
-
} else {
|
|
47
|
-
process.stdout.write(JSON.stringify(overlay, null, 2) + '\n');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Config loading
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
function loadConfig() {
|
|
56
|
-
let configPath = path.join('.forge', 'config.json');
|
|
57
|
-
if (!fs.existsSync(configPath)) {
|
|
58
|
-
// Try parent directories (for development/nested invocation)
|
|
59
|
-
const parentPath = path.join('..', '.forge', 'config.json');
|
|
60
|
-
if (fs.existsSync(parentPath)) configPath = parentPath;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let config = {};
|
|
64
|
-
try {
|
|
65
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
66
|
-
} catch (_) {}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
storeRoot: config.paths?.store || '.forge/store',
|
|
70
|
-
engineeringRoot: config.paths?.engineering || 'engineering',
|
|
71
|
-
commands: config.commands || {},
|
|
72
|
-
projectPrefix: derivePrefix(config),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function derivePrefix(config) {
|
|
77
|
-
// Derive the project prefix from config or directory name
|
|
78
|
-
const cwd = process.cwd();
|
|
79
|
-
const dirname = path.basename(cwd).toUpperCase().replace(/[^A-Z0-9-]/g, '-');
|
|
80
|
-
// Return first segment that looks like a prefix (upper-case short token)
|
|
81
|
-
return dirname.split('-')[0] || 'PROJ';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Task overlay builder
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
function buildTaskOverlay(taskId, config) {
|
|
89
|
-
const taskPath = path.join(config.storeRoot, 'tasks', `${taskId}.json`);
|
|
90
|
-
if (!fs.existsSync(taskPath)) {
|
|
91
|
-
// FR-015: Exit 1 for "task not found" per CLI convention (non-zero = error).
|
|
92
|
-
// This is intentional — the caller must know the task ID was invalid.
|
|
93
|
-
throw new Error(`Task not found: ${taskId} (looked at ${taskPath})`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const task = JSON.parse(fs.readFileSync(taskPath, 'utf8'));
|
|
97
|
-
const indexSlice = extractIndexSlice(taskId, task.sprintId || '', config.storeRoot);
|
|
98
|
-
const lastPhaseSummary = extractLastPhaseSummary(task);
|
|
99
|
-
|
|
100
|
-
const overlay = {
|
|
101
|
-
projectPrefix: config.projectPrefix,
|
|
102
|
-
sprintId: task.sprintId || '',
|
|
103
|
-
sprintDir: sprintDirFromId(task.sprintId || ''),
|
|
104
|
-
taskId: task.taskId,
|
|
105
|
-
taskDir: task.path || '',
|
|
106
|
-
taskStatus: task.status || '',
|
|
107
|
-
storeRoot: config.storeRoot,
|
|
108
|
-
indexSlice,
|
|
109
|
-
toolCommands: config.commands,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
if (lastPhaseSummary) {
|
|
113
|
-
overlay.lastPhaseSummary = lastPhaseSummary;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return overlay;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// Bug overlay builder
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
function buildBugOverlay(bugId, config) {
|
|
124
|
-
const bugPath = path.join(config.storeRoot, 'bugs', `${bugId}.json`);
|
|
125
|
-
if (!fs.existsSync(bugPath)) {
|
|
126
|
-
throw new Error(`Bug not found: ${bugId} (looked at ${bugPath})`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const bug = JSON.parse(fs.readFileSync(bugPath, 'utf8'));
|
|
130
|
-
const indexSlice = extractBugIndexSlice(bugId, config.storeRoot);
|
|
131
|
-
const lastPhaseSummary = extractLastPhaseSummary(bug);
|
|
132
|
-
|
|
133
|
-
const overlay = {
|
|
134
|
-
projectPrefix: config.projectPrefix,
|
|
135
|
-
bugId: bug.bugId,
|
|
136
|
-
bugDir: bug.path || '',
|
|
137
|
-
storeRoot: config.storeRoot,
|
|
138
|
-
indexSlice,
|
|
139
|
-
toolCommands: config.commands,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
if (lastPhaseSummary) {
|
|
143
|
-
overlay.lastPhaseSummary = lastPhaseSummary;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return overlay;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// Index slice extraction (queries the store directly — MASTER_INDEX.md is a
|
|
151
|
-
// downstream view and not the source of truth)
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
function extractIndexSlice(taskId, sprintId, storeRoot) {
|
|
155
|
-
const tasksDir = path.join(storeRoot, 'tasks');
|
|
156
|
-
if (!sprintId || !fs.existsSync(tasksDir)) return '';
|
|
157
|
-
|
|
158
|
-
const siblings = [];
|
|
159
|
-
for (const entry of fs.readdirSync(tasksDir)) {
|
|
160
|
-
if (!entry.endsWith('.json')) continue;
|
|
161
|
-
try {
|
|
162
|
-
const rec = JSON.parse(fs.readFileSync(path.join(tasksDir, entry), 'utf8'));
|
|
163
|
-
if (rec.sprintId === sprintId) siblings.push(rec);
|
|
164
|
-
} catch (_) { /* skip unreadable */ }
|
|
165
|
-
}
|
|
166
|
-
if (!siblings.length) return '';
|
|
167
|
-
|
|
168
|
-
siblings.sort((a, b) => (a.taskId || '').localeCompare(b.taskId || ''));
|
|
169
|
-
|
|
170
|
-
const lines = [
|
|
171
|
-
`**Sprint ${sprintId} tasks:**`,
|
|
172
|
-
'| Task | Title | Status |',
|
|
173
|
-
'|------|-------|--------|',
|
|
174
|
-
];
|
|
175
|
-
for (const t of siblings) {
|
|
176
|
-
const marker = t.taskId === taskId ? ' ← this' : '';
|
|
177
|
-
const title = (t.title || '').replace(/\|/g, '\\|').slice(0, 60);
|
|
178
|
-
lines.push(`| ${t.taskId}${marker} | ${title} | ${t.status || ''} |`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return budget(lines.join('\n'), 800);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function extractBugIndexSlice(bugId, storeRoot) {
|
|
185
|
-
const bugsDir = path.join(storeRoot, 'bugs');
|
|
186
|
-
if (!fs.existsSync(bugsDir)) return '';
|
|
187
|
-
|
|
188
|
-
const open = [];
|
|
189
|
-
let target = null;
|
|
190
|
-
for (const entry of fs.readdirSync(bugsDir)) {
|
|
191
|
-
if (!entry.endsWith('.json')) continue;
|
|
192
|
-
try {
|
|
193
|
-
const rec = JSON.parse(fs.readFileSync(path.join(bugsDir, entry), 'utf8'));
|
|
194
|
-
if (rec.bugId === bugId) target = rec;
|
|
195
|
-
else if (rec.status && !['fixed', 'verified'].includes(rec.status)) open.push(rec);
|
|
196
|
-
} catch (_) { /* skip */ }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const rows = [];
|
|
200
|
-
if (target) rows.push(target);
|
|
201
|
-
open.sort((a, b) => (a.bugId || '').localeCompare(b.bugId || ''));
|
|
202
|
-
rows.push(...open);
|
|
203
|
-
if (!rows.length) return '';
|
|
204
|
-
|
|
205
|
-
const lines = [
|
|
206
|
-
'**Active bugs:**',
|
|
207
|
-
'| Bug | Title | Severity | Status |',
|
|
208
|
-
'|-----|-------|----------|--------|',
|
|
209
|
-
];
|
|
210
|
-
for (const b of rows) {
|
|
211
|
-
const marker = b.bugId === bugId ? ' ← this' : '';
|
|
212
|
-
const title = (b.title || '').replace(/\|/g, '\\|').slice(0, 50);
|
|
213
|
-
lines.push(`| ${b.bugId}${marker} | ${title} | ${b.severity || ''} | ${b.status || ''} |`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return budget(lines.join('\n'), 800);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function budget(text, max) {
|
|
220
|
-
return text.length <= max ? text : text.slice(0, max - 3) + '...';
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// Phase summary helpers
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
|
|
227
|
-
function extractLastPhaseSummary(record) {
|
|
228
|
-
if (!record.summaries) return null;
|
|
229
|
-
// Find the last phase with a summary in canonical order
|
|
230
|
-
let last = null;
|
|
231
|
-
let lastPhase = null;
|
|
232
|
-
for (const phase of PHASE_ORDER) {
|
|
233
|
-
if (record.summaries[phase]) {
|
|
234
|
-
last = record.summaries[phase];
|
|
235
|
-
lastPhase = phase;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (!last) return null;
|
|
239
|
-
return { phase: lastPhase, ...last };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
// Helpers
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
|
|
246
|
-
function argValue(args, flag) {
|
|
247
|
-
const idx = args.indexOf(flag);
|
|
248
|
-
if (idx === -1) return null;
|
|
249
|
-
return args[idx + 1] || null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function sprintDirFromId(sprintId) {
|
|
253
|
-
if (!sprintId) return '';
|
|
254
|
-
return sprintId;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// Schema validation
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
function validateOverlay(overlay) {
|
|
262
|
-
const schemaPath = path.join(__dirname, '..', 'schemas', 'project-overlay.schema.json');
|
|
263
|
-
if (!fs.existsSync(schemaPath)) return; // schema optional at dev time
|
|
264
|
-
|
|
265
|
-
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
266
|
-
const errors = [];
|
|
267
|
-
|
|
268
|
-
for (const req of (schema.required || [])) {
|
|
269
|
-
if (overlay[req] === undefined) {
|
|
270
|
-
errors.push(`missing required field: ${req}`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (overlay.indexSlice && overlay.indexSlice.length > 800) {
|
|
275
|
-
errors.push(`indexSlice exceeds 800 chars (${overlay.indexSlice.length})`);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (errors.length > 0) {
|
|
279
|
-
throw new Error(`Overlay schema validation failed:\n${errors.join('\n')}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
// Markdown rendering
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
|
|
287
|
-
function renderMarkdown(overlay) {
|
|
288
|
-
const lines = ['### Project Overlay\n'];
|
|
289
|
-
|
|
290
|
-
if (overlay.taskId) {
|
|
291
|
-
lines.push(`**Task:** ${overlay.taskId} (${overlay.taskStatus || 'unknown'})`);
|
|
292
|
-
lines.push(`**Sprint:** ${overlay.sprintId || 'unknown'}`);
|
|
293
|
-
lines.push(`**Task dir:** ${overlay.taskDir || 'unknown'}`);
|
|
294
|
-
} else if (overlay.bugId) {
|
|
295
|
-
lines.push(`**Bug:** ${overlay.bugId}`);
|
|
296
|
-
lines.push(`**Bug dir:** ${overlay.bugDir || 'unknown'}`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
lines.push(`**Store root:** ${overlay.storeRoot}`);
|
|
300
|
-
|
|
301
|
-
if (overlay.indexSlice) {
|
|
302
|
-
lines.push('\n**Sprint context (from index):**');
|
|
303
|
-
lines.push('```');
|
|
304
|
-
lines.push(overlay.indexSlice);
|
|
305
|
-
lines.push('```');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (overlay.lastPhaseSummary) {
|
|
309
|
-
const s = overlay.lastPhaseSummary;
|
|
310
|
-
lines.push(`\n**Last phase (${s.phase}):** ${s.objective || ''}`);
|
|
311
|
-
if (s.key_changes && s.key_changes.length > 0) {
|
|
312
|
-
lines.push('Key changes:');
|
|
313
|
-
for (const c of s.key_changes.slice(0, 5)) {
|
|
314
|
-
lines.push(`- ${c}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (overlay.toolCommands) {
|
|
320
|
-
const cmds = overlay.toolCommands;
|
|
321
|
-
if (cmds.test) lines.push(`\n**Test command:** \`${cmds.test}\``);
|
|
322
|
-
if (cmds.lint) lines.push(`**Lint command:** \`${cmds.lint}\``);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return lines.join('\n') + '\n';
|
|
326
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* build-persona-pack.cjs — compile persona/skill YAML frontmatter from
|
|
6
|
-
* forge/meta/personas/* and forge/meta/skills/* into a compact JSON pack
|
|
7
|
-
* at .forge/cache/persona-pack.json. The pack is used by meta-orchestrate
|
|
8
|
-
* and meta-fix-bug to inject persona references (not verbatim prose) into
|
|
9
|
-
* subagent prompts.
|
|
10
|
-
*
|
|
11
|
-
* CLI:
|
|
12
|
-
* node build-persona-pack.cjs [--meta-root <path>] [--out <path>]
|
|
13
|
-
*
|
|
14
|
-
* Exported API:
|
|
15
|
-
* parseFrontmatter(content, filePath) → object (throws on missing/malformed)
|
|
16
|
-
* buildPack({ personaDir, skillDir }) → pack object
|
|
17
|
-
* computeSourceHash({ personaDir, skillDir }) → "sha256:..."
|
|
18
|
-
* writePack(pack, outPath) → atomic write
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const fs = require('fs');
|
|
22
|
-
const path = require('path');
|
|
23
|
-
const crypto = require('crypto');
|
|
24
|
-
|
|
25
|
-
// ── YAML frontmatter parser ──────────────────────────────────────────────────
|
|
26
|
-
// Narrow-scope parser: handles scalars, folded scalars (`>`), block lists
|
|
27
|
-
// (`- item`) under a key, and inline flow lists (`[a, b]`). Anything else
|
|
28
|
-
// throws a descriptive error with the source file path.
|
|
29
|
-
|
|
30
|
-
function parseFrontmatter(content, filePath) {
|
|
31
|
-
const lines = content.split(/\r?\n/);
|
|
32
|
-
if (lines[0] !== '---') {
|
|
33
|
-
throw new Error(`${filePath}: no frontmatter block found (missing opening '---')`);
|
|
34
|
-
}
|
|
35
|
-
let end = -1;
|
|
36
|
-
for (let i = 1; i < lines.length; i++) {
|
|
37
|
-
if (lines[i] === '---') { end = i; break; }
|
|
38
|
-
}
|
|
39
|
-
if (end === -1) {
|
|
40
|
-
throw new Error(`${filePath}: frontmatter block is unterminated (missing closing '---')`);
|
|
41
|
-
}
|
|
42
|
-
const body = lines.slice(1, end);
|
|
43
|
-
return parseBlock(body, filePath);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseBlock(lines, filePath) {
|
|
47
|
-
const out = {};
|
|
48
|
-
let i = 0;
|
|
49
|
-
while (i < lines.length) {
|
|
50
|
-
const raw = lines[i];
|
|
51
|
-
if (raw.trim() === '' || raw.trim().startsWith('#')) { i++; continue; }
|
|
52
|
-
const m = raw.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
53
|
-
if (!m) {
|
|
54
|
-
throw new Error(`${filePath}: cannot parse frontmatter line ${i + 1}: ${JSON.stringify(raw)}`);
|
|
55
|
-
}
|
|
56
|
-
const key = m[1];
|
|
57
|
-
const rest = m[2];
|
|
58
|
-
|
|
59
|
-
// Folded scalar: `>` — consume indented continuation lines
|
|
60
|
-
if (rest === '>' || rest === '>-' || rest === '>+') {
|
|
61
|
-
const chunks = [];
|
|
62
|
-
i++;
|
|
63
|
-
while (i < lines.length && /^\s+\S/.test(lines[i])) {
|
|
64
|
-
chunks.push(lines[i].trim());
|
|
65
|
-
i++;
|
|
66
|
-
}
|
|
67
|
-
out[key] = chunks.join(' ').trim();
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Inline flow list: `[a, b, c]`
|
|
72
|
-
if (/^\[.*\]$/.test(rest)) {
|
|
73
|
-
out[key] = rest
|
|
74
|
-
.slice(1, -1)
|
|
75
|
-
.split(',')
|
|
76
|
-
.map((s) => stripQuotes(s.trim()))
|
|
77
|
-
.filter((s) => s.length > 0);
|
|
78
|
-
i++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Block list: key with no inline value, followed by `- item` lines
|
|
83
|
-
if (rest === '') {
|
|
84
|
-
const items = [];
|
|
85
|
-
i++;
|
|
86
|
-
while (i < lines.length && /^\s*-\s+/.test(lines[i])) {
|
|
87
|
-
items.push(lines[i].replace(/^\s*-\s+/, '').trim());
|
|
88
|
-
i++;
|
|
89
|
-
}
|
|
90
|
-
if (items.length === 0) {
|
|
91
|
-
// empty map value (rare) — leave as empty string
|
|
92
|
-
out[key] = '';
|
|
93
|
-
} else {
|
|
94
|
-
out[key] = items;
|
|
95
|
-
}
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Plain scalar
|
|
100
|
-
out[key] = stripQuotes(rest.trim());
|
|
101
|
-
i++;
|
|
102
|
-
}
|
|
103
|
-
return out;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function stripQuotes(s) {
|
|
107
|
-
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
108
|
-
return s.slice(1, -1);
|
|
109
|
-
}
|
|
110
|
-
return s;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Pack building ────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
const REQUIRED_PERSONA_FIELDS = ['id', 'role', 'summary', 'responsibilities', 'outputs', 'file_ref'];
|
|
116
|
-
const REQUIRED_SKILL_FIELDS = ['id', 'applies_to', 'summary', 'capabilities', 'file_ref'];
|
|
117
|
-
|
|
118
|
-
function listMarkdown(dir) {
|
|
119
|
-
if (!fs.existsSync(dir)) return [];
|
|
120
|
-
return fs.readdirSync(dir)
|
|
121
|
-
.filter((f) => f.endsWith('.md') && f !== 'README.md')
|
|
122
|
-
.sort()
|
|
123
|
-
.map((f) => path.join(dir, f));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function loadEntries(dir, requiredFields) {
|
|
127
|
-
const entries = {};
|
|
128
|
-
for (const filePath of listMarkdown(dir)) {
|
|
129
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
130
|
-
let fm;
|
|
131
|
-
try {
|
|
132
|
-
fm = parseFrontmatter(content, filePath);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
// Re-throw with original path-bearing message intact.
|
|
135
|
-
throw err;
|
|
136
|
-
}
|
|
137
|
-
for (const field of requiredFields) {
|
|
138
|
-
if (!(field in fm)) {
|
|
139
|
-
throw new Error(`${filePath}: frontmatter missing required field '${field}'`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
entries[fm.id] = fm;
|
|
143
|
-
}
|
|
144
|
-
return entries;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function buildPack({ personaDir, skillDir }) {
|
|
148
|
-
const personas = loadEntries(personaDir, REQUIRED_PERSONA_FIELDS);
|
|
149
|
-
const skills = loadEntries(skillDir, REQUIRED_SKILL_FIELDS);
|
|
150
|
-
return {
|
|
151
|
-
version: 1,
|
|
152
|
-
built_at: new Date().toISOString(),
|
|
153
|
-
source_hash: computeSourceHash({ personaDir, skillDir }),
|
|
154
|
-
personas,
|
|
155
|
-
skills,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function computeSourceHash({ personaDir, skillDir }) {
|
|
160
|
-
const files = [...listMarkdown(personaDir), ...listMarkdown(skillDir)].sort();
|
|
161
|
-
const hash = crypto.createHash('sha256');
|
|
162
|
-
// FR-012: Content-based hashing for reproducibility.
|
|
163
|
-
// Old mtime-based hash was non-deterministic across runs after git checkout.
|
|
164
|
-
// New pattern: filePath\0 + fileContents + \0 — null-byte separators prevent
|
|
165
|
-
// concatenation ambiguity and make the hash a pure function of content.
|
|
166
|
-
for (const f of files) {
|
|
167
|
-
hash.update(`${f}\0`);
|
|
168
|
-
hash.update(fs.readFileSync(f));
|
|
169
|
-
hash.update('\0');
|
|
170
|
-
}
|
|
171
|
-
return `sha256:${hash.digest('hex')}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ── Atomic write ─────────────────────────────────────────────────────────────
|
|
175
|
-
|
|
176
|
-
function writePack(pack, outPath) {
|
|
177
|
-
const dir = path.dirname(outPath);
|
|
178
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
179
|
-
const tmp = outPath + '.tmp';
|
|
180
|
-
fs.writeFileSync(tmp, JSON.stringify(pack, null, 2) + '\n', 'utf8');
|
|
181
|
-
fs.renameSync(tmp, outPath);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
function parseArgs(argv) {
|
|
187
|
-
const out = {};
|
|
188
|
-
for (let i = 0; i < argv.length; i++) {
|
|
189
|
-
const a = argv[i];
|
|
190
|
-
if (a === '--meta-root') out.metaRoot = argv[++i];
|
|
191
|
-
else if (a === '--out') out.out = argv[++i];
|
|
192
|
-
else if (a === '--persona-dir') out.personaDir = argv[++i];
|
|
193
|
-
else if (a === '--skill-dir') out.skillDir = argv[++i];
|
|
194
|
-
}
|
|
195
|
-
return out;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function main() {
|
|
199
|
-
const args = parseArgs(process.argv.slice(2));
|
|
200
|
-
const metaRoot = args.metaRoot || path.resolve(__dirname, '..', 'meta');
|
|
201
|
-
const personaDir = args.personaDir || path.join(metaRoot, 'personas');
|
|
202
|
-
const skillDir = args.skillDir || path.join(metaRoot, 'skills');
|
|
203
|
-
const out = args.out || path.resolve(process.cwd(), '.forge/cache/persona-pack.json');
|
|
204
|
-
|
|
205
|
-
const pack = buildPack({ personaDir, skillDir });
|
|
206
|
-
writePack(pack, out);
|
|
207
|
-
process.stdout.write(
|
|
208
|
-
`persona-pack: wrote ${Object.keys(pack.personas).length} personas, ${Object.keys(pack.skills).length} skills → ${out}\n`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (require.main === module) {
|
|
213
|
-
try {
|
|
214
|
-
main();
|
|
215
|
-
} catch (err) {
|
|
216
|
-
process.stderr.write(`build-persona-pack: ${err.message}\n`);
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
module.exports = {
|
|
222
|
-
parseFrontmatter,
|
|
223
|
-
buildPack,
|
|
224
|
-
computeSourceHash,
|
|
225
|
-
writePack,
|
|
226
|
-
};
|