forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5

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 (196) hide show
  1. package/AGENTS.md +14 -7
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +6 -2
  4. package/bin/forge-cmd.js +21 -1
  5. package/bin/forge.js +16 -369
  6. package/docs/INDEX.md +1 -1
  7. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  8. package/docs/guides/MIGRATION.md +4 -4
  9. package/docs/guides/SETUP.md +16 -16
  10. package/docs/reference/COMMANDS.md +9 -4
  11. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  12. package/docs/reference/RELEASE.md +5 -3
  13. package/docs/reference/TOOLCHAIN.md +8 -0
  14. package/docs/reference/protected-state-surfaces.md +4 -4
  15. package/docs/reference/shepherd.md +117 -17
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/greptile-review-adapter.js +1 -1
  19. package/lib/adapters/pr-state-adapter.js +397 -100
  20. package/lib/agents-config.js +5 -0
  21. package/lib/audit-evidence.js +71 -110
  22. package/lib/capped-jsonl-log.js +236 -0
  23. package/lib/commands/_issue.js +31 -46
  24. package/lib/commands/_manifest.js +1 -1
  25. package/lib/commands/_registry.js +2 -2
  26. package/lib/commands/_resolve-command-opts.js +36 -29
  27. package/lib/commands/claim.js +2 -4
  28. package/lib/commands/clean.js +196 -32
  29. package/lib/commands/dev.js +4 -33
  30. package/lib/commands/hooks.js +358 -13
  31. package/lib/commands/insights.js +8 -3
  32. package/lib/commands/merge.js +600 -40
  33. package/lib/commands/plan.js +23 -115
  34. package/lib/commands/pr.js +1 -1
  35. package/lib/commands/preflight.js +11 -2
  36. package/lib/commands/prime.js +23 -3
  37. package/lib/commands/push.js +41 -51
  38. package/lib/commands/recall.js +60 -16
  39. package/lib/commands/recap.js +6 -1
  40. package/lib/commands/release.js +18 -4
  41. package/lib/commands/serve.js +5 -2
  42. package/lib/commands/setup.js +191 -95
  43. package/lib/commands/shepherd.js +49 -4
  44. package/lib/commands/ship.js +22 -23
  45. package/lib/commands/skill.js +383 -0
  46. package/lib/commands/status.js +54 -33
  47. package/lib/commands/test.js +56 -34
  48. package/lib/commands/worktree.js +247 -43
  49. package/lib/core/runtime-graph.js +89 -15
  50. package/lib/doc-assertions.js +297 -0
  51. package/lib/existing-tdd-gate.js +253 -0
  52. package/lib/forge-context.js +1 -4
  53. package/lib/forge-issues.js +64 -491
  54. package/lib/git-defaults.js +56 -0
  55. package/lib/harness-capability-matrix.js +5 -5
  56. package/lib/hook-renderer.js +147 -16
  57. package/lib/insights.js +96 -80
  58. package/lib/issue-backend.js +42 -3
  59. package/lib/kernel/backing-issue.js +14 -2
  60. package/lib/kernel/broker.js +44 -0
  61. package/lib/kernel/cli-broker-factory.js +12 -1
  62. package/lib/kernel/close-on-merge.js +154 -0
  63. package/lib/kernel/fs-class.js +42 -25
  64. package/lib/kernel/migrations.js +30 -2
  65. package/lib/kernel/schema.js +35 -0
  66. package/lib/kernel/sqlite-driver.js +292 -18
  67. package/lib/lefthook-wiring.js +21 -1
  68. package/lib/memory/router.js +16 -1
  69. package/lib/memory-digest.js +47 -15
  70. package/lib/memory-recall-events.js +145 -0
  71. package/lib/memory-recall.js +212 -0
  72. package/lib/merge-rules.js +8 -4
  73. package/lib/npm-publish-workflow.js +272 -0
  74. package/lib/orientation.js +371 -49
  75. package/lib/plugin-catalog.js +14 -4
  76. package/lib/pr-bundle.js +9 -6
  77. package/lib/pr-monitor/journal.js +18 -2
  78. package/lib/pr-monitor/reconcile-executor.js +842 -0
  79. package/lib/pr-monitor/reconcile-tick.js +138 -0
  80. package/lib/pr-monitor/reconcile.js +0 -0
  81. package/lib/pr-monitor/render-summary.js +196 -0
  82. package/lib/pr-monitor/shepherd-lease.js +252 -0
  83. package/lib/pr-monitor/watch-lifecycle.js +14 -2
  84. package/lib/pr-pull.js +98 -24
  85. package/lib/pr-shepherd.js +34 -8
  86. package/lib/preflight/gates.js +65 -18
  87. package/lib/preflight/runner.js +5 -0
  88. package/lib/project-memory.js +40 -0
  89. package/lib/protected-state-authority.js +305 -0
  90. package/lib/protected-state-surfaces.js +64 -44
  91. package/lib/release-readiness.js +51 -4
  92. package/lib/rules-sync.js +4 -0
  93. package/lib/runtime-health.js +15 -46
  94. package/lib/shell-utils.js +1 -1
  95. package/lib/skill-eval.js +750 -0
  96. package/lib/skills-sync.js +6 -3
  97. package/lib/smart-merge.js +28 -4
  98. package/lib/status/identity.js +46 -0
  99. package/lib/status/presenter.js +0 -35
  100. package/lib/status/snapshot.js +11 -16
  101. package/lib/symlink-utils.js +74 -26
  102. package/lib/upgrade-safety.js +47 -9
  103. package/lib/using-forge.js +328 -0
  104. package/lib/workflow/enforce-stage.js +5 -5
  105. package/lib/workflow/state-manager.js +23 -23
  106. package/package.json +6 -7
  107. package/rules/using-forge.md +24 -0
  108. package/scripts/doc-asserting-tests.js +158 -0
  109. package/scripts/forge-team/index.sh +0 -5
  110. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  111. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  112. package/scripts/lib/behavioral-eval-runner.js +310 -0
  113. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  114. package/scripts/lib/eval-evidence.js +328 -0
  115. package/scripts/lib/eval-runner.js +81 -41
  116. package/scripts/lib/immutable-eval-corpus.js +309 -0
  117. package/scripts/lib/promotion-evidence-loader.js +94 -0
  118. package/scripts/lib/promotion-scorecard.js +314 -0
  119. package/scripts/npm-release-receipt.js +134 -0
  120. package/scripts/process-tree.js +761 -0
  121. package/scripts/protected-state-check.js +47 -22
  122. package/scripts/run-command-eval.js +29 -1
  123. package/scripts/sync-d20-audit.js +172 -0
  124. package/scripts/test-full-suite.js +249 -37
  125. package/scripts/test.js +184 -44
  126. package/skills/claim-safety/SKILL.md +4 -0
  127. package/skills/claim-safety/evals/scorecard.json +41 -0
  128. package/skills/coverage.json +83 -0
  129. package/skills/dev/SKILL.md +4 -0
  130. package/skills/dev/evals/scorecard.json +41 -0
  131. package/skills/gates/SKILL.md +80 -0
  132. package/skills/gates/evals/evals.json +38 -0
  133. package/skills/gates/evals/scorecard.json +41 -0
  134. package/skills/hermes-forge/SKILL.md +1 -0
  135. package/skills/hermes-forge/evals/scorecard.json +41 -0
  136. package/skills/issue-basics/SKILL.md +1 -0
  137. package/skills/issue-basics/evals/scorecard.json +41 -0
  138. package/skills/kernel/SKILL.md +38 -0
  139. package/skills/kernel/evals/scorecard.json +41 -0
  140. package/skills/memory/SKILL.md +16 -1
  141. package/skills/memory/evals/scorecard.json +41 -0
  142. package/skills/parallel-deep-research/SKILL.md +1 -0
  143. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  144. package/skills/plan/SKILL.md +6 -0
  145. package/skills/plan/evals/scorecard.json +41 -0
  146. package/skills/portability/SKILL.md +47 -0
  147. package/skills/portability/evals/evals.json +34 -0
  148. package/skills/portability/evals/scorecard.json +41 -0
  149. package/skills/research/SKILL.md +1 -0
  150. package/skills/research/evals/scorecard.json +41 -0
  151. package/skills/review/SKILL.md +10 -11
  152. package/skills/review/evals/scorecard.json +41 -0
  153. package/skills/rollback/SKILL.md +5 -11
  154. package/skills/rollback/evals/scorecard.json +41 -0
  155. package/skills/setup/SKILL.md +91 -0
  156. package/skills/setup/evals/evals.json +42 -0
  157. package/skills/setup/evals/scorecard.json +41 -0
  158. package/skills/shepherd/SKILL.md +84 -38
  159. package/skills/shepherd/evals/evals.json +21 -9
  160. package/skills/shepherd/evals/scorecard.json +41 -0
  161. package/skills/ship/SKILL.md +10 -12
  162. package/skills/ship/evals/scorecard.json +41 -0
  163. package/skills/smith/SKILL.md +8 -0
  164. package/skills/smith/evals/scorecard.json +41 -0
  165. package/skills/sonarcloud/SKILL.md +1 -0
  166. package/skills/sonarcloud/evals/scorecard.json +41 -0
  167. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  168. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  169. package/skills/status/SKILL.md +3 -0
  170. package/skills/status/evals/scorecard.json +41 -0
  171. package/skills/triage-ready/SKILL.md +2 -0
  172. package/skills/triage-ready/evals/scorecard.json +41 -0
  173. package/skills/using-forge/SKILL.md +104 -0
  174. package/skills/using-forge/evals/scorecard.json +41 -0
  175. package/skills/validate/SKILL.md +4 -0
  176. package/skills/validate/evals/scorecard.json +41 -0
  177. package/skills/verify/SKILL.md +4 -0
  178. package/skills/verify/evals/scorecard.json +41 -0
  179. package/skills/worktree/SKILL.md +92 -0
  180. package/skills/worktree/evals/evals.json +38 -0
  181. package/skills/worktree/evals/scorecard.json +41 -0
  182. package/lib/adapters/beads-issue-adapter.js +0 -127
  183. package/lib/beads-nudge.js +0 -91
  184. package/lib/beads-setup.js +0 -538
  185. package/lib/beads-sync-scaffold.js +0 -189
  186. package/lib/commands/board.js +0 -64
  187. package/lib/pat-setup.js +0 -207
  188. package/lib/pr-monitor/render-sticky.js +0 -192
  189. package/lib/pr-monitor/upsert-sticky.js +0 -169
  190. package/lib/status/beads-snapshot.js +0 -145
  191. package/scripts/beads-context.sh +0 -577
  192. package/scripts/beads-migrate-to-dolt.sh +0 -7
  193. package/scripts/beads-upgrade-smoke.sh +0 -284
  194. package/scripts/forge-team/lib/dashboard.sh +0 -316
  195. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -28,6 +28,7 @@ const VERSION = packageJson.version;
