@entelligentsia/forgecli 0.8.4 → 0.9.0

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