@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.
- package/CHANGELOG.md +53 -0
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +17 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/config.d.ts +69 -0
- package/dist/bin/config.js +315 -0
- package/dist/bin/config.js.map +1 -0
- package/dist/bin/doctor.d.ts +1 -0
- package/dist/bin/doctor.js +12 -0
- package/dist/bin/doctor.js.map +1 -1
- package/dist/bin/forge.js +7 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/config-command.js +66 -0
- package/dist/extensions/forgecli/config-command.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +38 -0
- package/dist/extensions/forgecli/config-layer.js +68 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
- package/dist/extensions/forgecli/config-tui/component.js +236 -0
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
- package/dist/extensions/forgecli/config-tui/handler.js +240 -0
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
- package/dist/extensions/forgecli/config-tui/index.js +5 -0
- package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
- package/dist/extensions/forgecli/config-tui/keys.js +33 -0
- package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens.js +78 -0
- package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
- package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
- package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
- package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
- package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state.js +11 -0
- package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
- package/dist/extensions/forgecli/config-tui/theme.js +88 -0
- package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
- package/dist/extensions/forgecli/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config-writer.js +63 -0
- package/dist/extensions/forgecli/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +85 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
- package/dist/extensions/forgecli/forge-commands.js +3 -8
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
- package/dist/extensions/forgecli/forge-subagent.js +19 -0
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/index.js +16 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/input-router.js +133 -0
- package/dist/extensions/forgecli/input-router.js.map +1 -0
- package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/model-resolver.js +65 -0
- package/dist/extensions/forgecli/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/model-validator.js +107 -0
- package/dist/extensions/forgecli/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +59 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +93 -1
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.js +5 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/whats-new-widget.js +5 -2
- package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
- package/package.json +11 -3
- package/dist/extensions/forgecli/review-command.d.ts +0 -2
- package/dist/extensions/forgecli/review-command.js +0 -184
- package/dist/extensions/forgecli/review-command.js.map +0 -1
- package/dist/forge-payload/.tools/banners.cjs +0 -435
- package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
- package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
- package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
- package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
- package/dist/forge-payload/.tools/collate.cjs +0 -1041
- package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
- package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
- package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
- package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
- package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
- package/dist/forge-payload/.tools/lib/result.js +0 -40
- package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
- package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
- package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
- package/dist/forge-payload/.tools/lib/validate.js +0 -141
- package/dist/forge-payload/.tools/manage-config.cjs +0 -340
- package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
- package/dist/forge-payload/.tools/package.json +0 -3
- package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
- package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
- package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
- package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
- package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
- package/dist/forge-payload/.tools/seed-store.cjs +0 -237
- package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
- package/dist/forge-payload/.tools/store-query.cjs +0 -319
- package/dist/forge-payload/.tools/store.cjs +0 -315
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
- package/dist/forge-payload/.tools/validate-store.cjs +0 -593
|
@@ -1,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 };
|