forgedock 1.1.1 → 1.1.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 (171) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +20 -0
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +68 -19
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +80 -8
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/design-bench.md +5 -1
  27. package/commands/design-render-critique-loop.md +1 -1
  28. package/commands/diagnose.md +2 -1
  29. package/commands/explain.md +2 -1
  30. package/commands/forge-stats.md +121 -2
  31. package/commands/forgedock-init.md +1 -1
  32. package/commands/geo-audit.md +3 -0
  33. package/commands/incident-response.md +2 -1
  34. package/commands/issue.md +23 -2
  35. package/commands/milestone.md +77 -12
  36. package/commands/optimize.md +2 -1
  37. package/commands/orchestrate/config.md +67 -0
  38. package/commands/orchestrate/phase-1-resolve.md +159 -0
  39. package/commands/orchestrate/phase-2-triage.md +126 -0
  40. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  41. package/commands/orchestrate/phase-3-dependency.md +591 -0
  42. package/commands/orchestrate/phase-4-execution.md +962 -0
  43. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  44. package/commands/orchestrate/phase-6-report.md +198 -0
  45. package/commands/orchestrate/safety.md +62 -0
  46. package/commands/orchestrate.md +25 -2045
  47. package/commands/pipeline-health/config.md +34 -0
  48. package/commands/pipeline-health/phase-1-context.md +180 -0
  49. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  50. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  51. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  52. package/commands/pipeline-health/phase-5-report.md +284 -0
  53. package/commands/pipeline-health/phase-6-summary.md +52 -0
  54. package/commands/pipeline-health.md +25 -2401
  55. package/commands/pipeline-resume.md +24 -1
  56. package/commands/pipeline-status.md +65 -0
  57. package/commands/qa-sweep.md +4 -1
  58. package/commands/quality-gate.md +248 -10
  59. package/commands/replay.md +2 -1
  60. package/commands/review-pr-agents/api.md +135 -0
  61. package/commands/review-pr-agents/auth.md +137 -0
  62. package/commands/review-pr-agents/billing.md +101 -0
  63. package/commands/review-pr-agents/concurrency.md +145 -0
  64. package/commands/review-pr-agents/database.md +103 -0
  65. package/commands/review-pr-agents/frontend.md +89 -0
  66. package/commands/review-pr-agents/infra.md +385 -0
  67. package/commands/review-pr-agents/protocols.md +185 -0
  68. package/commands/review-pr-agents/scraper.md +185 -0
  69. package/commands/review-pr-agents/security.md +406 -0
  70. package/commands/review-pr-agents.md +45 -1722
  71. package/commands/review-pr-staging.md +196 -40
  72. package/commands/review-pr.md +591 -52
  73. package/commands/rollback.md +2 -1
  74. package/commands/scope.md +2 -1
  75. package/commands/security-audit.md +2 -1
  76. package/commands/signal-planner.md +623 -0
  77. package/commands/sync-ecosystem.md +1 -0
  78. package/commands/test-gate.md +17 -1
  79. package/commands/validate.md +2 -1
  80. package/commands/work-on/build/architect.md +28 -0
  81. package/commands/work-on/build/context.md +2 -0
  82. package/commands/work-on/build/implement.md +3 -1
  83. package/commands/work-on/build/validate.md +26 -7
  84. package/commands/work-on/build.md +163 -24
  85. package/commands/work-on/close.md +130 -3
  86. package/commands/work-on/decompose.md +19 -1
  87. package/commands/work-on/investigate.md +117 -2
  88. package/commands/work-on/review.md +24 -4
  89. package/commands/work-on-monolithic.md +7 -2
  90. package/commands/work-on.md +202 -4
  91. package/docs/BRAND.md +14 -7
  92. package/docs/CI.md +2 -2
  93. package/docs/CODEX.md +36 -1
  94. package/docs/CONFIG.md +5 -8
  95. package/docs/FORGE-PROTOCOL.md +15 -1064
  96. package/docs/anthropic-devrel-outreach.md +2 -2
  97. package/docs/articles/command-decomposition-token-savings.md +214 -0
  98. package/docs/articles/model-release-playbook.md +131 -0
  99. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  100. package/docs/claude-breakpoints.json +75 -5
  101. package/docs/design/abc-benchmark.md +1 -1
  102. package/docs/design/design-spec-schema.md +1 -1
  103. package/docs/design/divergent-generation.md +1 -1
  104. package/docs/design/eval-gate-runbook.md +178 -0
  105. package/docs/design/show-hn-draft.md +26 -1
  106. package/docs/design/token-cost-baseline-1298.md +59 -0
  107. package/docs/eval/README.md +117 -0
  108. package/docs/eval/corpus/seed-v1.json +6 -0
  109. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  110. package/docs/site/annotations-explained.md +24 -0
  111. package/docs/site/command-learning-path.md +51 -14
  112. package/docs/site/command-reference.md +339 -113
  113. package/docs/site/for-companies.md +95 -0
  114. package/docs/site/forge-annotation-protocol.md +12 -256
  115. package/docs/site/getting-started.md +43 -4
  116. package/docs/site/github-is-the-memory.md +113 -0
  117. package/docs/site/how-it-works.md +3 -13
  118. package/docs/site/index.md +9 -1
  119. package/docs/spec/eval-run-result.md +147 -0
  120. package/docs/spec/forge-protocol-v1.md +441 -18
  121. package/docs/spec/label-state-machine.md +55 -0
  122. package/docs/spec/review-protocol.md +158 -0
  123. package/docs/spec-graph-schema.md +28 -0
  124. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  125. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  126. package/examples/forgedock-demo/README.md +41 -22
  127. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  128. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  129. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  130. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  131. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  132. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  133. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  134. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  135. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  136. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  137. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  138. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  139. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  140. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  141. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  142. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  143. package/examples/forgedock-demo/src/db.js +36 -11
  144. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  145. package/examples/forgedock-demo/src/server.js +4 -1
  146. package/package.json +3 -1
  147. package/packages/protocol/LICENSE +21 -0
  148. package/packages/protocol/src/cli.js +135 -0
  149. package/packages/protocol/src/emit.js +146 -0
  150. package/packages/protocol/src/index.js +43 -0
  151. package/packages/protocol/src/parse.js +229 -0
  152. package/packages/protocol/src/types.js +171 -0
  153. package/packages/protocol/src/validate.js +122 -0
  154. package/scripts/bench-topology-cost.mjs +372 -0
  155. package/scripts/check-command-docs-drift.sh +198 -0
  156. package/scripts/check-protocol-restatements.sh +176 -0
  157. package/scripts/check-registry/README.md +72 -0
  158. package/scripts/check-registry/manifest.json +6 -0
  159. package/scripts/code-index.sh +454 -0
  160. package/scripts/conformance-check.mjs +168 -0
  161. package/scripts/doctor-pipeline-state.sh +484 -0
  162. package/scripts/eval-gate-baseline.json +17 -0
  163. package/scripts/eval-gate-scorecard.mjs +242 -0
  164. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  165. package/scripts/flaky-quarantine.sh +241 -0
  166. package/scripts/forge-annotation.sh +567 -0
  167. package/scripts/issue-dedup.sh +236 -0
  168. package/scripts/validate-annotation-node.mjs +233 -0
  169. package/scripts/worktree-lifecycle.sh +3 -3
  170. package/templates/devdocs/agent/using-forgedock.md +14 -0
  171. package/docs/site/.vitepress/config.mjs +0 -85
