@entelligentsia/forgecli 0.10.0 → 0.11.2

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 (188) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +21 -3
  3. package/dist/CHANGELOG-forge-plugin.md +90 -0
  4. package/dist/bin/config.js +6 -0
  5. package/dist/bin/config.js.map +1 -1
  6. package/dist/extensions/forgecli/add-pipeline.d.ts +19 -0
  7. package/dist/extensions/forgecli/add-pipeline.js +143 -0
  8. package/dist/extensions/forgecli/add-pipeline.js.map +1 -0
  9. package/dist/extensions/forgecli/add-task.d.ts +20 -0
  10. package/dist/extensions/forgecli/add-task.js +154 -0
  11. package/dist/extensions/forgecli/add-task.js.map +1 -0
  12. package/dist/extensions/forgecli/calibrate.d.ts +61 -0
  13. package/dist/extensions/forgecli/calibrate.js +488 -0
  14. package/dist/extensions/forgecli/calibrate.js.map +1 -0
  15. package/dist/extensions/forgecli/fix-bug.d.ts +9 -1
  16. package/dist/extensions/forgecli/fix-bug.js +155 -45
  17. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  18. package/dist/extensions/forgecli/forge-commands.js +15 -22
  19. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  20. package/dist/extensions/forgecli/forge-subagent.d.ts +16 -1
  21. package/dist/extensions/forgecli/forge-subagent.js +45 -8
  22. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  23. package/dist/extensions/forgecli/forge-update-command.d.ts +9 -0
  24. package/dist/extensions/forgecli/forge-update-command.js +106 -7
  25. package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
  26. package/dist/extensions/forgecli/health-check.d.ts +22 -1
  27. package/dist/extensions/forgecli/health-check.js +177 -4
  28. package/dist/extensions/forgecli/health-check.js.map +1 -1
  29. package/dist/extensions/forgecli/hook-dispatcher.d.ts +25 -1
  30. package/dist/extensions/forgecli/hook-dispatcher.js +104 -9
  31. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  32. package/dist/extensions/forgecli/hooks/check-update.d.ts +81 -0
  33. package/dist/extensions/forgecli/hooks/check-update.js +308 -0
  34. package/dist/extensions/forgecli/hooks/check-update.js.map +1 -0
  35. package/dist/extensions/forgecli/hooks/forge-permissions.d.ts +32 -0
  36. package/dist/extensions/forgecli/hooks/forge-permissions.js +119 -0
  37. package/dist/extensions/forgecli/hooks/forge-permissions.js.map +1 -0
  38. package/dist/extensions/forgecli/hooks/triage-error.d.ts +23 -0
  39. package/dist/extensions/forgecli/hooks/triage-error.js +62 -0
  40. package/dist/extensions/forgecli/hooks/triage-error.js.map +1 -0
  41. package/dist/extensions/forgecli/hooks/write-guard.d.ts +28 -0
  42. package/dist/extensions/forgecli/hooks/write-guard.js +225 -0
  43. package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -0
  44. package/dist/extensions/forgecli/index.js +60 -0
  45. package/dist/extensions/forgecli/index.js.map +1 -1
  46. package/dist/extensions/forgecli/init-context.d.ts +1 -1
  47. package/dist/extensions/forgecli/init-context.js +21 -6
  48. package/dist/extensions/forgecli/init-context.js.map +1 -1
  49. package/dist/extensions/forgecli/materialize.d.ts +16 -0
  50. package/dist/extensions/forgecli/materialize.js +195 -0
  51. package/dist/extensions/forgecli/materialize.js.map +1 -0
  52. package/dist/extensions/forgecli/migrate.d.ts +19 -0
  53. package/dist/extensions/forgecli/migrate.js +258 -0
  54. package/dist/extensions/forgecli/migrate.js.map +1 -0
  55. package/dist/extensions/forgecli/migration-engine.d.ts +111 -0
  56. package/dist/extensions/forgecli/migration-engine.js +533 -0
  57. package/dist/extensions/forgecli/migration-engine.js.map +1 -0
  58. package/dist/extensions/forgecli/quiz-agent.d.ts +17 -0
  59. package/dist/extensions/forgecli/quiz-agent.js +98 -0
  60. package/dist/extensions/forgecli/quiz-agent.js.map +1 -0
  61. package/dist/extensions/forgecli/remove-command.d.ts +17 -0
  62. package/dist/extensions/forgecli/remove-command.js +124 -0
  63. package/dist/extensions/forgecli/remove-command.js.map +1 -0
  64. package/dist/extensions/forgecli/report-bug.d.ts +25 -0
  65. package/dist/extensions/forgecli/report-bug.js +159 -0
  66. package/dist/extensions/forgecli/report-bug.js.map +1 -0
  67. package/dist/extensions/forgecli/retrospective.d.ts +19 -0
  68. package/dist/extensions/forgecli/retrospective.js +156 -0
  69. package/dist/extensions/forgecli/retrospective.js.map +1 -0
  70. package/dist/extensions/forgecli/run-sprint.js +36 -3
  71. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  72. package/dist/extensions/forgecli/run-task.d.ts +9 -1
  73. package/dist/extensions/forgecli/run-task.js +66 -13
  74. package/dist/extensions/forgecli/run-task.js.map +1 -1
  75. package/dist/extensions/forgecli/session-registry.d.ts +40 -2
  76. package/dist/extensions/forgecli/session-registry.js +71 -1
  77. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  78. package/dist/extensions/forgecli/status-command.d.ts +19 -0
  79. package/dist/extensions/forgecli/status-command.js +140 -0
  80. package/dist/extensions/forgecli/status-command.js.map +1 -0
  81. package/dist/extensions/forgecli/store-query.d.ts +22 -0
  82. package/dist/extensions/forgecli/store-query.js +107 -0
  83. package/dist/extensions/forgecli/store-query.js.map +1 -0
  84. package/dist/extensions/forgecli/store-repair.d.ts +17 -0
  85. package/dist/extensions/forgecli/store-repair.js +123 -0
  86. package/dist/extensions/forgecli/store-repair.js.map +1 -0
  87. package/dist/extensions/forgecli/test-orchestrate.js +1 -0
  88. package/dist/extensions/forgecli/test-orchestrate.js.map +1 -1
  89. package/dist/extensions/forgecli/thread-switcher.js +286 -41
  90. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  91. package/dist/extensions/forgecli/transition-guard.js +7 -2
  92. package/dist/extensions/forgecli/transition-guard.js.map +1 -1
  93. package/dist/extensions/forgecli/update-tools.d.ts +23 -0
  94. package/dist/extensions/forgecli/update-tools.js +136 -0
  95. package/dist/extensions/forgecli/update-tools.js.map +1 -0
  96. package/dist/extensions/forgecli/viewport-events.js +10 -0
  97. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  98. package/dist/extensions/forgecli/viewport-renderer.d.ts +18 -0
  99. package/dist/extensions/forgecli/viewport-renderer.js +27 -0
  100. package/dist/extensions/forgecli/viewport-renderer.js.map +1 -1
  101. package/dist/extensions/forgecli/viewport-theme.js +4 -0
  102. package/dist/extensions/forgecli/viewport-theme.js.map +1 -1
  103. package/dist/extensions/forgecli/whats-new-widget.d.ts +13 -8
  104. package/dist/extensions/forgecli/whats-new-widget.js +111 -42
  105. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  106. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +29 -3
  107. package/dist/forge-payload/.base-pack/workflows/commit_task.md +15 -8
  108. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +327 -185
  109. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +18 -10
  110. package/dist/forge-payload/.base-pack/workflows/plan_task.md +15 -9
  111. package/dist/forge-payload/.base-pack/workflows/review_code.md +14 -6
  112. package/dist/forge-payload/.base-pack/workflows/review_plan.md +18 -10
  113. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  114. package/dist/forge-payload/.schemas/bug.schema.json +3 -2
  115. package/dist/forge-payload/.schemas/config.schema.json +83 -0
  116. package/dist/forge-payload/.schemas/migrations.json +2049 -0
  117. package/dist/forge-payload/commands/regenerate.md +17 -1
  118. package/dist/forge-payload/meta/personas/README.md +16 -0
  119. package/dist/forge-payload/meta/personas/meta-architect.md +70 -0
  120. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +73 -0
  121. package/dist/forge-payload/meta/personas/meta-collator.md +72 -0
  122. package/dist/forge-payload/meta/personas/meta-engineer.md +70 -0
  123. package/dist/forge-payload/meta/personas/meta-orchestrator.md +71 -0
  124. package/dist/forge-payload/meta/personas/meta-product-manager.md +82 -0
  125. package/dist/forge-payload/meta/personas/meta-qa-engineer.md +91 -0
  126. package/dist/forge-payload/meta/personas/meta-supervisor.md +92 -0
  127. package/dist/forge-payload/meta/skill-recommendations.md +154 -0
  128. package/dist/forge-payload/meta/skills/meta-architect-skills.md +43 -0
  129. package/dist/forge-payload/meta/skills/meta-bug-fixer-skills.md +43 -0
  130. package/dist/forge-payload/meta/skills/meta-collator-skills.md +41 -0
  131. package/dist/forge-payload/meta/skills/meta-engineer-skills.md +43 -0
  132. package/dist/forge-payload/meta/skills/meta-generic-skills.md +58 -0
  133. package/dist/forge-payload/meta/skills/meta-qa-engineer-skills.md +46 -0
  134. package/dist/forge-payload/meta/skills/meta-supervisor-skills.md +43 -0
  135. package/dist/forge-payload/meta/store-schema/bug.schema.md +71 -0
  136. package/dist/forge-payload/meta/store-schema/event.schema.md +76 -0
  137. package/dist/forge-payload/meta/store-schema/feature.schema.md +65 -0
  138. package/dist/forge-payload/meta/store-schema/sprint.schema.md +64 -0
  139. package/dist/forge-payload/meta/store-schema/task.schema.md +78 -0
  140. package/dist/forge-payload/meta/templates/meta-code-review.md +26 -0
  141. package/dist/forge-payload/meta/templates/meta-plan-review.md +28 -0
  142. package/dist/forge-payload/meta/templates/meta-plan.md +28 -0
  143. package/dist/forge-payload/meta/templates/meta-progress.md +25 -0
  144. package/dist/forge-payload/meta/templates/meta-retrospective.md +28 -0
  145. package/dist/forge-payload/meta/templates/meta-sprint-manifest.md +26 -0
  146. package/dist/forge-payload/meta/templates/meta-sprint-requirements.md +91 -0
  147. package/dist/forge-payload/meta/templates/meta-task-prompt.md +26 -0
  148. package/dist/forge-payload/meta/tool-specs/collate.spec.md +88 -0
  149. package/dist/forge-payload/meta/tool-specs/generation-manifest.spec.md +139 -0
  150. package/dist/forge-payload/meta/tool-specs/manage-config.spec.md +143 -0
  151. package/dist/forge-payload/meta/tool-specs/seed-store.spec.md +91 -0
  152. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +328 -0
  153. package/dist/forge-payload/meta/tool-specs/validate-store.spec.md +191 -0
  154. package/dist/forge-payload/meta/workflows/_fragments/context-injection.md +75 -0
  155. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +73 -0
  156. package/dist/forge-payload/meta/workflows/_fragments/finalize.md +13 -0
  157. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +73 -0
  158. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +38 -0
  159. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +39 -0
  160. package/dist/forge-payload/meta/workflows/meta-approve.md +119 -0
  161. package/dist/forge-payload/meta/workflows/meta-collate.md +89 -0
  162. package/dist/forge-payload/meta/workflows/meta-commit.md +93 -0
  163. package/dist/forge-payload/meta/workflows/meta-enhance.md +286 -0
  164. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +501 -0
  165. package/dist/forge-payload/meta/workflows/meta-implement.md +132 -0
  166. package/dist/forge-payload/meta/workflows/meta-migrate.md +455 -0
  167. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +993 -0
  168. package/dist/forge-payload/meta/workflows/meta-plan-task.md +133 -0
  169. package/dist/forge-payload/meta/workflows/meta-quiz-agent.md +135 -0
  170. package/dist/forge-payload/meta/workflows/meta-retrospective.md +65 -0
  171. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +119 -0
  172. package/dist/forge-payload/meta/workflows/meta-review-plan.md +108 -0
  173. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +65 -0
  174. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +76 -0
  175. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +147 -0
  176. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +76 -0
  177. package/dist/forge-payload/meta/workflows/meta-update-plan.md +76 -0
  178. package/dist/forge-payload/meta/workflows/meta-validate.md +111 -0
  179. package/dist/forge-payload/tools/check-structure.cjs +344 -0
  180. package/dist/forge-payload/tools/collate.cjs +34 -9
  181. package/dist/forge-payload/tools/list-skills.js +76 -0
  182. package/dist/forge-payload/tools/parse-gates.cjs +8 -2
  183. package/dist/forge-payload/tools/store-cli.cjs +56 -11
  184. package/dist/forge-payload/tools/store.cjs +61 -0
  185. package/dist/forge-payload/tools/substitute-placeholders.cjs +60 -8
  186. package/dist/forge-payload/tools/validate-store.cjs +6 -2
  187. package/dist/forge-payload/tools/verify-integrity.cjs +86 -0
  188. package/package.json +2 -2
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Forge tool: check-structure
5
+ // Checks that all files listed in structure-manifest.json are present
6
+ // in a project's generated output.
7
+ // Usage: node check-structure.cjs [--strict] [--path <project-root>] [--validate-manifest] [--forge-root <path>]
8
+ // --path <project-root> Directory to check against (default: process.cwd())
9
+ // --strict Also report files present but NOT in the manifest (extra files)
10
+ // --validate-manifest Validate manifest against base-pack source files
11
+ // --forge-root <path> Path to the forge/ plugin directory (required with --validate-manifest)
12
+ //
13
+ // Reads .forge/config.json paths.* for directory overrides.
14
+ // Falls back to manifest dir field if config is absent or unparseable.
15
+ //
16
+ // Exit 0: all expected files present (or only extras found without --strict)
17
+ // Exit 1: any missing files detected; also exit 1 if extras found with --strict
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { getCommandsSubdir } = require('./lib/paths.cjs');
22
+
23
+ // ── Per-namespace verification logic ──────────────────────────────────────────
24
+ //
25
+ // Returns { present, missing, extra, total } without calling process.exit.
26
+ // - present: number of expected files found
27
+ // - missing: array of { nsKey, dir, filename }
28
+ // - extra: array of { nsKey, dir, filename } (populated only when strict=true)
29
+ // - total: total number of expected files across all namespaces
30
+
31
+ function checkNamespaces(manifest, projectRoot, options = {}) {
32
+ const { strict = false, configPaths = null } = options;
33
+
34
+ // Resolve config path overrides
35
+ let resolvedConfigPaths;
36
+ let projectPrefix = '';
37
+ if (configPaths !== null) {
38
+ resolvedConfigPaths = configPaths;
39
+ } else {
40
+ resolvedConfigPaths = {};
41
+ const configFile = path.join(projectRoot, '.forge', 'config.json');
42
+ if (fs.existsSync(configFile)) {
43
+ try {
44
+ const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
45
+ resolvedConfigPaths = (cfg && cfg.paths) ? cfg.paths : {};
46
+ projectPrefix = (cfg && cfg.project && cfg.project.prefix)
47
+ ? getCommandsSubdir(cfg.project.prefix)
48
+ : '';
49
+ } catch {
50
+ // unparseable — use defaults
51
+ }
52
+ }
53
+ }
54
+
55
+ let totalPresent = 0;
56
+ let totalExpected = 0;
57
+ const allMissing = [];
58
+ const allExtra = [];
59
+
60
+ for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
61
+ const logicalKey = ns.logicalKey || nsKey;
62
+ let resolvedDir = (resolvedConfigPaths[logicalKey] && typeof resolvedConfigPaths[logicalKey] === 'string')
63
+ ? resolvedConfigPaths[logicalKey]
64
+ : ns.dir;
65
+ if (ns.prefixed && projectPrefix) {
66
+ resolvedDir = resolvedDir + '/' + projectPrefix;
67
+ }
68
+ const absDir = path.join(projectRoot, resolvedDir);
69
+
70
+ const expected = ns.files || [];
71
+ totalExpected += expected.length;
72
+
73
+ const present = [];
74
+ const missing = [];
75
+ for (const filename of expected) {
76
+ const fullPath = path.join(absDir, filename);
77
+ if (fs.existsSync(fullPath)) {
78
+ present.push(filename);
79
+ } else {
80
+ missing.push({ nsKey, dir: resolvedDir, filename });
81
+ }
82
+ }
83
+
84
+ totalPresent += present.length;
85
+ allMissing.push(...missing);
86
+
87
+ if (strict) {
88
+ if (fs.existsSync(absDir)) {
89
+ try {
90
+ const expectedSet = new Set(expected);
91
+ const found = fs.readdirSync(absDir);
92
+ for (const f of found) {
93
+ if (!expectedSet.has(f)) {
94
+ allExtra.push({ nsKey, dir: resolvedDir, filename: f });
95
+ }
96
+ }
97
+ } catch {}
98
+ }
99
+ }
100
+ }
101
+
102
+ return {
103
+ present: totalPresent,
104
+ missing: allMissing,
105
+ extra: allExtra,
106
+ total: totalExpected,
107
+ };
108
+ }
109
+
110
+ // ── Manifest validation against base-pack ──────────────────────────────────────
111
+ //
112
+ // Convention-based reverse mapping: each manifest namespace's dir field maps
113
+ // to a base-pack source directory. For schemas, the source is forgeRoot/schemas/.
114
+ // This function validates that every file in the manifest has a corresponding
115
+ // base-pack source, and vice versa.
116
+ //
117
+ // Returns { manifestOnly, basePackOnly }
118
+
119
+ function validateManifest(manifest, forgeRoot) {
120
+ const basePackDir = path.join(forgeRoot, 'init', 'base-pack');
121
+
122
+ // Convention-based reverse mapping from manifest dir to base-pack source dir
123
+ const nsToSourceDir = {
124
+ personas: path.join(basePackDir, 'personas'),
125
+ skills: path.join(basePackDir, 'skills'),
126
+ workflows: path.join(basePackDir, 'workflows'),
127
+ fragments: path.join(basePackDir, 'workflows', '_fragments'),
128
+ templates: path.join(basePackDir, 'templates'),
129
+ commands: path.join(basePackDir, 'commands'),
130
+ // schemas: not base-pack-sourced — source is forgeRoot/schemas/
131
+ };
132
+
133
+ const manifestOnly = []; // Files in manifest but not in base-pack
134
+ const basePackOnly = []; // Files in base-pack but not in manifest
135
+
136
+ for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
137
+ // Skip schemas — they ship from forgeRoot/schemas/, not base-pack
138
+ if (nsKey === 'schemas') continue;
139
+
140
+ const sourceDir = nsToSourceDir[nsKey];
141
+ if (!sourceDir) continue;
142
+
143
+ const manifestFiles = new Set(ns.files || []);
144
+
145
+ // Read base-pack source directory
146
+ let basePackFiles = [];
147
+ try {
148
+ basePackFiles = fs.readdirSync(sourceDir).filter(f => {
149
+ // Only include regular files, skip subdirectories (like _fragments under workflows)
150
+ const stat = fs.statSync(path.join(sourceDir, f));
151
+ return stat.isFile();
152
+ });
153
+ } catch {
154
+ // Directory doesn't exist — all manifest files are manifestOnly
155
+ for (const f of manifestFiles) {
156
+ manifestOnly.push({ nsKey, filename: f, dir: ns.dir });
157
+ }
158
+ continue;
159
+ }
160
+
161
+ const basePackFileSet = new Set(basePackFiles);
162
+
163
+ // Files in manifest but not in base-pack
164
+ for (const f of manifestFiles) {
165
+ if (!basePackFileSet.has(f)) {
166
+ manifestOnly.push({ nsKey, filename: f, dir: ns.dir });
167
+ }
168
+ }
169
+
170
+ // Files in base-pack but not in manifest
171
+ for (const f of basePackFileSet) {
172
+ if (!manifestFiles.has(f)) {
173
+ basePackOnly.push({ nsKey, filename: f, dir: ns.dir });
174
+ }
175
+ }
176
+ }
177
+
178
+ return { manifestOnly, basePackOnly };
179
+ }
180
+
181
+ // ── Exports ────────────────────────────────────────────────────────────────────
182
+
183
+ module.exports = { checkNamespaces, validateManifest };
184
+
185
+ // ── CLI ────────────────────────────────────────────────────────────────────────
186
+
187
+ if (require.main === module) {
188
+ try {
189
+ // ── Parse arguments ──────────────────────────────────────────────────────────
190
+
191
+ const argv = process.argv.slice(2);
192
+ let projectRoot = process.cwd();
193
+ let strict = false;
194
+ let validateManifestFlag = false;
195
+ let forgeRoot = null;
196
+
197
+ for (let i = 0; i < argv.length; i++) {
198
+ if (argv[i] === '--path' && argv[i + 1]) {
199
+ projectRoot = path.resolve(argv[++i]);
200
+ } else if (argv[i] === '--strict') {
201
+ strict = true;
202
+ } else if (argv[i] === '--validate-manifest') {
203
+ validateManifestFlag = true;
204
+ } else if (argv[i] === '--forge-root' && argv[i + 1]) {
205
+ forgeRoot = path.resolve(argv[++i]);
206
+ }
207
+ }
208
+
209
+ // ── Load structure-manifest.json ─────────────────────────────────────────────
210
+
211
+ const manifestPath = forgeRoot
212
+ ? path.join(forgeRoot, 'schemas', 'structure-manifest.json')
213
+ : path.join(__dirname, '..', 'schemas', 'structure-manifest.json');
214
+ if (!fs.existsSync(manifestPath)) {
215
+ process.stderr.write(`× structure-manifest.json not found at ${manifestPath}\n`);
216
+ process.exit(1);
217
+ }
218
+
219
+ let manifest;
220
+ try {
221
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
222
+ } catch (e) {
223
+ process.stderr.write(`× Failed to parse structure-manifest.json: ${e.message}\n`);
224
+ process.exit(1);
225
+ }
226
+
227
+ // ── Validate manifest against base-pack (if requested) ──────────────────────
228
+
229
+ if (validateManifestFlag) {
230
+ if (!forgeRoot) {
231
+ process.stderr.write('× --validate-manifest requires --forge-root <path>\n');
232
+ process.exit(1);
233
+ }
234
+
235
+ const { manifestOnly, basePackOnly } = validateManifest(manifest, forgeRoot);
236
+
237
+ if (manifestOnly.length > 0) {
238
+ process.stdout.write('× Files in manifest but absent from base-pack:\n');
239
+ for (const { nsKey, filename, dir } of manifestOnly) {
240
+ process.stdout.write(` × ${dir}/${filename} (namespace: ${nsKey})\n`);
241
+ }
242
+ }
243
+
244
+ if (basePackOnly.length > 0) {
245
+ process.stdout.write('△ Files in base-pack but absent from manifest:\n');
246
+ for (const { nsKey, filename, dir } of basePackOnly) {
247
+ process.stdout.write(` △ ${dir}/${filename} (namespace: ${nsKey})\n`);
248
+ }
249
+ }
250
+
251
+ if (manifestOnly.length === 0 && basePackOnly.length === 0) {
252
+ process.stdout.write('〇 Manifest and base-pack are in sync.\n');
253
+ }
254
+
255
+ if (manifestOnly.length > 0) {
256
+ process.exit(1);
257
+ }
258
+ process.exit(0);
259
+ }
260
+
261
+ // ── Check namespaces ─────────────────────────────────────────────────────────
262
+
263
+ const result = checkNamespaces(manifest, projectRoot, { strict });
264
+
265
+ // ── Format output ────────────────────────────────────────────────────────────
266
+
267
+ // Group results by namespace for display
268
+ const byNs = {};
269
+ for (const m of result.missing) {
270
+ if (!byNs[m.nsKey]) byNs[m.nsKey] = { present: 0, missing: [], extra: [] };
271
+ byNs[m.nsKey].missing.push(m.filename);
272
+ }
273
+ for (const e of result.extra) {
274
+ if (!byNs[e.nsKey]) byNs[e.nsKey] = { present: 0, missing: [], extra: [] };
275
+ byNs[e.nsKey].extra.push(e.filename);
276
+ }
277
+
278
+ // Count present per namespace
279
+ for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
280
+ if (!byNs[nsKey]) byNs[nsKey] = { present: 0, missing: [], extra: [] };
281
+ const total = (ns.files || []).length;
282
+ const missingCount = byNs[nsKey].missing.length;
283
+ byNs[nsKey].present = total - missingCount;
284
+ }
285
+
286
+ const lines = [];
287
+ let anyMissing = result.missing.length > 0;
288
+ let anyExtra = result.extra.length > 0;
289
+
290
+ for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
291
+ const logicalKey = ns.logicalKey || nsKey;
292
+ const info = byNs[nsKey] || { present: 0, missing: [], extra: [] };
293
+ const total = (ns.files || []).length;
294
+ const dir = ns.dir;
295
+
296
+ if (info.missing.length === 0 && info.extra.length === 0) {
297
+ lines.push(`〇 ${dir}/ — ${info.present}/${total} present`);
298
+ } else {
299
+ if (info.missing.length > 0) {
300
+ lines.push(`× ${dir}/ — ${info.present}/${total} present, ${info.missing.length} missing:`);
301
+ for (const f of info.missing) {
302
+ lines.push(` × ${f}`);
303
+ }
304
+ }
305
+ if (info.extra.length > 0) {
306
+ if (info.missing.length === 0) {
307
+ lines.push(`△ ${dir}/ — ${info.present}/${total} present, ${info.extra.length} extra:`);
308
+ }
309
+ for (const f of info.extra) {
310
+ lines.push(` △ ${f} (not in manifest)`);
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ for (const line of lines) {
317
+ process.stdout.write(line + '\n');
318
+ }
319
+
320
+ if (!anyMissing && !anyExtra) {
321
+ process.stdout.write(`〇 Structure check: all ${result.total} expected files present.\n`);
322
+ process.exit(0);
323
+ }
324
+
325
+ const parts = [`${result.present} present`];
326
+ if (result.missing.length > 0) parts.push(`${result.missing.length} missing`);
327
+ if (strict && result.extra.length > 0) parts.push(`${result.extra.length} extra`);
328
+ process.stdout.write(`── Structure check: ${parts.join(', ')} (of ${result.total} expected)\n`);
329
+
330
+ if (anyMissing) {
331
+ process.exit(1);
332
+ }
333
+ // Extra-only without --strict → exit 0 already handled above
334
+ // Extra-only with --strict
335
+ if (strict && anyExtra) {
336
+ process.exit(1);
337
+ }
338
+ process.exit(0);
339
+
340
+ } catch (err) {
341
+ process.stderr.write(`× check-structure fatal: ${err.message}\n${err.stack}\n`);
342
+ process.exit(1);
343
+ }
344
+ } // end if (require.main === module)
@@ -952,10 +952,16 @@ for (const bug of allBugs) {
952
952
  // When purging events for a bug, aggregate cost data from event files
953
953
  // before they are deleted. The aggregated cost summary is embedded in
954
954
  // the bug's INDEX.md so the information survives the purge.
955
+ //
956
+ // Bug-phase events live in the shared `events/bugs/` virtual sprint dir
957
+ // (see meta-fix-bug.md § Event Emission). Read from that dir and filter
958
+ // by `event.bugId === bug.bugId` — the per-bug `events/{bugId}/` path
959
+ // never existed (silent data loss before this fix).
955
960
  let costTotals;
956
961
  if (PURGE_EVENTS && SPRINT_ARG && SPRINT_ARG === bug.bugId) {
957
- const { events } = loadSprintEvents(bug.bugId);
958
- const tokenEvents = events.filter(e => e.inputTokens !== undefined);
962
+ const { events } = loadSprintEvents('bugs');
963
+ const bugEvents = events.filter(e => e.bugId === bug.bugId);
964
+ const tokenEvents = bugEvents.filter(e => e.inputTokens !== undefined);
959
965
  if (tokenEvents.length > 0) {
960
966
  const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0, sources: new Set() };
961
967
  for (const e of tokenEvents) {
@@ -1022,16 +1028,35 @@ const tag = DRY_RUN ? '[dry-run] ' : '';
1022
1028
  console.log(`${tag}Collated: ${targetSprints.length} sprint(s), ${allBugs.length} bug(s) → MASTER_INDEX.md updated, ${sprintIndexesWritten} sprint INDEX(es), ${taskIndexesWritten} task INDEX(es), ${bugIndexesWritten} bug INDEX(es), ${costReportsWritten} COST_REPORT(s) written`);
1023
1029
 
1024
1030
  // --- Purge event directory if requested ---
1031
+ //
1032
+ // Bug arg → purge only the bug-matching events from the shared
1033
+ // `events/bugs/` dir (filter by event.bugId). Sprint arg → purge the
1034
+ // whole sprint event directory as before.
1025
1035
  if (PURGE_EVENTS) {
1026
- const relDir = path.relative(cwd, path.join(storeRoot, 'events', SPRINT_ARG));
1027
1036
  try {
1028
- const result = _getStore().purgeEvents(SPRINT_ARG, { dryRun: DRY_RUN });
1029
- if (result.fileCount === 0) {
1030
- console.log(`${tag}Purge: no events directory found for '${SPRINT_ARG}' — nothing to delete`);
1031
- } else if (DRY_RUN) {
1032
- console.log(`[dry-run] would purge: ${relDir}/ (${result.fileCount} file(s))`);
1037
+ let result;
1038
+ let relDir;
1039
+ if (IS_BUG_ARG) {
1040
+ relDir = path.relative(cwd, path.join(storeRoot, 'events', 'bugs'));
1041
+ result = _getStore().purgeBugEvents(SPRINT_ARG, { dryRun: DRY_RUN });
1042
+ const label = `'${SPRINT_ARG}' in ${relDir}/`;
1043
+ if (result.fileCount === 0) {
1044
+ console.log(`${tag}Purge: no events for ${label} — nothing to delete`);
1045
+ } else if (DRY_RUN) {
1046
+ console.log(`[dry-run] would purge: ${label} (${result.fileCount} file(s))`);
1047
+ } else {
1048
+ console.log(`Purged: ${label} (${result.fileCount} event file(s) deleted)`);
1049
+ }
1033
1050
  } else {
1034
- console.log(`Purged: ${relDir}/ (${result.fileCount} event file(s) deleted)`);
1051
+ relDir = path.relative(cwd, path.join(storeRoot, 'events', SPRINT_ARG));
1052
+ result = _getStore().purgeEvents(SPRINT_ARG, { dryRun: DRY_RUN });
1053
+ if (result.fileCount === 0) {
1054
+ console.log(`${tag}Purge: no events directory found for '${SPRINT_ARG}' — nothing to delete`);
1055
+ } else if (DRY_RUN) {
1056
+ console.log(`[dry-run] would purge: ${relDir}/ (${result.fileCount} file(s))`);
1057
+ } else {
1058
+ console.log(`Purged: ${relDir}/ (${result.fileCount} event file(s) deleted)`);
1059
+ }
1035
1060
  }
1036
1061
  } catch (err) {
1037
1062
  console.error(`Error: ${err.message}`);
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ // Forge skill query helper
3
+ //
4
+ // Usage:
5
+ // node list-skills.js — print all available skill names (one per line)
6
+ // node list-skills.js <skill-name> — exit 0 if available, exit 1 if not
7
+ //
8
+ // Sources checked:
9
+ // ~/.claude/plugins/installed_plugins.json — marketplace plugins
10
+ // scope "user" → always available
11
+ // scope "local" → only if projectPath matches cwd
12
+ // ~/.claude/skills/ — personal skills (subdirs with SKILL.md)
13
+ //
14
+ // Uses only Node.js built-ins — no npm dependencies required.
15
+ // Works on Linux, macOS, and Windows wherever Claude Code runs.
16
+
17
+ 'use strict';
18
+
19
+ // On unexpected failure, exit 0 so hook callers degrade gracefully
20
+ // (assume skill absent / no output) rather than propagating an error.
21
+ process.on('uncaughtException', () => process.exit(0));
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ const pluginsFile = process.env.CLAUDE_PLUGIN_DATA_ROOT
28
+ ? path.join(process.env.CLAUDE_PLUGIN_DATA_ROOT, 'installed_plugins.json')
29
+ : path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
30
+
31
+ const personalSkillsDir = process.env.CLAUDE_SKILLS_DIR
32
+ || path.join(os.homedir(), '.claude', 'skills');
33
+
34
+ const cwd = process.cwd();
35
+ const skills = new Set();
36
+
37
+ // Source 1: marketplace plugins from installed_plugins.json
38
+ if (fs.existsSync(pluginsFile)) {
39
+ try {
40
+ const data = JSON.parse(fs.readFileSync(pluginsFile, 'utf8'));
41
+ for (const [key, installs] of Object.entries(data.plugins || {})) {
42
+ for (const install of installs) {
43
+ const isUser = install.scope === 'user';
44
+ const isLocal = install.scope === 'local' && install.projectPath === cwd;
45
+ if (isUser || isLocal) {
46
+ skills.add(key.split('@')[0]);
47
+ }
48
+ }
49
+ }
50
+ } catch { /* non-fatal */ }
51
+ }
52
+
53
+ // Source 2: personal skills — subdirectories containing a SKILL.md
54
+ if (fs.existsSync(personalSkillsDir)) {
55
+ try {
56
+ for (const entry of fs.readdirSync(personalSkillsDir, { withFileTypes: true })) {
57
+ if (!entry.isDirectory()) continue;
58
+ const skillMd = path.join(personalSkillsDir, entry.name, 'SKILL.md');
59
+ if (fs.existsSync(skillMd)) {
60
+ skills.add(entry.name);
61
+ }
62
+ }
63
+ } catch { /* non-fatal */ }
64
+ }
65
+
66
+ const sorted = [...skills].sort();
67
+ const query = process.argv[2];
68
+
69
+ if (!query) {
70
+ // Print all available skill names
71
+ if (sorted.length > 0) process.stdout.write(sorted.join('\n') + '\n');
72
+ process.exit(0);
73
+ } else {
74
+ // Exit 0 if skill is available, 1 if not
75
+ process.exit(sorted.includes(query) ? 0 : 1);
76
+ }
@@ -18,7 +18,13 @@
18
18
  const FENCE_OPEN = /^```gates\s+phase=([A-Za-z0-9_-]+)\s*$/;
19
19
  const FENCE_CLOSE = /^```\s*$/;
20
20
 
21
- const VALID_VERDICTS = new Set(['approved', 'revision']);
21
+ // Verdicts a workflow can carry in an `after <phase> = <verdict>` directive.
22
+ // Mirrors `read-verdict.cjs § ALLOWED_VERDICTS` — both must accept the same
23
+ // set. `n/a` is the legitimate verdict for setup phases (plan, implement,
24
+ // triage) that do not produce an approval/revision signal. Rejecting it
25
+ // here caused EMBERGLOW-BUG-001 (v0.44.1) to halt at preflight exit 2
26
+ // when the new fix-bug meta tried to use `after triage = n/a`.
27
+ const VALID_VERDICTS = new Set(['approved', 'revision', 'n/a']);
22
28
 
23
29
  function parseGates(markdown) {
24
30
  if (typeof markdown !== 'string' || markdown.length === 0) return {};
@@ -142,7 +148,7 @@ function parseAfter(rest, lineNo) {
142
148
  const verdict = m[2].toLowerCase();
143
149
  if (!VALID_VERDICTS.has(verdict)) {
144
150
  throw new Error(
145
- `parse-gates: line ${lineNo}: "after" verdict must be "approved" or "revision", got "${m[2]}"`,
151
+ `parse-gates: line ${lineNo}: "after" verdict must be "approved", "revision", or "n/a", got "${m[2]}"`,
146
152
  );
147
153
  }
148
154
  return { phase: m[1], verdict };
@@ -140,6 +140,12 @@ const { suggest, suggestEntityType, formatSuggestion } = require('./lib/suggest.
140
140
  const VALID_SUMMARY_PHASES = new Set(['plan', 'review_plan', 'implementation', 'code_review', 'validation', 'triage', 'approve']);
141
141
 
142
142
  // Schema for a single phase summary (used by set-summary / set-bug-summary)
143
+ // Mirror of bug.schema.json § $defs.phaseSummary. Both definitions MUST stay
144
+ // in sync — this constant validates set-summary / set-bug-summary writes;
145
+ // the JSON schema validates entity reads. `route` is an optional triage-only
146
+ // field carrying the fix-bug pipeline route decision (A | B) and exists here
147
+ // for bug.summaries.triage in particular; non-triage phases simply do not
148
+ // set it.
143
149
  const PHASE_SUMMARY_SCHEMA = {
144
150
  type: 'object',
145
151
  required: ['objective', 'written_at'],
@@ -149,7 +155,8 @@ const PHASE_SUMMARY_SCHEMA = {
149
155
  findings: { type: 'array', items: { type: 'string', maxLength: 200 }, maxItems: 12 },
150
156
  verdict: { type: 'string', enum: ['approved', 'revision', 'n/a'] },
151
157
  written_at: { type: 'string' },
152
- artifact_ref:{ type: 'string' }
158
+ artifact_ref:{ type: 'string' },
159
+ route: { type: 'string', enum: ['A', 'B'] }
153
160
  },
154
161
  additionalProperties: false
155
162
  };
@@ -181,12 +188,21 @@ const SPRINT_TRANSITIONS = {
181
188
  };
182
189
 
183
190
  const BUG_TRANSITIONS = {
184
- reported: ['triaged'],
185
- triaged: ['in-progress'],
186
- 'in-progress': ['fixed', 'approved'], // fixed: code complete; approved: architect direct sign-off
187
- fixed: ['approved', 'in-progress'], // approved: architect sign-off; in-progress: revision
188
- approved: ['verified', 'in-progress', 'fixed'], // verified: commit; in-progress/ fixed: revision
189
- // Terminal: verified
191
+ reported: ['triaged'],
192
+ triaged: ['in-progress'],
193
+ 'in-progress': ['fixed'],
194
+ // Terminal: fixed
195
+ //
196
+ // The `approved` and `verified` enum members were removed (forge#GH-NNN).
197
+ // The architect-approve verdict signal for bugs travels through
198
+ // `bug.summaries.approve.verdict` (see read-verdict.cjs §
199
+ // BUG_PHASE_VERDICT_SOURCE), not `bug.status`. Keeping them in the enum
200
+ // created an LLM-translation trap whereby a task-shaped approve workflow
201
+ // run on a bug improvised `update-status bug ... approved`, which failed
202
+ // either at the schema layer (illegal transition out of terminal
203
+ // `verified`) or, post-FORGE-BUG-002 preflight defence, at the gate
204
+ // layer (forbid bug.status == approved). Dropping the enum removes the
205
+ // trap at its source.
190
206
  };
191
207
 
192
208
  const FEATURE_TRANSITIONS = {
@@ -205,7 +221,7 @@ const TRANSITION_MAP = {
205
221
  const TERMINAL_STATES = new Set([
206
222
  'committed', 'abandoned', // task
207
223
  'retrospective-done', // sprint
208
- 'verified', // bug
224
+ 'fixed', // bug
209
225
  'shipped', 'retired' // feature
210
226
  ]);
211
227
 
@@ -373,6 +389,32 @@ function listEntities(entity, filter) {
373
389
  case 'task': return store.listTasks(filter);
374
390
  case 'bug': return store.listBugs(filter);
375
391
  case 'feature': return store.listFeatures(filter);
392
+ case 'event': {
393
+ // Defect D fix: traverse all sub-directories under events/ — sprints write
394
+ // to events/<sprintId>/, bugs to events/bugs/, enhancements to events/enhancement/.
395
+ // Return the union of all event JSONs across every sub-directory, skipping
396
+ // sidecar files (_-prefixed) and non-JSON files.
397
+ const eventsBase = path.join(store.impl.storeRoot, 'events');
398
+ if (!fs.existsSync(eventsBase)) return [];
399
+ const allEvents = [];
400
+ const subDirs = fs.readdirSync(eventsBase, { withFileTypes: true });
401
+ for (const entry of subDirs) {
402
+ if (!entry.isDirectory()) continue;
403
+ const subDir = path.join(eventsBase, entry.name);
404
+ const files = fs.readdirSync(subDir).filter(
405
+ (f) => f.endsWith('.json') && !f.startsWith('_')
406
+ );
407
+ for (const file of files) {
408
+ try {
409
+ const rec = JSON.parse(fs.readFileSync(path.join(subDir, file), 'utf8'));
410
+ if (rec && (!filter || Object.entries(filter).every(([k, v]) => rec[k] === v))) {
411
+ allEvents.push(rec);
412
+ }
413
+ } catch (_) { /* skip malformed files */ }
414
+ }
415
+ }
416
+ return allEvents;
417
+ }
376
418
  default:
377
419
  console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'feature']))}`);
378
420
  process.exit(1);
@@ -619,7 +661,7 @@ function cmdList() {
619
661
  }
620
662
 
621
663
  if (!ENTITY_TYPES.includes(entity)) {
622
- console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'feature']))}`);
664
+ console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'event', 'feature']))}`);
623
665
  process.exit(1);
624
666
  }
625
667
 
@@ -785,10 +827,13 @@ function cmdEmit() {
785
827
  }
786
828
 
787
829
  // FK check: reject sprintIds that are not in the store and not reserved.
788
- // SYS-* prefix is reserved for system-generated events that predate sprint records.
830
+ // Reserved sprintIds:
831
+ // - SYS-* — system-generated events that predate sprint records
832
+ // - bugs — virtual sprint dir for fix-bug phase events (see
833
+ // meta/tool-specs/validate-store.spec.md §"event.sprintId")
789
834
  // --allow-synthetic bypasses the check for test-harness or synthetic events.
790
835
  if (!allowSynthetic) {
791
- const RESERVED_GLOB = /^SYS-/;
836
+ const RESERVED_GLOB = /^(SYS-|bugs$)/;
792
837
  if (!RESERVED_GLOB.test(sprintId)) {
793
838
  const validSprintIds = resolveValidSprintIds();
794
839
  if (!validSprintIds.includes(sprintId)) {