@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,290 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * build-context-pack.cjs — build a compact architecture context pack from
6
- * engineering/architecture/*.md files. Writes:
7
- * .forge/cache/context-pack.md (human-readable summary)
8
- * .forge/cache/context-pack.json (machine-readable index)
9
- *
10
- * CLI:
11
- * node build-context-pack.cjs [--arch-dir <path>] [--out-md <path>] [--out-json <path>]
12
- *
13
- * Exported API:
14
- * extractDoc(filePath) → { title, firstPara, sections, lineCount, filePath }
15
- * buildContextPack({ archDir, existingPackPath? }) → pack object (or { skipped: true })
16
- * computeSourceHash(archDir) → "sha256:..."
17
- * writeContextPack(pack, outMd, outJson) → void (atomic)
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const crypto = require('crypto');
23
-
24
- const PACK_LINE_LIMIT = 400;
25
-
26
- // ── Document extraction ──────────────────────────────────────────────────────
27
-
28
- /**
29
- * Extract H1 title, first paragraph, and ## Key / ## Summary sections from
30
- * a single markdown file.
31
- */
32
- function extractDoc(filePath) {
33
- const raw = fs.readFileSync(filePath, 'utf8');
34
- const lines = raw.split('\n');
35
- const lineCount = lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
36
-
37
- let title = '';
38
- let firstPara = '';
39
- const sections = {};
40
-
41
- let i = 0;
42
-
43
- // Find H1
44
- while (i < lines.length) {
45
- const m = lines[i].match(/^#\s+(.+)$/);
46
- if (m) {
47
- title = m[1].trim();
48
- i++;
49
- break;
50
- }
51
- i++;
52
- }
53
-
54
- // First paragraph: non-empty lines after H1, before next heading
55
- const firstParaLines = [];
56
- while (i < lines.length && !lines[i].startsWith('#')) {
57
- if (lines[i].trim()) firstParaLines.push(lines[i]);
58
- else if (firstParaLines.length) break; // blank line ends paragraph
59
- i++;
60
- }
61
- firstPara = firstParaLines.join(' ').trim();
62
-
63
- // Remaining: scan for ## Key * or ## Summary sections
64
- while (i < lines.length) {
65
- const hm = lines[i].match(/^##\s+(Key\s+\S.*|Summary)\s*$/i);
66
- if (hm) {
67
- const heading = hm[1].trim();
68
- i++;
69
- const sectionLines = [];
70
- while (i < lines.length && !lines[i].match(/^##/)) {
71
- sectionLines.push(lines[i]);
72
- i++;
73
- }
74
- // Trim leading/trailing blank lines
75
- while (sectionLines.length && !sectionLines[0].trim()) sectionLines.shift();
76
- while (sectionLines.length && !sectionLines[sectionLines.length - 1].trim()) sectionLines.pop();
77
- if (sectionLines.length) {
78
- sections[heading] = sectionLines.join('\n');
79
- }
80
- } else {
81
- i++;
82
- }
83
- }
84
-
85
- return { title, firstPara, sections, lineCount, filePath };
86
- }
87
-
88
- // ── Source hash ──────────────────────────────────────────────────────────────
89
-
90
- function listArchFiles(archDir) {
91
- if (!fs.existsSync(archDir)) {
92
- throw new Error(`Architecture directory not found: ${archDir}`);
93
- }
94
- return fs.readdirSync(archDir)
95
- .filter((f) => f.endsWith('.md') && !f.endsWith('.draft.md'))
96
- .sort()
97
- .map((f) => path.join(archDir, f));
98
- }
99
-
100
- function computeSourceHash(archDir) {
101
- const files = listArchFiles(archDir);
102
- const hash = crypto.createHash('sha256');
103
- // FR-012: Content-based hashing for reproducibility.
104
- // Old mtime-based hash was non-deterministic across runs after git checkout.
105
- // New pattern: filePath\0 + fileContents + \0 — null-byte separators prevent
106
- // concatenation ambiguity and make the hash a pure function of content.
107
- for (const f of files) {
108
- hash.update(`${f}\0`);
109
- hash.update(fs.readFileSync(f));
110
- hash.update('\0');
111
- }
112
- return `sha256:${hash.digest('hex')}`;
113
- }
114
-
115
- // ── Pack composition ─────────────────────────────────────────────────────────
116
-
117
- function composePack(docs, builtAt, sourceHash, archDir) {
118
- const lines = [];
119
-
120
- lines.push('# Architecture Context Pack', '');
121
- lines.push(`Built: ${builtAt}`);
122
- lines.push(`Source hash: ${sourceHash}`);
123
- lines.push('');
124
-
125
- // Aggregate Key/Summary sections from all docs, grouped by heading name
126
- const allHeadings = [];
127
- const headingDocs = {}; // heading → [{basename, content}]
128
- for (const doc of docs) {
129
- for (const [heading, content] of Object.entries(doc.sections)) {
130
- if (!headingDocs[heading]) {
131
- allHeadings.push(heading);
132
- headingDocs[heading] = [];
133
- }
134
- headingDocs[heading].push({ basename: path.basename(doc.filePath), content });
135
- }
136
- }
137
-
138
- if (allHeadings.length) {
139
- for (const heading of allHeadings) {
140
- lines.push(`## ${heading}`, '');
141
- for (const { basename, content } of headingDocs[heading]) {
142
- if (headingDocs[heading].length > 1) {
143
- lines.push(`*From ${basename}:*`, '');
144
- }
145
- lines.push(content, '');
146
- }
147
- }
148
- }
149
-
150
- // Document summaries
151
- lines.push('## Document summaries', '');
152
- for (const doc of docs) {
153
- const basename = path.basename(doc.filePath);
154
- lines.push(`### ${doc.title || basename}`);
155
- if (doc.firstPara) {
156
- lines.push('');
157
- lines.push(doc.firstPara);
158
- }
159
- lines.push('');
160
- }
161
-
162
- // File index
163
- lines.push('## File index', '');
164
- for (const doc of docs) {
165
- const relPath = path.relative(process.cwd(), doc.filePath);
166
- lines.push(`- ${relPath} — ${doc.title || path.basename(doc.filePath)} (${doc.lineCount} lines)`);
167
- }
168
-
169
- return lines;
170
- }
171
-
172
- // ── Build ────────────────────────────────────────────────────────────────────
173
-
174
- function buildContextPack({ archDir, existingPackPath }) {
175
- // Check for manual override
176
- if (existingPackPath && fs.existsSync(existingPackPath)) {
177
- const existing = fs.readFileSync(existingPackPath, 'utf8');
178
- if (/^---[\s\S]*?manual:\s*true[\s\S]*?---/m.test(existing)) {
179
- return { skipped: true };
180
- }
181
- }
182
-
183
- const files = listArchFiles(archDir);
184
- const docs = files.map((f) => extractDoc(f));
185
-
186
- const builtAt = new Date().toISOString();
187
- const sourceHash = computeSourceHash(archDir);
188
-
189
- const mdLines = composePack(docs, builtAt, sourceHash, archDir);
190
- let markdown = mdLines.join('\n') + '\n';
191
-
192
- // Enforce 400-line cap (count real lines, excluding trailing empty after final \n)
193
- const splitLines = markdown.split('\n');
194
- const realLineCount = splitLines[splitLines.length - 1] === '' ? splitLines.length - 1 : splitLines.length;
195
- if (realLineCount > PACK_LINE_LIMIT) {
196
- // Keep PACK_LINE_LIMIT - 1 lines so the marker fits within the cap
197
- const truncated = splitLines.slice(0, PACK_LINE_LIMIT - 1);
198
- truncated.push('<!-- TRUNCATED: pack exceeded 400 lines. Architecture KB has grown beyond summary capacity. Run /forge:regenerate after pruning docs. -->');
199
- markdown = truncated.join('\n') + '\n';
200
- }
201
-
202
- const sources = files.map((f) => {
203
- const stat = fs.statSync(f);
204
- return {
205
- path: path.relative(process.cwd(), f),
206
- size: stat.size,
207
- mtime: new Date(stat.mtimeMs).toISOString(),
208
- };
209
- });
210
-
211
- return {
212
- version: 1,
213
- built_at: builtAt,
214
- source_hash: sourceHash,
215
- sources,
216
- markdown,
217
- };
218
- }
219
-
220
- // ── Atomic write ─────────────────────────────────────────────────────────────
221
-
222
- function writeContextPack(pack, outMd, outJson) {
223
- fs.mkdirSync(path.dirname(outMd), { recursive: true });
224
- fs.mkdirSync(path.dirname(outJson), { recursive: true });
225
-
226
- // Write markdown
227
- const tmpMd = outMd + '.tmp';
228
- fs.writeFileSync(tmpMd, pack.markdown, 'utf8');
229
- fs.renameSync(tmpMd, outMd);
230
-
231
- // Write JSON index
232
- const jsonRecord = {
233
- version: pack.version,
234
- built_at: pack.built_at,
235
- source_hash: pack.source_hash,
236
- sources: pack.sources,
237
- summary_path: outMd,
238
- };
239
- const tmpJson = outJson + '.tmp';
240
- fs.writeFileSync(tmpJson, JSON.stringify(jsonRecord, null, 2) + '\n', 'utf8');
241
- fs.renameSync(tmpJson, outJson);
242
- }
243
-
244
- // ── CLI ──────────────────────────────────────────────────────────────────────
245
-
246
- function parseArgs(argv) {
247
- const out = {};
248
- for (let i = 0; i < argv.length; i++) {
249
- const a = argv[i];
250
- if (a === '--arch-dir') out.archDir = argv[++i];
251
- else if (a === '--out-md') out.outMd = argv[++i];
252
- else if (a === '--out-json') out.outJson = argv[++i];
253
- }
254
- return out;
255
- }
256
-
257
- function main() {
258
- const args = parseArgs(process.argv.slice(2));
259
- const archDir = args.archDir || path.resolve(process.cwd(), 'engineering/architecture');
260
- const outMd = args.outMd || path.resolve(process.cwd(), '.forge/cache/context-pack.md');
261
- const outJson = args.outJson || path.resolve(process.cwd(), '.forge/cache/context-pack.json');
262
-
263
- const pack = buildContextPack({ archDir, existingPackPath: outMd });
264
-
265
- if (pack.skipped) {
266
- process.stdout.write('context-pack: manual: true — skipping rebuild\n');
267
- return;
268
- }
269
-
270
- writeContextPack(pack, outMd, outJson);
271
- process.stdout.write(
272
- `context-pack: wrote ${pack.sources.length} sources → ${outMd}\n`,
273
- );
274
- }
275
-
276
- if (require.main === module) {
277
- try {
278
- main();
279
- } catch (err) {
280
- process.stderr.write(`build-context-pack: ${err.message}\n`);
281
- process.exit(1);
282
- }
283
- }
284
-
285
- module.exports = {
286
- extractDoc,
287
- buildContextPack,
288
- computeSourceHash,
289
- writeContextPack,
290
- };
@@ -1,322 +0,0 @@
1
- 'use strict';
2
- // build-init-context.cjs
3
- // Deterministic project brief builder for /forge:init Phase 7 fan-out.
4
- // Reads: .forge/config.json, .forge/personas/, .forge/templates/, {kb}/
5
- // Writes: .forge/init-context.md (markdown — for LLM prompts)
6
- // .forge/init-context.json (same data — for deterministic consumers)
7
- //
8
- // Uses only Node.js built-ins — no npm dependencies.
9
- // Exports: buildBrief, extractPersonaSymbol, parseEntities (for testing)
10
-
11
- 'use strict';
12
- const fs = require('fs');
13
- const path = require('path');
14
-
15
- // ── Skill → persona wiring (mirrors meta-skill-recommendations.md) ─────────
16
- // Maps installed skill name to the persona roles it wires into.
17
- const SKILL_PERSONA_MAP = {
18
- 'vue-best-practices': ['engineer', 'supervisor'],
19
- 'stripe-integration': ['engineer', 'bug-fixer'],
20
- 'frontend-design': ['engineer', 'supervisor'],
21
- 'typescript-lsp': ['engineer', 'supervisor'],
22
- 'pyright-lsp': ['engineer', 'supervisor'],
23
- 'ruby-lsp': ['engineer', 'supervisor'],
24
- 'gopls-lsp': ['engineer', 'supervisor'],
25
- 'rust-analyzer-lsp': ['engineer', 'supervisor'],
26
- 'jdtls-lsp': ['engineer', 'supervisor'],
27
- 'kotlin-lsp': ['engineer', 'supervisor'],
28
- 'csharp-lsp': ['engineer', 'supervisor'],
29
- 'clangd-lsp': ['engineer', 'supervisor'],
30
- 'php-lsp': ['engineer', 'supervisor'],
31
- 'swift-lsp': ['engineer', 'supervisor'],
32
- 'lua-lsp': ['engineer', 'supervisor'],
33
- 'security-guidance': ['supervisor', 'engineer'],
34
- 'mcp-server-dev': ['engineer'],
35
- 'agent-sdk-dev': ['engineer'],
36
- 'threejs-skills': ['engineer'],
37
- 'meta-webxr-skills': ['engineer'],
38
- 'freshdesk-api': ['engineer'],
39
- };
40
-
41
- // ── extractPersonaSymbol ──────────────────────────────────────────────────────
42
- // Reads the first 15 lines of a persona file and extracts the emoji symbol.
43
- // Handles two formats:
44
- // 1. First-line emoji (generated persona style):
45
- // 🗻 **emberglow Architect** — tagline
46
- // 2. YAML frontmatter "symbol:" line:
47
- // ---
48
- // symbol: 🏛
49
- // ---
50
- // Returns '·' if no symbol is found.
51
- function extractPersonaSymbol(content) {
52
- const lines = content.split('\n').slice(0, 15);
53
-
54
- // Format 1: first non-blank line starts with an emoji character
55
- // Unicode emoji range check — covers most common emoji blocks
56
- const firstNonBlank = lines.find(l => l.trim().length > 0);
57
- if (firstNonBlank) {
58
- // Match a leading emoji (one or more emoji codepoints before a space or end)
59
- const emojiMatch = firstNonBlank.match(/^(\p{Emoji_Presentation}|\p{Extended_Pictographic})/u);
60
- if (emojiMatch) return emojiMatch[0];
61
- }
62
-
63
- // Format 2: "symbol: <value>" in YAML frontmatter
64
- for (const line of lines) {
65
- const m = line.match(/^symbol:\s*(\S.*?)\s*$/i);
66
- if (m) return m[1].trim();
67
- }
68
-
69
- return '·';
70
- }
71
-
72
- // ── parseEntities ─────────────────────────────────────────────────────────────
73
- // Extracts domain entity names from MASTER_INDEX.md content.
74
- // Looks for a heading containing "Entities" or "Domain"; reads the first
75
- // non-empty content line(s) below it as either comma-separated names or
76
- // markdown list items. Returns a sorted, deduplicated array.
77
- function parseEntities(content) {
78
- const lines = content.split('\n');
79
- let inSection = false;
80
- const found = [];
81
-
82
- for (let i = 0; i < lines.length; i++) {
83
- const line = lines[i];
84
-
85
- if (/^#{1,4}\s.*(entities|domain)/i.test(line)) {
86
- inSection = true;
87
- continue;
88
- }
89
-
90
- if (inSection) {
91
- // Stop at the next heading
92
- if (/^#{1,4}\s/.test(line)) break;
93
-
94
- const trimmed = line.trim();
95
- if (!trimmed) continue;
96
-
97
- // List items: "- Entity" or "* Entity"
98
- if (/^[-*]\s+/.test(trimmed)) {
99
- const name = trimmed.replace(/^[-*]\s+/, '').trim();
100
- if (name) found.push(name);
101
- continue;
102
- }
103
-
104
- // Comma-separated line
105
- if (trimmed.includes(',')) {
106
- for (const part of trimmed.split(',')) {
107
- const name = part.trim();
108
- if (name) found.push(name);
109
- }
110
- break; // Only read one comma-separated line
111
- }
112
-
113
- // Single name on its own line
114
- if (trimmed && !trimmed.startsWith('#')) {
115
- found.push(trimmed);
116
- }
117
- }
118
- }
119
-
120
- // Deduplicate and sort
121
- return [...new Set(found)].sort();
122
- }
123
-
124
- // ── buildBrief ────────────────────────────────────────────────────────────────
125
- // Main builder. Takes options object:
126
- // { configPath, personasDir, templatesDir, kbPath }
127
- // Returns: { markdown: string, json: object }
128
- function buildBrief({ configPath, personasDir, templatesDir, kbPath }) {
129
- // 1. Read config
130
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
131
- const projectName = (config.project && config.project.name) || '';
132
- const projectPrefix = (config.project && config.project.prefix) || '';
133
- const commands = config.commands || {};
134
- const paths = config.paths || {};
135
- const installedSkills = Array.isArray(config.installedSkills) ? config.installedSkills : [];
136
-
137
- const syntaxCheck = commands.syntaxCheck || '';
138
- const testCmd = commands.test || '';
139
- const buildCmd = commands.build || '';
140
- const lintCmd = commands.lint || '';
141
-
142
- // 2. Personas — exclude README.md, sort by role name
143
- const personas = [];
144
- if (fs.existsSync(personasDir)) {
145
- const files = fs.readdirSync(personasDir)
146
- .filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md')
147
- .sort();
148
-
149
- for (const file of files) {
150
- const role = path.basename(file, '.md');
151
- const filePath = path.join(personasDir, file);
152
- const content = fs.readFileSync(filePath, 'utf8');
153
- const symbol = extractPersonaSymbol(content);
154
- // One-liner: first non-blank, non-YAML, non-heading line after the opening block
155
- const bodyLines = content.split('\n').filter(l => {
156
- const t = l.trim();
157
- return t && !t.startsWith('#') && !t.startsWith('---') && !t.match(/^\w+:/);
158
- });
159
- const oneLiner = bodyLines[0] ? bodyLines[0].trim() : '';
160
- personas.push({ role, file: path.join(personasDir, file), symbol, oneLiner });
161
- }
162
- }
163
-
164
- // 3. Templates — stems only, exclude README.md, sort
165
- const templates = [];
166
- if (fs.existsSync(templatesDir)) {
167
- const files = fs.readdirSync(templatesDir)
168
- .filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md')
169
- .sort();
170
- for (const f of files) {
171
- templates.push(path.basename(f, '.md'));
172
- }
173
- }
174
-
175
- // 4. Architecture docs — filenames only, sorted
176
- const archDir = path.join(kbPath, 'architecture');
177
- const architectureDocs = [];
178
- if (fs.existsSync(archDir)) {
179
- const files = fs.readdirSync(archDir)
180
- .filter(f => f.endsWith('.md'))
181
- .sort();
182
- architectureDocs.push(...files);
183
- }
184
-
185
- // 5. Domain entities — from MASTER_INDEX.md
186
- const masterIndexPath = path.join(kbPath, 'MASTER_INDEX.md');
187
- let entities = [];
188
- if (fs.existsSync(masterIndexPath)) {
189
- entities = parseEntities(fs.readFileSync(masterIndexPath, 'utf8'));
190
- }
191
-
192
- // 6. Skill wiring
193
- const skillWiring = installedSkills
194
- .filter(s => SKILL_PERSONA_MAP[s])
195
- .map(s => ({ skill: s, personas: SKILL_PERSONA_MAP[s] }))
196
- .sort((a, b) => a.skill.localeCompare(b.skill));
197
-
198
- // ── Build JSON ──────────────────────────────────────────────────────────────
199
- const json = {
200
- project: { name: projectName, prefix: projectPrefix },
201
- commands: { syntaxCheck, test: testCmd, build: buildCmd, lint: lintCmd },
202
- paths,
203
- personas,
204
- templates,
205
- architectureDocs,
206
- entities,
207
- skillWiring,
208
- };
209
-
210
- // ── Build Markdown ──────────────────────────────────────────────────────────
211
- const lines = [];
212
-
213
- lines.push(`# ${projectName || 'Project'} — Init Context`);
214
- lines.push('');
215
-
216
- // Commands
217
- lines.push('## Commands');
218
- lines.push(`{SYNTAX_CHECK} = ${syntaxCheck}`);
219
- lines.push(`{TEST_COMMAND} = ${testCmd}`);
220
- lines.push(`{BUILD_COMMAND} = ${buildCmd}`);
221
- lines.push(`{LINT_COMMAND} = ${lintCmd}`);
222
- lines.push('');
223
-
224
- // Paths
225
- lines.push('## Paths');
226
- for (const [key, val] of Object.entries(paths).sort(([a], [b]) => a.localeCompare(b))) {
227
- lines.push(`${key.padEnd(12)} = ${val}`);
228
- }
229
- lines.push('');
230
-
231
- // Personas
232
- lines.push('## Personas');
233
- for (const p of personas) {
234
- lines.push(`${p.role} | ${p.file} | ${p.symbol} | ${p.oneLiner}`);
235
- }
236
- lines.push('');
237
-
238
- // Templates
239
- lines.push('## Templates');
240
- lines.push(templates.join(', '));
241
- lines.push('');
242
-
243
- // Architecture Docs
244
- lines.push('## Architecture Docs');
245
- lines.push(architectureDocs.join(', '));
246
- lines.push('');
247
-
248
- // Domain Entities
249
- lines.push('## Domain Entities');
250
- lines.push(entities.join(', '));
251
- lines.push('');
252
-
253
- // Installed Skill Wiring
254
- lines.push('## Installed Skill Wiring');
255
- if (skillWiring.length === 0) {
256
- lines.push('(none)');
257
- } else {
258
- for (const { skill, personas: ps } of skillWiring) {
259
- lines.push(`${skill} → ${ps.join(', ')}`);
260
- }
261
- }
262
- lines.push('');
263
-
264
- const markdown = lines.join('\n');
265
-
266
- return { markdown, json };
267
- }
268
-
269
- // ── CLI entry point ───────────────────────────────────────────────────────────
270
-
271
- function parseCliArgs(argv) {
272
- const args = {};
273
- for (let i = 2; i < argv.length; i++) {
274
- if (argv[i].startsWith('--') && argv[i + 1] && !argv[i + 1].startsWith('--')) {
275
- args[argv[i].slice(2)] = argv[i + 1];
276
- i++;
277
- }
278
- }
279
- return args;
280
- }
281
-
282
- if (require.main === module) {
283
- const args = parseCliArgs(process.argv);
284
-
285
- const configPath = args['config'];
286
- const personasDir = args['personas'];
287
- const templatesDir = args['templates'];
288
- const kbPath = args['kb'];
289
- const outMd = args['out'];
290
- const outJson = args['json-out'];
291
-
292
- if (!configPath || !personasDir || !templatesDir || !kbPath || !outMd) {
293
- process.stderr.write(
294
- 'Usage: node build-init-context.cjs ' +
295
- '--config <path> --personas <dir> --templates <dir> --kb <dir> --out <md-path> ' +
296
- '[--json-out <json-path>]\n'
297
- );
298
- process.exit(1);
299
- }
300
-
301
- try {
302
- const { markdown, json } = buildBrief({ configPath, personasDir, templatesDir, kbPath });
303
- fs.mkdirSync(path.dirname(path.resolve(outMd)), { recursive: true });
304
- fs.writeFileSync(outMd, markdown, 'utf8');
305
-
306
- const jsonPath = outJson || outMd.replace(/\.md$/, '.json');
307
- fs.writeFileSync(jsonPath, JSON.stringify(json, null, 2) + '\n', 'utf8');
308
-
309
- // Print summary line to stdout (consumed by Phase 7 orchestrator)
310
- const nPersonas = json.personas.length;
311
- const nTemplates = json.templates.length;
312
- const nDocs = json.architectureDocs.length;
313
- process.stdout.write(
314
- `\u25CB Brief written \u2014 ${nPersonas} personas, ${nTemplates} templates, ${nDocs} architecture docs\n`
315
- );
316
- } catch (err) {
317
- process.stderr.write(`build-init-context: ${err.message}\n`);
318
- process.exit(1);
319
- }
320
- }
321
-
322
- module.exports = { buildBrief, extractPersonaSymbol, parseEntities };