@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,350 +0,0 @@
1
- 'use strict';
2
-
3
- // preflight-gate.cjs — evaluates a phase's declared gates against the current
4
- // task state, pre-spawn. Returns { ok, missing[] } so the orchestrator (or a
5
- // manual command) can halt loudly before invoking a subagent on broken
6
- // prerequisites.
7
- //
8
- // Pure function: only fs.existsSync / fs.statSync / fs.readFileSync. No writes,
9
- // no network, no process spawns.
10
-
11
- const fs = require('node:fs');
12
- const path = require('node:path');
13
- const { parseVerdict } = require('./parse-verdict.cjs');
14
-
15
- function preflight({ phase, gates, state = {}, substitutions = {}, verdictSources = {} }) {
16
- const spec = gates && gates[phase];
17
- if (!spec) {
18
- return { ok: false, missing: [`no gate definition registered for phase "${phase}" (unknown phase or missing gates block)`] };
19
- }
20
-
21
- const missing = [];
22
-
23
- for (const art of spec.artifacts || []) {
24
- const resolved = applySubstitutions(art.path, substitutions);
25
- let exists = false;
26
- let size = 0;
27
- try {
28
- const st = fs.statSync(resolved);
29
- exists = st.isFile();
30
- size = st.size;
31
- } catch (_) {
32
- exists = false;
33
- }
34
- if (!exists) {
35
- missing.push(`artifact missing: ${resolved}`);
36
- } else if (size < (art.minBytes || 0)) {
37
- missing.push(`artifact too small (stub): ${resolved} (${size} bytes, need >= ${art.minBytes})`);
38
- }
39
- }
40
-
41
- for (const pred of spec.require || []) {
42
- if (!evalPredicate(pred, state)) {
43
- missing.push(`require failed: ${describePredicate(pred)} (got ${JSON.stringify(readField(pred.field, state))})`);
44
- }
45
- }
46
-
47
- for (const pred of spec.forbid || []) {
48
- if (evalPredicate(pred, state)) {
49
- missing.push(`forbid triggered: ${describePredicate(pred)}`);
50
- }
51
- }
52
-
53
- for (const after of spec.after || []) {
54
- const src = verdictSources[after.phase];
55
- if (!src) {
56
- missing.push(`predecessor verdict source not provided for phase "${after.phase}"`);
57
- continue;
58
- }
59
- let contents;
60
- try {
61
- contents = fs.readFileSync(src, 'utf8');
62
- } catch (err) {
63
- missing.push(`cannot read predecessor review for "${after.phase}" at ${src}: ${err.code || err.message}`);
64
- continue;
65
- }
66
- const verdict = parseVerdict(contents);
67
- if (verdict === null) {
68
- missing.push(`predecessor review for "${after.phase}" has no parseable **Verdict:** line (${src})`);
69
- } else if (verdict !== after.verdict) {
70
- missing.push(`predecessor "${after.phase}" verdict is "${verdict}", expected "${after.verdict}"`);
71
- }
72
- }
73
-
74
- return { ok: missing.length === 0, missing };
75
- }
76
-
77
- function applySubstitutions(template, subs) {
78
- return template.replace(/\{(\w+)\}/g, (full, key) => {
79
- if (Object.prototype.hasOwnProperty.call(subs, key)) return String(subs[key]);
80
- return full;
81
- });
82
- }
83
-
84
- function readField(dottedPath, state) {
85
- const parts = dottedPath.split('.');
86
- let cur = state;
87
- for (const p of parts) {
88
- if (cur === null || cur === undefined) return undefined;
89
- cur = cur[p];
90
- }
91
- return cur;
92
- }
93
-
94
- function evalPredicate(pred, state) {
95
- const actual = readField(pred.field, state);
96
- switch (pred.op) {
97
- case '==':
98
- return String(actual) === String(pred.value);
99
- case '!=':
100
- return String(actual) !== String(pred.value);
101
- case 'in':
102
- return pred.value.map(String).includes(String(actual));
103
- default:
104
- throw new Error(`preflight-gate: unknown predicate op "${pred.op}"`);
105
- }
106
- }
107
-
108
- function describePredicate(pred) {
109
- if (pred.op === 'in') return `${pred.field} in [${pred.value.join(', ')}]`;
110
- return `${pred.field} ${pred.op} ${pred.value}`;
111
- }
112
-
113
- // Canonical review artifact filenames per phase. Centralised here so the
114
- // orchestrator, manual commands, and tests all agree on where a given phase's
115
- // verdict lives.
116
- const VERDICT_ARTIFACTS = {
117
- 'review-plan': 'PLAN_REVIEW.md',
118
- 'review-code': 'CODE_REVIEW.md',
119
- 'validate': 'VALIDATION_REPORT.md',
120
- 'approve': 'ARCHITECT_APPROVAL.md',
121
- };
122
-
123
- module.exports = { preflight };
124
-
125
- // CLI shim: `node preflight-gate.cjs --phase <name> --task <taskId> [--bug <bugId>] [--workflow <name>]`
126
- // exit codes: 0 ok, 1 gate(s) failed, 2 invalid args / missing definitions
127
- // Scan the sprint directory for a subdirectory matching the task ID prefix.
128
- // Returns the directory name (e.g. "FORGE-S12-T06-model-discovery") or null.
129
- function resolveTaskArtifactDir(taskRecord, engineeringRoot) {
130
- if (!taskRecord || !taskRecord.sprintId || !taskRecord.taskId) return null;
131
- const sprintDir = path.resolve(process.cwd(), engineeringRoot, 'sprints', taskRecord.sprintId);
132
- try {
133
- const entries = fs.readdirSync(sprintDir);
134
- for (const entry of entries) {
135
- try {
136
- if (fs.statSync(path.join(sprintDir, entry)).isDirectory() &&
137
- entry.startsWith(taskRecord.taskId + '-')) {
138
- return entry;
139
- }
140
- } catch (_) { /* skip unreadable entries */ }
141
- }
142
- } catch (_) { /* sprint directory not found */ }
143
- return null;
144
- }
145
-
146
- if (require.main === module) {
147
- const args = parseArgs(process.argv.slice(2));
148
- if (!args.phase || (!args.task && !args.bug)) {
149
- process.stderr.write('Usage: preflight-gate.cjs --phase <phaseName> --task <taskId> [--bug <bugId>]\n');
150
- process.exit(2);
151
- }
152
-
153
- const { parseGates } = require('./parse-gates.cjs');
154
- const store = require('./store.cjs');
155
-
156
- // Resolve store records and substitutions BEFORE calling loadWorkflowMarkdown
157
- // so the placeholder-key filter can use them to select the correct workflow file.
158
- // (Previously loadWorkflowMarkdown was called first, causing fix_bug.md to shadow
159
- // orchestrate_task.md for phases shared between the two workflows — forge#72.)
160
- const taskRecord = args.task ? safe(() => store.getTask(args.task)) : null;
161
- const bugRecord = args.bug ? safe(() => store.getBug(args.bug)) : null;
162
- const state = {};
163
- if (taskRecord) state.task = taskRecord;
164
- if (bugRecord) state.bug = bugRecord;
165
-
166
- // Resolve engineering root from config; path templates can reference {engineering}.
167
- let engineeringRoot = 'engineering';
168
- try {
169
- const cfg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), '.forge/config.json'), 'utf8'));
170
- if (cfg.paths && cfg.paths.engineering) engineeringRoot = cfg.paths.engineering;
171
- } catch (_) { /* fall back to default */ }
172
-
173
- // {task} / {bug} in path templates refer to the artifact directory suffix
174
- // (e.g., "FORGE-S12-T06-model-discovery", "BUG-007-broken-foo").
175
- // task.path is the primary source file in forge/, NOT the artifact directory.
176
- // Scan the sprint directory to find the correct artifact directory name.
177
- function lastSegment(p) {
178
- const parts = String(p || '').split('/').filter(Boolean);
179
- return parts[parts.length - 1] || '';
180
- }
181
- const taskArtifactDir = resolveTaskArtifactDir(taskRecord, engineeringRoot);
182
- const taskDir = taskArtifactDir
183
- || (taskRecord && taskRecord.path ? lastSegment(taskRecord.path) : args.task);
184
- const bugDir = bugRecord && bugRecord.path ? lastSegment(bugRecord.path) : args.bug;
185
-
186
- // Compute the full artifact directory path for verdict source resolution.
187
- let taskArtifactPath = null;
188
- if (taskArtifactDir && taskRecord && taskRecord.sprintId) {
189
- taskArtifactPath = path.join(engineeringRoot, 'sprints', taskRecord.sprintId, taskArtifactDir);
190
- } else if (taskRecord && taskRecord.path) {
191
- taskArtifactPath = taskRecord.path;
192
- }
193
-
194
- const substitutions = {
195
- engineering: engineeringRoot,
196
- sprint: taskRecord ? taskRecord.sprintId : undefined,
197
- task: taskDir,
198
- bug: bugDir,
199
- };
200
-
201
- // Now load the workflow, passing substitutions so the placeholder-key filter
202
- // can skip workflows whose gate block references keys not present in subs.
203
- const workflowMd = loadWorkflowMarkdown(args.phase, args.workflow, substitutions);
204
- if (!workflowMd) {
205
- process.stderr.write(`preflight-gate: could not locate workflow file defining phase "${args.phase}"\n`);
206
- process.exit(2);
207
- }
208
- let gates;
209
- try {
210
- gates = parseGates(workflowMd);
211
- } catch (err) {
212
- process.stderr.write(`preflight-gate: ${err.message}\n`);
213
- process.exit(2);
214
- }
215
- if (!gates[args.phase]) {
216
- process.stderr.write(`preflight-gate: no gates block for phase "${args.phase}"\n`);
217
- process.exit(2);
218
- }
219
-
220
- const verdictSources = resolveVerdictSources(gates[args.phase].after || [], taskArtifactPath, bugRecord);
221
-
222
- const result = preflight({ phase: args.phase, gates, state, substitutions, verdictSources });
223
- if (result.ok) process.exit(0);
224
- process.stderr.write(`Gate failed for phase "${args.phase}":\n`);
225
- for (const m of result.missing) process.stderr.write(` - ${m}\n`);
226
- process.exit(1);
227
- }
228
-
229
- function parseArgs(argv) {
230
- const out = {};
231
- for (let i = 0; i < argv.length; i++) {
232
- const a = argv[i];
233
- if (a === '--phase') out.phase = argv[++i];
234
- else if (a === '--task') out.task = argv[++i];
235
- else if (a === '--bug') out.bug = argv[++i];
236
- else if (a === '--workflow') out.workflow = argv[++i];
237
- }
238
- return out;
239
- }
240
-
241
- function safe(fn) {
242
- try { return fn(); } catch (_) { return null; }
243
- }
244
-
245
- // Extract the gate block body for a given phase from a workflow markdown string.
246
- // Returns the text between the opening and closing fence for that phase, or null.
247
- function extractGateBlockBody(md, phaseName) {
248
- const openPattern = new RegExp('^```gates\\s+phase=' + escapeRegex(phaseName) + '\\s*$', 'm');
249
- const openMatch = openPattern.exec(md);
250
- if (!openMatch) return null;
251
- const afterOpen = md.slice(openMatch.index + openMatch[0].length);
252
- const closeIdx = afterOpen.indexOf('\n```');
253
- if (closeIdx === -1) return afterOpen; // unterminated fence — return what we have
254
- return afterOpen.slice(0, closeIdx);
255
- }
256
-
257
- // Check whether all {placeholder} keys in a gate block body are satisfied by
258
- // the given substitutions map. Returns true if all placeholders are satisfied
259
- // (or there are none), false if any placeholder key is missing from subs.
260
- function gatePlaceholdersSatisfied(gateBody, subs) {
261
- const tokenRe = /\{(\w+)\}/g;
262
- let match;
263
- while ((match = tokenRe.exec(gateBody)) !== null) {
264
- const key = match[1];
265
- if (!Object.prototype.hasOwnProperty.call(subs, key) || subs[key] === undefined) {
266
- return false;
267
- }
268
- }
269
- return true;
270
- }
271
-
272
- function loadWorkflowMarkdown(phaseName, workflowName, substitutions) {
273
- const workflowsDir = path.resolve(process.cwd(), '.forge/workflows');
274
- let entries;
275
- try {
276
- entries = fs.readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
277
- } catch (_) {
278
- return null;
279
- }
280
- const fencePattern = new RegExp('^```gates\\s+phase=' + escapeRegex(phaseName) + '\\s*$', 'm');
281
-
282
- // If a specific workflow file was requested, try it first before scanning all files.
283
- // This prevents alphabetically-earlier files from shadowing the caller's workflow.
284
- if (workflowName) {
285
- const normalised = workflowName.endsWith('.md') ? workflowName : workflowName + '.md';
286
- if (entries.includes(normalised)) {
287
- const md = fs.readFileSync(path.join(workflowsDir, normalised), 'utf8');
288
- if (fencePattern.test(md)) return md;
289
- }
290
- }
291
-
292
- // Placeholder-key filter: when substitutions are provided, skip any workflow
293
- // whose gate block for this phase references a {key} not present in subs.
294
- // This prevents fix_bug.md (which uses {bug}) from shadowing orchestrate_task.md
295
- // (which uses {task}) when only --task is supplied and --bug is absent.
296
- // Fall-back: if NO workflow passes the filter, return the first match regardless
297
- // (preserves existing behaviour for malformed/unknown invocations).
298
- const subs = substitutions || {};
299
- const hasSubstitutions = Object.keys(subs).some(k => subs[k] !== undefined);
300
-
301
- let firstMatch = null; // fallback candidate
302
- for (const entry of entries) {
303
- const md = fs.readFileSync(path.join(workflowsDir, entry), 'utf8');
304
- if (!fencePattern.test(md)) continue;
305
-
306
- if (firstMatch === null) firstMatch = md; // remember first match for fallback
307
-
308
- if (!hasSubstitutions) {
309
- // No substitutions provided — use original first-match behaviour
310
- return md;
311
- }
312
-
313
- const gateBody = extractGateBlockBody(md, phaseName);
314
- if (gateBody !== null && gatePlaceholdersSatisfied(gateBody, subs)) {
315
- return md;
316
- }
317
- // Placeholder(s) unsatisfied — log at warn level and continue scanning
318
- process.stderr.write(
319
- `preflight-gate: skipping ${entry} for phase "${phaseName}" — gate block contains ` +
320
- `placeholder key(s) not present in supplied substitutions\n`
321
- );
322
- }
323
-
324
- // Fallback: no workflow passed the placeholder filter — return first match to
325
- // avoid total breakage for malformed invocations (preserves existing behaviour)
326
- if (firstMatch !== null) {
327
- process.stderr.write(
328
- `preflight-gate: placeholder filter matched no workflow for phase "${phaseName}" — ` +
329
- `falling back to first match\n`
330
- );
331
- return firstMatch;
332
- }
333
- return null;
334
- }
335
-
336
- function escapeRegex(s) {
337
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
338
- }
339
-
340
- function resolveVerdictSources(afterList, taskArtifactPath, bugRecord) {
341
- const sources = {};
342
- const base = taskArtifactPath || (bugRecord ? bugRecord.path : null);
343
- if (!base) return sources;
344
- for (const entry of afterList) {
345
- const filename = VERDICT_ARTIFACTS[entry.phase];
346
- if (!filename) continue; // unknown predecessor phase — preflight will flag it via missing-source
347
- sources[entry.phase] = path.resolve(process.cwd(), base, filename);
348
- }
349
- return sources;
350
- }
@@ -1,70 +0,0 @@
1
- # Sprint Plan — Architect Persona Prompt
2
-
3
- You are acting as the **Forge Architect** (🗻).
4
-
5
- Your role: decompose a sprint's requirements into a concrete, dependency-ordered task list. You are a senior technical architect who thinks carefully about sequencing, blast radius, and iron laws before writing a single line of output.
6
-
7
- ---
8
-
9
- ## Your Task
10
-
11
- Read the sprint requirements provided and produce a **JSON array** of task objects. Each task covers one coherent, implementable unit of work. Tasks must be small enough to be planned and implemented in a single sprint sub-task cycle.
12
-
13
- ---
14
-
15
- ## Output Format
16
-
17
- Emit **only** a JSON array — no preamble, no markdown fences, no commentary outside the JSON. The schema:
18
-
19
- ```json
20
- [
21
- {
22
- "taskId": "SPRINT_ID-T01",
23
- "title": "Short imperative title (≤80 chars)",
24
- "estimate": "S",
25
- "dependencies": [],
26
- "pipeline": "plan,implement,review,validate,approve,commit",
27
- "acceptanceCriteria": [
28
- "Specific, verifiable criterion 1",
29
- "Specific, verifiable criterion 2"
30
- ]
31
- }
32
- ]
33
- ```
34
-
35
- ### Field rules
36
-
37
- | Field | Type | Rule |
38
- |---|---|---|
39
- | `taskId` | string | `{SPRINT_ID}-T{NN}` — sequential integers starting at 01. Use the sprint ID from the requirements. |
40
- | `title` | string | Imperative phrase, ≤80 chars. No trailing punctuation. |
41
- | `estimate` | enum | One of: `S` (≤4h), `M` (≤1d), `L` (≤2d), `XL` (≤5d). |
42
- | `dependencies` | string[] | Task IDs of tasks this task depends on. Empty array if none. No circular deps. |
43
- | `pipeline` | string | Default: `"plan,implement,review,validate,approve,commit"`. Adjust only for trivial chores. |
44
- | `acceptanceCriteria` | string[] | 2–8 specific, verifiable criteria. Each criterion must be independently checkable. No vague phrases like "works correctly". |
45
-
46
- ### Dependency rules
47
-
48
- - Dependencies form a DAG (directed acyclic graph). Circular dependencies are invalid.
49
- - A release/packaging task always depends on all feature tasks.
50
- - Tasks that share no code boundary may be declared parallel (no mutual dep).
51
- - Order tasks so lower-numbered tasks are prerequisites for higher-numbered tasks where possible.
52
-
53
- ---
54
-
55
- ## Quality Checklist
56
-
57
- Before emitting output, verify:
58
-
59
- - [ ] Every `taskId` is unique and follows the `{SPRINT_ID}-T{NN}` pattern
60
- - [ ] No circular dependencies
61
- - [ ] Each task has at least 2 acceptance criteria
62
- - [ ] Estimates are realistic (L tasks should not attempt to cover more than 2 full days of work)
63
- - [ ] A release or packaging task (if applicable) is listed last and depends on all feature tasks
64
- - [ ] The JSON is valid — parseable without error
65
-
66
- ---
67
-
68
- ## Sprint Requirements
69
-
70
- {SPRINT_REQUIREMENTS}
@@ -1,53 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "TaskList",
4
- "description": "JSON Schema for the LLM-generated task list produced by forge:sprint-plan.",
5
- "type": "array",
6
- "minItems": 1,
7
- "items": {
8
- "type": "object",
9
- "required": ["taskId", "title", "estimate", "dependencies", "pipeline", "acceptanceCriteria"],
10
- "additionalProperties": false,
11
- "properties": {
12
- "taskId": {
13
- "type": "string",
14
- "pattern": "^[A-Z][A-Z0-9_-]+-T[0-9]+$",
15
- "description": "Task identifier in the form {SPRINT_ID}-T{NN}, e.g. FORGE-S20-T01"
16
- },
17
- "title": {
18
- "type": "string",
19
- "minLength": 1,
20
- "maxLength": 80,
21
- "description": "Short imperative title for the task"
22
- },
23
- "estimate": {
24
- "type": "string",
25
- "enum": ["S", "M", "L", "XL"],
26
- "description": "Size estimate: S (≤4h), M (≤1d), L (≤2d), XL (≤5d)"
27
- },
28
- "dependencies": {
29
- "type": "array",
30
- "items": {
31
- "type": "string",
32
- "pattern": "^[A-Z][A-Z0-9_-]+-T[0-9]+$"
33
- },
34
- "description": "Task IDs this task depends on. Empty array if no dependencies."
35
- },
36
- "pipeline": {
37
- "type": "string",
38
- "minLength": 1,
39
- "description": "Comma-separated pipeline phase list, e.g. plan,implement,review,validate,approve,commit"
40
- },
41
- "acceptanceCriteria": {
42
- "type": "array",
43
- "minItems": 2,
44
- "maxItems": 12,
45
- "items": {
46
- "type": "string",
47
- "minLength": 10
48
- },
49
- "description": "Specific, verifiable acceptance criteria (2–12 items)"
50
- }
51
- }
52
- }
53
- }