forge-workflow 0.1.0-beta.4 → 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 (119) 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 +20 -0
  5. package/bin/forge.js +16 -374
  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 +8 -5
  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 +54 -25
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/pr-state-adapter.js +344 -142
  19. package/lib/audit-evidence.js +71 -110
  20. package/lib/capped-jsonl-log.js +236 -0
  21. package/lib/commands/_registry.js +2 -2
  22. package/lib/commands/clean.js +196 -32
  23. package/lib/commands/dev.js +4 -33
  24. package/lib/commands/hooks.js +223 -25
  25. package/lib/commands/insights.js +8 -3
  26. package/lib/commands/merge.js +600 -40
  27. package/lib/commands/pr.js +1 -1
  28. package/lib/commands/preflight.js +11 -2
  29. package/lib/commands/prime.js +21 -8
  30. package/lib/commands/push.js +41 -51
  31. package/lib/commands/recall.js +60 -16
  32. package/lib/commands/recap.js +6 -1
  33. package/lib/commands/release.js +17 -2
  34. package/lib/commands/setup.js +191 -94
  35. package/lib/commands/shepherd.js +13 -1
  36. package/lib/commands/ship.js +22 -23
  37. package/lib/commands/skill.js +119 -11
  38. package/lib/commands/status.js +17 -1
  39. package/lib/commands/test.js +24 -34
  40. package/lib/commands/worktree.js +220 -42
  41. package/lib/core/runtime-graph.js +1 -1
  42. package/lib/doc-assertions.js +297 -0
  43. package/lib/existing-tdd-gate.js +253 -0
  44. package/lib/forge-context.js +1 -4
  45. package/lib/forge-issues.js +56 -32
  46. package/lib/git-defaults.js +56 -0
  47. package/lib/harness-capability-matrix.js +3 -3
  48. package/lib/hook-renderer.js +93 -4
  49. package/lib/insights.js +96 -80
  50. package/lib/kernel/backing-issue.js +14 -2
  51. package/lib/kernel/broker.js +16 -0
  52. package/lib/kernel/cli-broker-factory.js +12 -1
  53. package/lib/kernel/close-on-merge.js +154 -0
  54. package/lib/kernel/fs-class.js +42 -25
  55. package/lib/kernel/sqlite-driver.js +153 -29
  56. package/lib/lefthook-wiring.js +21 -1
  57. package/lib/memory/router.js +16 -1
  58. package/lib/memory-digest.js +47 -15
  59. package/lib/memory-recall-events.js +145 -0
  60. package/lib/memory-recall.js +71 -10
  61. package/lib/merge-rules.js +8 -4
  62. package/lib/npm-publish-workflow.js +272 -0
  63. package/lib/orientation.js +68 -43
  64. package/lib/plugin-catalog.js +14 -4
  65. package/lib/pr-bundle.js +5 -6
  66. package/lib/pr-monitor/journal.js +18 -2
  67. package/lib/pr-monitor/reconcile-executor.js +224 -41
  68. package/lib/pr-monitor/render-summary.js +196 -0
  69. package/lib/pr-monitor/shepherd-lease.js +10 -1
  70. package/lib/pr-monitor/watch-lifecycle.js +13 -1
  71. package/lib/pr-pull.js +33 -14
  72. package/lib/pr-shepherd.js +34 -8
  73. package/lib/preflight/gates.js +65 -18
  74. package/lib/preflight/runner.js +5 -0
  75. package/lib/project-memory.js +33 -1
  76. package/lib/protected-state-authority.js +305 -0
  77. package/lib/protected-state-surfaces.js +64 -44
  78. package/lib/release-readiness.js +51 -4
  79. package/lib/shell-utils.js +1 -1
  80. package/lib/skills-sync.js +6 -3
  81. package/lib/smart-merge.js +28 -4
  82. package/lib/symlink-utils.js +74 -26
  83. package/lib/upgrade-safety.js +39 -0
  84. package/lib/using-forge.js +19 -6
  85. package/package.json +6 -7
  86. package/scripts/doc-asserting-tests.js +158 -0
  87. package/scripts/lib/behavioral-eval-runner.js +310 -0
  88. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  89. package/scripts/lib/eval-evidence.js +328 -0
  90. package/scripts/lib/eval-runner.js +81 -41
  91. package/scripts/lib/immutable-eval-corpus.js +309 -0
  92. package/scripts/lib/promotion-evidence-loader.js +94 -0
  93. package/scripts/lib/promotion-scorecard.js +314 -0
  94. package/scripts/npm-release-receipt.js +134 -0
  95. package/scripts/process-tree.js +761 -0
  96. package/scripts/protected-state-check.js +47 -22
  97. package/scripts/run-command-eval.js +29 -1
  98. package/scripts/sync-d20-audit.js +172 -0
  99. package/scripts/test-full-suite.js +249 -37
  100. package/scripts/test.js +176 -43
  101. package/skills/review/SKILL.md +4 -11
  102. package/skills/review/evals/scorecard.json +3 -3
  103. package/skills/rollback/SKILL.md +4 -11
  104. package/skills/rollback/evals/scorecard.json +3 -3
  105. package/skills/shepherd/SKILL.md +20 -14
  106. package/skills/shepherd/evals/scorecard.json +2 -2
  107. package/skills/ship/SKILL.md +4 -12
  108. package/skills/ship/evals/scorecard.json +3 -3
  109. package/skills/worktree/SKILL.md +6 -1
  110. package/skills/worktree/evals/scorecard.json +2 -2
  111. package/lib/beads-setup.js +0 -538
  112. package/lib/beads-sync-scaffold.js +0 -189
  113. package/lib/pat-setup.js +0 -207
  114. package/lib/pr-monitor/render-sticky.js +0 -206
  115. package/lib/pr-monitor/upsert-sticky.js +0 -169
  116. package/scripts/beads-context.sh +0 -577
  117. package/scripts/beads-migrate-to-dolt.sh +0 -7
  118. package/scripts/beads-upgrade-smoke.sh +0 -284
  119. 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',