28
28
  // packaged-ASSET read below goes through `getPackageRoot(packageDir)`; the
29
29
  // module-`require` paths above keep `packageDir` (bundler handles those).
30
30
  const { getPackageRoot } = require('../package-root');
31
+ const { cleanupDeprecatedSyncFiles } = require('../deprecated-sync-cleanup');
31
32
 
32
33
  // Load PluginManager for discoverable agent architecture
33
34
  const PluginManager = require('../plugin-manager');
@@ -53,9 +54,8 @@ const { askYesNo: _askYesNoBase } = require('../ui-utils');
53
54
  const contextMerge = require('../context-merge');
54
55
  const projectDiscovery = require('../project-discovery');
55
56
 
56
- // Load lib modules for symlink, beads, and PAT setup
57
+ // Load lib modules for symlink setup
57
58
  const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require('../symlink-utils');
58
- const { scaffoldBeadsSync } = require('../beads-sync-scaffold');
59
59
  const { resolveSyncBackend } = require('../sync-backend');
60
60
  const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
61
61
 
@@ -69,11 +69,13 @@ const { smartMergeAgentsMd } = require('../smart-merge');
69
69
  const { checkLefthookStatus } = require('../lefthook-check');
70
70
  const {
71
71
  FORGE_USER_LEFTHOOK_YML,
72
+ FORGE_USER_LEFTHOOK_YML_NO_TDD,
72
73
  forgeShouldWriteLefthookConfig,
73
74
  installNativeGitHooks,
74
75
  verifyHooksActive,
75
76
  resolveGitHooksDir,
76
77
  } = require('../lefthook-wiring');
