@entelligentsia/forgecli 1.0.10 → 1.0.14

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 (167) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/CHANGELOG-forge-plugin.md +150 -0
  3. package/dist/bin/forge.js +0 -0
  4. package/dist/extensions/forgecli/config-layer.d.ts +16 -0
  5. package/dist/extensions/forgecli/config-layer.js +5 -0
  6. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  7. package/dist/extensions/forgecli/dashboard/component.d.ts +102 -0
  8. package/dist/extensions/forgecli/dashboard/component.js +882 -0
  9. package/dist/extensions/forgecli/dashboard/component.js.map +1 -0
  10. package/dist/extensions/forgecli/dashboard/register.d.ts +2 -0
  11. package/dist/extensions/forgecli/dashboard/register.js +45 -0
  12. package/dist/extensions/forgecli/dashboard/register.js.map +1 -0
  13. package/dist/extensions/forgecli/dashboard/view-model.d.ts +35 -0
  14. package/dist/extensions/forgecli/dashboard/view-model.js +54 -0
  15. package/dist/extensions/forgecli/dashboard/view-model.js.map +1 -0
  16. package/dist/extensions/forgecli/fix-bug.js +72 -7
  17. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  18. package/dist/extensions/forgecli/forge-cli-schema.json +4 -0
  19. package/dist/extensions/forgecli/forge-commands.js +1 -0
  20. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  21. package/dist/extensions/forgecli/forge-init/phase4-register.js +53 -0
  22. package/dist/extensions/forgecli/forge-init/phase4-register.js.map +1 -1
  23. package/dist/extensions/forgecli/forge-subagent.js +6 -4
  24. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  25. package/dist/extensions/forgecli/index.js +5 -0
  26. package/dist/extensions/forgecli/index.js.map +1 -1
  27. package/dist/extensions/forgecli/lib/halt-advisor.d.ts +54 -0
  28. package/dist/extensions/forgecli/lib/halt-advisor.js +90 -0
  29. package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -0
  30. package/dist/extensions/forgecli/migration-engine.js +25 -12
  31. package/dist/extensions/forgecli/migration-engine.js.map +1 -1
  32. package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +25 -0
  33. package/dist/extensions/forgecli/orchestrator-status-bar.js +183 -0
  34. package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -0
  35. package/dist/extensions/forgecli/orchestrator-tree.d.ts +96 -0
  36. package/dist/extensions/forgecli/orchestrator-tree.js +390 -0
  37. package/dist/extensions/forgecli/orchestrator-tree.js.map +1 -0
  38. package/dist/extensions/forgecli/project-orientation.js +12 -8
  39. package/dist/extensions/forgecli/project-orientation.js.map +1 -1
  40. package/dist/extensions/forgecli/regenerate.d.ts +16 -0
  41. package/dist/extensions/forgecli/regenerate.js +110 -0
  42. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  43. package/dist/extensions/forgecli/run-sprint.js +33 -3
  44. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  45. package/dist/extensions/forgecli/run-task.d.ts +32 -0
  46. package/dist/extensions/forgecli/run-task.js +185 -12
  47. package/dist/extensions/forgecli/run-task.js.map +1 -1
  48. package/dist/extensions/forgecli/thread-switcher.js +105 -764
  49. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  50. package/dist/extensions/forgecli/viewport-events.js +32 -0
  51. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  52. package/dist/forge-payload/.base-pack/commands/fix-bug.md +1 -1
  53. package/dist/forge-payload/.base-pack/commands/run-sprint.md +1 -1
  54. package/dist/forge-payload/.base-pack/commands/run-task.md +1 -1
  55. package/dist/forge-payload/.base-pack/personas/architect.md +1 -1
  56. package/dist/forge-payload/.base-pack/personas/bug-fixer.md +1 -1
  57. package/dist/forge-payload/.base-pack/personas/collator.md +3 -3
  58. package/dist/forge-payload/.base-pack/personas/engineer.md +1 -1
  59. package/dist/forge-payload/.base-pack/personas/librarian.md +1 -1
  60. package/dist/forge-payload/.base-pack/personas/orchestrator.md +1 -1
  61. package/dist/forge-payload/.base-pack/personas/product-manager.md +1 -1
  62. package/dist/forge-payload/.base-pack/personas/qa-engineer.md +1 -1
  63. package/dist/forge-payload/.base-pack/personas/supervisor.md +1 -1
  64. package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +1 -1
  65. package/dist/forge-payload/.base-pack/workflows/_fragments/friction-emit.md +1 -1
  66. package/dist/forge-payload/.base-pack/workflows/_fragments/iron-laws.md +1 -1
  67. package/dist/forge-payload/.base-pack/workflows/_fragments/progress-reporting.md +2 -2
  68. package/dist/forge-payload/.base-pack/workflows/_fragments/store-cli-verbs.md +11 -2
  69. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +6 -7
  70. package/dist/forge-payload/.base-pack/workflows/architect_review_sprint_completion.md +2 -2
  71. package/dist/forge-payload/.base-pack/workflows/architect_sprint_intake.md +2 -2
  72. package/dist/forge-payload/.base-pack/workflows/architect_sprint_plan.md +5 -5
  73. package/dist/forge-payload/.base-pack/workflows/collator_agent.md +4 -6
  74. package/dist/forge-payload/.base-pack/workflows/commit_task.md +5 -6
  75. package/dist/forge-payload/.base-pack/workflows/enhance.md +5 -5
  76. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +6 -7
  77. package/dist/forge-payload/.base-pack/workflows/migrate_structural.md +12 -13
  78. package/dist/forge-payload/.base-pack/workflows/plan_task.md +5 -6
  79. package/dist/forge-payload/.base-pack/workflows/review_code.md +8 -8
  80. package/dist/forge-payload/.base-pack/workflows/review_plan.md +8 -8
  81. package/dist/forge-payload/.base-pack/workflows/sprint_retrospective.md +3 -3
  82. package/dist/forge-payload/.base-pack/workflows/triage.md +12 -9
  83. package/dist/forge-payload/.base-pack/workflows/update_implementation.md +2 -2
  84. package/dist/forge-payload/.base-pack/workflows/update_plan.md +2 -2
  85. package/dist/forge-payload/.base-pack/workflows/validate_task.md +5 -6
  86. package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +490 -0
  87. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-sprint.js +416 -0
  88. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +608 -0
  89. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  90. package/dist/forge-payload/.schemas/config.schema.json +2 -3
  91. package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
  92. package/dist/forge-payload/.schemas/event.schema.json +16 -0
  93. package/dist/forge-payload/.schemas/migrations.json +236 -0
  94. package/dist/forge-payload/commands/health.md +29 -0
  95. package/dist/forge-payload/commands/rebuild.md +143 -15
  96. package/dist/forge-payload/commands/update.md +28 -27
  97. package/dist/forge-payload/hooks/preflight-session.cjs +99 -0
  98. package/dist/forge-payload/init/phases/phase-3-materialize.md +18 -5
  99. package/dist/forge-payload/integrity.json +7 -6
  100. package/dist/forge-payload/meta/fragments/tool-discipline.md +1 -1
  101. package/dist/forge-payload/meta/personas/meta-architect.md +1 -1
  102. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +1 -1
  103. package/dist/forge-payload/meta/personas/meta-collator.md +7 -7
  104. package/dist/forge-payload/meta/personas/meta-engineer.md +1 -1
  105. package/dist/forge-payload/meta/personas/meta-orchestrator.md +1 -1
  106. package/dist/forge-payload/meta/personas/meta-supervisor.md +1 -1
  107. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +1 -1
  108. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +1 -1
  109. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +1 -1
  110. package/dist/forge-payload/meta/workflows/_fragments/iron-laws.md +1 -1
  111. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +2 -2
  112. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +11 -2
  113. package/dist/forge-payload/meta/workflows/meta-approve.md +6 -7
  114. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +12 -9
  115. package/dist/forge-payload/meta/workflows/meta-collate.md +5 -7
  116. package/dist/forge-payload/meta/workflows/meta-commit.md +5 -6
  117. package/dist/forge-payload/meta/workflows/meta-enhance.md +5 -5
  118. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +35 -11
  119. package/dist/forge-payload/meta/workflows/meta-implement.md +15 -7
  120. package/dist/forge-payload/meta/workflows/meta-migrate.md +13 -14
  121. package/dist/forge-payload/meta/workflows/meta-new-sprint.md +3 -3
  122. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +138 -39
  123. package/dist/forge-payload/meta/workflows/meta-plan-sprint.md +6 -6
  124. package/dist/forge-payload/meta/workflows/meta-plan-task.md +12 -6
  125. package/dist/forge-payload/meta/workflows/meta-retro.md +4 -4
  126. package/dist/forge-payload/meta/workflows/meta-retrospective.md +4 -4
  127. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +8 -8
  128. package/dist/forge-payload/meta/workflows/meta-review-plan.md +8 -8
  129. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +3 -3
  130. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +3 -3
  131. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +6 -6
  132. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +2 -2
  133. package/dist/forge-payload/meta/workflows/meta-update-plan.md +2 -2
  134. package/dist/forge-payload/meta/workflows/meta-validate.md +5 -6
  135. package/dist/forge-payload/schemas/config.schema.json +2 -3
  136. package/dist/forge-payload/schemas/enum-catalog.json +2 -2
  137. package/dist/forge-payload/schemas/event.schema.json +16 -0
  138. package/dist/forge-payload/schemas/structure-manifest.json +75 -73
  139. package/dist/forge-payload/skills/refresh-kb-links/SKILL.md +14 -7
  140. package/dist/forge-payload/tools/banners.cjs +29 -10
  141. package/dist/forge-payload/tools/check-structure.cjs +88 -7
  142. package/dist/forge-payload/tools/collate.cjs +16 -2
  143. package/dist/forge-payload/tools/manage-config.cjs +5 -7
  144. package/dist/forge-payload/tools/parse-gates.cjs +73 -1
  145. package/dist/forge-payload/tools/postflight-gate.cjs +252 -0
  146. package/dist/forge-payload/tools/preflight-gate.cjs +47 -0
  147. package/dist/forge-payload/tools/substitute-placeholders.cjs +5 -4
  148. package/dist/forge-payload/tools/verify-phase.cjs +17 -0
  149. package/package.json +1 -1
  150. package/dist/bin/forgecli.d.ts +0 -2
  151. package/dist/bin/forgecli.js +0 -6
  152. package/dist/bin/forgecli.js.map +0 -1
  153. package/dist/extensions/forgecli/config-tui/index.d.ts +0 -5
  154. package/dist/extensions/forgecli/config-tui/index.js +0 -5
  155. package/dist/extensions/forgecli/config-tui/index.js.map +0 -1
  156. package/dist/extensions/forgecli/loaders/persona-skill-loader.d.ts +0 -45
  157. package/dist/extensions/forgecli/loaders/persona-skill-loader.js +0 -227
  158. package/dist/extensions/forgecli/loaders/persona-skill-loader.js.map +0 -1
  159. package/dist/extensions/forgecli/loaders/template-render.d.ts +0 -20
  160. package/dist/extensions/forgecli/loaders/template-render.js +0 -85
  161. package/dist/extensions/forgecli/loaders/template-render.js.map +0 -1
  162. package/dist/extensions/forgecli/loaders/workflow-loader.d.ts +0 -41
  163. package/dist/extensions/forgecli/loaders/workflow-loader.js +0 -164
  164. package/dist/extensions/forgecli/loaders/workflow-loader.js.map +0 -1
  165. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +0 -446
  166. package/dist/forge-payload/.base-pack/workflows/orchestrate_task.md +0 -928
  167. package/dist/forge-payload/.base-pack/workflows/run_sprint.md +0 -225