@@ -218,7 +218,6 @@ function validateAgents(agentList) {
218
218
  // Prerequisite check function
219
219
  function checkPrerequisites(options = {}) {
220
220
  const requireGithubCli = options.requireGithubCli !== false;
221
- const requireBeadsCli = options.requireBeadsCli === true;
222
221
  const requireJq = options.requireJq === true;
223
222
  const commandRunner = options.commandRunner || safeExec;
224
223
  const errors = [];
@@ -255,15 +254,13 @@ function checkPrerequisites(options = {}) {
255
254
  }
256
255
  }
257
256
 
258
- if (requireBeadsCli) {
259
- // Issue tracking runs on the local Forge Kernel store, which auto-provisions
260
- // on first use — there is no external CLI to install. Surface only whether
261
- // team sync is wired up yet.
262
- if (resolveSyncBackend({ projectRoot, env: options.env }) === 'local-noop') {
263
- console.log(' ✓ Kernel issue store (local-noop sync: single-machine until a sync server is configured)');
264
- } else {
265
- console.log(' ✓ Kernel issue store');
266
- }
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');
267
264
  }
268
265
 
269
266
  // Check Node.js version
@@ -800,9 +797,7 @@ async function detectProjectStatus() {
800
797
  claudeMdLines: 0,
801
798
  // Project tools status — the Kernel issue store is always present (it
802
799
  // auto-provisions on first use), so issue tracking needs no install probe.
803
- hasBeads: true,
804
800
  hasSkills: isSkillsInitialized(),
805
- beadsInstallType: 'kernel',
806
801
  skillsInstallType: checkForSkills(),
807
802
  // Enhanced: Auto-detected project context
808
803
  autoDetected: null
@@ -2063,7 +2058,8 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
2063
2058
 
2064
2059
  const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile, { symlinkOnly });
2065
2060
  if (result) {
2066
- 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}`);
2067
2063
  }
2068
2064
  }
2069
2065
 
@@ -2071,7 +2067,7 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
2071
2067
 
2072
2068
 
2073
2069
  // Setup specific agent
2074
- async function setupAgent(agentKey, skipFiles = {}) {
2070
+ async function setupAgent(agentKey, skipFiles = {}, options = {}) {
2075
2071
  const agent = AGENTS[agentKey];
2076
2072
  if (!agent) return;
2077
2073
 
@@ -2111,11 +2107,13 @@ async function setupAgent(agentKey, skipFiles = {}) {
2111
2107
  // Projects Forge's TDD-gate + protected-path enforcement onto each harness's
2112
2108
  // native hook surface. Codex hooks are GLOBAL-config scope and intentionally
2113
2109
  // not written at project setup (see lib/hook-renderer.js).
2114
- if (agentKey === 'claude') {
2115
- setupClaudeHooksConfig();
2116
- }
2117
- if (agent.customSetup === 'cursor') {
2118
- setupCursorHooksConfig();
2110
+ if (!options.skillsOnly) {
2111
+ if (agentKey === 'claude') {
2112
+ setupClaudeHooksConfig();
2113
+ }
2114
+ if (agent.customSetup === 'cursor') {
2115
+ setupCursorHooksConfig();
2116
+ }
2119
2117
  }
2120
2118
 
2121
2119
  // Create link file (SYMLINK_ONLY = --symlink flag disables copy fallback)
@@ -2756,6 +2754,48 @@ function resolveHookEnforcementState(root = projectRoot) {
2756
2754
  }
2757
2755
  }
2758
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
+
2759
2799
  function describeHookEnforcement(root = projectRoot) {
2760
2800
  const state = resolveHookEnforcementState(root);
2761
2801
  if (!state.resolved) {
@@ -2837,6 +2877,32 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
2837
2877
  // init deliberately degrades to a warning rather than failing, and repair runs inside
2838
2878
  // another command's flow.
2839
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
+
2840
2906
  // Honest header: when the TDD gate is disabled in config the hooks install but are inert,
2841
2907
  // so don't announce "(TDD enforcement)" for a state the user turned off (issue eda6d866).
2842
2908
  console.log(
@@ -2876,7 +2942,18 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
2876
2942
  // clobber a config that already has active jobs (kernel c713fce7).
2877
2943
  const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
2878
2944
  if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
2879
- 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
+ );
2880
2957
  actionLog.add('lefthook.yml', 'created');
2881
2958
  console.log(' ✓ Created lefthook.yml');
2882
2959
  }
@@ -2924,7 +3001,12 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
2924
3001
  // inert (B3). This repair path only warns (never sets a failure exit code) since it can
2925
3002
  // run mid-stage under enforce-stage's repairWorkflowRuntimeAssets().
2926
3003
  if (!lefthookInstalled) {
2927
- 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
+ );
2928
3010
  if (native.installed) {
2929
3011
  console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
2930
3012
  } else if (native.skipped && native.skipped.length > 0) {
@@ -3093,7 +3175,7 @@ function initializeSkills(installType) {
3093
3175
 
3094
3176
  // Ensure the issue store during interactive setup. No prompt: the Forge Kernel
3095
3177
  // ships with Forge and auto-provisions, so there is nothing to install or pick.
3096
- async function promptBeadsSetup(_question) {
3178
+ async function promptIssueStoreSetup(_question) {
3097
3179
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
3098
3180
  console.log('Issue Store (Forge Kernel)');
3099
3181
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -3219,17 +3301,14 @@ async function promptSkillsSetup(question) {
3219
3301
  console.log('');
3220
3302
  }
3221
3303
 
3222
- // Interactive setup for Beads and Skills
3223
-
3224
-
3225
- // Interactive setup for Beads and Skills
3304
+ // Interactive setup for the issue store and Skills
3226
3305
  async function setupProjectTools(rl, question) {
3227
3306
  console.log('');
3228
3307
  console.log('═══════════════════════════════════════════════════════════');
3229
3308
  console.log(' STEP 2: Project Tools (Recommended)');
3230
3309
  console.log('═══════════════════════════════════════════════════════════');
3231
3310
  console.log('');
3232
- console.log('Forge recommends three tools for enhanced workflows:');
3311
+ console.log('Forge recommends these tools for enhanced workflows:');
3233
3312
  console.log('');
3234
3313
  console.log('• Issue tracking (Forge Kernel) - zero-install, git-backed');
3235
3314
  console.log(' Persists tasks across sessions, tracks dependencies.');
@@ -3241,7 +3320,7 @@ async function setupProjectTools(rl, question) {
3241
3320
  console.log('');
3242
3321
 
3243
3322
  // Use helper functions to reduce complexity
3244
- await promptBeadsSetup(question);
3323
+ await promptIssueStoreSetup(question);
3245
3324
  await promptSkillsSetup(question);
3246
3325
  }
3247
3326
 
@@ -3395,7 +3474,8 @@ async function ensureGitHooksInstalled(targetRoot = projectRoot) {
3395
3474
 
3396
3475
 
3397
3476
  // Quick setup with defaults
3398
- async function quickSetup(selectedAgents, skipExternal) {
3477
+ async function quickSetup(selectedAgents, skipExternal, options = {}) {
3478
+ const skillsOnly = options.skillsOnly === true;
3399
3479
  showBanner('Quick Setup');
3400
3480
  console.log('');
3401
3481
  console.log('Quick mode: Using defaults...');
@@ -3403,7 +3483,6 @@ async function quickSetup(selectedAgents, skipExternal) {
3403
3483
 
3404
3484
  // Check prerequisites
3405
3485
  checkPrerequisites({
3406
- requireBeadsCli: true,
3407
3486
  requireGithubCli: true,
3408
3487
  requireJq: true,
3409
3488
  });
@@ -3423,41 +3502,46 @@ async function quickSetup(selectedAgents, skipExternal) {
3423
3502
 
3424
3503
  ensureWorkflowShellPolicy(selectedAgents);
3425
3504
 
3426
- // Auto-install lefthook if missing
3427
- autoInstallLefthook();
3505
+ // Auto-install lefthook if missing unless the caller explicitly requested skills only.
3506
+ if (!skillsOnly) {
3507
+ autoInstallLefthook();
3508
+ }
3428
3509
 
3429
3510
  // Auto-setup project tools (Kernel issue store, Skills)
3430
3511
  await autoSetupToolsInQuickMode();
3431
3512
 
3432
3513
  // Setup Claude first if selected, then setup remaining agents
3433
3514
  if (selectedAgents.includes('claude')) {
3434
- await setupAgent('claude');
3515
+ await setupAgent('claude', {}, { skillsOnly });
3435
3516
  }
3436
- await setupSelectedAgents(selectedAgents);
3517
+ await setupSelectedAgents(selectedAgents, undefined, { skillsOnly });
3437
3518
  ensureWorkflowRuntimeAssets(selectedAgents);
3438
3519
 
3439
3520
  // Detect Husky and migrate before installing Lefthook hooks
3440
- await handleHuskyMigration();
3521
+ if (!skillsOnly) {
3522
+ await handleHuskyMigration();
3523
+ }
3441
3524
 
3442
3525
  // Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
3443
3526
  // so an inert-hooks result must fail non-zero (B3), not end green.
3444
3527
  console.log('');
3445
- installGitHooks({ loud: true });
3528
+ if (skillsOnly) {
3529
+ console.log('Skipping git and harness hooks (--skills-only).');
3530
+ } else {
3531
+ installGitHooks({ loud: true });
3532
+ }
3446
3533
 
3447
3534
  // Configure external services with defaults (unless skipped)
3448
3535
  configureDefaultExternalServices(skipExternal);
3449
3536
 
3450
- // --sync flag: scaffold Beads GitHub sync workflows without prompting
3451
- if (SYNC_ENABLED) {
3452
- await handleSyncScaffold();
3453
- }
3537
+ removeDeprecatedSyncFiles();
3454
3538
 
3455
3539
  // Progressive setup summary
3456
3540
  console.log('');
3457
3541
  console.log(renderSetupSummary(actionLog, selectedAgents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
3458
3542
  printSetupNotes();
3459
3543
  // One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
3460
- // Hooks + Beads already handled above, so init skips those side effects.
3544
+ // Hooks + issue store already handled above, so init skips those side effects.
3461
3545
  await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
3462
3546
  console.log('');
3463
3547
  }
@@ -3617,13 +3701,13 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
3617
3701
 
3618
3702
 
3619
3703
  // Helper: Setup all selected agents - extracted to reduce cognitive complexity
3620
- async function setupSelectedAgents(selectedAgents, skipFiles) {
3704
+ async function setupSelectedAgents(selectedAgents, skipFiles, options = {}) {
3621
3705
  const totalAgents = selectedAgents.length;
3622
3706
  for (const [index, agentKey] of selectedAgents.entries()) {
3623
3707
  const agent = AGENTS[agentKey];
3624
3708
  console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
3625
3709
  if (agentKey !== 'claude') { // Claude already done above
3626
- await setupAgent(agentKey, skipFiles);
3710
+ await setupAgent(agentKey, skipFiles, options);
3627
3711
  }
3628
3712
  }
3629
3713
 
@@ -3691,7 +3775,6 @@ async function interactiveSetupWithFlags(flags) {
3691
3775
 
3692
3776
  // Check agent-independent prerequisites first
3693
3777
  checkPrerequisites({
3694
- requireBeadsCli: true,
3695
3778
  requireGithubCli: false,
3696
3779
  requireJq: true,
3697
3780
  });
@@ -3740,11 +3823,11 @@ async function interactiveSetupWithFlags(flags) {
3740
3823
 
3741
3824
  // Setup Claude first if selected (delegated to helper), then remaining agents
3742
3825
  if (selectedAgents.includes('claude')) {
3743
- await setupAgent('claude', skipFiles);
3826
+ await setupAgent('claude', skipFiles, flags);
3744
3827
  }
3745
3828
 
3746
3829
  // Setup each selected agent with progress indication (delegated to helper)
3747
- await setupSelectedAgents(selectedAgents, skipFiles);
3830
+ await setupSelectedAgents(selectedAgents, skipFiles, flags);
3748
3831
  ensureWorkflowRuntimeAssets(selectedAgents);
3749
3832
 
3750
3833
  // Handle external services step (delegated to helper)
@@ -3758,7 +3841,7 @@ async function interactiveSetupWithFlags(flags) {
3758
3841
 
3759
3842
  // One-step onboarding: this path never writes .forge/config.yaml itself, so
3760
3843
  // run `forge init` when the config is still absent (ac0b38c7).
3761
- await finalizeWorkflowConfig();
3844
+ await finalizeWorkflowConfig({ hooksAlreadyInstalled: flags.skillsOnly === true });
3762
3845
  }
3763
3846
 
3764
3847
  // Main
@@ -3831,7 +3914,7 @@ function determineSelectedAgents(flags) {
3831
3914
  // Shared setup executor — used by handleSetupCommand
3832
3915
 
3833
3916
  // Dry-run setup — enumerate planned actions without writing files
3834
- 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
3835
3918
  const collector = new ActionCollector();
3836
3919
 
3837
3920
  // Helper: add create or skip based on whether file exists
@@ -3901,10 +3984,12 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
3901
3984
  }
3902
3985
  }
3903
3986
 
3904
- // Git hooks
3905
- addFileAction('lefthook.yml', 'Git hook configuration');
3906
- addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
3907
- 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
+ }
3908
3993
 
3909
3994
  // Print dry-run summary
3910
3995
  console.log('');
@@ -3919,14 +4004,19 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
3919
4004
 
3920
4005
 
3921
4006
  async function executeSetup(config) {
3922
- const { agents, skipExternal, keepExisting = false, commandRunner } = config;
4007
+ const {
4008
+ agents,
4009
+ skipExternal,
4010
+ keepExisting = false,
4011
+ commandRunner,
4012
+ skillsOnly = false,
4013
+ } = config;
3923
4014
 
3924
4015
  showBanner('Installing for specified agents...');
3925
4016
  console.log('');
3926
4017
 
3927
4018
  // Check prerequisites
3928
4019
  checkPrerequisites({
3929
- requireBeadsCli: true,
3930
4020
  requireGithubCli: requiresGithubCliForSetup(agents),
3931
4021
  requireJq: true,
3932
4022
  commandRunner,
@@ -3953,60 +4043,62 @@ async function executeSetup(config) {
3953
4043
 
3954
4044
  // Setup Claude first if selected, then remaining agents
3955
4045
  if (agents.includes('claude')) {
3956
- await setupAgent('claude', skipFiles);
4046
+ await setupAgent('claude', skipFiles, { skillsOnly });
3957
4047
  }
3958
- await setupSelectedAgents(agents, skipFiles);
4048
+ await setupSelectedAgents(agents, skipFiles, { skillsOnly });
3959
4049
  ensureWorkflowRuntimeAssets(agents);
3960
4050
  ensureWorkflowShellPolicy(agents);
3961
- repairDeclaredLefthookDependency(agents);
4051
+ if (!skillsOnly) {
4052
+ repairDeclaredLefthookDependency(agents);
4053
+ }
3962
4054
 
3963
4055
  // Detect Husky and migrate before installing Lefthook hooks
3964
- await handleHuskyMigration();
4056
+ if (!skillsOnly) {
4057
+ await handleHuskyMigration();
4058
+ }
3965
4059
 
3966
4060
  // Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
3967
4061
  // so an inert-hooks result must fail non-zero (B3), not end green.
3968
4062
  console.log('');
3969
- installGitHooks({ loud: true });
4063
+ if (skillsOnly) {
4064
+ console.log('Skipping git and harness hooks (--skills-only).');
4065
+ } else {
4066
+ installGitHooks({ loud: true });
4067
+ }
3970
4068
 
3971
4069
  // External services (unless skipped)
3972
4070
  await handleExternalServices(skipExternal, agents);
3973
4071
 
3974
- // --sync flag: scaffold Beads GitHub sync workflows without prompting
3975
- if (SYNC_ENABLED) {
3976
- await handleSyncScaffold();
3977
- }
4072
+ removeDeprecatedSyncFiles();
3978
4073
 
3979
4074
  // Progressive setup summary
3980
4075
  console.log('');
3981
4076
  console.log(renderSetupSummary(actionLog, agents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
3982
4077
  printSetupNotes();
3983
4078
  // One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
3984
- // Hooks + Beads already handled above, so init skips those side effects.
4079
+ // Hooks + issue store already handled above, so init skips those side effects.
3985
4080
  await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
3986
4081
  console.log('');
3987
4082
  }
3988
4083
 
3989
- // Helper: Scaffold Beads GitHub sync when --sync flag is provided
3990
-
3991
-
3992
- // Helper: Scaffold Beads GitHub sync when --sync flag is provided
3993
- async function handleSyncScaffold() {
3994
- console.log('');
3995
- 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() {
3996
4092
  try {
3997
- const result = scaffoldBeadsSync(projectRoot, getPackageRoot(packageDir));
3998
- console.log(` ${result.message}`);
3999
- for (const f of result.filesRemoved || []) {
4000
- 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}`);
4001
4096
  }