package/bin/forgedock.mjs CHANGED
@@ -27,8 +27,20 @@ import {
27
27
  backupExisting,
28
28
  manualLowConfidenceKeys,
29
29
  } from "./journey.mjs";
30
- import { removeSessionStartHook } from "./settings-hook.mjs";
31
- import { resolveState, setOptOut, clearNudgeSeen } from "./registry.mjs";
30
+ import {
31
+ removeSessionStartHook,
32
+ removeSubagentStopHook,
33
+ removePreToolUseHook,
34
+ removeSubagentStopEnforceHook,
35
+ } from "./settings-hook.mjs";
36
+ import {
37
+ resolveState,
38
+ setOptOut,
39
+ clearNudgeSeen,
40
+ detectClaudeVersion,
41
+ loadBreakpoints,
42
+ hasFeature,
43
+ } from "./registry.mjs";
32
44
  import { renderMark, ember } from "./cinema.mjs";
33
45
  import {
34
46
  renderLogo,
@@ -58,10 +70,54 @@ const COMMANDS_DIR = join(FORGE_HOME, "commands");
58
70
  // os.homedir() as the always-available fallback (no hard exit — see #744).
59
71
  const HOME = process.env.HOME || process.env.USERPROFILE || homedir();
60
72
 
61
- const TARGET_DIR = join(HOME, ".claude", "commands");
62
- const MANIFEST_PATH = join(HOME, ".claude", "forgedock", "copied-commands.json");
73
+ // ---------------------------------------------------------------------------
74
+ // Install-mode path resolution
75
+ // ---------------------------------------------------------------------------
76
+ // ForgeDock installs commands globally, to ~/.claude/. That is the only
77
+ // location the installer (bin/journey.mjs: forge()) ever writes to — it
78
+ // resolves its target from ctx.home (HOME/USERPROFILE/os.homedir()), never
79
+ // from process.cwd(). A prior refactor (#1288/#1500) added a project-scoped-
80
+ // by-default detection layer here without updating the installer to match,
81
+ // producing a split-brain where status/doctor/uninstall could report a
82
+ // "project-scoped" location that install() never actually wrote to (see
83
+ // #1589). This was backed out: detection now always agrees with what the
84
+ // installer does.
85
+ //
86
+ // --global is still accepted as a CLI flag for backward compatibility with
87
+ // existing invocations, but it is now a no-op — global is the only supported
88
+ // install location.
89
+
90
+ const GLOBAL_CLAUDE_DIR = join(HOME, ".claude");
91
+
92
+ /**
93
+ * Resolve the install target directories for the current invocation.
94
+ * Always resolves to the global (~/.claude) location — see the block
95
+ * comment above for why project-scoped resolution was removed.
96
+ *
97
+ * @returns {{ targetDir: string, scriptsTargetDir: string, manifestPath: string, isGlobal: boolean }}
98
+ */
99
+ function resolveInstallPaths() {
100
+ return {
101
+ targetDir: join(GLOBAL_CLAUDE_DIR, "commands"),
102
+ scriptsTargetDir: join(GLOBAL_CLAUDE_DIR, "scripts"),
103
+ manifestPath: join(GLOBAL_CLAUDE_DIR, "forgedock", "copied-commands.json"),
104
+ isGlobal: true,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Resolve install paths for uninstall/update/doctor/status. Kept as a
110
+ * distinct entry point (rather than calling resolveInstallPaths() directly)
111
+ * since these commands conceptually "detect" an existing install; today
112
+ * that always resolves to the same global location as resolveInstallPaths().
113
+ *
114
+ * @returns {{ targetDir: string, scriptsTargetDir: string, manifestPath: string, isGlobal: boolean }}
115
+ */
116
+ function detectInstallPaths() {
117
+ return resolveInstallPaths();
118
+ }
119
+
63
120
  const SCRIPTS_DIR = join(FORGE_HOME, "scripts");
64
- const SCRIPTS_TARGET_DIR = join(HOME, ".claude", "scripts");
65
121
 
66
122
  /**
67
123
  * Allowlist of pipeline-agent scripts that `uninstall` cleans up from
@@ -84,12 +140,15 @@ const PIPELINE_SCRIPTS = new Set([
84
140
  // --minimal is init-only. Subcommands with their own flag parsing (run,
85
141
  // run-issue, demo) receive restArgs — everything after the command token.
86
142
  const rawArgs = process.argv.slice(2);
87
- const FLAGS = new Set(["--fast", "--manual", "--verbose", "--minimal"]);
143
+ const FLAGS = new Set(["--fast", "--manual", "--verbose", "--minimal", "--extras"]);
88
144
  const flags = rawArgs.filter((a) => FLAGS.has(a));
89
- const positional = rawArgs.filter((a) => !FLAGS.has(a));
145
+ // --global is stripped from positionals/restArgs for backward compatibility
146
+ // with existing invocations, but is otherwise a no-op — see the install-mode
147
+ // path resolution block above (global is now the only install location).
148
+ const positional = rawArgs.filter((a) => !FLAGS.has(a) && a !== "--global");
90
149
  const command = positional[0] || "install";
91
- const cmdIdx = rawArgs.findIndex((a) => !FLAGS.has(a));
92
- const restArgs = cmdIdx === -1 ? [] : rawArgs.slice(cmdIdx + 1);
150
+ const cmdIdx = rawArgs.findIndex((a) => !FLAGS.has(a) && a !== "--global");
151
+ const restArgs = cmdIdx === -1 ? [] : rawArgs.slice(cmdIdx + 1).filter((a) => a !== "--global");
93
152
 
94
153
  // ---------------------------------------------------------------------------
95
154
  // SessionStart hook — settings.json helpers
@@ -492,6 +551,11 @@ function ctx() {
492
551
  const c = makeCtx({ argv: flags });
493
552
  c.forgeHome = FORGE_HOME;
494
553
  c.home = HOME;
554
+ c.includeExtras = flags.includes("--extras");
555
+ // Detect installed Claude Code version for version-gated install paths (#1252).
556
+ // Fail-open: null means version is unknown; callers must handle gracefully.
557
+ c.claudeVersion = detectClaudeVersion();
558
+ c.breakpoints = loadBreakpoints(FORGE_HOME);
495
559
  return c;
496
560
  }
497
561
 
@@ -506,7 +570,8 @@ function statusScreen(c) {
506
570
  const configured = existsSync(join(c.cwd, "forge.yaml"));
507
571
  c.stdout.write(` directory ${state}\n`);
508
572
  c.stdout.write(` forge.yaml ${configured ? "present" : "missing"}\n`);
509
- c.stdout.write(` commands ${existsSync(TARGET_DIR) ? "installed at " + TARGET_DIR : "not installed"}\n`);
573
+ const { targetDir: statusTargetDir } = detectInstallPaths();
574
+ c.stdout.write(` commands ${existsSync(statusTargetDir) ? "installed at " + statusTargetDir : "not installed"}\n`);
510
575
  if (state === "managed-optedout") {
511
576
  c.stdout.write(`\n ForgeDock is disabled (opted out) here. Re-enable: npx forgedock enable\n`);
512
577
  } else if (state === "unmanaged") {
@@ -546,7 +611,7 @@ async function initFlow(c) {
546
611
  stagingBranch: draft.branches.staging.value,
547
612
  });
548
613
  atomicWriteFile(outputPath, content);
549
- celebrate(c, { written: true, todoCount: 0 });
614
+ celebrate(c, { written: true, todoCount: 0, isMinimal: true });
550
615
  return 0;
551
616
  }
552
617
 
@@ -581,6 +646,10 @@ async function uninstall() {
581
646
  console.log(`${BOLD}ForgeDock${RESET} — Removing pipeline commands`);
582
647
  console.log("");
583
648
 
649
+ const { targetDir, scriptsTargetDir, manifestPath } = detectInstallPaths();
650
+ console.log(` Mode: global (~/.claude)`);
651
+ console.log("");
652
+
584
653
  const files = await findMarkdownFiles(COMMANDS_DIR);
585
654
  let removed = 0;
586
655
 
@@ -591,7 +660,7 @@ async function uninstall() {
591
660
  // installs that predate the manifest.
592
661
  let manifest = null;
593
662
  try {
594
- manifest = JSON.parse(await readFile(MANIFEST_PATH, "utf-8"));
663
+ manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
595
664
  } catch {
596
665
  manifest = null;
597
666
  }
@@ -602,7 +671,7 @@ async function uninstall() {
602
671
 
603
672
  for (const file of files) {
604
673
  const rel = relative(COMMANDS_DIR, file);
605
- const target = join(TARGET_DIR, rel);
674
+ const target = join(targetDir, rel);
606
675
 
607
676
  try {
608
677
  const stats = await lstat(target);
@@ -640,7 +709,7 @@ async function uninstall() {
640
709
  // them — the manifest is the only record of ForgeDock's ownership over them.
641
710
  const manifestRels = Object.keys(manifestFiles);
642
711
  for (const rel of manifestRels) {
643
- const target = join(TARGET_DIR, rel);
712
+ const target = join(targetDir, rel);
644
713
  try {
645
714
  const stats = await lstat(target);
646
715
  if (stats.isFile() && !stats.isSymbolicLink()) {
@@ -661,8 +730,8 @@ async function uninstall() {
661
730
  if (manifest && manifestRels.length > 0) {
662
731
  manifest.files = {};
663
732
  try {
664
- await mkdir(dirname(MANIFEST_PATH), { recursive: true });
665
- await writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
733
+ await mkdir(dirname(manifestPath), { recursive: true });
734
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
666
735
  } catch {
667
736
  // Best-effort — a failed manifest write is non-fatal
668
737
  }
@@ -672,13 +741,13 @@ async function uninstall() {
672
741
  console.log(`Done. Removed: ${removed} commands.`);
673
742
  console.log("");
674
743
 
675
- // Remove scripts installed by ForgeDock from ~/.claude/scripts/
744
+ // Remove scripts installed by ForgeDock from the detected scripts target dir
676
745
  const scriptFiles = await findScriptFiles(SCRIPTS_DIR);
677
746
  let scriptsRemoved = 0;
678
747
 
679
748
  for (const file of scriptFiles) {
680
749
  const rel = relative(SCRIPTS_DIR, file);
681
- const target = join(SCRIPTS_TARGET_DIR, rel);
750
+ const target = join(scriptsTargetDir, rel);
682
751
 
683
752
  try {
684
753
  const stats = await lstat(target);
@@ -711,9 +780,8 @@ async function uninstall() {
711
780
 
712
781
  // Remove the SessionStart hook from ~/.claude/settings.json via the
713
782
  // hardened settings-hook module (never writes through malformed JSON).
714
- const { status: hookRemoveResult } = removeSessionStartHook(
715
- join(HOME, ".claude", "settings.json"),
716
- );
783
+ const settingsJsonPath = join(HOME, ".claude", "settings.json");
784
+ const { status: hookRemoveResult } = removeSessionStartHook(settingsJsonPath);
717
785
  if (hookRemoveResult === "removed") {
718
786
  console.log(
719
787
  ` ${GREEN}✔${RESET} Removed SessionStart hook from ${CYAN}~/.claude/settings.json${RESET}`,
@@ -727,6 +795,25 @@ async function uninstall() {
727
795
  ` ${YELLOW}⚠${RESET} Could not update ~/.claude/settings.json — remove the hook manually.`,
728
796
  );
729
797
  }
798
+
799
+ // Remove enforcement hooks (#1250): PreToolUse, SubagentStop interactive
800
+ // engine adapter, and SubagentStop annotation verifier.
801
+ const { status: preToolUseRemoveResult } = removePreToolUseHook(settingsJsonPath);
802
+ if (preToolUseRemoveResult === "removed") {
803
+ console.log(` ${GREEN}✔${RESET} Removed PreToolUse enforcement hook from ${CYAN}~/.claude/settings.json${RESET}`);
804
+ } else if (preToolUseRemoveResult !== "absent") {
805
+ console.log(` ${YELLOW}⚠${RESET} Could not remove PreToolUse hook — check ${CYAN}~/.claude/settings.json${RESET} manually.`);
806
+ }
807
+ const { status: subagentStopRemoveResult } = removeSubagentStopHook(settingsJsonPath);
808
+ if (subagentStopRemoveResult === "removed") {
809
+ console.log(` ${GREEN}✔${RESET} Removed SubagentStop hook from ${CYAN}~/.claude/settings.json${RESET}`);
810
+ }
811
+ const { status: subagentStopEnforceRemoveResult } = removeSubagentStopEnforceHook(settingsJsonPath);
812
+ if (subagentStopEnforceRemoveResult === "removed") {
813
+ console.log(` ${GREEN}✔${RESET} Removed SubagentStop enforcement hook from ${CYAN}~/.claude/settings.json${RESET}`);
814
+ } else if (subagentStopEnforceRemoveResult !== "absent") {
815
+ console.log(` ${YELLOW}⚠${RESET} Could not remove SubagentStop enforcement hook — check ${CYAN}~/.claude/settings.json${RESET} manually.`);
816
+ }
730
817
  console.log("");
731
818
 
732
819
  // Remove ForgeDock managed block from CLAUDE.md and AGENTS.md (if present).
@@ -832,6 +919,8 @@ async function update() {
832
919
  console.log(`${BOLD}ForgeDock${RESET} — Checking for updates`);
833
920
  console.log("");
834
921
 
922
+ console.log(` Mode: global (~/.claude)`);
923
+
835
924
  // Check if installed via npm (no .git directory) or via git clone
836
925
  const gitDir = join(FORGE_HOME, ".git");
837
926
  if (existsSync(gitDir)) {
@@ -1035,6 +1124,10 @@ async function doctor() {
1035
1124
  console.log(`${BOLD}ForgeDock Doctor${RESET} — Installation Health Check`);
1036
1125
  console.log("");
1037
1126
 
1127
+ const { targetDir: TARGET_DIR } = detectInstallPaths();
1128
+ console.log(` Mode: global (~/.claude)`);
1129
+ console.log("");
1130
+
1038
1131
  let failures = 0;
1039
1132
  let warnings = 0;
1040
1133
 
@@ -1277,6 +1370,41 @@ async function doctor() {
1277
1370
  }
1278
1371
  }
1279
1372
 
1373
+ // ── Check 5b: 2026 feature-gated install capabilities ─────────────────────
1374
+ {
1375
+ const detectedVer = detectClaudeVersion();
1376
+ const breakpoints = loadBreakpoints(FORGE_HOME);
1377
+ if (detectedVer === null) {
1378
+ warn(
1379
+ "Version-gated features",
1380
+ "claude CLI not detected — cannot assess 2026 feature availability. Install or add claude to PATH.",
1381
+ );
1382
+ } else {
1383
+ const vStr = Array.isArray(detectedVer) ? detectedVer.join(".") : String(detectedVer);
1384
+ const featureChecks = [
1385
+ ["effort levels", "effort_levels", "2.1.154"],
1386
+ ["SubagentStop context injection", "stop_subagent_stop_additional_context", "2.1.163"],
1387
+ ["Tool(param:value) permission rules", "tool_param_value_permission_rules", "2.1.178"],
1388
+ ["skills/commands merge", "skills_commands_merge", "2.1.196"],
1389
+ ["Sonnet 5 default + 1M context", "sonnet_5_default_1m_context", "2.1.197"],
1390
+ ];
1391
+ const unavailable = featureChecks
1392
+ .filter(([, key]) => !hasFeature(breakpoints, key, vStr))
1393
+ .map(([label, , minVer]) => `${label} (requires v${minVer}+)`);
1394
+ if (unavailable.length === 0) {
1395
+ pass(
1396
+ "Version-gated features",
1397
+ `v${vStr} — all 2026 features available`,
1398
+ );
1399
+ } else {
1400
+ warn(
1401
+ "Version-gated features",
1402
+ `v${vStr} — unavailable: ${unavailable.join(", ")}. Update Claude Code to unlock.`,
1403
+ );
1404
+ }
1405
+ }
1406
+ }
1407
+
1280
1408
  // ── Check 6: CLAUDE.md legacy managed block (cwd, informational) ──────────
1281
1409
  // The journey-based install never injects a CLAUDE.md block — session
1282
1410
  // context comes from the SessionStart hook (see Check 5). A block's absence
@@ -1578,7 +1706,7 @@ function help() {
1578
1706
  const commands = [
1579
1707
  ["Command", "Description"],
1580
1708
  ["npx forgedock", "Guided setup: install commands + configure repo (default)"],
1581
- ["npx forgedock install", "Install commands"],
1709
+ ["npx forgedock install", "Install commands to ~/.claude/"],
1582
1710
  ["npx forgedock init", "Generate forge.yaml config for your project"],
1583
1711
  ["npx forgedock init --minimal", "Generate a minimal forge.yaml (required sections only)"],
1584
1712
  ["npx forgedock enable [dir]", "Activate ForgeDock in a directory"],
@@ -1586,8 +1714,11 @@ function help() {
1586
1714
  ["npx forgedock status [dir]", "Show ForgeDock state for a directory"],
1587
1715
  ["npx forgedock run <cmd> [args]", "Run a command headlessly via the Anthropic API"],
1588
1716
  ["npx forgedock run-issue <issue>", "Drive one issue through the durable engine"],
1717
+ ["npx forgedock resume-stalled [--dry-run]", "Fleet stall recovery — re-dispatch expired-lease issues"],
1589
1718
  ["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
1590
1719
  ["npx forgedock labels [setup] [--repo owner/repo]", "Bootstrap ForgeDock-managed labels on a GitHub repo (idempotent)"],
1720
+ ["npx forgedock watch [--repo owner/repo]", "Live per-agent orchestration view (Ctrl+C to exit)"],
1721
+ ["npx forgedock report [--days 30] [--md] [--json]", "30-day pipeline impact receipts for your repo"],
1591
1722
  ["npx forgedock doctor", "Check installation health"],
1592
1723
  ["npx forgedock update", "Pull latest & reinstall"],
1593
1724
  ["npx forgedock uninstall", "Remove commands"],
@@ -1615,7 +1746,7 @@ function help() {
1615
1746
  *
1616
1747
  * Flags:
1617
1748
  * --dry-run Preview the assembled prompt + tool plan; no API call.
1618
- * --model <id> Override the model (default: claude-sonnet-4-5 or $FORGEDOCK_MODEL).
1749
+ * --model <id> Override the model (default: claude-sonnet-5 or $FORGEDOCK_MODEL).
1619
1750
  * --max-iterations <n> Bound the tool-use loop (default: 50).
1620
1751
  *
1621
1752
  * The live loop requires ANTHROPIC_API_KEY and the optional @anthropic-ai/sdk
@@ -1713,13 +1844,215 @@ async function demo() {
1713
1844
  }
1714
1845
  }
1715
1846
 
1847
+ /**
1848
+ * `forgedock watch` — live per-agent orchestration view
1849
+ *
1850
+ * Polls GitHub every 5 seconds for in-flight issues (those with workflow:*
1851
+ * labels) and renders a per-agent table with issue#, title, phase, elapsed
1852
+ * time, and staleness status. All data is sourced from FORGE:HEARTBEAT
1853
+ * comments written by the pipeline at each phase boundary.
1854
+ *
1855
+ * Stalled agents (no HEARTBEAT update within pipeline.stall_timeout_minutes)
1856
+ * are highlighted in yellow.
1857
+ *
1858
+ * Run: npx forgedock watch [--repo owner/repo]
1859
+ * Exit: Ctrl+C
1860
+ */
1861
+ async function watch() {
1862
+ const POLL_INTERVAL_MS = 5000;
1863
+ const USE_ANSI_WATCH = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
1864
+
1865
+ // ── Resolve repo ──────────────────────────────────────────────────────────
1866
+ const watchRepo = resolveLabelsRepo(restArgs);
1867
+ if (!watchRepo) {
1868
+ process.stderr.write(
1869
+ `${RED}No repository found.${RESET}\n` +
1870
+ ` Run from a directory with ${cyan("forge.yaml")}, or pass ${cyan("--repo owner/repo")}.\n`,
1871
+ );
1872
+ process.exitCode = 1;
1873
+ return;
1874
+ }
1875
+
1876
+ // ── Validate gh CLI auth ───────────────────────────────────────────────────
1877
+ try {
1878
+ execSync("gh auth status", { stdio: ["ignore", "pipe", "ignore"] });
1879
+ } catch {
1880
+ process.stderr.write(
1881
+ `${RED}gh CLI is not authenticated.${RESET}\n` +
1882
+ ` Fix: run ${cyan("gh auth login")} then retry.\n`,
1883
+ );
1884
+ process.exitCode = 1;
1885
+ return;
1886
+ }
1887
+
1888
+ // ── Resolve stall timeout ─────────────────────────────────────────────────
1889
+ let stallMinutes = 15;
1890
+ const forgeYamlPath = join(process.cwd(), "forge.yaml");
1891
+ if (existsSync(forgeYamlPath)) {
1892
+ try {
1893
+ const raw = readFileSync(forgeYamlPath, "utf-8");
1894
+ const m = raw.match(/^\s*stall_timeout_minutes:\s*(\d+)/m);
1895
+ if (m) stallMinutes = parseInt(m[1], 10);
1896
+ } catch { /* ignore */ }
1897
+ }
1898
+
1899
+ // ── Helpers ────────────────────────────────────────────────────────────────
1900
+ function extractPhase(heartbeatBody) {
1901
+ const m = heartbeatBody.match(/\*\*Phase\*\*:\s*(.+)/);
1902
+ return m ? m[1].trim() : "unknown";
1903
+ }
1904
+
1905
+ function extractTimestamp(heartbeatBody) {
1906
+ const m = heartbeatBody.match(/\*\*Timestamp\*\*:\s*(\S+)/);
1907
+ return m ? m[1].trim() : null;
1908
+ }
1909
+
1910
+ function elapsedMin(isoTimestamp) {
1911
+ if (!isoTimestamp) return null;
1912
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
1913
+ return Math.floor(ms / 60000);
1914
+ }
1915
+
1916
+ function elapsedStr(minutes) {
1917
+ if (minutes === null) return "—";
1918
+ if (minutes < 60) return `${minutes}m`;
1919
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
1920
+ }
1921
+
1922
+ // ── Cleanup on exit ────────────────────────────────────────────────────────
1923
+ let intervalHandle;
1924
+ function cleanup() {
1925
+ if (intervalHandle) clearTimeout(intervalHandle);
1926
+ if (USE_ANSI_WATCH) {
1927
+ process.stdout.write("\x1b[?25h"); // show cursor
1928
+ }
1929
+ process.exit(0);
1930
+ }
1931
+ process.on("SIGINT", cleanup);
1932
+ process.on("SIGTERM", cleanup);
1933
+
1934
+ if (USE_ANSI_WATCH) {
1935
+ process.stdout.write("\x1b[?25l"); // hide cursor
1936
+ }
1937
+
1938
+ // ── Render loop ────────────────────────────────────────────────────────────
1939
+ const WORKFLOW_LABELS = [
1940
+ "workflow:investigating",
1941
+ "workflow:ready-to-build",
1942
+ "workflow:building",
1943
+ "workflow:in-review",
1944
+ "needs-human",
1945
+ ];
1946
+
1947
+ async function render() {
1948
+ const rows = [["#", "Title", "Phase", "Elapsed", "Status"]];
1949
+ let anyIssues = false;
1950
+
1951
+ for (const label of WORKFLOW_LABELS) {
1952
+ let issueJson;
1953
+ try {
1954
+ // Use execFileSync with argument array to avoid shell injection via watchRepo
1955
+ issueJson = execFileSync(
1956
+ "gh",
1957
+ ["issue", "list", "-R", watchRepo, "--state", "open",
1958
+ "--label", label, "--limit", "30", "--json", "number,title"],
1959
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] },
1960
+ );
1961
+ } catch {
1962
+ continue;
1963
+ }
1964
+
1965
+ let issues;
1966
+ try { issues = JSON.parse(issueJson); } catch { continue; }
1967
+
1968
+ for (const issue of issues) {
1969
+ anyIssues = true;
1970
+ let phase = label.replace("workflow:", "");
1971
+ let elapsed = null;
1972
+
1973
+ // Fetch latest FORGE:HEARTBEAT comment for phase + timestamp
1974
+ try {
1975
+ const commentsJson = execFileSync(
1976
+ "gh",
1977
+ ["api", `repos/${watchRepo}/issues/${issue.number}/comments`,
1978
+ "--jq", "[.[] | select(.body | contains(\"FORGE:HEARTBEAT\"))] | last | .body // \"\""],
1979
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] },
1980
+ ).trim();
1981
+ if (commentsJson && commentsJson !== '""' && commentsJson !== "") {
1982
+ const body = commentsJson.replace(/^"|"$/g, "").replace(/\\n/g, "\n");
1983
+ phase = extractPhase(body) || phase;
1984
+ const ts = extractTimestamp(body);
1985
+ elapsed = elapsedMin(ts);
1986
+ }
1987
+ } catch { /* use label-derived phase */ }
1988
+
1989
+ const isStalled = elapsed !== null && elapsed >= stallMinutes;
1990
+ const isBlocked = label === "needs-human";
1991
+ const status = isBlocked ? "BLOCKED" : isStalled ? "STALLED" : "running";
1992
+
1993
+ const titleTrunc = issue.title.length > 38 ? issue.title.slice(0, 35) + "..." : issue.title;
1994
+ const phaseShort = phase.length > 30 ? phase.slice(0, 27) + "..." : phase;
1995
+
1996
+ const statusColored = USE_ANSI_WATCH
1997
+ ? (isBlocked ? `\x1b[31m${status}\x1b[0m` : isStalled ? `\x1b[33m${status}\x1b[0m` : `\x1b[32m${status}\x1b[0m`)
1998
+ : status;
1999
+
2000
+ const titleColored = USE_ANSI_WATCH && (isStalled || isBlocked)
2001
+ ? `\x1b[33m${titleTrunc}\x1b[0m`
2002
+ : titleTrunc;
2003
+
2004
+ rows.push([
2005
+ `#${issue.number}`,
2006
+ titleColored,
2007
+ phaseShort,
2008
+ elapsedStr(elapsed),
2009
+ statusColored,
2010
+ ]);
2011
+ }
2012
+ }
2013
+
2014
+ const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
2015
+
2016
+ if (USE_ANSI_WATCH) {
2017
+ process.stdout.write("\x1b[2J\x1b[H"); // clear screen + home
2018
+ } else {
2019
+ process.stdout.write(`\n${"─".repeat(60)}\n`);
2020
+ }
2021
+
2022
+ process.stdout.write(`${BOLD}ForgeDock Watch${RESET} — ${dim(watchRepo)} ${dim(now)}\n\n`);
2023
+
2024
+ if (!anyIssues || rows.length === 1) {
2025
+ process.stdout.write(` ${dim("No in-flight issues. All quiet.")}\n`);
2026
+ } else {
2027
+ process.stdout.write(table(rows, { header: true }));
2028
+ }
2029
+
2030
+ if (USE_ANSI_WATCH) {
2031
+ process.stdout.write(`\n${dim("Refreshing every 5s — Ctrl+C to exit")}\n`);
2032
+ }
2033
+ }
2034
+
2035
+ // Self-scheduling render loop — each render awaits completion before
2036
+ // scheduling the next, preventing overlapping cycles when GitHub API
2037
+ // latency exceeds the poll interval (forge#1428). The scheduled timeout is
2038
+ // the ONLY handle keeping the event loop alive for this command — it must
2039
+ // stay ref'd, or Node exits after the first render despite printing
2040
+ // "Refreshing every Ns — Ctrl+C to exit" (forge#1593). The only intended
2041
+ // exit path is SIGINT/SIGTERM → cleanup(), which restores the cursor.
2042
+ async function scheduleRender() {
2043
+ await render();
2044
+ intervalHandle = setTimeout(scheduleRender, POLL_INTERVAL_MS);
2045
+ }
2046
+ await scheduleRender();
2047
+ }
2048
+
1716
2049
  // The journey-routed commands render their own branded marks (hero/compact);