@@ -132,9 +132,17 @@ function validateManifest(manifest, forgeRoot) {
132
132
  fragments: path.join(basePackDir, 'workflows', '_fragments'),
133
133
  templates: path.join(basePackDir, 'templates'),
134
134
  commands: path.join(basePackDir, 'commands'),
135
+ 'workflows-js': path.join(basePackDir, 'workflows-js'),
136
+ // tools: not base-pack-sourced — source is forgeRoot/tools/ (verbatim vendor).
137
+ // Uses recursive enumeration to include lib/*.cjs with the lib/ prefix.
138
+ tools: path.join(forgeRoot, 'tools'),
135
139
  // schemas: not base-pack-sourced — source is forgeRoot/schemas/
136
140
  };
137
141
 
142
+ // Namespaces that require recursive enumeration (files may include path prefixes
143
+ // like lib/*.cjs). The walker returns relative paths with forward slashes.
144
+ const recursiveNs = new Set(['tools']);
145
+
138
146
  const manifestOnly = []; // Files in manifest but not in base-pack
139
147
  const basePackOnly = []; // Files in base-pack but not in manifest
140
148
 
@@ -147,14 +155,31 @@ function validateManifest(manifest, forgeRoot) {
147
155
 
148
156
  const manifestFiles = new Set(ns.files || []);
149
157
 
150
- // Read base-pack source directory
158
+ // Read base-pack source directory (flat or recursive depending on namespace)
151
159
  let basePackFiles = [];
152
160
  try {
153
- basePackFiles = fs.readdirSync(sourceDir).filter(f => {
154
- // Only include regular files, skip subdirectories (like _fragments under workflows)
155
- const stat = fs.statSync(path.join(sourceDir, f));
156
- return stat.isFile();
157
- });
161
+ if (recursiveNs.has(nsKey)) {
162
+ // Recursive walk: enumerate all files, returning paths relative to sourceDir
163
+ // (with forward-slash separators). Excludes *.test.cjs files.
164
+ const walkDir = (dir, prefix) => {
165
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
166
+ for (const entry of entries) {
167
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
168
+ if (entry.isDirectory()) {
169
+ walkDir(path.join(dir, entry.name), relPath);
170
+ } else if (entry.isFile() && !entry.name.endsWith('.test.cjs')) {
171
+ basePackFiles.push(relPath);
172
+ }
173
+ }
174
+ };
175
+ walkDir(sourceDir, '');
176
+ } else {
177
+ basePackFiles = fs.readdirSync(sourceDir).filter(f => {
178
+ // Only include regular files, skip subdirectories (like _fragments under workflows)
179
+ const stat = fs.statSync(path.join(sourceDir, f));
180
+ return stat.isFile();
181
+ });
182
+ }
158
183
  } catch {
159
184
  // Directory doesn't exist — all manifest files are manifestOnly
160
185
  for (const f of manifestFiles) {
@@ -183,9 +208,65 @@ function validateManifest(manifest, forgeRoot) {
183
208
  return { manifestOnly, basePackOnly };
184
209
  }
185
210
 
211
+ // Checks whether the vendored .forge/tools/ directory is present, and whether
212
+ // the version marker (.forge/tools/.forge-tools-version) matches the active
213
+ // plugin version from .forge/config.json (paths.forgeRef).
214
+ //
215
+ // Returns:
216
+ // { present, vendoredVersion, activeVersion, stale, reason }
217
+ //
218
+ // present — true if .forge/tools/ directory exists
219
+ // vendoredVersion — version string from .forge-tools-version (or null)
220
+ // activeVersion — version string from paths.forgeRef in config (or null)
221
+ // stale — true when: dir absent=false; marker absent=true; versions differ=true
222
+ // reason — 'ok' | 'missing' | 'marker-absent' | 'version-mismatch'
223
+ function checkToolsVersion(projectRoot) {
224
+ const toolsDir = path.join(projectRoot, '.forge', 'tools');
225
+ const markerFile = path.join(toolsDir, '.forge-tools-version');
226
+ const configFile = path.join(projectRoot, '.forge', 'config.json');
227
+
228
+ // Dir absent → not stale (just missing)
229
+ if (!fs.existsSync(toolsDir)) {
230
+ return { present: false, vendoredVersion: null, activeVersion: null, stale: false, reason: 'missing' };
231
+ }
232
+
233
+ // Read active version from config
234
+ let activeVersion = null;
235
+ if (fs.existsSync(configFile)) {
236
+ try {
237
+ const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
238
+ activeVersion = (cfg && cfg.paths && cfg.paths.forgeRef) ? cfg.paths.forgeRef : null;
239
+ } catch {
240
+ // unparseable — leave activeVersion null
241
+ }
242
+ }
243
+
244
+ // Marker absent → stale
245
+ if (!fs.existsSync(markerFile)) {
246
+ return { present: true, vendoredVersion: null, activeVersion, stale: true, reason: 'marker-absent' };
247
+ }
248
+
249
+ // Read vendored version from marker
250
+ let vendoredVersion = null;
251
+ try {
252
+ const marker = JSON.parse(fs.readFileSync(markerFile, 'utf8'));
253
+ vendoredVersion = (marker && marker.version) ? marker.version : null;
254
+ } catch {
255
+ // unparseable marker → treat as marker-absent
256
+ return { present: true, vendoredVersion: null, activeVersion, stale: true, reason: 'marker-absent' };
257
+ }
258
+
259
+ // Version mismatch → stale
260
+ if (activeVersion && vendoredVersion && vendoredVersion !== activeVersion) {
261
+ return { present: true, vendoredVersion, activeVersion, stale: true, reason: 'version-mismatch' };
262
+ }
263
+
264
+ return { present: true, vendoredVersion, activeVersion, stale: false, reason: 'ok' };
265
+ }
266
+
186
267
  // ── Exports ────────────────────────────────────────────────────────────────────
187
268
 
188
- module.exports = { checkNamespaces, validateManifest };
269
+ module.exports = { checkNamespaces, validateManifest, checkToolsVersion };
189
270
 
190
271
  // ── CLI ────────────────────────────────────────────────────────────────────────
191
272
 
@@ -108,9 +108,23 @@ function resolveTaskDir(task, sprintDirPath, engPath) {
108
108
  const normalizedTaskPath = task.path ? task.path.replace(/\\/g, '/').replace(/\/$/, '') : null;
109
109
  const normalizedEngPath = engPath ? engPath.replace(/\\/g, '/').replace(/\/$/, '') : '';
110
110
 
111
- // Case 1: path is under the engineering root — it IS the task directory
111
+ // Case 1: path is under the engineering root — it IS (or is a file inside)
112
+ // the task directory. Some store records carry task.path pointing at a file
113
+ // inside the task dir (e.g. .../FORGE-S22-T04/PLAN.md) rather than the dir
114
+ // itself; in that case basename(path) is the filename, not the dir. Only
115
+ // trust Case 1 when its candidate resolves to a real directory on disk —
116
+ // otherwise fall through to the filesystem scan, which resolves by taskId.
112
117
  if (normalizedTaskPath && normalizedEngPath && normalizedTaskPath.startsWith(normalizedEngPath + '/')) {
113
- return resultOk(path.basename(normalizedTaskPath));
118
+ const candidate = path.basename(normalizedTaskPath);
119
+ // Can't verify against disk — preserve legacy behaviour and trust the path.
120
+ if (!sprintDirPath) return resultOk(candidate);
121
+ const candidatePath = path.join(sprintDirPath, candidate);
122
+ // Trust Case 1 only when the candidate is a real directory. If it's a file
123
+ // (task.path pointed at a doc inside the dir) or absent, fall through to the
124
+ // filesystem scan below, which resolves the dir by taskId.
125
+ if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory()) {
126
+ return resultOk(candidate);
127
+ }
114
128
  }
115
129
 
116
130
  // Case 2 (and fallback for case 1 missing): filesystem scan
@@ -418,7 +418,7 @@ if (subcmd === 'resolve-forge-root') {
418
418
 
419
419
  const { config } = readConfig();
420
420
  const forgeRef = getByPath(config, 'paths.forgeRef');
421
- const forgeRoot = getByPath(config, 'paths.forgeRoot');
421
+ // Note: paths.forgeRoot read removed in FORGE-S29-T03 (Priority 3 deprecated).
422
422
 
423
423
  // Priority 2: Scan cache/marketplace directories by forgeRef
424
424
  if (forgeRef && typeof forgeRef === 'string') {
@@ -442,14 +442,12 @@ if (subcmd === 'resolve-forge-root') {
442
442
  }
443
443
  }
444
444
 
445
- // Priority 3: Fallback to paths.forgeRoot (deprecated but still read)
446
- if (forgeRoot && typeof forgeRoot === 'string') {
447
- console.log(forgeRoot);
448
- process.exit(0);
449
- }
445
+ // Priority 3 (paths.forgeRoot fallback) removed FORGE-S29-T03.
446
+ // paths.forgeRoot is deprecated and no longer used as a resolution fallback.
447
+ // Relying on it masks misconfigured projects and blocks the FORGE_ROOT retirement.
450
448
 
451
449
  // No resolution possible
452
- console.error('× Cannot resolve Forge plugin root: no CLAUDE_PLUGIN_ROOT env var, no forgeRef cache match, and no forgeRoot in config.');
450
+ console.error('× Cannot resolve Forge plugin root: no CLAUDE_PLUGIN_ROOT env var and no forgeRef cache match. Run /forge:update to refresh.');
453
451
  process.exit(1);
454
452
  }
455
453
 
@@ -154,4 +154,76 @@ function parseAfter(rest, lineNo) {
154
154
  return { phase: m[1], verdict };
155
155
  }
156
156
 
157
- module.exports = { parseGates };
157
+ // parseOutputs same fence-scan logic as parseGates but for ```outputs phase=X blocks.
158
+ // Closed grammar: only `artifact` and `require` directives are allowed.
159
+ // Unknown directives throw (parity with parseGates).
160
+ const OUTPUTS_FENCE_OPEN = /^```outputs\s+phase=([A-Za-z0-9_-]+)\s*$/;
161
+
162
+ function parseOutputs(markdown) {
163
+ if (typeof markdown !== 'string' || markdown.length === 0) return {};
164
+ const lines = markdown.split('\n');
165
+ const result = {};
166
+ let currentPhase = null;
167
+ let inFence = false;
168
+ let fenceStartLine = -1;
169
+ let fenceBuffer = [];
170
+ for (let i = 0; i < lines.length; i++) {
171
+ const line = lines[i];
172
+ const lineNo = i + 1;
173
+ if (!inFence) {
174
+ const m = line.match(OUTPUTS_FENCE_OPEN);
175
+ if (m) {
176
+ currentPhase = m[1];
177
+ if (result[currentPhase]) {
178
+ throw new Error(
179
+ `parse-gates: line ${lineNo}: duplicate outputs block for phase "${currentPhase}"`,
180
+ );
181
+ }
182
+ inFence = true;
183
+ fenceStartLine = lineNo;
184
+ fenceBuffer = [];
185
+ }
186
+ continue;
187
+ }
188
+ // Inside an outputs fence
189
+ if (FENCE_CLOSE.test(line)) {
190
+ result[currentPhase] = parseOutputsBlock(fenceBuffer, fenceStartLine);
191
+ inFence = false;
192
+ currentPhase = null;
193
+ fenceBuffer = [];
194
+ continue;
195
+ }
196
+ fenceBuffer.push({ text: line, lineNo });
197
+ }
198
+ if (inFence) {
199
+ throw new Error(`parse-gates: unterminated \`\`\`outputs fence opened at line ${fenceStartLine}`);
200
+ }
201
+ return result;
202
+ }
203
+
204
+ function parseOutputsBlock(bufferedLines, _fenceStartLine) {
205
+ const spec = { artifacts: [], require: [] };
206
+ for (const { text, lineNo } of bufferedLines) {
207
+ const trimmed = text.trim();
208
+ if (trimmed === '' || trimmed.startsWith('#')) continue;
209
+ const firstSpace = trimmed.search(/\s/);
210
+ if (firstSpace < 0) {
211
+ throw new Error(`parse-gates: line ${lineNo}: malformed directive "${trimmed}"`);
212
+ }
213
+ const directive = trimmed.slice(0, firstSpace);
214
+ const rest = trimmed.slice(firstSpace + 1).trim();
215
+ switch (directive) {
216
+ case 'artifact':
217
+ spec.artifacts.push(parseArtifact(rest, lineNo));
218
+ break;
219
+ case 'require':
220
+ spec.require.push(parsePredicate(rest, lineNo));
221
+ break;
222
+ default:
223
+ throw new Error(`parse-gates: line ${lineNo}: unknown directive "${directive}"`);
224
+ }
225
+ }
226
+ return spec;
227
+ }
228
+
229
+ module.exports = { parseGates, parseOutputs };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ // postflight-gate.cjs — evaluates a phase's declared outputs block against
4
+ // the filesystem / task state, post-subagent-return. Returns structured failure
5
+ // data so the orchestrator (run-task.ts) can halt before FSM advance when outputs
6
+ // are not satisfied.
7
+ //
8
+ // Pure function: only fs.existsSync / fs.statSync / fs.readFileSync. No writes,
9
+ // no network, no LLM, no process spawns.
10
+ //
11
+ // CLI shim: node postflight-gate.cjs --phase <name> --task <taskId>
12
+ // Exit codes: 0 (ok), 1 (guard failed), 2 (invalid args / parse error)
13
+ // Stdout on exit 1: single JSON line { phase, reasonCode, detail, remediation }
14
+ //
15
+ // New reasonCodes (FORGE-S26-T19):
16
+ // output-missing — artifact path does not exist
17
+ // output-stub — artifact exists but size < min= bytes
18
+ // require-failed — require predicate over state field failed
19
+ // tool-error — internal parse or store-read failure
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ /**
25
+ * postflight({ phase, outputs, state, substitutions })
26
+ *
27
+ * @param {string} phase - Phase name (e.g. 'implement')
28
+ * @param {object} outputs - Parsed outputs spec from parseOutputs() — { [phase]: { artifacts, require } }
29
+ * @param {object} [state] - Task/bug state for require predicates (e.g. { task: {...} })
30
+ * @param {object} [substitutions] - Template variable substitutions (e.g. { engineering, sprint, task })
31
+ * @returns {{ ok: boolean, missing: string[], reasonCode: string, detail: string, remediation: string }}
32
+ */
33
+ function postflight({ phase, outputs, state = {}, substitutions = {} }) {
34
+ const spec = outputs && outputs[phase];
35
+ if (!spec) {
36
+ // No outputs block for this phase — pass through (no-op)
37
+ return { ok: true, missing: [], reasonCode: null, detail: '', remediation: '' };
38
+ }
39
+
40
+ const missing = [];
41
+
42
+ for (const art of spec.artifacts || []) {
43
+ const resolved = applySubstitutions(art.path, substitutions);
44
+ let exists = false;
45
+ let size = 0;
46
+ try {
47
+ const st = fs.statSync(resolved);
48
+ exists = st.isFile();
49
+ size = st.size;
50
+ } catch (_) {
51
+ exists = false;
52
+ }
53
+ if (!exists) {
54
+ missing.push(`output-missing: artifact absent: ${resolved}`);
55
+ } else if (size < (art.minBytes || 0)) {
56
+ missing.push(
57
+ `output-stub: artifact too small: ${resolved} (${size} bytes, need >= ${art.minBytes})`,
58
+ );
59
+ }
60
+ }
61
+
62
+ for (const pred of spec.require || []) {
63
+ if (!evalPredicate(pred, state)) {
64
+ missing.push(
65
+ `require-failed: ${describePredicate(pred)} (got ${JSON.stringify(readField(pred.field, state))})`,
66
+ );
67
+ }
68
+ }
69
+
70
+ if (missing.length === 0) {
71
+ return { ok: true, missing: [], reasonCode: null, detail: '', remediation: '' };
72
+ }
73
+
74
+ const { reasonCode, detail, remediation } = buildStructuredFailure(phase, missing);
75
+ return { ok: false, missing, reasonCode, detail, remediation };
76
+ }
77
+
78
+ function applySubstitutions(template, subs) {
79
+ return template.replace(/\{(\w+)\}/g, (full, key) => {
80
+ if (Object.prototype.hasOwnProperty.call(subs, key)) return String(subs[key]);
81
+ return full;
82
+ });
83
+ }
84
+
85
+ function readField(dottedPath, state) {
86
+ const parts = dottedPath.split('.');
87
+ let cur = state;
88
+ for (const p of parts) {
89
+ if (cur === null || cur === undefined) return undefined;
90
+ cur = cur[p];
91
+ }
92
+ return cur;
93
+ }
94
+
95
+ function evalPredicate(pred, state) {
96
+ const actual = readField(pred.field, state);
97
+ switch (pred.op) {
98
+ case '==':
99
+ return String(actual) === String(pred.value);
100
+ case '!=':
101
+ return String(actual) !== String(pred.value);
102
+ case 'in':
103
+ return (pred.value || []).map(String).includes(String(actual));
104
+ default:
105
+ throw new Error(`postflight-gate: unknown predicate op "${pred.op}"`);
106
+ }
107
+ }
108
+
109
+ function describePredicate(pred) {
110
+ if (pred.op === 'in') return `${pred.field} in [${(pred.value || []).join(', ')}]`;
111
+ return `${pred.field} ${pred.op} ${pred.value}`;
112
+ }
113
+
114
+ function buildStructuredFailure(phase, missing) {
115
+ let reasonCode = 'tool-error';
116
+ const detailParts = [];
117
+
118
+ for (const m of missing) {
119
+ detailParts.push(m);
120
+ if (reasonCode === 'tool-error') {
121
+ if (/^output-missing/i.test(m)) {
122
+ reasonCode = 'output-missing';
123
+ } else if (/^output-stub/i.test(m)) {
124
+ reasonCode = 'output-stub';
125
+ } else if (/^require-failed/i.test(m)) {
126
+ reasonCode = 'require-failed';
127
+ }
128
+ }
129
+ }
130
+
131
+ const detail = detailParts.join('; ');
132
+
133
+ const remediationMap = {
134
+ 'output-missing': 'Re-run the phase that produces this artifact (e.g. /forge:implement), then retry.',
135
+ 'output-stub': 'The artifact was produced but appears incomplete (stub). Ensure the phase wrote a complete file.',
136
+ 'require-failed': 'Correct the task/bug state so it satisfies the postflight require predicate, then retry.',
137
+ 'tool-error': 'Check the outputs block configuration and store records; run node .forge/tools/postflight-gate.cjs manually for diagnostics.',
138
+ };
139
+
140
+ return {
141
+ reasonCode,
142
+ detail,
143
+ remediation: remediationMap[reasonCode],
144
+ };
145
+ }
146
+
147
+ module.exports = { postflight };
148
+
149
+ // CLI shim — only runs when invoked directly
150
+ if (require.main === module) {
151
+ const args = parseArgs(process.argv.slice(2));
152
+ if (!args.phase || !args.task) {
153
+ process.stderr.write('Usage: postflight-gate.cjs --phase <phaseName> --task <taskId>\n');
154
+ process.exit(2);
155
+ }
156
+
157
+ const { parseOutputs } = require('./parse-gates.cjs');
158
+
159
+ // Resolve config
160
+ let engineeringRoot = 'engineering';
161
+ try {
162
+ const cfg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), '.forge/config.json'), 'utf8'));
163
+ if (cfg.paths && cfg.paths.engineering) engineeringRoot = cfg.paths.engineering;
164
+ } catch (_) { /* fall back to default */ }
165
+
166
+ // Load task record
167
+ let taskRecord = null;
168
+ try {
169
+ const store = require('./store.cjs');
170
+ taskRecord = store.getTask(args.task);
171
+ } catch (_) {
172
+ // store.cjs not available or task not found — continue with substitutions from args
173
+ }
174
+
175
+ // Build substitutions
176
+ const sprintId = taskRecord ? taskRecord.sprintId : undefined;
177
+ const substitutions = {
178
+ engineering: engineeringRoot,
179
+ sprint: sprintId,
180
+ task: args.task,
181
+ };
182
+
183
+ // Load workflow markdown (scan .forge/workflows/ for phase)
184
+ const workflowMd = loadWorkflowMarkdown(args.phase);
185
+ if (!workflowMd) {
186
+ process.stderr.write(`postflight-gate: no outputs block defined for phase "${args.phase}" — skipping\n`);
187
+ process.exit(0);
188
+ }
189
+
190
+ let outputs;
191
+ try {
192
+ outputs = parseOutputs(workflowMd);
193
+ } catch (err) {
194
+ process.stderr.write(`postflight-gate: ${err.message}\n`);
195
+ process.exit(2);
196
+ }
197
+
198
+ if (!outputs[args.phase]) {
199
+ process.stderr.write(`postflight-gate: no outputs block for phase "${args.phase}" — skipping\n`);
200
+ process.exit(0);
201
+ }
202
+
203
+ // Build state from task record
204
+ const state = {};
205
+ if (taskRecord) state.task = taskRecord;
206
+
207
+ const result = postflight({ phase: args.phase, outputs, state, substitutions });
208
+
209
+ if (result.ok) process.exit(0);
210
+
211
+ process.stderr.write(`Postflight guard failed for phase "${args.phase}":\n`);
212
+ for (const m of result.missing) process.stderr.write(` - ${m}\n`);
213
+
214
+ // Emit structured JSON on stdout for orchestrators
215
+ process.stdout.write(JSON.stringify({
216
+ phase: args.phase,
217
+ reasonCode: result.reasonCode,
218
+ detail: result.detail,
219
+ remediation: result.remediation,
220
+ }) + '\n');
221
+ process.exit(1);
222
+ }
223
+
224
+ function parseArgs(argv) {
225
+ const out = {};
226
+ for (let i = 0; i < argv.length; i++) {
227
+ const a = argv[i];
228
+ if (a === '--phase') out.phase = argv[++i];
229
+ else if (a === '--task') out.task = argv[++i];
230
+ }
231
+ return out;
232
+ }
233
+
234
+ function loadWorkflowMarkdown(phaseName) {
235
+ const workflowsDir = path.resolve(process.cwd(), '.forge/workflows');
236
+ let entries;
237
+ try {
238
+ entries = fs.readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
239
+ } catch (_) {
240
+ return null;
241
+ }
242
+ const fencePattern = new RegExp('^```outputs\\s+phase=' + escapeRegex(phaseName) + '\\s*$', 'm');
243
+ for (const entry of entries) {
244
+ const md = fs.readFileSync(path.join(workflowsDir, entry), 'utf8');
245
+ if (fencePattern.test(md)) return md;
246
+ }
247
+ return null;
248
+ }
249
+
250
+ function escapeRegex(s) {
251
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
252
+ }
@@ -349,9 +349,56 @@ if (require.main === module) {
349
349
  if (result.ok) process.exit(0);
350
350
  process.stderr.write(`Gate failed for phase "${args.phase}":\n`);
351
351
  for (const m of result.missing) process.stderr.write(` - ${m}\n`);
352
+ // Emit structured JSON on stdout for orchestrators to parse and render.
353
+ // Shape: { phase, reasonCode, detail, remediation }
354
+ const structured = buildStructuredFailure(args.phase, result.missing);
355
+ process.stdout.write(JSON.stringify(structured) + '\n');
352
356
  process.exit(1);
353
357
  }