4002
4097
  } catch (err) {
4003
- console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
4098
+ console.error(` Error removing deprecated sync files: ${err.message}`);
4004
4099
  }
4005
4100
  }
4006
4101
 
4007
- // Helper: Handle setup command in non-quick mode
4008
-
4009
-
4010
4102
  // Helper: Handle setup command in non-quick mode
4011
4103
  async function handleSetupCommand(selectedAgents, flags) {
4012
4104
  if (!Array.isArray(selectedAgents) || selectedAgents.length === 0) {
@@ -4024,6 +4116,7 @@ async function handleSetupCommand(selectedAgents, flags) {
4024
4116
  skipExternal: flags.skipExternal,
4025
4117
  keepExisting: flags.keep,
4026
4118
  commandRunner: flags.commandRunner,
4119
+ skillsOnly: flags.skillsOnly,
4027
4120
  });
4028
4121
  } finally {
4029
4122
  projectRoot = savedRoot;
@@ -4168,6 +4261,7 @@ const SETUP_FLAG_DEFAULTS = Object.freeze({
4168
4261
  sync: false,
4169
4262
  symlink: false,
4170
4263
  nonInteractive: false,
4264
+ skillsOnly: false,
4171
4265
  });
4172
4266
 
4173
4267
  const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
@@ -4186,6 +4280,8 @@ const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
4186
4280
  '--symlink': { symlink: true },
4187
4281
  '--yes': { yes: true, nonInteractive: true },
4188
4282
  '-y': { yes: true, nonInteractive: true },
4283
+ '--skills-only': { skillsOnly: true },
4284
+ '--no-hooks': { skillsOnly: true },
4189
4285
  });
