forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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.
- package/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/lib/commands/setup.js
CHANGED
|
@@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|
|
13
13
|
const os = require('node:os');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const readline = require('node:readline');
|
|
16
|
-
const { execSync } = require('node:child_process');
|
|
16
|
+
const { execFileSync, execSync } = require('node:child_process');
|
|
17
17
|
|
|
18
18
|
// Compute packageDir relative to this file (lib/commands/setup.js -> project root).
|
|
19
19
|
// packageDir stays a runtime value for asset copying; requires use static
|
|
@@ -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
|
|
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,
|
|
@@ -89,6 +91,10 @@ const initCommand = require('./init');
|
|
|
89
91
|
const fileUtils = require('../file-utils');
|
|
90
92
|
const detectionUtils = require('../detection-utils');
|
|
91
93
|
const { detectHusky, migrateHusky } = require('../husky-migration');
|
|
94
|
+
const {
|
|
95
|
+
issueGeneratedHarnessClaudeAuthorization,
|
|
96
|
+
completeGeneratedHarnessClaudeAuthorization,
|
|
97
|
+
} = require('../protected-state-authority');
|
|
92
98
|
|
|
93
99
|
// --- Module-level state (deferred from ForgeContext migration) ---
|
|
94
100
|
// Follow-up tracked in forge-vi7v: migrate setup state to a ForgeContext instance passed via handler args.
|
|
@@ -99,7 +105,6 @@ let FORCE_MODE = false;
|
|
|
99
105
|
let VERBOSE_MODE = false;
|
|
100
106
|
let NON_INTERACTIVE = false;
|
|
101
107
|
let SYMLINK_ONLY = false;
|
|
102
|
-
let SYNC_ENABLED = false;
|
|
103
108
|
let actionLog = new SetupActionLog();
|
|
104
109
|
let PKG_MANAGER = 'npm';
|
|
105
110
|
let SETUP_NOTES = [];
|
|
@@ -162,6 +167,92 @@ function safeExec(cmd, opts = {}) {
|
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Resolve the current source revision without passing Git arguments through a shell.
|
|
172
|
+
* @param {string} cwd - Repository working directory
|
|
173
|
+
* @param {Function} commandRunner - execFileSync-compatible runner for tests
|
|
174
|
+
* @returns {string} Full source HEAD, or empty when Git cannot resolve it
|
|
175
|
+
*/
|
|
176
|
+
function resolveCurrentSourceHead(cwd = projectRoot, commandRunner = execFileSync) {
|
|
177
|
+
try {
|
|
178
|
+
return commandRunner('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
179
|
+
cwd,
|
|
180
|
+
stdio: 'pipe',
|
|
181
|
+
}).toString().trim();
|
|
182
|
+
} catch (_e) { // NOSONAR — fail closed when the source revision is unavailable
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Normalize Git's Windows checkout line endings before authorizing the exact
|
|
189
|
+
* root CLAUDE.md pointer bytes.
|
|
190
|
+
* @param {Buffer|string} content - Existing CLAUDE.md bytes
|
|
191
|
+
* @returns {Buffer} LF-normalized content
|
|
192
|
+
*/
|
|
193
|
+
function normalizeClaudePointerContent(content) {
|
|
194
|
+
return Buffer.from(String(content || '').replace(/\r\n/g, '\n'));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readCleanClaudePointerWorktreeContent(cwd, commandRunner) {
|
|
198
|
+
const tempIndexDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-claude-index-'));
|
|
199
|
+
const tempIndex = path.join(tempIndexDir, 'index');
|
|
200
|
+
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
201
|
+
try {
|
|
202
|
+
commandRunner('git', ['read-tree', 'HEAD'], {
|
|
203
|
+
cwd,
|
|
204
|
+
env,
|
|
205
|
+
stdio: 'pipe',
|
|
206
|
+
});
|
|
207
|
+
commandRunner('git', ['add', '--', 'CLAUDE.md'], {
|
|
208
|
+
cwd,
|
|
209
|
+
env,
|
|
210
|
+
stdio: 'pipe',
|
|
211
|
+
});
|
|
212
|
+
return commandRunner('git', ['show', ':CLAUDE.md'], {
|
|
213
|
+
cwd,
|
|
214
|
+
env,
|
|
215
|
+
encoding: null,
|
|
216
|
+
stdio: 'pipe',
|
|
217
|
+
});
|
|
218
|
+
} finally {
|
|
219
|
+
fs.rmSync(tempIndexDir, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read the exact staged CLAUDE.md bytes that the protected-state hook will hash.
|
|
225
|
+
* @param {string} cwd - Repository working directory
|
|
226
|
+
* @param {Function} commandRunner - execFileSync-compatible runner for tests
|
|
227
|
+
* @returns {Buffer} Staged bytes, or the worktree bytes when the file is untracked
|
|
228
|
+
*/
|
|
229
|
+
function readStagedClaudePointerContent(cwd = projectRoot, commandRunner = execFileSync) {
|
|
230
|
+
try {
|
|
231
|
+
const stagedContent = commandRunner('git', ['show', ':CLAUDE.md'], {
|
|
232
|
+
cwd,
|
|
233
|
+
encoding: null,
|
|
234
|
+
stdio: 'pipe',
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
return readCleanClaudePointerWorktreeContent(cwd, commandRunner);
|
|
238
|
+
} catch (_worktreeError) {
|
|
239
|
+
return stagedContent;
|
|
240
|
+
}
|
|
241
|
+
} catch (showError) {
|
|
242
|
+
try {
|
|
243
|
+
commandRunner('git', ['ls-files', '--error-unmatch', '--', 'CLAUDE.md'], {
|
|
244
|
+
cwd,
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
stdio: 'pipe',
|
|
247
|
+
});
|
|
248
|
+
throw showError;
|
|
249
|
+
} catch (trackedError) {
|
|
250
|
+
if (trackedError?.status !== 1) throw showError;
|
|
251
|
+
}
|
|
252
|
+
return readCleanClaudePointerWorktreeContent(cwd, commandRunner);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
165
256
|
/**
|
|
166
257
|
* Detect package manager
|
|
167
258
|
*/
|
|
@@ -174,7 +265,6 @@ function detectPackageManager() {
|
|
|
174
265
|
|
|
175
266
|
|
|
176
267
|
const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
|
|
177
|
-
'scripts/beads-context.sh',
|
|
178
268
|
'scripts/conflict-detect.sh',
|
|
179
269
|
'scripts/dep-guard-analyze.js',
|
|
180
270
|
'scripts/dep-guard.sh',
|
|
@@ -218,7 +308,6 @@ function validateAgents(agentList) {
|
|
|
218
308
|
// Prerequisite check function
|
|
219
309
|
function checkPrerequisites(options = {}) {
|
|
220
310
|
const requireGithubCli = options.requireGithubCli !== false;
|
|
221
|
-
const requireBeadsCli = options.requireBeadsCli === true;
|
|
222
311
|
const requireJq = options.requireJq === true;
|
|
223
312
|
const commandRunner = options.commandRunner || safeExec;
|
|
224
313
|
const errors = [];
|
|
@@ -255,15 +344,13 @@ function checkPrerequisites(options = {}) {
|
|
|
255
344
|
}
|
|
256
345
|
}
|
|
257
346
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
console.log(' ✓ Kernel issue store');
|
|
266
|
-
}
|
|
347
|
+
// Issue tracking runs on the local Forge Kernel store, which auto-provisions
|
|
348
|
+
// on first use — there is no external CLI to install. Surface only whether
|
|
349
|
+
// team sync is wired up yet.
|
|
350
|
+
if (resolveSyncBackend({ projectRoot, env: options.env }) === 'local-noop') {
|
|
351
|
+
console.log(' ✓ Kernel issue store (local-noop sync: single-machine until a sync server is configured)');
|
|
352
|
+
} else {
|
|
353
|
+
console.log(' ✓ Kernel issue store');
|
|
267
354
|
}
|
|
268
355
|
|
|
269
356
|
// Check Node.js version
|
|
@@ -800,9 +887,7 @@ async function detectProjectStatus() {
|
|
|
800
887
|
claudeMdLines: 0,
|
|
801
888
|
// Project tools status — the Kernel issue store is always present (it
|
|
802
889
|
// auto-provisions on first use), so issue tracking needs no install probe.
|
|
803
|
-
hasBeads: true,
|
|
804
890
|
hasSkills: isSkillsInitialized(),
|
|
805
|
-
beadsInstallType: 'kernel',
|
|
806
891
|
skillsInstallType: checkForSkills(),
|
|
807
892
|
// Enhanced: Auto-detected project context
|
|
808
893
|
autoDetected: null
|
|
@@ -1654,6 +1739,13 @@ function ensureDirWithNote(dir, purpose) { // eslint-disable-line no-unused-vars
|
|
|
1654
1739
|
|
|
1655
1740
|
// Setup core documentation and directories
|
|
1656
1741
|
function setupCoreDocs() {
|
|
1742
|
+
// Review skills reference this root contract in every installed repository.
|
|
1743
|
+
// It is a consumer-owned extension point after creation, so setup never overwrites it.
|
|
1744
|
+
const codingStandardsDest = path.join(projectRoot, 'CODING_STANDARDS.md');
|
|
1745
|
+
if (!fs.existsSync(codingStandardsDest)) {
|
|
1746
|
+
copyFile(path.join(getPackageRoot(packageDir), 'CODING_STANDARDS.md'), 'CODING_STANDARDS.md');
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1657
1749
|
// docs/planning/ and docs/research/ are created lazily on first use
|
|
1658
1750
|
// by /plan Phase 1 and Phase 2 respectively, via ensureDirWithNote().
|
|
1659
1751
|
// TEMPLATE.md and PROGRESS.md are also deferred to first use.
|
|
@@ -2058,12 +2150,40 @@ function setupCursorHooksConfig() {
|
|
|
2058
2150
|
|
|
2059
2151
|
// Helper: Create agent link file
|
|
2060
2152
|
// When symlinkOnly is true (--symlink flag), skip copy fallback
|
|
2061
|
-
function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
2153
|
+
async function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
2062
2154
|
if (!shouldLinkAgentsMd(agent)) return;
|
|
2063
2155
|
|
|
2064
2156
|
const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile, { symlinkOnly });
|
|
2065
2157
|
if (result) {
|
|
2066
|
-
|
|
2158
|
+
const action = result === 'linked' ? 'Linked' : result === 'copied' ? 'Copied' : 'Preserved import';
|
|
2159
|
+
console.log(` ${action}: ${agent.linkFile}`);
|
|
2160
|
+
}
|
|
2161
|
+
if (agent.linkFile !== 'CLAUDE.md' || result !== 'existing-import') return;
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
const sourceHead = resolveCurrentSourceHead();
|
|
2165
|
+
const content = readStagedClaudePointerContent();
|
|
2166
|
+
const actor = process.env.FORGE_PROTECTED_STATE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
|
|
2167
|
+
const authorization = await issueGeneratedHarnessClaudeAuthorization(projectRoot, {
|
|
2168
|
+
actor,
|
|
2169
|
+
content,
|
|
2170
|
+
sourceHead,
|
|
2171
|
+
});
|
|
2172
|
+
if (!authorization.success) {
|
|
2173
|
+
console.warn(` Warning: Could not authorize existing CLAUDE.md import: ${authorization.error}`);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const completion = await completeGeneratedHarnessClaudeAuthorization(projectRoot, {
|
|
2177
|
+
actor,
|
|
2178
|
+
capabilityId: authorization.capabilityId,
|
|
2179
|
+
content,
|
|
2180
|
+
sourceHead,
|
|
2181
|
+
});
|
|
2182
|
+
if (!completion.success) {
|
|
2183
|
+
console.warn(` Warning: Could not complete CLAUDE.md import authorization: ${completion.error}`);
|
|
2184
|
+
}
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
console.warn(` Warning: Could not record CLAUDE.md import authorization: ${error.message}`);
|
|
2067
2187
|
}
|
|
2068
2188
|
}
|
|
2069
2189
|
|
|
@@ -2071,7 +2191,7 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
|
2071
2191
|
|
|
2072
2192
|
|
|
2073
2193
|
// Setup specific agent
|
|
2074
|
-
async function setupAgent(agentKey, skipFiles = {}) {
|
|
2194
|
+
async function setupAgent(agentKey, skipFiles = {}, options = {}) {
|
|
2075
2195
|
const agent = AGENTS[agentKey];
|
|
2076
2196
|
if (!agent) return;
|
|
2077
2197
|
|
|
@@ -2111,15 +2231,17 @@ async function setupAgent(agentKey, skipFiles = {}) {
|
|
|
2111
2231
|
// Projects Forge's TDD-gate + protected-path enforcement onto each harness's
|
|
2112
2232
|
// native hook surface. Codex hooks are GLOBAL-config scope and intentionally
|
|
2113
2233
|
// not written at project setup (see lib/hook-renderer.js).
|
|
2114
|
-
if (
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2234
|
+
if (!options.skillsOnly) {
|
|
2235
|
+
if (agentKey === 'claude') {
|
|
2236
|
+
setupClaudeHooksConfig();
|
|
2237
|
+
}
|
|
2238
|
+
if (agent.customSetup === 'cursor') {
|
|
2239
|
+
setupCursorHooksConfig();
|
|
2240
|
+
}
|
|
2119
2241
|
}
|
|
2120
2242
|
|
|
2121
2243
|
// Create link file (SYMLINK_ONLY = --symlink flag disables copy fallback)
|
|
2122
|
-
createAgentLinkFile(agent, SYMLINK_ONLY);
|
|
2244
|
+
await createAgentLinkFile(agent, SYMLINK_ONLY);
|
|
2123
2245
|
}
|
|
2124
2246
|
|
|
2125
2247
|
|
|
@@ -2756,6 +2878,48 @@ function resolveHookEnforcementState(root = projectRoot) {
|
|
|
2756
2878
|
}
|
|
2757
2879
|
}
|
|
2758
2880
|
|
|
2881
|
+
// True when .forge/config.yaml already states a rail.tdd_intent choice EXPLICITLY (either
|
|
2882
|
+
// config shape the resolver accepts). Setup must never overwrite that: otherwise the
|
|
2883
|
+
// `forge gate enable rail.tdd_intent` we advertise would be undone by the next `forge setup`.
|
|
2884
|
+
function hasExplicitTddRailChoice(root) {
|
|
2885
|
+
try {
|
|
2886
|
+
const { loadRawConfig } = require('../config-writer');
|
|
2887
|
+
const config = loadRawConfig(root) || {};
|
|
2888
|
+
const viaGates = config.workflow && config.workflow.gates && config.workflow.gates['rail.tdd_intent'];
|
|
2889
|
+
const viaRails = config.rails && config.rails['rail.tdd_intent'];
|
|
2890
|
+
return Boolean(
|
|
2891
|
+
(viaGates && viaGates.enabled !== undefined) || (viaRails && viaRails.enabled !== undefined)
|
|
2892
|
+
);
|
|
2893
|
+
} catch {
|
|
2894
|
+
return false; // unreadable config → treat as unset; setConfigOverride reports any real failure
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
/**
|
|
2899
|
+
* Defer Forge's TDD gate to a pre-existing one: turn `rail.tdd_intent` off in
|
|
2900
|
+
* .forge/config.yaml so the installed hook scripts are genuinely inert, instead of stacking a
|
|
2901
|
+
* second gate on the same commit (kernel 5b425a85 / 2699b234). No-op when nothing was detected
|
|
2902
|
+
* (a repo with no gate keeps the default-ON rail) or when the user already chose explicitly.
|
|
2903
|
+
*
|
|
2904
|
+
* @returns {{ deferred: boolean, reason?: string }}
|
|
2905
|
+
*/
|
|
2906
|
+
function applyExistingGateDeferral(root, detection) {
|
|
2907
|
+
if (!detection || !detection.found) return { deferred: false, reason: 'no-existing-gate' };
|
|
2908
|
+
if (hasExplicitTddRailChoice(root)) return { deferred: false, reason: 'explicit-user-choice' };
|
|
2909
|
+
|
|
2910
|
+
const { setConfigOverride } = require('../config-writer');
|
|
2911
|
+
try {
|
|
2912
|
+
setConfigOverride(root, ['workflow', 'gates', 'rail.tdd_intent', 'enabled'], false);
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
// An unreadable/malformed .forge/config.yaml must not make `forge setup` fail outright
|
|
2915
|
+
// just because a pre-existing gate was detected. Report and DON'T defer: the caller keys
|
|
2916
|
+
// hook wiring off `deferred`, so falling back to the normal install keeps config and
|
|
2917
|
+
// wiring agreeing, instead of turning the gate off in neither place or in only one.
|
|
2918
|
+
return { deferred: false, reason: 'config-write-failed', error: error.message };
|
|
2919
|
+
}
|
|
2920
|
+
return { deferred: true };
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2759
2923
|
function describeHookEnforcement(root = projectRoot) {
|
|
2760
2924
|
const state = resolveHookEnforcementState(root);
|
|
2761
2925
|
if (!state.resolved) {
|
|
@@ -2837,6 +3001,32 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
|
|
|
2837
3001
|
// init deliberately degrades to a warning rather than failing, and repair runs inside
|
|
2838
3002
|
// another command's flow.
|
|
2839
3003
|
const loud = options.loud === true;
|
|
3004
|
+
|
|
3005
|
+
// BEFORE wiring anything: does this repo already enforce TDD/source-test coupling on
|
|
3006
|
+
// pre-commit? If so, defer to it rather than silently stacking a second gate on the same
|
|
3007
|
+
// commit — the in-the-wild beta.3 report (kernel 5b425a85 / 2699b234). The deferral is
|
|
3008
|
+
// VISIBLE (report below) and reversible with one command.
|
|
3009
|
+
const existingGate = detectExistingTddGate(projectRoot);
|
|
3010
|
+
const deferral = applyExistingGateDeferral(projectRoot, existingGate);
|
|
3011
|
+
// Only claim a deferral when one actually happened. describeExistingGateDeferral() states
|
|
3012
|
+
// that Forge did NOT install its gate and that rail.tdd_intent is off — both false when we
|
|
3013
|
+
// declined to defer, so printing it unconditionally would misreport the installed state.
|
|
3014
|
+
if (deferral.deferred) {
|
|
3015
|
+
console.warn(describeExistingGateDeferral(existingGate));
|
|
3016
|
+
addSetupNote(`Deferred Forge's TDD gate to the existing pre-commit gate in ${existingGate.source}.`);
|
|
3017
|
+
} else if (existingGate.found) {
|
|
3018
|
+
const why = deferral.reason === 'explicit-user-choice'
|
|
3019
|
+
? 'rail.tdd_intent is set explicitly in .forge/config.yaml, so your choice stands'
|
|
3020
|
+
: `Forge could not update .forge/config.yaml (${deferral.error})`;
|
|
3021
|
+
console.warn(
|
|
3022
|
+
` ⚠ Detected an existing pre-commit TDD/coupling gate in ${existingGate.source}, but did NOT defer to it:\n` +
|
|
3023
|
+
` ${why}.\n` +
|
|
3024
|
+
' Forge\'s own TDD gate is installed as usual, so BOTH gates run on the same commit.\n' +
|
|
3025
|
+
' Want only yours? Run: forge gate disable rail.tdd_intent'
|
|
3026
|
+
);
|
|
3027
|
+
addSetupNote(`Existing pre-commit gate in ${existingGate.source} detected; Forge's TDD gate installed anyway (${deferral.reason}).`);
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2840
3030
|
// Honest header: when the TDD gate is disabled in config the hooks install but are inert,
|
|
2841
3031
|
// so don't announce "(TDD enforcement)" for a state the user turned off (issue eda6d866).
|
|
2842
3032
|
console.log(
|
|
@@ -2876,7 +3066,18 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
|
|
|
2876
3066
|
// clobber a config that already has active jobs (kernel c713fce7).
|
|
2877
3067
|
const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
|
|
2878
3068
|
if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
|
|
2879
|
-
|
|
3069
|
+
// When we actually DEFERRED, write the variant WITHOUT Forge's forge-tdd job so the
|
|
3070
|
+
// commit is never blocked by two gates. Key this off `deferral.deferred`, not off
|
|
3071
|
+
// `existingGate.found`: applyExistingGateDeferral declines to defer when the user has
|
|
3072
|
+
// an explicit rail.tdd_intent choice, and suppressing the wiring anyway would leave
|
|
3073
|
+
// config saying "enabled" while the hook was never installed — making
|
|
3074
|
+
// `forge gate enable rail.tdd_intent` silently ineffective in exactly the repos this
|
|
3075
|
+
// feature targets.
|
|
3076
|
+
fs.writeFileSync(
|
|
3077
|
+
lefthookTarget,
|
|
3078
|
+
deferral.deferred ? FORGE_USER_LEFTHOOK_YML_NO_TDD : FORGE_USER_LEFTHOOK_YML,
|
|
3079
|
+
'utf8'
|
|
3080
|
+
);
|
|
2880
3081
|
actionLog.add('lefthook.yml', 'created');
|
|
2881
3082
|
console.log(' ✓ Created lefthook.yml');
|
|
2882
3083
|
}
|
|
@@ -2924,7 +3125,12 @@ function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin
|
|
|
2924
3125
|
// inert (B3). This repair path only warns (never sets a failure exit code) since it can
|
|
2925
3126
|
// run mid-stage under enforce-stage's repairWorkflowRuntimeAssets().
|
|
2926
3127
|
if (!lefthookInstalled) {
|
|
2927
|
-
|
|
3128
|
+
// Skip the native pre-commit hook when the repo already has its own TDD/coupling gate —
|
|
3129
|
+
// pre-push is still wired, so only the double-gated surface is given up.
|
|
3130
|
+
const native = installNativeGitHooks(
|
|
3131
|
+
projectRoot,
|
|
3132
|
+
existingGate.found ? { skipHooks: ['pre-commit'] } : {}
|
|
3133
|
+
);
|
|
2928
3134
|
if (native.installed) {
|
|
2929
3135
|
console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
|
|
2930
3136
|
} else if (native.skipped && native.skipped.length > 0) {
|
|
@@ -3093,7 +3299,7 @@ function initializeSkills(installType) {
|
|
|
3093
3299
|
|
|
3094
3300
|
// Ensure the issue store during interactive setup. No prompt: the Forge Kernel
|
|
3095
3301
|
// ships with Forge and auto-provisions, so there is nothing to install or pick.
|
|
3096
|
-
async function
|
|
3302
|
+
async function promptIssueStoreSetup(_question) {
|
|
3097
3303
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
3098
3304
|
console.log('Issue Store (Forge Kernel)');
|
|
3099
3305
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
@@ -3219,17 +3425,14 @@ async function promptSkillsSetup(question) {
|
|
|
3219
3425
|
console.log('');
|
|
3220
3426
|
}
|
|
3221
3427
|
|
|
3222
|
-
// Interactive setup for
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
// Interactive setup for Beads and Skills
|
|
3428
|
+
// Interactive setup for the issue store and Skills
|
|
3226
3429
|
async function setupProjectTools(rl, question) {
|
|
3227
3430
|
console.log('');
|
|
3228
3431
|
console.log('═══════════════════════════════════════════════════════════');
|
|
3229
3432
|
console.log(' STEP 2: Project Tools (Recommended)');
|
|
3230
3433
|
console.log('═══════════════════════════════════════════════════════════');
|
|
3231
3434
|
console.log('');
|
|
3232
|
-
console.log('Forge recommends
|
|
3435
|
+
console.log('Forge recommends these tools for enhanced workflows:');
|
|
3233
3436
|
console.log('');
|
|
3234
3437
|
console.log('• Issue tracking (Forge Kernel) - zero-install, git-backed');
|
|
3235
3438
|
console.log(' Persists tasks across sessions, tracks dependencies.');
|
|
@@ -3241,7 +3444,7 @@ async function setupProjectTools(rl, question) {
|
|
|
3241
3444
|
console.log('');
|
|
3242
3445
|
|
|
3243
3446
|
// Use helper functions to reduce complexity
|
|
3244
|
-
await
|
|
3447
|
+
await promptIssueStoreSetup(question);
|
|
3245
3448
|
await promptSkillsSetup(question);
|
|
3246
3449
|
}
|
|
3247
3450
|
|
|
@@ -3395,7 +3598,8 @@ async function ensureGitHooksInstalled(targetRoot = projectRoot) {
|
|
|
3395
3598
|
|
|
3396
3599
|
|
|
3397
3600
|
// Quick setup with defaults
|
|
3398
|
-
async function quickSetup(selectedAgents, skipExternal) {
|
|
3601
|
+
async function quickSetup(selectedAgents, skipExternal, options = {}) {
|
|
3602
|
+
const skillsOnly = options.skillsOnly === true;
|
|
3399
3603
|
showBanner('Quick Setup');
|
|
3400
3604
|
console.log('');
|
|
3401
3605
|
console.log('Quick mode: Using defaults...');
|
|
@@ -3403,7 +3607,6 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3403
3607
|
|
|
3404
3608
|
// Check prerequisites
|
|
3405
3609
|
checkPrerequisites({
|
|
3406
|
-
requireBeadsCli: true,
|
|
3407
3610
|
requireGithubCli: true,
|
|
3408
3611
|
requireJq: true,
|
|
3409
3612
|
});
|
|
@@ -3423,41 +3626,46 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3423
3626
|
|
|
3424
3627
|
ensureWorkflowShellPolicy(selectedAgents);
|
|
3425
3628
|
|
|
3426
|
-
// Auto-install lefthook if missing
|
|
3427
|
-
|
|
3629
|
+
// Auto-install lefthook if missing unless the caller explicitly requested skills only.
|
|
3630
|
+
if (!skillsOnly) {
|
|
3631
|
+
autoInstallLefthook();
|
|
3632
|
+
}
|
|
3428
3633
|
|
|
3429
3634
|
// Auto-setup project tools (Kernel issue store, Skills)
|
|
3430
3635
|
await autoSetupToolsInQuickMode();
|
|
3431
3636
|
|
|
3432
3637
|
// Setup Claude first if selected, then setup remaining agents
|
|
3433
3638
|
if (selectedAgents.includes('claude')) {
|
|
3434
|
-
await setupAgent('claude');
|
|
3639
|
+
await setupAgent('claude', {}, { skillsOnly });
|
|
3435
3640
|
}
|
|
3436
|
-
await setupSelectedAgents(selectedAgents);
|
|
3641
|
+
await setupSelectedAgents(selectedAgents, undefined, { skillsOnly });
|
|
3437
3642
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3438
3643
|
|
|
3439
3644
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3440
|
-
|
|
3645
|
+
if (!skillsOnly) {
|
|
3646
|
+
await handleHuskyMigration();
|
|
3647
|
+
}
|
|
3441
3648
|
|
|
3442
3649
|
// Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
|
|
3443
3650
|
// so an inert-hooks result must fail non-zero (B3), not end green.
|
|
3444
3651
|
console.log('');
|
|
3445
|
-
|
|
3652
|
+
if (skillsOnly) {
|
|
3653
|
+
console.log('Skipping git and harness hooks (--skills-only).');
|
|
3654
|
+
} else {
|
|
3655
|
+
installGitHooks({ loud: true });
|
|
3656
|
+
}
|
|
3446
3657
|
|
|
3447
3658
|
// Configure external services with defaults (unless skipped)
|
|
3448
3659
|
configureDefaultExternalServices(skipExternal);
|
|
3449
3660
|
|
|
3450
|
-
|
|
3451
|
-
if (SYNC_ENABLED) {
|
|
3452
|
-
await handleSyncScaffold();
|
|
3453
|
-
}
|
|
3661
|
+
removeDeprecatedSyncFiles();
|
|
3454
3662
|
|
|
3455
3663
|
// Progressive setup summary
|
|
3456
3664
|
console.log('');
|
|
3457
3665
|
console.log(renderSetupSummary(actionLog, selectedAgents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
3458
3666
|
printSetupNotes();
|
|
3459
3667
|
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
3460
|
-
// Hooks +
|
|
3668
|
+
// Hooks + issue store already handled above, so init skips those side effects.
|
|
3461
3669
|
await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
|
|
3462
3670
|
console.log('');
|
|
3463
3671
|
}
|
|
@@ -3617,13 +3825,13 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3617
3825
|
|
|
3618
3826
|
|
|
3619
3827
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3620
|
-
async function setupSelectedAgents(selectedAgents, skipFiles) {
|
|
3828
|
+
async function setupSelectedAgents(selectedAgents, skipFiles, options = {}) {
|
|
3621
3829
|
const totalAgents = selectedAgents.length;
|
|
3622
3830
|
for (const [index, agentKey] of selectedAgents.entries()) {
|
|
3623
3831
|
const agent = AGENTS[agentKey];
|
|
3624
3832
|
console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
|
|
3625
3833
|
if (agentKey !== 'claude') { // Claude already done above
|
|
3626
|
-
await setupAgent(agentKey, skipFiles);
|
|
3834
|
+
await setupAgent(agentKey, skipFiles, options);
|
|
3627
3835
|
}
|
|
3628
3836
|
}
|
|
3629
3837
|
|
|
@@ -3691,7 +3899,6 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3691
3899
|
|
|
3692
3900
|
// Check agent-independent prerequisites first
|
|
3693
3901
|
checkPrerequisites({
|
|
3694
|
-
requireBeadsCli: true,
|
|
3695
3902
|
requireGithubCli: false,
|
|
3696
3903
|
requireJq: true,
|
|
3697
3904
|
});
|
|
@@ -3740,11 +3947,11 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3740
3947
|
|
|
3741
3948
|
// Setup Claude first if selected (delegated to helper), then remaining agents
|
|
3742
3949
|
if (selectedAgents.includes('claude')) {
|
|
3743
|
-
await setupAgent('claude', skipFiles);
|
|
3950
|
+
await setupAgent('claude', skipFiles, flags);
|
|
3744
3951
|
}
|
|
3745
3952
|
|
|
3746
3953
|
// Setup each selected agent with progress indication (delegated to helper)
|
|
3747
|
-
await setupSelectedAgents(selectedAgents, skipFiles);
|
|
3954
|
+
await setupSelectedAgents(selectedAgents, skipFiles, flags);
|
|
3748
3955
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3749
3956
|
|
|
3750
3957
|
// Handle external services step (delegated to helper)
|
|
@@ -3758,7 +3965,7 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3758
3965
|
|
|
3759
3966
|
// One-step onboarding: this path never writes .forge/config.yaml itself, so
|
|
3760
3967
|
// run `forge init` when the config is still absent (ac0b38c7).
|
|
3761
|
-
await finalizeWorkflowConfig();
|
|
3968
|
+
await finalizeWorkflowConfig({ hooksAlreadyInstalled: flags.skillsOnly === true });
|
|
3762
3969
|
}
|
|
3763
3970
|
|
|
3764
3971
|
// Main
|
|
@@ -3831,7 +4038,7 @@ function determineSelectedAgents(flags) {
|
|
|
3831
4038
|
// Shared setup executor — used by handleSetupCommand
|
|
3832
4039
|
|
|
3833
4040
|
// Dry-run setup — enumerate planned actions without writing files
|
|
3834
|
-
function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
4041
|
+
function dryRunSetup(agents, options = {}) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3835
4042
|
const collector = new ActionCollector();
|
|
3836
4043
|
|
|
3837
4044
|
// Helper: add create or skip based on whether file exists
|
|
@@ -3844,8 +4051,9 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3844
4051
|
}
|
|
3845
4052
|
}
|
|
3846
4053
|
|
|
3847
|
-
//
|
|
4054
|
+
// Root policy files
|
|
3848
4055
|
addFileAction('AGENTS.md', 'Copy workflow documentation');
|
|
4056
|
+
addFileAction('CODING_STANDARDS.md', 'Install Forge coding standards');
|
|
3849
4057
|
|
|
3850
4058
|
// Per-agent planned actions
|
|
3851
4059
|
for (const agentKey of agents) {
|
|
@@ -3901,10 +4109,12 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3901
4109
|
}
|
|
3902
4110
|
}
|
|
3903
4111
|
|
|
3904
|
-
// Git hooks
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
4112
|
+
// Git and harness hooks are opt-out for skills-only setup.
|
|
4113
|
+
if (options.skillsOnly !== true) {
|
|
4114
|
+
addFileAction('lefthook.yml', 'Git hook configuration');
|
|
4115
|
+
addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
|
|
4116
|
+
addFileAction('.forge/hooks/forge-native-hook.js', 'Native-hook enforcement adapter (Claude/Cursor)');
|
|
4117
|
+
}
|
|
3908
4118
|
|
|
3909
4119
|
// Print dry-run summary
|
|
3910
4120
|
console.log('');
|
|
@@ -3919,14 +4129,19 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3919
4129
|
|
|
3920
4130
|
|
|
3921
4131
|
async function executeSetup(config) {
|
|
3922
|
-
const {
|
|
4132
|
+
const {
|
|
4133
|
+
agents,
|
|
4134
|
+
skipExternal,
|
|
4135
|
+
keepExisting = false,
|
|
4136
|
+
commandRunner,
|
|
4137
|
+
skillsOnly = false,
|
|
4138
|
+
} = config;
|
|
3923
4139
|
|
|
3924
4140
|
showBanner('Installing for specified agents...');
|
|
3925
4141
|
console.log('');
|
|
3926
4142
|
|
|
3927
4143
|
// Check prerequisites
|
|
3928
4144
|
checkPrerequisites({
|
|
3929
|
-
requireBeadsCli: true,
|
|
3930
4145
|
requireGithubCli: requiresGithubCliForSetup(agents),
|
|
3931
4146
|
requireJq: true,
|
|
3932
4147
|
commandRunner,
|
|
@@ -3953,60 +4168,62 @@ async function executeSetup(config) {
|
|
|
3953
4168
|
|
|
3954
4169
|
// Setup Claude first if selected, then remaining agents
|
|
3955
4170
|
if (agents.includes('claude')) {
|
|
3956
|
-
await setupAgent('claude', skipFiles);
|
|
4171
|
+
await setupAgent('claude', skipFiles, { skillsOnly });
|
|
3957
4172
|
}
|
|
3958
|
-
await setupSelectedAgents(agents, skipFiles);
|
|
4173
|
+
await setupSelectedAgents(agents, skipFiles, { skillsOnly });
|
|
3959
4174
|
ensureWorkflowRuntimeAssets(agents);
|
|
3960
4175
|
ensureWorkflowShellPolicy(agents);
|
|
3961
|
-
|
|
4176
|
+
if (!skillsOnly) {
|
|
4177
|
+
repairDeclaredLefthookDependency(agents);
|
|
4178
|
+
}
|
|
3962
4179
|
|
|
3963
4180
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3964
|
-
|
|
4181
|
+
if (!skillsOnly) {
|
|
4182
|
+
await handleHuskyMigration();
|
|
4183
|
+
}
|
|
3965
4184
|
|
|
3966
4185
|
// Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
|
|
3967
4186
|
// so an inert-hooks result must fail non-zero (B3), not end green.
|
|
3968
4187
|
console.log('');
|
|
3969
|
-
|
|
4188
|
+
if (skillsOnly) {
|
|
4189
|
+
console.log('Skipping git and harness hooks (--skills-only).');
|
|
4190
|
+
} else {
|
|
4191
|
+
installGitHooks({ loud: true });
|
|
4192
|
+
}
|
|
3970
4193
|
|
|
3971
4194
|
// External services (unless skipped)
|
|
3972
4195
|
await handleExternalServices(skipExternal, agents);
|
|
3973
4196
|
|
|
3974
|
-
|
|
3975
|
-
if (SYNC_ENABLED) {
|
|
3976
|
-
await handleSyncScaffold();
|
|
3977
|
-
}
|
|
4197
|
+
removeDeprecatedSyncFiles();
|
|
3978
4198
|
|
|
3979
4199
|
// Progressive setup summary
|
|
3980
4200
|
console.log('');
|
|
3981
4201
|
console.log(renderSetupSummary(actionLog, agents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
3982
4202
|
printSetupNotes();
|
|
3983
4203
|
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
3984
|
-
// Hooks +
|
|
4204
|
+
// Hooks + issue store already handled above, so init skips those side effects.
|
|
3985
4205
|
await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
|
|
3986
4206
|
console.log('');
|
|
3987
4207
|
}
|
|
3988
4208
|
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4209
|
+
/**
|
|
4210
|
+
* Remove generated Beads/GitHub sync files left behind by older installs.
|
|
4211
|
+
*
|
|
4212
|
+
* Runs on every setup — there is no longer a flag for it. Only files whose
|
|
4213
|
+
* content matches a known generated template are removed, so user-owned files
|
|
4214
|
+
* at the same paths are preserved.
|
|
4215
|
+
*/
|
|
4216
|
+
function removeDeprecatedSyncFiles() {
|
|
3996
4217
|
try {
|
|
3997
|
-
const result =
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
console.log(` Removed deprecated sync file: ${f}`);
|
|
4218
|
+
const result = cleanupDeprecatedSyncFiles(projectRoot, { packageDir: getPackageRoot(packageDir) });
|
|
4219
|
+
for (const file of result.removed || []) {
|
|
4220
|
+
console.log(` Removed deprecated sync file: ${file}`);
|
|
4001
4221
|
}
|
|
4002
4222
|
} catch (err) {
|
|
4003
|
-
console.error(` Error
|
|
4223
|
+
console.error(` Error removing deprecated sync files: ${err.message}`);
|
|
4004
4224
|
}
|
|
4005
4225
|
}
|
|
4006
4226
|
|
|
4007
|
-
// Helper: Handle setup command in non-quick mode
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
4227
|
// Helper: Handle setup command in non-quick mode
|
|
4011
4228
|
async function handleSetupCommand(selectedAgents, flags) {
|
|
4012
4229
|
if (!Array.isArray(selectedAgents) || selectedAgents.length === 0) {
|
|
@@ -4024,6 +4241,7 @@ async function handleSetupCommand(selectedAgents, flags) {
|
|
|
4024
4241
|
skipExternal: flags.skipExternal,
|
|
4025
4242
|
keepExisting: flags.keep,
|
|
4026
4243
|
commandRunner: flags.commandRunner,
|
|
4244
|
+
skillsOnly: flags.skillsOnly,
|
|
4027
4245
|
});
|
|
4028
4246
|
} finally {
|
|
4029
4247
|
projectRoot = savedRoot;
|
|
@@ -4168,6 +4386,7 @@ const SETUP_FLAG_DEFAULTS = Object.freeze({
|
|
|
4168
4386
|
sync: false,
|
|
4169
4387
|
symlink: false,
|
|
4170
4388
|
nonInteractive: false,
|
|
4389
|
+
skillsOnly: false,
|
|
4171
4390
|
});
|
|
4172
4391
|
|
|
4173
4392
|
const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
|
|
@@ -4186,6 +4405,8 @@ const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
|
|
|
4186
4405
|
'--symlink': { symlink: true },
|
|
4187
4406
|
'--yes': { yes: true, nonInteractive: true },
|
|
4188
4407
|
'-y': { yes: true, nonInteractive: true },
|
|
4408
|
+
'--skills-only': { skillsOnly: true },
|
|
4409
|
+
'--no-hooks': { skillsOnly: true },
|
|
4189
4410
|
});
|
|
4190
4411
|
|
|
4191
4412
|
function parseAgentFlag(argv, currentIndex, isFlagToken) {
|
|
@@ -4262,9 +4483,9 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4262
4483
|
standard: Boolean(flags.standard || setupFlags.standard),
|
|
4263
4484
|
full: Boolean(flags.full || setupFlags.full),
|
|
4264
4485
|
skipExternal: Boolean(flags.skipExternal || setupFlags.skipExternal),
|
|
4265
|
-
sync: Boolean(flags.sync || setupFlags.sync),
|
|
4266
4486
|
symlink: Boolean(flags.symlink || setupFlags.symlink),
|
|
4267
4487
|
nonInteractive: Boolean(flags.nonInteractive || setupFlags.nonInteractive),
|
|
4488
|
+
skillsOnly: Boolean(flags.skillsOnly || setupFlags.skillsOnly),
|
|
4268
4489
|
};
|
|
4269
4490
|
}
|
|
4270
4491
|
|
|
@@ -4309,7 +4530,6 @@ module.exports = {
|
|
|
4309
4530
|
if (flags.verbose) VERBOSE_MODE = true;
|
|
4310
4531
|
if (flags.nonInteractive || flags.yes) NON_INTERACTIVE = true;
|
|
4311
4532
|
if (flags.symlink) SYMLINK_ONLY = true;
|
|
4312
|
-
if (flags.sync) SYNC_ENABLED = true;
|
|
4313
4533
|
actionLog = new SetupActionLog();
|
|
4314
4534
|
resetSetupNotes();
|
|
4315
4535
|
PKG_MANAGER = detectPackageManager();
|
|
@@ -4327,6 +4547,12 @@ module.exports = {
|
|
|
4327
4547
|
}
|
|
4328
4548
|
|
|
4329
4549
|
const [profile] = selectedProfiles;
|
|
4550
|
+
if (flags.skillsOnly) {
|
|
4551
|
+
return {
|
|
4552
|
+
success: false,
|
|
4553
|
+
error: `--skills-only cannot be combined with --${profile}.`,
|
|
4554
|
+
};
|
|
4555
|
+
}
|
|
4330
4556
|
return initCommand.handler([`--profile=${profile}`, '--yes', ...(flags.force ? ['--force'] : [])], flags, projectRoot);
|
|
4331
4557
|
}
|
|
4332
4558
|
|
|
@@ -4351,7 +4577,7 @@ module.exports = {
|
|
|
4351
4577
|
|
|
4352
4578
|
if (flags.dryRun) {
|
|
4353
4579
|
if (selectedAgents.length === 0) selectedAgents = ['claude'];
|
|
4354
|
-
dryRunSetup(selectedAgents);
|
|
4580
|
+
dryRunSetup(selectedAgents, { skillsOnly: flags.skillsOnly });
|
|
4355
4581
|
return { success: true };
|
|
4356
4582
|
}
|
|
4357
4583
|
|
|
@@ -4360,12 +4586,7 @@ module.exports = {
|
|
|
4360
4586
|
if (selectedAgents.length === 0 || (flags.yes && !flags.agents)) {
|
|
4361
4587
|
selectedAgents = Object.keys(AGENTS);
|
|
4362
4588
|
}
|
|
4363
|
-
await quickSetup(selectedAgents, flags.skipExternal);
|
|
4364
|
-
return { success: true };
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
if (flags.sync && selectedAgents.length === 0) {
|
|
4368
|
-
await handleSyncScaffold();
|
|
4589
|
+
await quickSetup(selectedAgents, flags.skipExternal, { skillsOnly: flags.skillsOnly });
|
|
4369
4590
|
return { success: true };
|
|
4370
4591
|
}
|
|
4371
4592
|
|
|
@@ -4406,6 +4627,8 @@ module.exports = {
|
|
|
4406
4627
|
ensureGitHooksInstalled,
|
|
4407
4628
|
forgeShouldWriteLefthookConfig,
|
|
4408
4629
|
FORGE_USER_LEFTHOOK_YML,
|
|
4630
|
+
applyExistingGateDeferral,
|
|
4631
|
+
installGitHooks,
|
|
4409
4632
|
setupClaudeMcpConfig,
|
|
4410
4633
|
setupCursorMcpConfig,
|
|
4411
4634
|
setupClaudePermissions,
|
|
@@ -4428,16 +4651,18 @@ module.exports = {
|
|
|
4428
4651
|
repairRuntimeReadiness,
|
|
4429
4652
|
_showBanner: showBanner,
|
|
4430
4653
|
backupAndRemoveLegacyCursorRules,
|
|
4654
|
+
resolveCurrentSourceHead,
|
|
4655
|
+
normalizeClaudePointerContent,
|
|
4656
|
+
readStagedClaudePointerContent,
|
|
4431
4657
|
|
|
4432
4658
|
// State accessors for testing
|
|
4433
|
-
_getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY,
|
|
4659
|
+
_getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY, PKG_MANAGER }),
|
|
4434
4660
|
_setState: (state) => {
|
|
4435
4661
|
if (state.projectRoot !== undefined) projectRoot = state.projectRoot;
|
|
4436
4662
|
if (state.FORCE_MODE !== undefined) FORCE_MODE = state.FORCE_MODE;
|
|
4437
4663
|
if (state.VERBOSE_MODE !== undefined) VERBOSE_MODE = state.VERBOSE_MODE;
|
|
4438
4664
|
if (state.NON_INTERACTIVE !== undefined) NON_INTERACTIVE = state.NON_INTERACTIVE;
|
|
4439
4665
|
if (state.SYMLINK_ONLY !== undefined) SYMLINK_ONLY = state.SYMLINK_ONLY;
|
|
4440
|
-
if (state.SYNC_ENABLED !== undefined) SYNC_ENABLED = state.SYNC_ENABLED;
|
|
4441
4666
|
if (state.PKG_MANAGER !== undefined) PKG_MANAGER = state.PKG_MANAGER;
|
|
4442
4667
|
},
|
|
4443
4668
|
};
|