forge-workflow 0.0.8 → 0.0.10
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/.claude/commands/dev.md +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/premerge.md +2 -2
- package/.claude/commands/review.md +5 -2
- package/.claude/commands/ship.md +6 -5
- package/.claude/commands/status.md +4 -4
- package/.claude/rules/greptile-review-process.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/premerge.md +2 -2
- package/.cline/workflows/review.md +5 -2
- package/.cline/workflows/ship.md +6 -5
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/premerge/SKILL.md +2 -2
- package/.codex/skills/review/SKILL.md +5 -2
- package/.codex/skills/ship/SKILL.md +6 -5
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/premerge.md +2 -2
- package/.cursor/commands/review.md +5 -2
- package/.cursor/commands/ship.md +6 -5
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/premerge.prompt.md +2 -2
- package/.github/prompts/review.prompt.md +5 -2
- package/.github/prompts/ship.prompt.md +6 -5
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +44 -11
- package/.github/workflows/github-to-beads.yml +11 -8
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/premerge.md +2 -2
- package/.kilocode/workflows/review.md +5 -2
- package/.kilocode/workflows/ship.md +6 -5
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/premerge.md +2 -2
- package/.opencode/commands/review.md +5 -2
- package/.opencode/commands/ship.md +6 -5
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/premerge.md +2 -2
- package/.roo/commands/review.md +5 -2
- package/.roo/commands/ship.md +6 -5
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +10 -9
- package/CLAUDE.md +12 -0
- package/README.md +12 -6
- package/bin/forge.js +24 -8
- package/docs/BEADS_GITHUB_SYNC.md +32 -2
- package/docs/EXAMPLES.md +22 -22
- package/docs/ROADMAP.md +3 -3
- package/docs/TOOLCHAIN.md +150 -160
- package/lib/agents/codex.plugin.json +3 -0
- package/lib/agents-config.js +18 -12
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/codex-skills.js +54 -1
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/plan.js +5 -2
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +247 -27
- package/lib/commands/ship.js +188 -5
- package/lib/commands/status.js +187 -19
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +125 -25
- package/lib/commands/validate.js +232 -9
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/setup-action-log.js +2 -0
- package/lib/setup-summary-renderer.js +15 -11
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +14 -8
- package/lib/workflow/state-manager.js +349 -0
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -222
- package/scripts/forge-team/lib/hooks.sh +1 -1
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/lib/verify.sh +1 -1
- package/scripts/forge-team/lib/workload.sh +56 -27
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/forge-team/tests/workload.test.sh +35 -4
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/github-beads-sync/run-bd.mjs +4 -2
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +84 -330
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +39 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +335 -38
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
package/lib/commands/test.js
CHANGED
|
@@ -60,19 +60,7 @@ function checkBeadsConnectivity(execFileSync) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
* Get changed files relative to main branch, mapped to test file paths.
|
|
65
|
-
*
|
|
66
|
-
* Falls back to `git diff --name-only HEAD` if merge-base fails.
|
|
67
|
-
*
|
|
68
|
-
* @param {string} _projectRoot - Project root (unused, git uses cwd)
|
|
69
|
-
* @param {Function} execFileSync - Injected execFileSync
|
|
70
|
-
* @returns {string[]} Array of test file paths (e.g. ['test/foo.test.js'])
|
|
71
|
-
*/
|
|
72
|
-
function getAffectedTestFiles(_projectRoot, execFileSync) {
|
|
73
|
-
let diffRef;
|
|
74
|
-
|
|
75
|
-
// Detect default branch, then try merge-base
|
|
63
|
+
function resolveBaseBranch(execFileSync) {
|
|
76
64
|
let baseBranch = 'main';
|
|
77
65
|
try {
|
|
78
66
|
baseBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'origin/HEAD'], {
|
|
@@ -88,17 +76,39 @@ function getAffectedTestFiles(_projectRoot, execFileSync) {
|
|
|
88
76
|
} catch (_e2) { /* intentional: branch doesn't exist, try next name */ } // NOSONAR S2486
|
|
89
77
|
}
|
|
90
78
|
}
|
|
79
|
+
return baseBranch;
|
|
80
|
+
}
|
|
91
81
|
|
|
82
|
+
function resolveDiffRef(execFileSync, options = {}) {
|
|
83
|
+
if (options.sinceUpstream) {
|
|
84
|
+
try {
|
|
85
|
+
const upstreamRef = execFileSync('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], {
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: 'pipe',
|
|
88
|
+
timeout: 3000,
|
|
89
|
+
}).trim();
|
|
90
|
+
if (upstreamRef) {
|
|
91
|
+
return `${upstreamRef}...HEAD`;
|
|
92
|
+
}
|
|
93
|
+
} catch (_e) { // NOSONAR S2486
|
|
94
|
+
/* intentional: branch may not track a remote yet, fall back to base branch */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const baseBranch = resolveBaseBranch(execFileSync);
|
|
92
99
|
try {
|
|
93
100
|
const mergeBase = execFileSync('git', ['merge-base', 'HEAD', baseBranch], {
|
|
94
101
|
encoding: 'utf8',
|
|
95
102
|
timeout: 5000,
|
|
96
103
|
}).trim();
|
|
97
|
-
|
|
104
|
+
return `${mergeBase}...HEAD`;
|
|
98
105
|
} catch (_e) { /* intentional: merge-base failed, fallback to diff against HEAD */ // NOSONAR S2486
|
|
99
|
-
|
|
106
|
+
return 'HEAD';
|
|
100
107
|
}
|
|
108
|
+
}
|
|
101
109
|
|
|
110
|
+
function getChangedFiles(execFileSync, options = {}) {
|
|
111
|
+
const diffRef = resolveDiffRef(execFileSync, options);
|
|
102
112
|
let output;
|
|
103
113
|
try {
|
|
104
114
|
output = execFileSync('git', ['diff', '--name-only', diffRef], {
|
|
@@ -111,23 +121,111 @@ function getAffectedTestFiles(_projectRoot, execFileSync) {
|
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
if (!output) return [];
|
|
124
|
+
return output.split('\n').filter(Boolean);
|
|
125
|
+
}
|
|
114
126
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Get changed files relative to main branch, mapped to test file paths.
|
|
129
|
+
*
|
|
130
|
+
* Falls back to `git diff --name-only HEAD` if merge-base fails.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} projectRoot - Project root for test file existence checks
|
|
133
|
+
* @param {Function} execFileSync - Injected execFileSync
|
|
134
|
+
* @param {Object} [fs] - Injected fs module
|
|
135
|
+
* @param {Object} [options] - Diff selection options
|
|
136
|
+
* @returns {string[]} Array of test file paths (e.g. ['test/foo.test.js'])
|
|
137
|
+
*/
|
|
138
|
+
function getAffectedTestFiles(projectRoot, execFileSync, fs = defaultFs, options = {}) {
|
|
139
|
+
const changedFiles = getChangedFiles(execFileSync, options);
|
|
140
|
+
const testFiles = new Set();
|
|
119
141
|
for (const file of changedFiles) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
for (const candidate of getTestCandidatesForChangedFile(file)) {
|
|
143
|
+
if (fs.existsSync(path.join(projectRoot, candidate))) {
|
|
144
|
+
testFiles.add(candidate);
|
|
145
|
+
}
|
|
124
146
|
}
|
|
125
147
|
}
|
|
126
148
|
|
|
127
|
-
return testFiles;
|
|
149
|
+
return Array.from(testFiles).sort((left, right) => left.localeCompare(right));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getTestCandidatesForChangedFile(file) {
|
|
153
|
+
if (!file) return [];
|
|
154
|
+
|
|
155
|
+
if (file === 'lib/lefthook-check.js') {
|
|
156
|
+
return ['test/lefthook-check.test.js', 'test/runtime-health.test.js'];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (file === 'lib/runtime-health.js') {
|
|
160
|
+
return ['test/runtime-health.test.js'];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (file === 'scripts/test.js') {
|
|
164
|
+
return ['test/scripts/test-runner.test.js'];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (file.startsWith('test/') && file.endsWith('.test.js')) {
|
|
168
|
+
return [file];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (file.startsWith('lib/') && file.endsWith('.js')) {
|
|
172
|
+
const relative = file.slice('lib/'.length);
|
|
173
|
+
return [`test/${relative.replace(/\.js$/, '.test.js')}`];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (file.startsWith('scripts/') && (file.endsWith('.js') || file.endsWith('.sh'))) {
|
|
177
|
+
const relative = file.slice('scripts/'.length).replace(/\.(js|sh)$/, '.test.js');
|
|
178
|
+
return [
|
|
179
|
+
`test/scripts/${relative}`,
|
|
180
|
+
`test/${relative}`,
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (file.startsWith('.github/workflows/')) {
|
|
185
|
+
const workflowName = path.basename(file, path.extname(file));
|
|
186
|
+
return [
|
|
187
|
+
`test/workflows/${workflowName}.test.js`,
|
|
188
|
+
'test/ci-workflow.test.js',
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (file.startsWith('.github/agentic-workflows/') || file === '.github/behavioral-test-scores.json') {
|
|
193
|
+
return [
|
|
194
|
+
'test/scripts/behavioral-judge.test.js',
|
|
195
|
+
'test/structural/agentic-workflow-sync.test.js',
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (file.startsWith('.claude/commands/') && file.endsWith('.md')) {
|
|
200
|
+
return [
|
|
201
|
+
'test/command-sync-check.test.js',
|
|
202
|
+
'test/structural/command-sync.test.js',
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (file.startsWith('.forge/')
|
|
207
|
+
|| file.startsWith('.cursor/')
|
|
208
|
+
|| file.startsWith('.cline/')
|
|
209
|
+
|| file.startsWith('.roo/')
|
|
210
|
+
|| file.startsWith('.kilocode/')
|
|
211
|
+
|| file.startsWith('.opencode/')
|
|
212
|
+
|| file.startsWith('.codex/skills/')
|
|
213
|
+
|| file.startsWith('.github/prompts/')) {
|
|
214
|
+
return [
|
|
215
|
+
'test/agent-gaps.test.js',
|
|
216
|
+
'test/command-sync-check.test.js',
|
|
217
|
+
'test/scripts/check-agents.test.js',
|
|
218
|
+
'test/structural/command-sync.test.js',
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return [];
|
|
128
223
|
}
|
|
129
224
|
|
|
130
225
|
module.exports = {
|
|
226
|
+
getChangedFiles,
|
|
227
|
+
getAffectedTestFiles,
|
|
228
|
+
getTestCandidatesForChangedFile,
|
|
131
229
|
name: 'test',
|
|
132
230
|
description: 'Run tests with smart defaults (timeout, Beads skip, affected-only)',
|
|
133
231
|
usage: 'forge test [--affected]',
|
|
@@ -181,7 +279,9 @@ module.exports = {
|
|
|
181
279
|
|
|
182
280
|
// 5. --affected flag: find changed test files
|
|
183
281
|
if (flags['--affected'] || flags.affected) {
|
|
184
|
-
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync
|
|
282
|
+
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync, fs, {
|
|
283
|
+
sinceUpstream: flags.sinceUpstream || flags['--since-upstream'],
|
|
284
|
+
});
|
|
185
285
|
if (affectedTests.length > 0) {
|
|
186
286
|
testArgs = ['run', 'test', ...affectedTests];
|
|
187
287
|
}
|
package/lib/commands/validate.js
CHANGED
|
@@ -14,19 +14,91 @@ const path = require('node:path');
|
|
|
14
14
|
|
|
15
15
|
// Constants
|
|
16
16
|
const CHECK_TYPES = {
|
|
17
|
+
CONFLICT_MARKERS: 'conflictMarkers',
|
|
17
18
|
TYPE_CHECK: 'typeCheck',
|
|
18
19
|
LINT: 'lint',
|
|
19
20
|
SECURITY: 'security',
|
|
20
21
|
TESTS: 'tests',
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const CONFLICT_MARKER_IGNORE_DIRS = new Set([
|
|
25
|
+
'.beads',
|
|
26
|
+
'.git',
|
|
27
|
+
'.next',
|
|
28
|
+
'coverage',
|
|
29
|
+
'dist',
|
|
30
|
+
'node_modules',
|
|
31
|
+
]);
|
|
32
|
+
const CONFLICT_MARKER_ALLOWED_DOT_DIRS = new Set([
|
|
33
|
+
'.claude',
|
|
34
|
+
'.cline',
|
|
35
|
+
'.codex',
|
|
36
|
+
'.cursor',
|
|
37
|
+
'.forge',
|
|
38
|
+
'.github',
|
|
39
|
+
'.husky',
|
|
40
|
+
'.kilocode',
|
|
41
|
+
'.opencode',
|
|
42
|
+
'.roo',
|
|
43
|
+
'.sonarlint',
|
|
44
|
+
]);
|
|
45
|
+
const CONFLICT_START_PATTERN = /^<<<<<<<(?: .*)?$/;
|
|
46
|
+
const CONFLICT_DIVIDER_PATTERN = /^=======$/;
|
|
47
|
+
const CONFLICT_END_PATTERN = /^>>>>>>>.*$/;
|
|
48
|
+
const GIT_CONFLICT_SCAN_MAX_BUFFER = 16 * 1024 * 1024;
|
|
49
|
+
const VALIDATION_COMMAND_TIMEOUT_MS = 600000;
|
|
50
|
+
const VALIDATION_COMMAND_TIMEOUT_MINUTES = Math.round(VALIDATION_COMMAND_TIMEOUT_MS / 60000);
|
|
51
|
+
|
|
52
|
+
function validationTimeoutMessage(label) {
|
|
53
|
+
return `${label} timed out after ${VALIDATION_COMMAND_TIMEOUT_MINUTES} minutes`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listConflictScanFiles(rootDir) {
|
|
57
|
+
try {
|
|
58
|
+
const output = execFileSync(
|
|
59
|
+
'git',
|
|
60
|
+
['ls-files', '-z', '--cached', '--others', '--exclude-standard'],
|
|
61
|
+
{ encoding: 'utf8', cwd: rootDir, timeout: 120000, maxBuffer: GIT_CONFLICT_SCAN_MAX_BUFFER },
|
|
62
|
+
);
|
|
63
|
+
return output
|
|
64
|
+
.split('\0')
|
|
65
|
+
.filter(file => file.length > 0)
|
|
66
|
+
.map(file => path.join(rootDir, file));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!isGitListingUnavailableError(error)) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shouldSkipConflictScanPath(relativePath) {
|
|
76
|
+
const segments = String(relativePath || '')
|
|
77
|
+
.split(/[\\/]+/)
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
const directorySegments = segments.slice(0, -1);
|
|
80
|
+
return directorySegments.some(segment => (
|
|
81
|
+
(segment.startsWith('.') && !CONFLICT_MARKER_ALLOWED_DOT_DIRS.has(segment))
|
|
82
|
+
|| CONFLICT_MARKER_IGNORE_DIRS.has(segment)
|
|
83
|
+
));
|
|
84
|
+
}
|
|
85
|
+
|
|
23
86
|
function getExecOptions() {
|
|
24
|
-
return { encoding: 'utf8', cwd: process.cwd(), timeout:
|
|
87
|
+
return { encoding: 'utf8', cwd: process.cwd(), timeout: VALIDATION_COMMAND_TIMEOUT_MS };
|
|
25
88
|
}
|
|
26
89
|
|
|
27
90
|
const ERROR_PATTERNS = {
|
|
28
91
|
COMMAND_NOT_FOUND: ['ENOENT', 'not found'],
|
|
29
92
|
NO_LOCK_FILE: ['requires an existing', 'package-lock'],
|
|
93
|
+
GIT_LISTING_UNAVAILABLE: [
|
|
94
|
+
'not a git repository',
|
|
95
|
+
'maxBuffer', // git ls-files output exceeds maxBuffer
|
|
96
|
+
'ENOBUFS', // OS-level buffer overflow
|
|
97
|
+
'ETIMEDOUT', // subprocess timeout
|
|
98
|
+
'EPERM', // permission denied
|
|
99
|
+
'EACCES', // access denied
|
|
100
|
+
'spawnSync', // general spawn failure
|
|
101
|
+
],
|
|
30
102
|
};
|
|
31
103
|
|
|
32
104
|
/**
|
|
@@ -39,6 +111,14 @@ function isCommandNotFound(error) {
|
|
|
39
111
|
);
|
|
40
112
|
}
|
|
41
113
|
|
|
114
|
+
function isGitListingUnavailableError(error) {
|
|
115
|
+
if (!error || typeof error.message !== 'string') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return isCommandNotFound(error)
|
|
119
|
+
|| ERROR_PATTERNS.GIT_LISTING_UNAVAILABLE.some(pattern => error.message.includes(pattern));
|
|
120
|
+
}
|
|
121
|
+
|
|
42
122
|
/**
|
|
43
123
|
* Parse number from regex match
|
|
44
124
|
* @private
|
|
@@ -57,6 +137,131 @@ function getCheckStatus(check) {
|
|
|
57
137
|
return check.success ? 'PASS' : 'FAIL';
|
|
58
138
|
}
|
|
59
139
|
|
|
140
|
+
function scanForConflictMarkers(rootDir = process.cwd()) {
|
|
141
|
+
const filesWithMarkers = [];
|
|
142
|
+
|
|
143
|
+
function inspectFile(absolutePath) {
|
|
144
|
+
const relativePath = path.relative(rootDir, absolutePath) || path.basename(absolutePath);
|
|
145
|
+
if (shouldSkipConflictScanPath(relativePath)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (!fs.lstatSync(absolutePath).isFile()) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (!canSkipConflictScanStatError(error)) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const startLine = findConflictMarkerStartLine(readConflictScanFile(absolutePath));
|
|
161
|
+
if (startLine === null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
filesWithMarkers.push({ path: relativePath, line: startLine });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function walk(currentDir) {
|
|
169
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
170
|
+
if (shouldSkipConflictScanEntry(entry)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (entry.isDirectory()) {
|
|
175
|
+
walk(path.join(currentDir, entry.name));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!entry.isFile()) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
inspectFile(path.join(currentDir, entry.name));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const trackedFiles = listConflictScanFiles(rootDir);
|
|
188
|
+
if (trackedFiles) {
|
|
189
|
+
for (const absolutePath of trackedFiles) {
|
|
190
|
+
inspectFile(absolutePath);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
walk(rootDir);
|
|
194
|
+
}
|
|
195
|
+
filesWithMarkers.sort((left, right) => {
|
|
196
|
+
if (left.path === right.path) {
|
|
197
|
+
return left.line - right.line;
|
|
198
|
+
}
|
|
199
|
+
return left.path.localeCompare(right.path);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
success: filesWithMarkers.length === 0,
|
|
204
|
+
files: filesWithMarkers,
|
|
205
|
+
message: filesWithMarkers.length === 0
|
|
206
|
+
? 'No conflict markers found'
|
|
207
|
+
: `Conflict markers found in ${filesWithMarkers.length} file(s)`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function shouldSkipConflictScanEntry(entry) {
|
|
212
|
+
return entry.isDirectory() && (
|
|
213
|
+
(entry.name.startsWith('.') && !CONFLICT_MARKER_ALLOWED_DOT_DIRS.has(entry.name))
|
|
214
|
+
|| CONFLICT_MARKER_IGNORE_DIRS.has(entry.name)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function canSkipConflictScanStatError(error) {
|
|
219
|
+
return Boolean(error && ['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readConflictScanFile(absolutePath) {
|
|
223
|
+
try {
|
|
224
|
+
return fs.readFileSync(absolutePath, 'utf8');
|
|
225
|
+
} catch (_error) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findConflictMarkerStartLine(content) {
|
|
231
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
232
|
+
let activeStartLine = null;
|
|
233
|
+
let sawDivider = false;
|
|
234
|
+
|
|
235
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
236
|
+
const line = lines[index];
|
|
237
|
+
const lineNumber = index + 1;
|
|
238
|
+
if (activeStartLine === null) {
|
|
239
|
+
if (CONFLICT_START_PATTERN.test(line)) {
|
|
240
|
+
activeStartLine = lineNumber;
|
|
241
|
+
sawDivider = false;
|
|
242
|
+
} else if (CONFLICT_END_PATTERN.test(line)) {
|
|
243
|
+
return lineNumber;
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!sawDivider) {
|
|
249
|
+
if (CONFLICT_DIVIDER_PATTERN.test(line)) {
|
|
250
|
+
sawDivider = true;
|
|
251
|
+
} else if (CONFLICT_START_PATTERN.test(line)) {
|
|
252
|
+
activeStartLine = index + 1;
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (CONFLICT_END_PATTERN.test(line)) {
|
|
258
|
+
return activeStartLine;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return activeStartLine;
|
|
263
|
+
}
|
|
264
|
+
|
|
60
265
|
/**
|
|
61
266
|
* Parse vulnerability counts from audit output
|
|
62
267
|
* @private
|
|
@@ -109,7 +314,7 @@ async function runTypeCheck() {
|
|
|
109
314
|
return {
|
|
110
315
|
success: false,
|
|
111
316
|
duration: Date.now() - startTime,
|
|
112
|
-
message: 'Type check
|
|
317
|
+
message: validationTimeoutMessage('Type check'),
|
|
113
318
|
};
|
|
114
319
|
}
|
|
115
320
|
|
|
@@ -166,7 +371,7 @@ async function runLint() {
|
|
|
166
371
|
return {
|
|
167
372
|
success: false,
|
|
168
373
|
duration: Date.now() - startTime,
|
|
169
|
-
message: 'Lint check
|
|
374
|
+
message: validationTimeoutMessage('Lint check'),
|
|
170
375
|
};
|
|
171
376
|
}
|
|
172
377
|
|
|
@@ -238,7 +443,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
238
443
|
return {
|
|
239
444
|
success: false,
|
|
240
445
|
duration: Date.now() - startTime,
|
|
241
|
-
message: 'Security audit
|
|
446
|
+
message: validationTimeoutMessage('Security audit'),
|
|
242
447
|
};
|
|
243
448
|
}
|
|
244
449
|
// bun audit exits non-zero when vulnerabilities are found; stdout still contains the data
|
|
@@ -267,7 +472,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
267
472
|
return {
|
|
268
473
|
success: false,
|
|
269
474
|
duration: Date.now() - startTime,
|
|
270
|
-
message: 'Security audit
|
|
475
|
+
message: validationTimeoutMessage('Security audit'),
|
|
271
476
|
};
|
|
272
477
|
}
|
|
273
478
|
// npm audit exits non-zero when it finds any vulnerability - stdout still has JSON
|
|
@@ -322,7 +527,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
322
527
|
return {
|
|
323
528
|
success: false,
|
|
324
529
|
duration: Date.now() - startTime,
|
|
325
|
-
message: 'Security audit
|
|
530
|
+
message: validationTimeoutMessage('Security audit'),
|
|
326
531
|
};
|
|
327
532
|
}
|
|
328
533
|
|
|
@@ -365,7 +570,7 @@ async function runAllTests() {
|
|
|
365
570
|
const startTime = Date.now();
|
|
366
571
|
|
|
367
572
|
try {
|
|
368
|
-
const result = execFileSync('bun', ['test'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
573
|
+
const result = execFileSync('bun', ['test', '--timeout', '30000'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
369
574
|
|
|
370
575
|
// Parse bun test output
|
|
371
576
|
const passed = parseNumber(/(\d+) pass/.exec(result)); // NOSONAR S5852 - bounded \d+ pattern
|
|
@@ -392,7 +597,7 @@ async function runAllTests() {
|
|
|
392
597
|
passed: 0,
|
|
393
598
|
failed: 0,
|
|
394
599
|
total: 0,
|
|
395
|
-
message: 'Test execution
|
|
600
|
+
message: validationTimeoutMessage('Test execution'),
|
|
396
601
|
};
|
|
397
602
|
}
|
|
398
603
|
|
|
@@ -450,13 +655,29 @@ async function runAllTests() {
|
|
|
450
655
|
* console.log(result.summary);
|
|
451
656
|
*/
|
|
452
657
|
async function executeValidate(options = {}) { // NOSONAR S3776
|
|
453
|
-
const { skip = [], continueOnError = true } = options || {};
|
|
658
|
+
const { skip = [], continueOnError = true, rootDir = process.cwd() } = options || {};
|
|
454
659
|
|
|
455
660
|
const checks = {};
|
|
456
661
|
const failedChecks = [];
|
|
457
662
|
const errors = [];
|
|
458
663
|
const startTime = Date.now();
|
|
459
664
|
|
|
665
|
+
// 0. Conflict markers
|
|
666
|
+
if (!skip.includes(CHECK_TYPES.CONFLICT_MARKERS)) {
|
|
667
|
+
try {
|
|
668
|
+
checks.conflictMarkers = scanForConflictMarkers(rootDir);
|
|
669
|
+
if (!checks.conflictMarkers.success) {
|
|
670
|
+
failedChecks.push(CHECK_TYPES.CONFLICT_MARKERS);
|
|
671
|
+
if (!continueOnError) {
|
|
672
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
errors.push(`Conflict marker scan error: ${error.message}`);
|
|
677
|
+
checks.conflictMarkers = { success: false, message: error.message };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
460
681
|
// 1. Type checking
|
|
461
682
|
if (!skip.includes(CHECK_TYPES.TYPE_CHECK)) {
|
|
462
683
|
try {
|
|
@@ -535,6 +756,7 @@ function buildResult(checks, failedChecks, errors, startTime) {
|
|
|
535
756
|
// Build summary using getCheckStatus helper
|
|
536
757
|
const checkResults = [];
|
|
537
758
|
const checkLabels = {
|
|
759
|
+
conflictMarkers: 'Conflict Markers',
|
|
538
760
|
typeCheck: 'Type',
|
|
539
761
|
lint: 'Lint',
|
|
540
762
|
security: 'Security',
|
|
@@ -610,6 +832,7 @@ module.exports = {
|
|
|
610
832
|
runLint,
|
|
611
833
|
runSecurityScan,
|
|
612
834
|
runAllTests,
|
|
835
|
+
scanForConflictMarkers,
|
|
613
836
|
executeValidate,
|
|
614
837
|
executeDebugMode,
|
|
615
838
|
};
|
package/lib/commands/worktree.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { execFileSync, spawnSync } = require('node:child_process');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { bootstrapBeads } = require('../beads-bootstrap');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Forge Worktree Command
|
|
@@ -10,8 +11,8 @@ const path = require('node:path');
|
|
|
10
11
|
* Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
|
|
11
12
|
*
|
|
12
13
|
* Subcommands:
|
|
13
|
-
* create <slug>
|
|
14
|
-
* remove <slug>
|
|
14
|
+
* create <slug> - Create worktree at .worktrees/<slug> with branch + beads + install
|
|
15
|
+
* remove <slug> - Remove worktree via git worktree remove
|
|
15
16
|
*
|
|
16
17
|
* @module commands/worktree
|
|
17
18
|
*/
|
|
@@ -31,25 +32,10 @@ function detectPackageManager(projectRoot, fsApi) {
|
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
* Copy .beads/ directory excluding daemon.lock.
|
|
36
|
-
* @param {string} source - Source .beads path
|
|
37
|
-
* @param {string} dest - Destination .beads path
|
|
38
|
-
* @param {object} fsApi - fs module (for DI)
|
|
39
|
-
*/
|
|
40
|
-
function copyBeadsDir(source, dest, fsApi) {
|
|
41
|
-
fsApi.mkdirSync(dest, { recursive: true });
|
|
42
|
-
const entries = fsApi.readdirSync(source);
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
if (entry === 'daemon.lock') continue;
|
|
45
|
-
fsApi.cpSync(path.join(source, entry), path.join(dest, entry), { recursive: true });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
35
|
/**
|
|
50
36
|
* Stop a Dolt server running in a worktree to release file locks.
|
|
51
37
|
* Tries graceful `bd dolt stop` first, then falls back to PID-based kill
|
|
52
|
-
* from lock files. Never kills ALL Dolt processes
|
|
38
|
+
* from lock files. Never kills ALL Dolt processes - only the specific PID.
|
|
53
39
|
*
|
|
54
40
|
* @param {string} worktreePath - Absolute path to the worktree
|
|
55
41
|
* @param {object} [opts] - Options for dependency injection
|
|
@@ -106,46 +92,29 @@ function branchExists(branchName, runFile) {
|
|
|
106
92
|
}
|
|
107
93
|
}
|
|
108
94
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* @param {string} projectRoot - Absolute path to the project root
|
|
113
|
-
* @param {string} platform - OS platform string (e.g. 'win32')
|
|
114
|
-
* @param {object} fsApi - fs module (for DI)
|
|
115
|
-
* @param {Function} exec - execFileSync-compatible function
|
|
116
|
-
* @returns {string|null} Warning message if beads setup had issues, null otherwise
|
|
117
|
-
*/
|
|
118
|
-
function setupBeads(worktreePath, projectRoot, platform, fsApi, exec) {
|
|
119
|
-
const beadsSource = path.resolve(projectRoot, '.beads');
|
|
120
|
-
const beadsDest = path.resolve(worktreePath, '.beads');
|
|
121
|
-
|
|
122
|
-
if (!fsApi.existsSync(beadsSource)) {
|
|
123
|
-
return 'Beads not installed — skipping .beads setup';
|
|
95
|
+
function setupBeadsWithBootstrap(worktreePath, projectRoot, platform, fsApi, exec, options = {}) {
|
|
96
|
+
if (!fsApi.existsSync(path.resolve(projectRoot, '.beads'))) {
|
|
97
|
+
return 'Beads not installed - skipping .beads setup';
|
|
124
98
|
}
|
|
125
99
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} catch (symlinkErr) {
|
|
134
|
-
if (symlinkErr.code === 'EPERM') {
|
|
135
|
-
copyBeadsDir(beadsSource, beadsDest, fsApi);
|
|
136
|
-
} else {
|
|
137
|
-
throw symlinkErr;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
100
|
+
const bootstrapResult = bootstrapBeads(worktreePath, {
|
|
101
|
+
_exec: exec,
|
|
102
|
+
_fs: fsApi,
|
|
103
|
+
_platform: platform,
|
|
104
|
+
mainProjectRoot: projectRoot,
|
|
105
|
+
_safeBeadsInit: options._safeBeadsInit,
|
|
106
|
+
});
|
|
140
107
|
|
|
141
|
-
// Verify beads accessible (warn if fails, don't block)
|
|
142
108
|
try {
|
|
143
109
|
exec('bd', ['--version'], { cwd: worktreePath, timeout: 5000, stdio: 'pipe' });
|
|
144
110
|
} catch (_bdErr) { /* intentional: bd may not be on PATH */ // NOSONAR S2486
|
|
145
|
-
|
|
111
|
+
if (bootstrapResult.success === false && bootstrapResult.warning) {
|
|
112
|
+
return bootstrapResult.warning;
|
|
113
|
+
}
|
|
114
|
+
return 'Beads verification failed - .beads linked but bd may not work';
|
|
146
115
|
}
|
|
147
116
|
|
|
148
|
-
return
|
|
117
|
+
return bootstrapResult.warning;
|
|
149
118
|
}
|
|
150
119
|
|
|
151
120
|
/**
|
|
@@ -179,13 +148,17 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
179
148
|
const branchName = flags['--branch'] || flags.branch || `feat/${slug}`;
|
|
180
149
|
const worktreesDir = path.resolve(projectRoot, '.worktrees');
|
|
181
150
|
|
|
182
|
-
// Guard: Detect bare repo state
|
|
151
|
+
// Guard: Detect bare repo state - worktrees created from bare repos produce broken branches
|
|
183
152
|
// git rev-parse --show-toplevel throws in a bare repo (no working tree)
|
|
184
153
|
try {
|
|
185
154
|
runFile('git', ['-C', projectRoot, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
186
|
-
} catch (
|
|
155
|
+
} catch (error) {
|
|
187
156
|
/* intentional: show-toplevel fails in bare repos */
|
|
188
|
-
|
|
157
|
+
const errMsg = String(error?.stderr || error?.message || '').trim();
|
|
158
|
+
if (/must be run in a work tree|this operation must be run in a work tree/i.test(errMsg)) {
|
|
159
|
+
return { success: false, error: 'bare repo detected' };
|
|
160
|
+
}
|
|
161
|
+
return { success: false, error: errMsg || 'git rev-parse --show-toplevel failed' };
|
|
189
162
|
}
|
|
190
163
|
|
|
191
164
|
// Security: Validate slug doesn't contain path traversal (OWASP A01/A03)
|
|
@@ -217,7 +190,7 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
217
190
|
}
|
|
218
191
|
|
|
219
192
|
// Step 3: Beads integration
|
|
220
|
-
const beadsWarning =
|
|
193
|
+
const beadsWarning = setupBeadsWithBootstrap(worktreePath, projectRoot, platform, fsApi, runFile, opts);
|
|
221
194
|
|
|
222
195
|
// Step 4: Run package install
|
|
223
196
|
runInstall(worktreePath, projectRoot, runSpawn, fsApi);
|