4190
4286
 
4191
4287
  function parseAgentFlag(argv, currentIndex, isFlagToken) {
@@ -4262,9 +4358,9 @@ function mergeSetupFlags(flags, argv) {
4262
4358
  standard: Boolean(flags.standard || setupFlags.standard),
4263
4359
  full: Boolean(flags.full || setupFlags.full),
4264
4360
  skipExternal: Boolean(flags.skipExternal || setupFlags.skipExternal),
4265
- sync: Boolean(flags.sync || setupFlags.sync),
4266
4361
  symlink: Boolean(flags.symlink || setupFlags.symlink),
4267
4362
  nonInteractive: Boolean(flags.nonInteractive || setupFlags.nonInteractive),
4363
+ skillsOnly: Boolean(flags.skillsOnly || setupFlags.skillsOnly),
4268
4364
  };
4269
4365
  }
4270
4366
 
@@ -4309,7 +4405,6 @@ module.exports = {
4309
4405
  if (flags.verbose) VERBOSE_MODE = true;
4310
4406
  if (flags.nonInteractive || flags.yes) NON_INTERACTIVE = true;
4311
4407
  if (flags.symlink) SYMLINK_ONLY = true;
4312
- if (flags.sync) SYNC_ENABLED = true;
4313
4408
  actionLog = new SetupActionLog();
4314
4409
  resetSetupNotes();
4315
4410
  PKG_MANAGER = detectPackageManager();
@@ -4327,6 +4422,12 @@ module.exports = {
4327
4422
  }
4328
4423
 
4329
4424
  const [profile] = selectedProfiles;
4425
+ if (flags.skillsOnly) {
4426
+ return {
4427
+ success: false,
4428
+ error: `--skills-only cannot be combined with --${profile}.`,
4429
+ };
4430
+ }
4330
4431
  return initCommand.handler([`--profile=${profile}`, '--yes', ...(flags.force ? ['--force'] : [])], flags, projectRoot);
4331
4432
  }
