@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
@@ -56,6 +56,7 @@ class Store {
56
56
  * @returns {{ purged: boolean, fileCount: number, files: string[] }}
57
57
  */
58
58
  purgeEvents(sprintId, opts) { return this.impl.purgeEvents(sprintId, opts); }
59
+ purgeBugEvents(bugId, opts) { return this.impl.purgeBugEvents(bugId, opts); }
59
60
  /**
60
61
  * List event filenames for a sprint directory.
61
62
  * @param {string} sprintId
@@ -287,6 +288,66 @@ class FSImpl {
287
288
  return { purged: true, fileCount: files.length, files };
288
289
  }
289
290
 
291
+ /**
292
+ * Purge only the events belonging to a specific bug from the shared
293
+ * `.forge/store/events/bugs/` virtual sprint dir.
294
+ *
295
+ * Bugs share a single events directory (`events/bugs/`) — see
296
+ * `meta-fix-bug.md § Event Emission` and `validate-store.spec.md`. Purging
297
+ * by bug therefore must filter primary events by `event.bugId === bugId`
298
+ * and sweep sidecars whose filename pattern `_{eventId}_usage.json`
299
+ * matches a purged primary. The `events/bugs/` directory itself is
300
+ * never removed — other bugs' events remain.
301
+ *
302
+ * Returns `{ purged, fileCount, files }` matching `purgeEvents`.
303
+ */
304
+ purgeBugEvents(bugId, { dryRun = false } = {}) {
305
+ const eventsBugsDir = path.join(this.storeRoot, 'events', 'bugs');
306
+ if (!fs.existsSync(eventsBugsDir)) {
307
+ return { purged: false, fileCount: 0, files: [] };
308
+ }
309
+
310
+ const all = fs.readdirSync(eventsBugsDir).filter(f => f.endsWith('.json'));
311
+ const primaries = all.filter(f => !f.startsWith('_'));
312
+ const sidecars = all.filter(f => f.startsWith('_') && f.endsWith('_usage.json'));
313
+
314
+ // Identify primaries whose payload.bugId matches the requested bug.
315
+ const matchedPrimaries = [];
316
+ const matchedEventIds = new Set();
317
+ for (const filename of primaries) {
318
+ const filePath = path.join(eventsBugsDir, filename);
319
+ let payload;
320
+ try {
321
+ payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
322
+ } catch (_) {
323
+ continue; // malformed file — skip
324
+ }
325
+ if (payload && payload.bugId === bugId) {
326
+ matchedPrimaries.push(filename);
327
+ if (payload.eventId) matchedEventIds.add(payload.eventId);
328
+ }
329
+ }
330
+
331
+ // Match sidecars by filename pattern: `_{eventId}_usage.json`.
332
+ const matchedSidecars = sidecars.filter(filename => {
333
+ const m = filename.match(/^_(.+)_usage\.json$/);
334
+ return m && matchedEventIds.has(m[1]);
335
+ });
336
+
337
+ const allMatches = [...matchedPrimaries, ...matchedSidecars];
338
+ if (dryRun) {
339
+ return { purged: false, fileCount: allMatches.length, files: allMatches };
340
+ }
341
+ for (const filename of allMatches) {
342
+ fs.unlinkSync(path.join(eventsBugsDir, filename));
343
+ }
344
+ return {
345
+ purged: allMatches.length > 0,
346
+ fileCount: allMatches.length,
347
+ files: allMatches,
348
+ };
349
+ }
350
+
290
351
  /**
291
352
  * List all event filenames for a sprint directory.
292
353
  * Returns { filename, id } objects for ALL .json files including
@@ -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
  }
@@ -494,8 +494,12 @@ for (const sprint of allSprints) {
494
494
 
495
495
  // --- Pass 2b: Orphan event directories ---
496
496
  // Scan .forge/store/events/ for subdirectories whose name does not match any
497
- // known sprintId and is not a reserved SYS-* prefix.
498
- const RESERVED_EVENT_PREFIX = /^SYS-/;
497
+ // known sprintId and is not a reserved virtual dir.
498
+ // Reserved:
499
+ // - SYS-* — system-generated events that predate sprint records
500
+ // - bugs — virtual sprint dir for fix-bug phase events (see
501
+ // meta/tool-specs/validate-store.spec.md §"event.sprintId")
502
+ const RESERVED_EVENT_PREFIX = /^(SYS-|bugs$)/;
499
503
  const eventsBaseDir = path.join(storeRootFromConfig, 'events');
500
504
  if (fs.existsSync(eventsBaseDir)) {
501
505
  let eventDirEntries;
@@ -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.0",
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.43.19",
52
+ "bundledVersion": "0.44.6",
53
53
  "forgeRoot": "../forge/forge"
54
54
  },
55
55
  "scripts": {