78
+ const { detectExistingTddGate, describeExistingGateDeferral } = require('../existing-tdd-gate');
77
79
  const { resolveShellRuntime } = require('../runtime-health');
78
80
  const {
79
81
  buildCodexSkillInstallPlan,
@@ -99,7 +101,6 @@ let FORCE_MODE = false;
99
101
  let VERBOSE_MODE = false;
100
102
  let NON_INTERACTIVE = false;
101
103
  let SYMLINK_ONLY = false;
102
- let SYNC_ENABLED = false;
103
104
  let actionLog = new SetupActionLog();
104
105
  let PKG_MANAGER = 'npm';
105
106
  let SETUP_NOTES = [];
@@ -174,7 +175,6 @@ function detectPackageManager() {
174
175
 
175
176
 
176
177
  const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
177
- 'scripts/beads-context.sh',
178
178
  'scripts/conflict-detect.sh',
179
179
  'scripts/dep-guard-analyze.js',
180
180
  'scripts/dep-guard.sh',
@@ -188,7 +188,6 @@ const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
188
188
  'scripts/forge-team/index.sh',
189
189
  'scripts/forge-team/lib/agent-prompt.sh',
190
190
  'scripts/forge-team/lib/claim.sh',
191
- 'scripts/forge-team/lib/dashboard.sh',
192
191
  'scripts/forge-team/lib/epic.sh',
193
192
  'scripts/forge-team/lib/hooks.sh',
194
193
  'scripts/forge-team/lib/identity.sh',
@@ -219,7 +218,6 @@ function validateAgents(agentList) {
219
218
  // Prerequisite check function
220
219
  function checkPrerequisites(options = {}) {
221
220
  const requireGithubCli = options.requireGithubCli !== false;
222
- const requireBeadsCli = options.requireBeadsCli === true;
223
221
  const requireJq = options.requireJq === true;
224
222
  const commandRunner = options.commandRunner || safeExec;
225
223
  const errors = [];
@@ -256,15 +254,13 @@ function checkPrerequisites(options = {}) {
256
254
  }
257
255
  }
258
256
 
259
- if (requireBeadsCli) {
260
- // Issue tracking runs on the local Forge Kernel store, which auto-provisions
261
- // on first use — there is no external CLI to install. Surface only whether
262
- // team sync is wired up yet.
263
- if (resolveSyncBackend({ projectRoot, env: options.env }) === 'local-noop') {
264
- console.log(' ✓ Kernel issue store (local-noop sync: single-machine until a sync server is configured)');
265
- } else {
266
- console.log(' ✓ Kernel issue store');
267
- }
257
+ // Issue tracking runs on the local Forge Kernel store, which auto-provisions
258
+ // on first use — there is no external CLI to install. Surface only whether
259
+ // team sync is wired up yet.
260
+ if (resolveSyncBackend({ projectRoot, env: options.env }) === 'local-noop') {
261
+ console.log(' ✓ Kernel issue store (local-noop sync: single-machine until a sync server is configured)');
262
+ } else {
263
+ console.log(' ✓ Kernel issue store');
268
264
  }
269
265
 
270
266
  // Check Node.js version
@@ -801,9 +797,7 @@ async function detectProjectStatus() {
801
797
  claudeMdLines: 0,
802
798
  // Project tools status — the Kernel issue store is always present (it
803
799
  // auto-provisions on first use), so issue tracking needs no install probe.
804
- hasBeads: true,
805
800
  hasSkills: isSkillsInitialized(),
806
- beadsInstallType: 'kernel',
807
801
  skillsInstallType: checkForSkills(),
808
802
  // Enhanced: Auto-detected project context
809
803
  autoDetected: null
@@ -2064,7 +2058,8 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
2064
2058
 
2065
2059
  const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile, { symlinkOnly });
2066
2060
  if (result) {
2067
- console.log(` ${result === 'linked' ? 'Linked' : 'Copied'}: ${agent.linkFile}`);
2061
+ const action = result === 'linked' ? 'Linked' : result === 'copied' ? 'Copied' : 'Preserved import';
2062
+ console.log(` ${action}: ${agent.linkFile}`);
2068
2063
  }
2069
2064
  }
2070
2065
 
@@ -2072,7 +2067,7 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
2072
2067
 
2073
2068
 
2074
2069
  // Setup specific agent
2075
- async function setupAgent(agentKey, skipFiles = {}) {
2070
+ async function setupAgent(agentKey, skipFiles = {}, options = {}) {
2076
2071
  const agent = AGENTS[agentKey];
2077
2072
  if (!agent) return;
2078
2073
 
@@ -2112,11 +2107,13 @@ async function setupAgent(agentKey, skipFiles = {}) {
2112
2107
  // Projects Forge's TDD-gate + protected-path enforcement onto each harness's
2113
2108
  // native hook surface. Codex hooks are GLOBAL-config scope and intentionally
2114
2109
  // not written at project setup (see lib/hook-renderer.js).
2115
- if (agentKey === 'claude') {
2116
- setupClaudeHooksConfig();
2117
- }
2118
- if (agent.customSetup === 'cursor') {
2119
- setupCursorHooksConfig();
2110
+ if (!options.skillsOnly) {
2111
+ if (agentKey === 'claude') {
2112
+ setupClaudeHooksConfig();
2113
+ }
2114
+ if (agent.customSetup === 'cursor') {
2115
+ setupCursorHooksConfig();
2116
+ }
2120
2117
  }
2121
2118
 
2122
2119
  // Create link file (SYMLINK_ONLY = --symlink flag disables copy fallback)
@@ -2757,6 +2754,48 @@ function resolveHookEnforcementState(root = projectRoot) {
2757
2754
  }
2758
2755
  }
2759
2756
 
2757
+ // True when .forge/config.yaml already states a rail.tdd_intent choice EXPLICITLY (either
2758
+ // config shape the resolver accepts). Setup must never overwrite that: otherwise the
2759
+ // `forge gate enable rail.tdd_intent` we advertise would be undone by the next `forge setup`.
2760
+ function hasExplicitTddRailChoice(root) {
2761
+ try {
2762
+ const { loadRawConfig } = require('../config-writer');
2763
+ const config = loadRawConfig(root) || {};
2764
+ const viaGates = config.workflow && config.workflow.gates && config.workflow.gates['rail.tdd_intent'];
2765
+ const viaRails = config.rails && config.rails['rail.tdd_intent'];
2766
+ return Boolean(
2767
+ (viaGates && viaGates.enabled !== undefined) || (viaRails && viaRails.enabled !== undefined)
2768
+ );
2769
+ } catch {
2770
+ return false; // unreadable config → treat as unset; setConfigOverride reports any real failure
2771
+ }
2772
+ }
2773
+
2774
+ /**
2775
+ * Defer Forge's TDD gate to a pre-existing one: turn `rail.tdd_intent` off in
2776
+ * .forge/config.yaml so the installed hook scripts are genuinely inert, instead of stacking a
2777
+ * second gate on the same commit (kernel 5b425a85 / 2699b234). No-op when nothing was detected
2778
+ * (a repo with no gate keeps the default-ON rail) or when the user already chose explicitly.
2779
+ *
2780
+ * @returns {{ deferred: boolean, reason?: string }}
2781
+ */
2782
+ function applyExistingGateDeferral(root, detection) {
2783
+ if (!detection || !detection.found) return { deferred: false, reason: 'no-existing-gate' };
2784
+ if (hasExplicitTddRailChoice(root)) return { deferred: false, reason: 'explicit-user-choice' };
2785
+
2786
+ const { setConfigOverride } = require('../config-writer');
2787
+ try {
2788
+ setConfigOverride(root, ['workflow', 'gates', 'rail.tdd_intent', 'enabled'], false);
2789
+ } catch (error) {
2790
+ // An unreadable/malformed .forge/config.yaml must not make `forge setup` fail outright
2791
+ // just because a pre-existing gate was detected. Report and DON'T defer: the caller keys
2792
+ // hook wiring off `deferred`, so falling back to the normal install keeps config and
2793
+ // wiring agreeing, instead of turning the gate off in neither place or in only one.
2794
+ return { deferred: false, reason: 'config-write-failed', error: error.message };
2795
+ }
2796
+ return { deferred: true };
2797
+ }
2798
+
2760
2799
  function describeHookEnforcement(root = projectRoot) {
2761
2800
  const state = resolveHookEnforcementState(root);
2762
2801
  if (!state.resolved) {
@@ -2838,6 +2877,32 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
2838
2877
  // init deliberately degrades to a warning rather than failing, and repair runs inside
2839
2878
  // another command's flow.
2840
2879
  const loud = options.loud === true;
2880
+
2881
+ // BEFORE wiring anything: does this repo already enforce TDD/source-test coupling on
2882
+ // pre-commit? If so, defer to it rather than silently stacking a second gate on the same
2883
+ // commit — the in-the-wild beta.3 report (kernel 5b425a85 / 2699b234). The deferral is
2884
+ // VISIBLE (report below) and reversible with one command.
2885
+ const existingGate = detectExistingTddGate(projectRoot);
2886
+ const deferral = applyExistingGateDeferral(projectRoot, existingGate);
2887
+ // Only claim a deferral when one actually happened. describeExistingGateDeferral() states
2888
+ // that Forge did NOT install its gate and that rail.tdd_intent is off — both false when we
2889
+ // declined to defer, so printing it unconditionally would misreport the installed state.
2890
+ if (deferral.deferred) {
2891
+ console.warn(describeExistingGateDeferral(existingGate));
2892
+ addSetupNote(`Deferred Forge's TDD gate to the existing pre-commit gate in ${existingGate.source}.`);
2893
+ } else if (existingGate.found) {
2894
+ const why = deferral.reason === 'explicit-user-choice'
2895
+ ? 'rail.tdd_intent is set explicitly in .forge/config.yaml, so your choice stands'
2896
+ : `Forge could not update .forge/config.yaml (${deferral.error})`;
2897
+ console.warn(
2898
+ ` ⚠ Detected an existing pre-commit TDD/coupling gate in ${existingGate.source}, but did NOT defer to it:\n` +
2899
+ ` ${why}.\n` +
2900
+ ' Forge\'s own TDD gate is installed as usual, so BOTH gates run on the same commit.\n' +
2901
+ ' Want only yours? Run: forge gate disable rail.tdd_intent'
2902
+ );
2903
+ addSetupNote(`Existing pre-commit gate in ${existingGate.source} detected; Forge's TDD gate installed anyway (${deferral.reason}).`);
2904
+ }
2905
+
2841
2906
  // Honest header: when the TDD gate is disabled in config the hooks install but are inert,
2842
2907
  // so don't announce "(TDD enforcement)" for a state the user turned off (issue eda6d866).
2843
2908
  console.log(
@@ -2877,7 +2942,18 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
2877
2942
  // clobber a config that already has active jobs (kernel c713fce7).
2878
2943
  const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
2879
2944
  if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
2880
- fs.writeFileSync(lefthookTarget, FORGE_USER_LEFTHOOK_YML, 'utf8');
2945
+ // When we actually DEFERRED, write the variant WITHOUT Forge's forge-tdd job so the
2946
+ // commit is never blocked by two gates. Key this off `deferral.deferred`, not off
2947
+ // `existingGate.found`: applyExistingGateDeferral declines to defer when the user has
2948
+ // an explicit rail.tdd_intent choice, and suppressing the wiring anyway would leave
2949
+ // config saying "enabled" while the hook was never installed — making
2950
+ // `forge gate enable rail.tdd_intent` silently ineffective in exactly the repos this
2951
+ // feature targets.
2952
+ fs.writeFileSync(
2953
+ lefthookTarget,
2954
+ deferral.deferred ? FORGE_USER_LEFTHOOK_YML_NO_TDD : FORGE_USER_LEFTHOOK_YML,
2955
+ 'utf8'
2956
+ );
2881
2957
  actionLog.add('lefthook.yml', 'created');
2882
2958
  console.log(' ✓ Created lefthook.yml');
2883
2959
  }
@@ -2925,7 +3001,12 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
2925
3001
  // inert (B3). This repair path only warns (never sets a failure exit code) since it can
2926
3002
  // run mid-stage under enforce-stage's repairWorkflowRuntimeAssets().
2927
3003
  if (!lefthookInstalled) {
2928
- const native = installNativeGitHooks(projectRoot);
3004
+ // Skip the native pre-commit hook when the repo already has its own TDD/coupling gate —
3005
+ // pre-push is still wired, so only the double-gated surface is given up.
3006
+ const native = installNativeGitHooks(
3007
+ projectRoot,
3008
+ existingGate.found ? { skipHooks: ['pre-commit'] } : {}
3009
+ );
2929
3010
  if (native.installed) {
2930
3011
  console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
2931
3012
  } else if (native.skipped && native.skipped.length > 0) {
@@ -3094,7 +3175,7 @@ function initializeSkills(installType) {
3094
3175
 
3095
3176
  // Ensure the issue store during interactive setup. No prompt: the Forge Kernel
3096
3177
  // ships with Forge and auto-provisions, so there is nothing to install or pick.
3097
- async function promptBeadsSetup(_question) {
3178
+ async function promptIssueStoreSetup(_question) {
3098
3179
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
3099
3180
  console.log('Issue Store (Forge Kernel)');
3100
3181
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -3220,17 +3301,14 @@ async function promptSkillsSetup(question) {
3220
3301
  console.log('');
3221
3302
  }
3222
3303
 
3223
- // Interactive setup for Beads and Skills
3224
-
3225
-
3226
- // Interactive setup for Beads and Skills
3304
+ // Interactive setup for the issue store and Skills
3227
3305
  async function setupProjectTools(rl, question) {
3228
3306
  console.log('');
3229
3307
  console.log('═══════════════════════════════════════════════════════════');
3230
3308
  console.log(' STEP 2: Project Tools (Recommended)');
3231
3309
  console.log('═══════════════════════════════════════════════════════════');
3232
3310
  console.log('');
3233
- console.log('Forge recommends three tools for enhanced workflows:');
3311
+ console.log('Forge recommends these tools for enhanced workflows:');
3234
3312
  console.log('');
3235
3313
  console.log('• Issue tracking (Forge Kernel) - zero-install, git-backed');
3236
3314
  console.log(' Persists tasks across sessions, tracks dependencies.');
@@ -3242,7 +3320,7 @@ async function setupProjectTools(rl, question) {
3242
3320
  console.log('');
3243
3321
 
3244
3322
  // Use helper functions to reduce complexity
3245
- await promptBeadsSetup(question);
3323
+ await promptIssueStoreSetup(question);
3246
3324
  await promptSkillsSetup(question);
3247
3325
  }
3248
3326
 
@@ -3396,7 +3474,8 @@ async function ensureGitHooksInstalled(targetRoot = projectRoot) {
3396
3474
 
3397
3475
 
3398
3476
  // Quick setup with defaults
3399
- async function quickSetup(selectedAgents, skipExternal) {
3477
+ async function quickSetup(selectedAgents, skipExternal, options = {}) {
3478
+ const skillsOnly = options.skillsOnly === true;
3400
3479
  showBanner('Quick Setup');
3401
3480
  console.log('');
3402
3481
  console.log('Quick mode: Using defaults...');
@@ -3404,7 +3483,6 @@ async function quickSetup(selectedAgents, skipExternal) {
3404
3483
 
3405
3484
  // Check prerequisites
3406
3485
  checkPrerequisites({
3407
- requireBeadsCli: true,
3408
3486
  requireGithubCli: true,
3409
3487
  requireJq: true,
3410
3488
  });
@@ -3424,41 +3502,46 @@ async function quickSetup(selectedAgents, skipExternal) {
3424
3502
 
3425
3503
  ensureWorkflowShellPolicy(selectedAgents);
3426
3504
 
3427
- // Auto-install lefthook if missing
3428
- autoInstallLefthook();
3505
+ // Auto-install lefthook if missing unless the caller explicitly requested skills only.
3506
+ if (!skillsOnly) {
3507
+ autoInstallLefthook();
3508
+ }
3429
3509
 
3430
3510
  // Auto-setup project tools (Kernel issue store, Skills)
3431
3511
  await autoSetupToolsInQuickMode();
3432
3512
 
3433
3513
  // Setup Claude first if selected, then setup remaining agents
3434
3514
  if (selectedAgents.includes('claude')) {
3435
- await setupAgent('claude');
3515
+ await setupAgent('claude', {}, { skillsOnly });
3436
3516
  }
3437
- await setupSelectedAgents(selectedAgents);
3517
+ await setupSelectedAgents(selectedAgents, undefined, { skillsOnly });
3438
3518
  ensureWorkflowRuntimeAssets(selectedAgents);
3439
3519
 
3440
3520
  // Detect Husky and migrate before installing Lefthook hooks
3441
- await handleHuskyMigration();
3521
+ if (!skillsOnly) {
3522
+ await handleHuskyMigration();
3523
+ }
3442
3524
 
3443
3525
  // Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
3444
3526
  // so an inert-hooks result must fail non-zero (B3), not end green.
3445
3527
  console.log('');
3446
- installGitHooks({ loud: true });
3528
+ if (skillsOnly) {
3529
+ console.log('Skipping git and harness hooks (--skills-only).');
3530
+ } else {
3531
+ installGitHooks({ loud: true });
3532
+ }
3447
3533
 
3448
3534
  // Configure external services with defaults (unless skipped)
3449
3535
  configureDefaultExternalServices(skipExternal);
3450
3536
 
3451
- // --sync flag: scaffold Beads GitHub sync workflows without prompting
3452
- if (SYNC_ENABLED) {
3453
- await handleSyncScaffold();
3454
- }
3537
+ removeDeprecatedSyncFiles();
3455
3538
 
3456
3539
  // Progressive setup summary
3457
3540
  console.log('');
3458
3541
  console.log(renderSetupSummary(actionLog, selectedAgents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
3459
3542
  printSetupNotes();
3460
3543
  // One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
3461
- // Hooks + Beads already handled above, so init skips those side effects.
3544
+ // Hooks + issue store already handled above, so init skips those side effects.
3462
3545
  await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
3463
3546
  console.log('');
3464
3547
  }
@@ -3618,13 +3701,13 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
3618
3701
 
3619
3702
 
3620
3703
  // Helper: Setup all selected agents - extracted to reduce cognitive complexity
3621
- async function setupSelectedAgents(selectedAgents, skipFiles) {
3704
+ async function setupSelectedAgents(selectedAgents, skipFiles, options = {}) {
3622
3705
  const totalAgents = selectedAgents.length;
3623
3706
  for (const [index, agentKey] of selectedAgents.entries()) {
3624
3707
  const agent = AGENTS[agentKey];
3625
3708
  console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
3626
3709
  if (agentKey !== 'claude') { // Claude already done above
3627
- await setupAgent(agentKey, skipFiles);
3710
+ await setupAgent(agentKey, skipFiles, options);
3628
3711
  }
3629
3712
  }
3630
3713
 
@@ -3692,7 +3775,6 @@ async function interactiveSetupWithFlags(flags) {
3692
3775
 
3693
3776
  // Check agent-independent prerequisites first
3694
3777
  checkPrerequisites({
3695
- requireBeadsCli: true,
3696
3778
  requireGithubCli: false,
3697
3779
  requireJq: true,
3698
3780
  });
@@ -3741,11 +3823,11 @@ async function interactiveSetupWithFlags(flags) {
3741
3823
 
3742
3824
  // Setup Claude first if selected (delegated to helper), then remaining agents
3743
3825
  if (selectedAgents.includes('claude')) {
3744
- await setupAgent('claude', skipFiles);
3826
+ await setupAgent('claude', skipFiles, flags);
3745
3827
  }
3746
3828
 
3747
3829
  // Setup each selected agent with progress indication (delegated to helper)
3748
- await setupSelectedAgents(selectedAgents, skipFiles);
3830
+ await setupSelectedAgents(selectedAgents, skipFiles, flags);
3749
3831
  ensureWorkflowRuntimeAssets(selectedAgents);
3750
3832
 
3751
3833
  // Handle external services step (delegated to helper)
@@ -3759,7 +3841,7 @@ async function interactiveSetupWithFlags(flags) {
3759
3841
 
3760
3842
  // One-step onboarding: this path never writes .forge/config.yaml itself, so
3761
3843
  // run `forge init` when the config is still absent (ac0b38c7).
3762
- await finalizeWorkflowConfig();
3844
+ await finalizeWorkflowConfig({ hooksAlreadyInstalled: flags.skillsOnly === true });
3763
3845
  }
3764
3846
 
3765
3847
  // Main
@@ -3832,7 +3914,7 @@ function determineSelectedAgents(flags) {
3832
3914
  // Shared setup executor — used by handleSetupCommand
3833
3915
 
3834
3916
  // Dry-run setup — enumerate planned actions without writing files
3835
- function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
3917
+ function dryRunSetup(agents, options = {}) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
3836
3918
  const collector = new ActionCollector();
3837
3919
 
3838
3920
  // Helper: add create or skip based on whether file exists
@@ -3902,10 +3984,12 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
3902
3984
  }
3903
3985
  }
3904
3986
 
3905
- // Git hooks
3906
- addFileAction('lefthook.yml', 'Git hook configuration');
3907
- addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
3908
- addFileAction('.forge/hooks/forge-native-hook.js', 'Native-hook enforcement adapter (Claude/Cursor)');
3987
+ // Git and harness hooks are opt-out for skills-only setup.
3988
+ if (options.skillsOnly !== true) {
3989
+ addFileAction('lefthook.yml', 'Git hook configuration');
3990
+ addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
3991
+ addFileAction('.forge/hooks/forge-native-hook.js', 'Native-hook enforcement adapter (Claude/Cursor)');
3992
+ }
3909
3993
 
3910
3994
  // Print dry-run summary
3911
3995
  console.log('');
@@ -3920,14 +4004,19 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
3920
4004
 
3921
4005
 
3922
4006
  async function executeSetup(config) {
3923
- const { agents, skipExternal, keepExisting = false, commandRunner } = config;
4007
+ const {
4008
+ agents,
4009
+ skipExternal,
4010
+ keepExisting = false,
4011
+ commandRunner,
4012
+ skillsOnly = false,
4013
+ } = config;
3924
4014
 
3925
4015
  showBanner('Installing for specified agents...');
3926
4016
  console.log('');
3927
4017
 
3928
4018
  // Check prerequisites
3929
4019
  checkPrerequisites({
3930
- requireBeadsCli: true,
3931
4020
  requireGithubCli: requiresGithubCliForSetup(agents),
3932
4021
  requireJq: true,
3933
4022
  commandRunner,
@@ -3954,60 +4043,62 @@ async function executeSetup(config) {
3954
4043
 
3955
4044
  // Setup Claude first if selected, then remaining agents
3956
4045
  if (agents.includes('claude')) {
3957
- await setupAgent('claude', skipFiles);
4046
+ await setupAgent('claude', skipFiles, { skillsOnly });
3958
4047
  }
3959
- await setupSelectedAgents(agents, skipFiles);
4048
+ await setupSelectedAgents(agents, skipFiles, { skillsOnly });
3960
4049
  ensureWorkflowRuntimeAssets(agents);
3961
4050
  ensureWorkflowShellPolicy(agents);
3962
- repairDeclaredLefthookDependency(agents);
4051
+ if (!skillsOnly) {
4052
+ repairDeclaredLefthookDependency(agents);
4053
+ }
3963
4054
 
3964
4055
  // Detect Husky and migrate before installing Lefthook hooks
3965
- await handleHuskyMigration();
4056
+ if (!skillsOnly) {
4057
+ await handleHuskyMigration();
4058
+ }
3966
4059
 
3967
4060
  // Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
3968
4061
  // so an inert-hooks result must fail non-zero (B3), not end green.
3969
4062
  console.log('');
3970
- installGitHooks({ loud: true });
4063
+ if (skillsOnly) {
4064
+ console.log('Skipping git and harness hooks (--skills-only).');
4065
+ } else {
4066
+ installGitHooks({ loud: true });
4067
+ }
3971
4068
 
3972
4069
  // External services (unless skipped)
3973
4070
  await handleExternalServices(skipExternal, agents);
3974
4071
 
3975
- // --sync flag: scaffold Beads GitHub sync workflows without prompting
3976
- if (SYNC_ENABLED) {
3977
- await handleSyncScaffold();
3978
- }
4072
+ removeDeprecatedSyncFiles();
3979
4073
 
3980
4074
  // Progressive setup summary
3981
4075
  console.log('');
3982
4076
  console.log(renderSetupSummary(actionLog, agents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
3983
4077
  printSetupNotes();
3984
4078
  // One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
3985
- // Hooks + Beads already handled above, so init skips those side effects.
4079
+ // Hooks + issue store already handled above, so init skips those side effects.
3986
4080
  await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
3987
4081
  console.log('');
3988
4082
  }
3989
4083
 
3990
- // Helper: Scaffold Beads GitHub sync when --sync flag is provided
3991
-
3992
-
3993
- // Helper: Scaffold Beads GitHub sync when --sync flag is provided
3994
- async function handleSyncScaffold() {
3995
- console.log('');
3996
- console.log('Beads GitHub sync scaffolding is deprecated (--sync).');
4084
+ /**
4085
+ * Remove generated Beads/GitHub sync files left behind by older installs.
4086
+ *
4087
+ * Runs on every setup — there is no longer a flag for it. Only files whose
4088
+ * content matches a known generated template are removed, so user-owned files
4089
+ * at the same paths are preserved.
4090
+ */
4091
+ function removeDeprecatedSyncFiles() {
3997
4092
  try {
3998
- const result = scaffoldBeadsSync(projectRoot, getPackageRoot(packageDir));
3999
- console.log(` ${result.message}`);
4000
- for (const f of result.filesRemoved || []) {
4001
- console.log(` Removed deprecated sync file: ${f}`);
4093
+ const result = cleanupDeprecatedSyncFiles(projectRoot, { packageDir: getPackageRoot(packageDir) });
4094
+ for (const file of result.removed || []) {
4095
+ console.log(` Removed deprecated sync file: ${file}`);
4002
4096
  }
4003
4097
  } catch (err) {
4004
- console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
4098
+ console.error(` Error removing deprecated sync files: ${err.message}`);
4005
4099
  }
4006
4100
  }
4007
4101
 
4008
- // Helper: Handle setup command in non-quick mode
4009
-
4010
-
4011
4102
  // Helper: Handle setup command in non-quick mode
4012
4103
  async function handleSetupCommand(selectedAgents, flags) {
4013
4104
  if (!Array.isArray(selectedAgents) || selectedAgents.length === 0) {
@@ -4025,6 +4116,7 @@ async function handleSetupCommand(selectedAgents, flags) {
4025
4116
  skipExternal: flags.skipExternal,
4026
4117
  keepExisting: flags.keep,
4027
4118
  commandRunner: flags.commandRunner,
4119
+ skillsOnly: flags.skillsOnly,
4028
4120
  });
4029
4121
  } finally {
4030
4122
  projectRoot = savedRoot;
@@ -4169,6 +4261,7 @@ const SETUP_FLAG_DEFAULTS = Object.freeze({
4169
4261
  sync: false,
4170
4262
  symlink: false,
4171
4263
  nonInteractive: false,
4264
+ skillsOnly: false,
4172
4265
  });
4173
4266
 
4174
4267
  const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
@@ -4187,6 +4280,8 @@ const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
4187
4280
  '--symlink': { symlink: true },
4188
4281
  '--yes': { yes: true, nonInteractive: true },
4189
4282
  '-y': { yes: true, nonInteractive: true },
4283
+ '--skills-only': { skillsOnly: true },
4284
+ '--no-hooks': { skillsOnly: true },
4190
4285
  });
4191
4286
 
4192
4287
  function parseAgentFlag(argv, currentIndex, isFlagToken) {
@@ -4263,9 +4358,9 @@ function mergeSetupFlags(flags, argv) {
4263
4358
  standard: Boolean(flags.standard || setupFlags.standard),
4264
4359
  full: Boolean(flags.full || setupFlags.full),
4265
4360
  skipExternal: Boolean(flags.skipExternal || setupFlags.skipExternal),
4266
- sync: Boolean(flags.sync || setupFlags.sync),
4267
4361
  symlink: Boolean(flags.symlink || setupFlags.symlink),
4268
4362
  nonInteractive: Boolean(flags.nonInteractive || setupFlags.nonInteractive),
4363
+ skillsOnly: Boolean(flags.skillsOnly || setupFlags.skillsOnly),
4269
4364
  };
4270
4365
  }
4271
4366
 
@@ -4310,7 +4405,6 @@ module.exports = {
4310
4405
  if (flags.verbose) VERBOSE_MODE = true;
4311
4406
  if (flags.nonInteractive || flags.yes) NON_INTERACTIVE = true;
4312
4407
  if (flags.symlink) SYMLINK_ONLY = true;
4313
- if (flags.sync) SYNC_ENABLED = true;
4314
4408
  actionLog = new SetupActionLog();
4315
4409
  resetSetupNotes();
4316
4410
  PKG_MANAGER = detectPackageManager();
@@ -4328,6 +4422,12 @@ module.exports = {
4328
4422
  }
4329
4423
 
4330
4424
  const [profile] = selectedProfiles;
4425
+ if (flags.skillsOnly) {
4426
+ return {
4427
+ success: false,
4428
+ error: `--skills-only cannot be combined with --${profile}.`,
4429
+ };
4430
+ }
4331
4431
  return initCommand.handler([`--profile=${profile}`, '--yes', ...(flags.force ? ['--force'] : [])], flags, projectRoot);
4332
4432
  }
4333
4433
 
@@ -4352,7 +4452,7 @@ module.exports = {
4352
4452
 
4353
4453
  if (flags.dryRun) {
4354
4454
  if (selectedAgents.length === 0) selectedAgents = ['claude'];
4355
- dryRunSetup(selectedAgents);
4455
+ dryRunSetup(selectedAgents, { skillsOnly: flags.skillsOnly });
4356
4456
  return { success: true };
4357
4457
  }
4358
4458
 
@@ -4361,12 +4461,7 @@ module.exports = {
4361
4461
  if (selectedAgents.length === 0 || (flags.yes && !flags.agents)) {
4362
4462
  selectedAgents = Object.keys(AGENTS);
4363
4463
  }
4364
- await quickSetup(selectedAgents, flags.skipExternal);
4365
- return { success: true };
4366
- }
4367
-
4368
- if (flags.sync && selectedAgents.length === 0) {
4369
- await handleSyncScaffold();
4464
+ await quickSetup(selectedAgents, flags.skipExternal, { skillsOnly: flags.skillsOnly });
4370
4465
  return { success: true };
4371
4466
  }
4372
4467
 
@@ -4407,6 +4502,8 @@ module.exports = {
4407
4502
  ensureGitHooksInstalled,
4408
4503
  forgeShouldWriteLefthookConfig,
4409
4504
  FORGE_USER_LEFTHOOK_YML,
4505
+ applyExistingGateDeferral,
4506
+ installGitHooks,
4410
4507
  setupClaudeMcpConfig,
4411
4508
  setupCursorMcpConfig,
4412
4509
  setupClaudePermissions,
@@ -4431,14 +4528,13 @@ module.exports = {
4431
4528
  backupAndRemoveLegacyCursorRules,
4432
4529
 
4433
4530
  // State accessors for testing
4434
- _getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY, SYNC_ENABLED, PKG_MANAGER }),
4531
+ _getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY, PKG_MANAGER }),
4435
4532
  _setState: (state) => {
4436
4533
  if (state.projectRoot !== undefined) projectRoot = state.projectRoot;
4437
4534
  if (state.FORCE_MODE !== undefined) FORCE_MODE = state.FORCE_MODE;
4438
4535
  if (state.VERBOSE_MODE !== undefined) VERBOSE_MODE = state.VERBOSE_MODE;
4439
4536
  if (state.NON_INTERACTIVE !== undefined) NON_INTERACTIVE = state.NON_INTERACTIVE;
4440
4537
  if (state.SYMLINK_ONLY !== undefined) SYMLINK_ONLY = state.SYMLINK_ONLY;
4441
- if (state.SYNC_ENABLED !== undefined) SYNC_ENABLED = state.SYNC_ENABLED;
4442
4538
  if (state.PKG_MANAGER !== undefined) PKG_MANAGER = state.PKG_MANAGER;
4443
4539
  },
4444
4540
  };