4332
4433
 
@@ -4351,7 +4452,7 @@ module.exports = {
4351
4452
 
4352
4453
  if (flags.dryRun) {
4353
4454
  if (selectedAgents.length === 0) selectedAgents = ['claude'];
4354
- dryRunSetup(selectedAgents);
4455
+ dryRunSetup(selectedAgents, { skillsOnly: flags.skillsOnly });
4355
4456
  return { success: true };
4356
4457
  }
4357
4458
 
@@ -4360,12 +4461,7 @@ module.exports = {
4360
4461
  if (selectedAgents.length === 0 || (flags.yes && !flags.agents)) {
4361
4462
  selectedAgents = Object.keys(AGENTS);
4362
4463
  }
4363
- await quickSetup(selectedAgents, flags.skipExternal);
4364
- return { success: true };
4365
- }
4366
-
4367
- if (flags.sync && selectedAgents.length === 0) {
4368
- await handleSyncScaffold();
4464
+ await quickSetup(selectedAgents, flags.skipExternal, { skillsOnly: flags.skillsOnly });
4369
4465
  return { success: true };
4370
4466
  }
4371
4467
 
@@ -4406,6 +4502,8 @@ module.exports = {
4406
4502
  ensureGitHooksInstalled,
4407
4503
  forgeShouldWriteLefthookConfig,
4408
4504
  FORGE_USER_LEFTHOOK_YML,
4505
+ applyExistingGateDeferral,
4506
+ installGitHooks,
4409
4507
  setupClaudeMcpConfig,
4410
4508
  setupCursorMcpConfig,
4411
4509
  setupClaudePermissions,
@@ -4430,14 +4528,13 @@ module.exports = {
4430
4528
  backupAndRemoveLegacyCursorRules,
4431
4529
 
4432
4530
  // State accessors for testing
4433
- _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 }),
4434
4532
  _setState: (state) => {
4435
4533
  if (state.projectRoot !== undefined) projectRoot = state.projectRoot;
4436
4534
  if (state.FORCE_MODE !== undefined) FORCE_MODE = state.FORCE_MODE;
4437
4535
  if (state.VERBOSE_MODE !== undefined) VERBOSE_MODE = state.VERBOSE_MODE;
4438
4536
  if (state.NON_INTERACTIVE !== undefined) NON_INTERACTIVE = state.NON_INTERACTIVE;
4439
4537
  if (state.SYMLINK_ONLY !== undefined) SYMLINK_ONLY = state.SYMLINK_ONLY;
4440
- if (state.SYNC_ENABLED !== undefined) SYNC_ENABLED = state.SYNC_ENABLED;
4441
4538
  if (state.PKG_MANAGER !== undefined) PKG_MANAGER = state.PKG_MANAGER;
4442
4539
  },
4443
4540
  };