1717
2050
  // splash() would double-brand them. Engine-surface commands, help, and
1718
2051
  // unknown-command output keep the logo.
1719
- const SPLASH_COMMANDS = new Set(["run", "run-issue", "demo", "doctor", "help", "--help", "-h"]);
2052
+ const SPLASH_COMMANDS = new Set(["run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
1720
2053
  const KNOWN_COMMANDS = new Set([
1721
2054
  "install", "init", "enable", "disable", "status", "uninstall", "update",
1722
- "run", "run-issue", "demo", "doctor", "help", "--help", "-h",
2055
+ "run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "labels", "help", "--help", "-h",
1723
2056
  ]);
1724
2057
  if (SPLASH_COMMANDS.has(command) || !KNOWN_COMMANDS.has(command)) splash();
1725
2058
 
@@ -1727,6 +2060,32 @@ let exitCode = 0;
1727
2060
  switch (command) {
1728
2061
  case "install": {
1729
2062
  const c = ctx();
2063
+ // Version-gated install path (#1252): detect Claude Code version and advise
2064
+ // on features available or missing. Fail-open — null version skips checks.
2065
+ if (c.claudeVersion) {
2066
+ // Gate: effort levels require v2.1.154+
2067
+ if (!hasFeature(c.breakpoints, "effort_levels", c.claudeVersion)) {
2068
+ process.stderr.write(
2069
+ ` ${YELLOW}note${RESET} Claude Code v${c.claudeVersion} detected — effort-level frontmatter requires v2.1.154+. ` +
2070
+ `Some orchestration features will use fallback mode.\n`
2071
+ );
2072
+ }
2073
+ // Gate: tool permission rules require v2.1.178+
2074
+ if (!hasFeature(c.breakpoints, "tool_param_value_permission_rules", c.claudeVersion)) {
2075
+ process.stderr.write(
2076
+ ` ${YELLOW}note${RESET} Claude Code v${c.claudeVersion} detected — Tool(param:value) permission rules require v2.1.178+. ` +
2077
+ `Hook-based enforcement (#1250) will not be installed.\n`
2078
+ );
2079
+ } else {
2080
+ // v2.1.178+ supports permission rules — suggest opt-in rules for pipeline safety.
2081
+ process.stderr.write(
2082
+ ` ${CYAN}tip${RESET} Claude Code v${c.claudeVersion} supports permission rules. ` +
2083
+ `Consider adding to ~/.claude/settings.json:\n` +
2084
+ ` { "permissions": { "deny": ["Bash(git push origin main*)"] } }\n` +
2085
+ ` This prevents accidental direct pushes to main.\n`
2086
+ );
2087
+ }
2088
+ }
1730
2089
  if (existsSync(join(c.cwd, "forge.yaml")) && resolveState(c.cwd) === "managed-active") {
1731
2090
  statusScreen(c);
1732
2091
  } else {
@@ -1771,7 +2130,23 @@ switch (command) {
1771
2130
  break;
1772
2131
  case "run-issue": {
1773
2132
  const { runFromCli } = await import("./engine-cli.mjs");
1774
- await runFromCli(restArgs);
2133
+ try {
2134
+ await runFromCli(restArgs);
2135
+ } catch (err) {
2136
+ process.stderr.write(`${RED}${err.message}${RESET}\n`);
2137
+ exitCode = 1;
2138
+ }
2139
+ break;
2140
+ }
2141
+ case "resume-stalled": {
2142
+ const { resumeStalledFromCli } = await import("./engine-cli.mjs");
2143
+ try {
2144
+ const result = await resumeStalledFromCli(restArgs);
2145
+ if (result && result.failed && result.failed.length > 0) exitCode = 1;
2146
+ } catch (err) {
2147
+ process.stderr.write(`${RED}${err.message}${RESET}\n`);
2148
+ exitCode = 1;
2149
+ }
1775
2150
  break;
1776
2151
  }
1777
2152
  case "demo":
@@ -1780,6 +2155,14 @@ switch (command) {
1780
2155
  case "doctor":
1781
2156
  exitCode = await doctor();
1782
2157
  break;
2158
+ case "report": {
2159
+ const { runReport } = await import("./report.mjs");
2160
+ await runReport(restArgs);
2161
+ break;
2162
+ }
2163
+ case "watch":
2164
+ await watch();
2165
+ break;
1783
2166
  case "labels": {
1784
2167
  const subcommand = positional[1];
1785
2168
  if (!subcommand || subcommand === "setup" || subcommand.startsWith("--")) {