@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.
Files changed (170) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +165 -2
  3. package/dist/bin/argv.d.ts +2 -2
  4. package/dist/bin/argv.js +17 -0
  5. package/dist/bin/argv.js.map +1 -1
  6. package/dist/bin/config.d.ts +69 -0
  7. package/dist/bin/config.js +315 -0
  8. package/dist/bin/config.js.map +1 -0
  9. package/dist/bin/doctor.d.ts +1 -0
  10. package/dist/bin/doctor.js +12 -0
  11. package/dist/bin/doctor.js.map +1 -1
  12. package/dist/bin/forge.js +7 -0
  13. package/dist/bin/forge.js.map +1 -1
  14. package/dist/extensions/forgecli/config-command.d.ts +8 -0
  15. package/dist/extensions/forgecli/config-command.js +66 -0
  16. package/dist/extensions/forgecli/config-command.js.map +1 -0
  17. package/dist/extensions/forgecli/config-layer.d.ts +38 -0
  18. package/dist/extensions/forgecli/config-layer.js +68 -0
  19. package/dist/extensions/forgecli/config-layer.js.map +1 -0
  20. package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
  21. package/dist/extensions/forgecli/config-tui/component.js +236 -0
  22. package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
  23. package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
  24. package/dist/extensions/forgecli/config-tui/handler.js +240 -0
  25. package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
  26. package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
  27. package/dist/extensions/forgecli/config-tui/index.js +5 -0
  28. package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
  29. package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
  30. package/dist/extensions/forgecli/config-tui/keys.js +33 -0
  31. package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
  32. package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
  33. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
  34. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
  35. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
  36. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
  37. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
  38. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
  39. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
  40. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
  41. package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
  42. package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
  43. package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
  44. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
  45. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
  46. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
  47. package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
  48. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
  49. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
  50. package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
  51. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
  52. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
  53. package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
  54. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
  55. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
  56. package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
  57. package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
  58. package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
  59. package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
  60. package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
  61. package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
  62. package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
  63. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
  64. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
  65. package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
  66. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
  67. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
  68. package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
  69. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
  70. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
  71. package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
  72. package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
  73. package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
  74. package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
  75. package/dist/extensions/forgecli/config-tui/screens.js +78 -0
  76. package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
  77. package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
  78. package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
  79. package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
  80. package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
  81. package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
  82. package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
  83. package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
  84. package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
  85. package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
  86. package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
  87. package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
  88. package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
  89. package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
  90. package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
  91. package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
  92. package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
  93. package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
  94. package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
  95. package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
  96. package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
  97. package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
  98. package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
  99. package/dist/extensions/forgecli/config-tui/state.js +11 -0
  100. package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
  101. package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
  102. package/dist/extensions/forgecli/config-tui/theme.js +88 -0
  103. package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
  104. package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
  105. package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
  106. package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
  107. package/dist/extensions/forgecli/config-writer.d.ts +16 -0
  108. package/dist/extensions/forgecli/config-writer.js +63 -0
  109. package/dist/extensions/forgecli/config-writer.js.map +1 -0
  110. package/dist/extensions/forgecli/fix-bug.js +85 -1
  111. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  112. package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
  113. package/dist/extensions/forgecli/forge-commands.js +3 -8
  114. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  115. package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
  116. package/dist/extensions/forgecli/forge-subagent.js +19 -0
  117. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  118. package/dist/extensions/forgecli/index.js +16 -0
  119. package/dist/extensions/forgecli/index.js.map +1 -1
  120. package/dist/extensions/forgecli/input-router.d.ts +33 -0
  121. package/dist/extensions/forgecli/input-router.js +133 -0
  122. package/dist/extensions/forgecli/input-router.js.map +1 -0
  123. package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
  124. package/dist/extensions/forgecli/model-resolver.js +65 -0
  125. package/dist/extensions/forgecli/model-resolver.js.map +1 -0
  126. package/dist/extensions/forgecli/model-validator.d.ts +29 -0
  127. package/dist/extensions/forgecli/model-validator.js +107 -0
  128. package/dist/extensions/forgecli/model-validator.js.map +1 -0
  129. package/dist/extensions/forgecli/run-sprint.js +59 -0
  130. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  131. package/dist/extensions/forgecli/run-task.js +93 -1
  132. package/dist/extensions/forgecli/run-task.js.map +1 -1
  133. package/dist/extensions/forgecli/thread-switcher.js +5 -2
  134. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  135. package/dist/extensions/forgecli/whats-new-widget.js +5 -2
  136. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  137. package/package.json +11 -3
  138. package/dist/extensions/forgecli/review-command.d.ts +0 -2
  139. package/dist/extensions/forgecli/review-command.js +0 -184
  140. package/dist/extensions/forgecli/review-command.js.map +0 -1
  141. package/dist/forge-payload/.tools/banners.cjs +0 -435
  142. package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
  143. package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
  144. package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
  145. package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
  146. package/dist/forge-payload/.tools/collate.cjs +0 -1041
  147. package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
  148. package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
  149. package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
  150. package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
  151. package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
  152. package/dist/forge-payload/.tools/lib/result.js +0 -40
  153. package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
  154. package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
  155. package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
  156. package/dist/forge-payload/.tools/lib/validate.js +0 -141
  157. package/dist/forge-payload/.tools/manage-config.cjs +0 -340
  158. package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
  159. package/dist/forge-payload/.tools/package.json +0 -3
  160. package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
  161. package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
  162. package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
  163. package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
  164. package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
  165. package/dist/forge-payload/.tools/seed-store.cjs +0 -237
  166. package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
  167. package/dist/forge-payload/.tools/store-query.cjs +0 -319
  168. package/dist/forge-payload/.tools/store.cjs +0 -315
  169. package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
  170. 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
- };