@@ -39,6 +39,7 @@ const { watchLoop } = require('../pr-monitor/watch');
39
39
  const { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
40
40
  const reconcileExecutor = require('../pr-monitor/reconcile-executor');
41
41
  const monitorJournal = require('../pr-monitor/journal');
42
+ const brokerMod = require('../kernel/broker');
42
43
  const { EVENT_TYPES: T } = require('../pr-monitor/events');
43
44
  const { autoShepherdRailEnabled } = require('./ship');
44
45
 
@@ -197,7 +198,18 @@ async function buildMonitorContext(pr, projectRoot, deps) {
197
198
  if (!validation.valid) {
198
199
  return { error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
199
200
  }
200
- dir = dir || monitorJournal.journalDir({ root: projectRoot || process.cwd(), repo: ctx.repo, pr: ctx.pr });
201
+ let gitCommonDir = deps.gitCommonDir;
202
+ if (!gitCommonDir) {
203
+ try {
204
+ const resolveGitCommonDir = deps.resolveGitCommonDir || brokerMod.resolveGitCommonDir;
205
+ gitCommonDir = resolveGitCommonDir(projectRoot || process.cwd(), { warn: () => {} });
206
+ } catch {
207
+ /* unavailable common-dir keeps the legacy per-root journal fallback */
208
+ }
209
+ }
210
+ dir = dir || monitorJournal.journalDir({
211
+ root: projectRoot || process.cwd(), gitCommonDir, repo: ctx.repo, pr: ctx.pr,
212
+ });
201
213
  gather = gather || (() => gatherMonitorSnapshot({ ...ctx, adapter, self: deps.self }));
202
214
  enrich = enrich || makeCheckFailureEnricher({
203
215
  ...ctx,