354
358
 
359
+ // Build a structured gate-failure object for orchestrators.
360
+ // Maps the human-readable `missing[]` strings to a typed { phase, reasonCode, detail, remediation }.
361
+ // reasonCode is derived from the dominant failure pattern:
362
+ // artifact-missing — artifact missing / too small
363
+ // predecessor-verdict-missing — after-clause verdict absent or wrong
364
+ // illegal-status — require/forbid predicate fired
365
+ // tool-error — internal error / unrecognised pattern
366
+ // When multiple failures exist, reasonCode reflects the first recognised pattern;
367
+ // detail combines all messages; only a single JSON object is ever emitted.
368
+ function buildStructuredFailure(phase, missing) {
369
+ let reasonCode = 'tool-error';
370
+ const detailParts = [];
371
+
372
+ for (const m of missing) {
373
+ detailParts.push(m);
374
+ if (reasonCode === 'tool-error') {
375
+ if (/^artifact (missing|too small)/i.test(m)) {
376
+ reasonCode = 'artifact-missing';
377
+ } else if (/^predecessor verdict (missing|unreadable)/i.test(m) || /verdict is "/i.test(m)) {
378
+ reasonCode = 'predecessor-verdict-missing';
379
+ } else if (/^(require failed|forbid triggered)/i.test(m)) {
380
+ reasonCode = 'illegal-status';
381
+ }
382
+ }
383
+ }
384
+
385
+ const detail = detailParts.join('; ');
386
+
387
+ const remediationMap = {
388
+ 'artifact-missing': `Re-run the phase that produces this artifact (e.g. /forge:plan or /forge:implement), then retry.`,
389
+ 'predecessor-verdict-missing': `Ensure the predecessor review phase completed and recorded a verdict via set-summary, then retry.`,
390
+ 'illegal-status': `Correct the task/bug status (use store-cli update-status) so it satisfies the gate predicate, then retry.`,
391
+ 'tool-error': `Check the gate configuration and store records for this task; run node .forge/tools/preflight-gate.cjs manually for diagnostics.`,
392
+ };
393
+
394
+ return {
395
+ phase,
396
+ reasonCode,
397
+ detail,
398
+ remediation: remediationMap[reasonCode],
399
+ };
400
+ }
401
+
355
402
  function parseArgs(argv) {
356
403
  const out = {};
357
404
  for (let i = 0; i < argv.length; i++) {
@@ -120,10 +120,11 @@ const RUNTIME_PASSTHROUGH_KEYS = new Set([
120
120
  * project prefix via getCommandsSubdir() — see walkBasePack.
121
121
  */
122
122
  const SUBDIR_OUTPUT_MAP = {
123
- personas: path.join('.forge', 'personas'),
124
- skills: path.join('.forge', 'skills'),
125
- workflows: path.join('.forge', 'workflows'),
126
- templates: path.join('.forge', 'templates'),
123
+ personas: path.join('.forge', 'personas'),
124
+ skills: path.join('.forge', 'skills'),
125
+ workflows: path.join('.forge', 'workflows'),
126
+ templates: path.join('.forge', 'templates'),
127
+ 'workflows-js': path.join('.claude', 'workflows'),
127
128
  };
128
129
 
129
130
  /**
@@ -194,6 +194,14 @@ function verifyPhase2(cwd, kbPath) {
194
194
 
195
195
  const PHASE3_DIRS = ['workflows', 'personas', 'skills', 'templates'];
196
196
 
197
+ // JS workflow files that substitute-placeholders.cjs emits from
198
+ // base-pack/workflows-js/ into .claude/workflows/ (FORGE-S28-T01).
199
+ // These are checked in addition to the .forge/ directory checks.
200
+ const PHASE3_JS_FILES = [
201
+ path.join('.claude', 'workflows', 'wfl-run-task.js'),
202
+ path.join('.claude', 'workflows', 'wfl-run-sprint.js'),
203
+ ];
204
+
197
205
  function verifyPhase3(cwd) {
198
206
  const missing = [];
199
207
  const checked = [];
@@ -213,6 +221,15 @@ function verifyPhase3(cwd) {
213
221
  }
214
222
  }
215
223
 
224
+ // Assert generated JS workflow files are present (FORGE-S28-T01)
225
+ for (const relFile of PHASE3_JS_FILES) {
226
+ checked.push(relFile);
227
+ const absFile = path.join(cwd, relFile);
228
+ if (!fs.existsSync(absFile)) {
229
+ missing.push(relFile);
230
+ }
231
+ }
232
+
216
233
  return {
217
234
  phase: 3,
218
235
  ok: missing.length === 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entelligentsia/forgecli",
3
- "version": "1.0.10",
3
+ "version": "1.0.14",
4
4
  "description": "Forge SDLC ported onto @earendil-works/pi-coding-agent \u2014 production launcher with three bin aliases (forge/forgecli/4ge). Bundles a curated fork of pi-coding-agent vendored under earendil-works names.",
5
5
  "license": "MIT",
6
6
  "author": "Entelligentsia",
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- // Stage 1 scaffold (FORGE-S15-T01). Real CLI entry lands in FORGE-S15-T03.
3
- console.log("forgecli stub — see FORGE-S15-T03");
4
- process.exit(0);
5
- export {};
6
- //# sourceMappingURL=forgecli.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"forgecli.js","sourceRoot":"","sources":["../../src/bin/forgecli.ts"],"names":[],"mappings":";AACA,2EAA2E;AAE3E,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC"}
@@ -1,5 +0,0 @@
1
- export { createConfigTuiComponent, type ConfigTuiComponentOptions } from "./component.js";
2
- export type { ConfigLayer } from "../config-writer.js";
3
- export type { View, ConfigBuffer, AvailableModel, InitOptions, ConfigTuiState, ConfigTuiAction, PhaseOverride, ResolvedPersonaEntry, PersonaPickerEntry, PipelineOverrideSummary, TierAssignment, } from "./state/model.js";
4
- export { CANONICAL_PHASES, initialState, reducer, getActiveView, listResolvedPersonas, listPersonaPickerEntries, uniqueProviders, listPipelineOverrideSummaries, getPhaseOverride, getTierAssignment, getAllTierAssignments, getTierForPersona, getPersonasInTier, writePersonaEntry, deletePersonaEntry, writePhaseOverride, clearPhaseOverride, writeTierAssignment, isConfigEmpty, personaSourceLabel, } from "./state.js";
5
- export type { InputResult, Screen } from "./screens/types.js";
@@ -1,5 +0,0 @@
1
- // Config-TUI public re-exports.
2
- // Phase 3: theming + width safety + data-driven menus + auth error surfacing.
3
- export { createConfigTuiComponent } from "./component.js";
4
- export { CANONICAL_PHASES, initialState, reducer, getActiveView, listResolvedPersonas, listPersonaPickerEntries, uniqueProviders, listPipelineOverrideSummaries, getPhaseOverride, getTierAssignment, getAllTierAssignments, getTierForPersona, getPersonasInTier, writePersonaEntry, deletePersonaEntry, writePhaseOverride, clearPhaseOverride, writeTierAssignment, isConfigEmpty, personaSourceLabel, } from "./state.js";
5
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/extensions/forgecli/config-tui/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,8EAA8E;AAE9E,OAAO,EAAE,wBAAwB,EAAkC,MAAM,gBAAgB,CAAC;AAkB1F,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,wBAAwB,EACxB,eAAe,EACf,6BAA6B,EAC7B,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,kBAAkB,GACnB,MAAM,YAAY,CAAC"}