@entelligentsia/forgecli 0.10.1 → 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 (161) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +21 -3
  3. package/dist/CHANGELOG-forge-plugin.md +22 -0
  4. package/dist/extensions/forgecli/add-pipeline.d.ts +19 -0
  5. package/dist/extensions/forgecli/add-pipeline.js +143 -0
  6. package/dist/extensions/forgecli/add-pipeline.js.map +1 -0
  7. package/dist/extensions/forgecli/add-task.d.ts +20 -0
  8. package/dist/extensions/forgecli/add-task.js +154 -0
  9. package/dist/extensions/forgecli/add-task.js.map +1 -0
  10. package/dist/extensions/forgecli/calibrate.d.ts +61 -0
  11. package/dist/extensions/forgecli/calibrate.js +488 -0
  12. package/dist/extensions/forgecli/calibrate.js.map +1 -0
  13. package/dist/extensions/forgecli/fix-bug.d.ts +9 -1
  14. package/dist/extensions/forgecli/fix-bug.js +70 -8
  15. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  16. package/dist/extensions/forgecli/forge-commands.js +15 -22
  17. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  18. package/dist/extensions/forgecli/forge-subagent.js +34 -7
  19. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  20. package/dist/extensions/forgecli/forge-update-command.d.ts +9 -0
  21. package/dist/extensions/forgecli/forge-update-command.js +106 -7
  22. package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
  23. package/dist/extensions/forgecli/health-check.d.ts +22 -1
  24. package/dist/extensions/forgecli/health-check.js +177 -4
  25. package/dist/extensions/forgecli/health-check.js.map +1 -1
  26. package/dist/extensions/forgecli/hook-dispatcher.d.ts +25 -1
  27. package/dist/extensions/forgecli/hook-dispatcher.js +104 -9
  28. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  29. package/dist/extensions/forgecli/hooks/check-update.d.ts +81 -0
  30. package/dist/extensions/forgecli/hooks/check-update.js +308 -0
  31. package/dist/extensions/forgecli/hooks/check-update.js.map +1 -0
  32. package/dist/extensions/forgecli/hooks/forge-permissions.d.ts +32 -0
  33. package/dist/extensions/forgecli/hooks/forge-permissions.js +119 -0
  34. package/dist/extensions/forgecli/hooks/forge-permissions.js.map +1 -0
  35. package/dist/extensions/forgecli/hooks/triage-error.d.ts +23 -0
  36. package/dist/extensions/forgecli/hooks/triage-error.js +62 -0
  37. package/dist/extensions/forgecli/hooks/triage-error.js.map +1 -0
  38. package/dist/extensions/forgecli/hooks/write-guard.d.ts +28 -0
  39. package/dist/extensions/forgecli/hooks/write-guard.js +225 -0
  40. package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -0
  41. package/dist/extensions/forgecli/index.js +60 -0
  42. package/dist/extensions/forgecli/index.js.map +1 -1
  43. package/dist/extensions/forgecli/init-context.d.ts +1 -1
  44. package/dist/extensions/forgecli/init-context.js +21 -6
  45. package/dist/extensions/forgecli/init-context.js.map +1 -1
  46. package/dist/extensions/forgecli/materialize.d.ts +16 -0
  47. package/dist/extensions/forgecli/materialize.js +195 -0
  48. package/dist/extensions/forgecli/materialize.js.map +1 -0
  49. package/dist/extensions/forgecli/migrate.d.ts +19 -0
  50. package/dist/extensions/forgecli/migrate.js +258 -0
  51. package/dist/extensions/forgecli/migrate.js.map +1 -0
  52. package/dist/extensions/forgecli/migration-engine.d.ts +111 -0
  53. package/dist/extensions/forgecli/migration-engine.js +533 -0
  54. package/dist/extensions/forgecli/migration-engine.js.map +1 -0
  55. package/dist/extensions/forgecli/quiz-agent.d.ts +17 -0
  56. package/dist/extensions/forgecli/quiz-agent.js +98 -0
  57. package/dist/extensions/forgecli/quiz-agent.js.map +1 -0
  58. package/dist/extensions/forgecli/remove-command.d.ts +17 -0
  59. package/dist/extensions/forgecli/remove-command.js +124 -0
  60. package/dist/extensions/forgecli/remove-command.js.map +1 -0
  61. package/dist/extensions/forgecli/report-bug.d.ts +25 -0
  62. package/dist/extensions/forgecli/report-bug.js +159 -0
  63. package/dist/extensions/forgecli/report-bug.js.map +1 -0
  64. package/dist/extensions/forgecli/retrospective.d.ts +19 -0
  65. package/dist/extensions/forgecli/retrospective.js +156 -0
  66. package/dist/extensions/forgecli/retrospective.js.map +1 -0
  67. package/dist/extensions/forgecli/run-sprint.js +34 -0
  68. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  69. package/dist/extensions/forgecli/run-task.d.ts +9 -1
  70. package/dist/extensions/forgecli/run-task.js +64 -10
  71. package/dist/extensions/forgecli/run-task.js.map +1 -1
  72. package/dist/extensions/forgecli/session-registry.d.ts +27 -2
  73. package/dist/extensions/forgecli/session-registry.js +52 -1
  74. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  75. package/dist/extensions/forgecli/status-command.d.ts +19 -0
  76. package/dist/extensions/forgecli/status-command.js +140 -0
  77. package/dist/extensions/forgecli/status-command.js.map +1 -0
  78. package/dist/extensions/forgecli/store-query.d.ts +22 -0
  79. package/dist/extensions/forgecli/store-query.js +107 -0
  80. package/dist/extensions/forgecli/store-query.js.map +1 -0
  81. package/dist/extensions/forgecli/store-repair.d.ts +17 -0
  82. package/dist/extensions/forgecli/store-repair.js +123 -0
  83. package/dist/extensions/forgecli/store-repair.js.map +1 -0
  84. package/dist/extensions/forgecli/thread-switcher.js +213 -28
  85. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  86. package/dist/extensions/forgecli/update-tools.d.ts +23 -0
  87. package/dist/extensions/forgecli/update-tools.js +136 -0
  88. package/dist/extensions/forgecli/update-tools.js.map +1 -0
  89. package/dist/extensions/forgecli/viewport-theme.js +4 -0
  90. package/dist/extensions/forgecli/viewport-theme.js.map +1 -1
  91. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  92. package/dist/forge-payload/.schemas/config.schema.json +83 -0
  93. package/dist/forge-payload/.schemas/migrations.json +2049 -0
  94. package/dist/forge-payload/commands/regenerate.md +17 -1
  95. package/dist/forge-payload/meta/personas/README.md +16 -0
  96. package/dist/forge-payload/meta/personas/meta-architect.md +70 -0
  97. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +73 -0
  98. package/dist/forge-payload/meta/personas/meta-collator.md +72 -0
  99. package/dist/forge-payload/meta/personas/meta-engineer.md +70 -0
  100. package/dist/forge-payload/meta/personas/meta-orchestrator.md +71 -0
  101. package/dist/forge-payload/meta/personas/meta-product-manager.md +82 -0
  102. package/dist/forge-payload/meta/personas/meta-qa-engineer.md +91 -0
  103. package/dist/forge-payload/meta/personas/meta-supervisor.md +92 -0
  104. package/dist/forge-payload/meta/skill-recommendations.md +154 -0
  105. package/dist/forge-payload/meta/skills/meta-architect-skills.md +43 -0
  106. package/dist/forge-payload/meta/skills/meta-bug-fixer-skills.md +43 -0
  107. package/dist/forge-payload/meta/skills/meta-collator-skills.md +41 -0
  108. package/dist/forge-payload/meta/skills/meta-engineer-skills.md +43 -0
  109. package/dist/forge-payload/meta/skills/meta-generic-skills.md +58 -0
  110. package/dist/forge-payload/meta/skills/meta-qa-engineer-skills.md +46 -0
  111. package/dist/forge-payload/meta/skills/meta-supervisor-skills.md +43 -0
  112. package/dist/forge-payload/meta/store-schema/bug.schema.md +71 -0
  113. package/dist/forge-payload/meta/store-schema/event.schema.md +76 -0
  114. package/dist/forge-payload/meta/store-schema/feature.schema.md +65 -0
  115. package/dist/forge-payload/meta/store-schema/sprint.schema.md +64 -0
  116. package/dist/forge-payload/meta/store-schema/task.schema.md +78 -0
  117. package/dist/forge-payload/meta/templates/meta-code-review.md +26 -0
  118. package/dist/forge-payload/meta/templates/meta-plan-review.md +28 -0
  119. package/dist/forge-payload/meta/templates/meta-plan.md +28 -0
  120. package/dist/forge-payload/meta/templates/meta-progress.md +25 -0
  121. package/dist/forge-payload/meta/templates/meta-retrospective.md +28 -0
  122. package/dist/forge-payload/meta/templates/meta-sprint-manifest.md +26 -0
  123. package/dist/forge-payload/meta/templates/meta-sprint-requirements.md +91 -0
  124. package/dist/forge-payload/meta/templates/meta-task-prompt.md +26 -0
  125. package/dist/forge-payload/meta/tool-specs/collate.spec.md +88 -0
  126. package/dist/forge-payload/meta/tool-specs/generation-manifest.spec.md +139 -0
  127. package/dist/forge-payload/meta/tool-specs/manage-config.spec.md +143 -0
  128. package/dist/forge-payload/meta/tool-specs/seed-store.spec.md +91 -0
  129. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +328 -0
  130. package/dist/forge-payload/meta/tool-specs/validate-store.spec.md +191 -0
  131. package/dist/forge-payload/meta/workflows/_fragments/context-injection.md +75 -0
  132. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +73 -0
  133. package/dist/forge-payload/meta/workflows/_fragments/finalize.md +13 -0
  134. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +73 -0
  135. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +38 -0
  136. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +39 -0
  137. package/dist/forge-payload/meta/workflows/meta-approve.md +119 -0
  138. package/dist/forge-payload/meta/workflows/meta-collate.md +89 -0
  139. package/dist/forge-payload/meta/workflows/meta-commit.md +93 -0
  140. package/dist/forge-payload/meta/workflows/meta-enhance.md +286 -0
  141. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +501 -0
  142. package/dist/forge-payload/meta/workflows/meta-implement.md +132 -0
  143. package/dist/forge-payload/meta/workflows/meta-migrate.md +455 -0
  144. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +993 -0
  145. package/dist/forge-payload/meta/workflows/meta-plan-task.md +133 -0
  146. package/dist/forge-payload/meta/workflows/meta-quiz-agent.md +135 -0
  147. package/dist/forge-payload/meta/workflows/meta-retrospective.md +65 -0
  148. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +119 -0
  149. package/dist/forge-payload/meta/workflows/meta-review-plan.md +108 -0
  150. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +65 -0
  151. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +76 -0
  152. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +147 -0
  153. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +76 -0
  154. package/dist/forge-payload/meta/workflows/meta-update-plan.md +76 -0
  155. package/dist/forge-payload/meta/workflows/meta-validate.md +111 -0
  156. package/dist/forge-payload/tools/check-structure.cjs +344 -0
  157. package/dist/forge-payload/tools/list-skills.js +76 -0
  158. package/dist/forge-payload/tools/store-cli.cjs +27 -1
  159. package/dist/forge-payload/tools/substitute-placeholders.cjs +60 -8
  160. package/dist/forge-payload/tools/verify-integrity.cjs +86 -0
  161. 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)
@@ -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
+ }
@@ -389,6 +389,32 @@ function listEntities(entity, filter) {
389
389
  case 'task': return store.listTasks(filter);
390
390
  case 'bug': return store.listBugs(filter);
391
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
+ }
392
418
  default:
393
419
  console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'feature']))}`);
394
420
  process.exit(1);
@@ -635,7 +661,7 @@ function cmdList() {
635
661
  }
636
662
 
637
663
  if (!ENTITY_TYPES.includes(entity)) {
638
- 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']))}`);
639
665
  process.exit(1);
640
666
  }
641
667
 
@@ -343,13 +343,14 @@ function renderDeploymentTable(envs) {
343
343
  * Advisory Note 3: every output path is resolved and checked to be under outRoot
344
344
  * to prevent path traversal via symlinks or '..' segments.
345
345
  *
346
- * @param {string} basePack — absolute path to the base-pack directory
346
+ * @param {string} basePack — absolute path to the base-pack directory
347
347
  * @param {Map<string, string>} map
348
- * @param {string} outRoot — absolute project root (e.g. '/home/user/myproject')
349
- * @param {boolean} dryRun — if true, perform no writes
350
- * @param {{ warn: function }} io — pluggable stderr for warnings
348
+ * @param {string} outRoot — absolute project root (e.g. '/home/user/myproject')
349
+ * @param {boolean} dryRun — if true, perform no writes
350
+ * @param {{ warn: function }} io — pluggable stderr for warnings
351
+ * @param {Set<string>|null} categoryFilter — Defect E: if set, only walk matching subdirs
351
352
  */
352
- function walkBasePack(basePack, map, outRoot, dryRun, io) {
353
+ function walkBasePack(basePack, map, outRoot, dryRun, io, categoryFilter) {
353
354
  const warn = (io && io.warn) || ((msg) => process.stderr.write(msg + '\n'));
354
355
 
355
356
  // Extract prefix from substitution map for commands path computation
@@ -363,6 +364,11 @@ function walkBasePack(basePack, map, outRoot, dryRun, io) {
363
364
  const stat = fs.statSync(subdirPath);
364
365
  if (!stat.isDirectory()) continue;
365
366
 
367
+ // Defect E: skip subdirs not in the category filter when one is provided
368
+ if (categoryFilter !== null && categoryFilter !== undefined && !categoryFilter.has(subdir)) {
369
+ continue;
370
+ }
371
+
366
372
  let relOutputDir;
367
373
  if (subdir === 'commands') {
368
374
  relOutputDir = path.join('.claude', 'commands', commandsSubdir);
@@ -464,6 +470,32 @@ function walkBasePackPi(src, outRoot, dryRun, io) {
464
470
  if (require.main === module) {
465
471
  try {
466
472
  const argv = process.argv.slice(2);
473
+
474
+ // ── Defect C fix: short-circuit --help/-h BEFORE any path resolution ────────
475
+ if (argv.includes('--help') || argv.includes('-h')) {
476
+ process.stdout.write([
477
+ 'substitute-placeholders.cjs — Phase 3 (Materialize) engine.',
478
+ '',
479
+ 'Usage:',
480
+ ' node substitute-placeholders.cjs [options]',
481
+ '',
482
+ 'Options:',
483
+ ' --target <claude-code|pi> Output layout target (default: claude-code)',
484
+ ' --src <path> Base-pack source dir for --target pi',
485
+ ' --forge-root <path> Forge plugin root directory',
486
+ ' --base-pack <path> Base-pack directory (probes .base-pack/ then init/base-pack/)',
487
+ ' --config <path> Path to .forge/config.json',
488
+ ' --context <path> Path to project-context.json',
489
+ ' --rules <path> Path to build-base-pack-rules.json',
490
+ ' --out <path> Output root directory (default: cwd)',
491
+ ' --dry-run Preview without writing',
492
+ ' --category <name[,name]> Limit materialisation to named subdirectories',
493
+ ' (personas, skills, workflows, templates, commands)',
494
+ ' --help, -h Show this message and exit',
495
+ ].join('\n') + '\n');
496
+ process.exit(0);
497
+ }
498
+
467
499
  const args = parseCliArgs(argv);
468
500
 
469
501
  const dryRun = args.dryRun || false;
@@ -515,8 +547,20 @@ if (require.main === module) {
515
547
  // Resolve forge root
516
548
  const forgeRoot = args.forgeRoot || resolveForgeRoot();
517
549
 
518
- // Resolve base-pack
519
- const basePack = args.basePack || path.join(forgeRoot, 'init', 'base-pack');
550
+ // Resolve base-pack — Defect C fix: when no --base-pack flag given,
551
+ // probe .base-pack/ (bundled layout) before init/base-pack (source layout).
552
+ let basePack;
553
+ if (args.basePack) {
554
+ basePack = args.basePack;
555
+ } else {
556
+ const dotBasePack = path.resolve(process.cwd(), '.base-pack');
557
+ const initBasePack = path.join(forgeRoot, 'init', 'base-pack');
558
+ if (fs.existsSync(dotBasePack)) {
559
+ basePack = dotBasePack;
560
+ } else {
561
+ basePack = initBasePack;
562
+ }
563
+ }
520
564
  if (!fs.existsSync(basePack)) {
521
565
  process.stderr.write(`substitute-placeholders: base-pack not found at ${basePack}\n`);
522
566
  process.exit(1);
@@ -563,8 +607,15 @@ if (require.main === module) {
563
607
  process.exit(1);
564
608
  }
565
609
 
610
+ // Defect E: parse --category flag into a Set<string> filter (or null for all)
611
+ let categoryFilter = null;
612
+ if (args.category) {
613
+ const cats = args.category.split(',').map((c) => c.trim()).filter(Boolean);
614
+ categoryFilter = new Set(cats);
615
+ }
616
+
566
617
  // Walk and materialise
567
- walkBasePack(basePack, map, outRoot, dryRun, null);
618
+ walkBasePack(basePack, map, outRoot, dryRun, null, categoryFilter);
568
619
 
569
620
  if (dryRun) {
570
621
  process.stdout.write('substitute-placeholders: dry run complete (no files written)\n');
@@ -593,6 +644,7 @@ function parseCliArgs(argv) {
593
644
  if (a === '--context' && argv[i + 1]) { args.context = argv[++i]; continue; }
594
645
  if (a === '--rules' && argv[i + 1]) { args.rules = argv[++i]; continue; }
595
646
  if (a === '--out' && argv[i + 1]) { args.out = argv[++i]; continue; }
647
+ if (a === '--category' && argv[i + 1]) { args.category = argv[++i]; continue; }
596
648
  }
597
649
  return args;
598
650
  }
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Forge tool: verify-integrity
5
+ // Runtime integrity verifier — reads integrity.json, re-hashes each file, reports drift.
6
+ // Usage: node verify-integrity.cjs [--forge-root <path>]
7
+ // Exit codes: 0 = all clean, 1 = one or more files modified or missing, 2 = manifest missing
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const crypto = require('crypto');
12
+
13
+ function computeHash(filePath) {
14
+ const content = fs.readFileSync(filePath);
15
+ return crypto.createHash('sha256').update(content).digest('hex');
16
+ }
17
+
18
+ function verifyIntegrity(forgeRoot) {
19
+ const manifestPath = path.join(forgeRoot, 'integrity.json');
20
+
21
+ if (!fs.existsSync(manifestPath)) {
22
+ const output = '× integrity.json not found — run /forge:update to restore';
23
+ return { exitCode: 1, output, modified: [], missing: [] };
24
+ }
25
+
26
+ let manifest;
27
+ try {
28
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
29
+ } catch (e) {
30
+ const output = `× integrity.json is not valid JSON: ${e.message}`;
31
+ return { exitCode: 1, output, modified: [], missing: [] };
32
+ }
33
+
34
+ const { files } = manifest;
35
+ if (!files || typeof files !== 'object') {
36
+ const output = '× integrity.json has no "files" field';
37
+ return { exitCode: 1, output, modified: [], missing: [] };
38
+ }
39
+
40
+ const modified = [];
41
+ const missing = [];
42
+
43
+ for (const [rel, expectedHash] of Object.entries(files)) {
44
+ const abs = path.join(forgeRoot, rel);
45
+ if (!fs.existsSync(abs)) {
46
+ missing.push(rel);
47
+ continue;
48
+ }
49
+ const actual = computeHash(abs);
50
+ if (actual !== expectedHash) {
51
+ modified.push(rel);
52
+ }
53
+ }
54
+
55
+ const totalFiles = Object.keys(files).length;
56
+ const problemCount = modified.length + missing.length;
57
+
58
+ if (problemCount === 0) {
59
+ const output = `〇 Plugin integrity — all ${totalFiles} files unmodified`;
60
+ return { exitCode: 0, output, modified: [], missing: [] };
61
+ }
62
+
63
+ const lines = [`△ Plugin integrity — ${problemCount} file${problemCount === 1 ? '' : 's'} modified or missing:`];
64
+ for (const f of modified) {
65
+ lines.push(` · ${f} (hash mismatch — run /forge:update to restore)`);
66
+ }
67
+ for (const f of missing) {
68
+ lines.push(` · ${f} (missing — run /forge:update to restore)`);
69
+ }
70
+
71
+ return { exitCode: 1, output: lines.join('\n'), modified, missing };
72
+ }
73
+
74
+ module.exports = { verifyIntegrity };
75
+
76
+ if (require.main === module) {
77
+ const args = process.argv.slice(2);
78
+ const forgeRootIdx = args.indexOf('--forge-root');
79
+ const forgeRoot = forgeRootIdx !== -1
80
+ ? path.resolve(args[forgeRootIdx + 1])
81
+ : path.resolve(__dirname, '..');
82
+
83
+ const result = verifyIntegrity(forgeRoot);
84
+ console.log(result.output);
85
+ process.exit(result.exitCode);
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entelligentsia/forgecli",
3
- "version": "0.10.1",
3
+ "version": "0.11.2",
4
4
  "description": "Forge SDLC ported onto @earendil-works/pi-coding-agent — 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",
@@ -49,7 +49,7 @@
49
49
  ]
50
50
  },
51
51
  "forge": {
52
- "bundledVersion": "0.44.3",
52
+ "bundledVersion": "0.44.6",
53
53
  "forgeRoot": "../forge/forge"
54
54
  },
55
55
  "scripts": {