forge-workflow 0.0.8 → 0.0.9
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/premerge.md +2 -2
- package/.claude/commands/review.md +5 -2
- package/.claude/commands/ship.md +4 -3
- package/.claude/rules/greptile-review-process.md +4 -4
- package/.cline/workflows/premerge.md +2 -2
- package/.cline/workflows/review.md +5 -2
- package/.cline/workflows/ship.md +4 -3
- package/.codex/skills/premerge/SKILL.md +2 -2
- package/.codex/skills/review/SKILL.md +5 -2
- package/.codex/skills/ship/SKILL.md +4 -3
- package/.cursor/commands/premerge.md +2 -2
- package/.cursor/commands/review.md +5 -2
- package/.cursor/commands/ship.md +4 -3
- package/.github/prompts/premerge.prompt.md +2 -2
- package/.github/prompts/review.prompt.md +5 -2
- package/.github/prompts/ship.prompt.md +4 -3
- package/.github/workflows/beads-to-github.yml +1 -1
- package/.github/workflows/github-to-beads.yml +1 -1
- package/.kilocode/workflows/premerge.md +2 -2
- package/.kilocode/workflows/review.md +5 -2
- package/.kilocode/workflows/ship.md +4 -3
- package/.opencode/commands/premerge.md +2 -2
- package/.opencode/commands/review.md +5 -2
- package/.opencode/commands/ship.md +4 -3
- package/.roo/commands/premerge.md +2 -2
- package/.roo/commands/review.md +5 -2
- package/.roo/commands/ship.md +4 -3
- package/AGENTS.md +9 -9
- package/README.md +12 -6
- package/bin/forge.js +14 -3
- package/docs/BEADS_GITHUB_SYNC.md +6 -2
- package/docs/EXAMPLES.md +22 -22
- package/docs/ROADMAP.md +3 -3
- package/docs/TOOLCHAIN.md +60 -52
- package/lib/agents/codex.plugin.json +3 -0
- package/lib/agents-config.js +18 -12
- package/lib/codex-skills.js +54 -1
- package/lib/commands/plan.js +5 -2
- package/lib/commands/setup.js +231 -17
- package/lib/commands/ship.js +188 -5
- package/lib/commands/status.js +20 -33
- package/lib/commands/test.js +90 -25
- package/lib/commands/validate.js +218 -1
- package/lib/setup-action-log.js +2 -0
- package/lib/setup-summary-renderer.js +15 -11
- package/lib/workflow/enforce-stage.js +12 -8
- package/lib/workflow/state-manager.js +193 -0
- package/package.json +1 -1
- package/scripts/dep-guard.sh +11 -1
- package/scripts/forge-team/lib/hooks.sh +1 -1
- package/scripts/forge-team/lib/verify.sh +1 -1
- package/scripts/forge-team/lib/workload.sh +56 -27
- package/scripts/forge-team/tests/workload.test.sh +35 -4
- package/scripts/github-beads-sync/run-bd.mjs +4 -2
- package/scripts/smart-status.sh +10 -1
- package/scripts/sync-utils.sh +39 -0
- package/scripts/test.js +144 -38
package/lib/commands/status.js
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
* Detects workflow stage (1-9) with confidence scoring
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { secureExecFileSync } = require('../shell-utils');
|
|
7
6
|
const {
|
|
8
7
|
readWorkflowState,
|
|
9
8
|
getAllowedTransitionsForWorkflowState,
|
|
10
9
|
} = require('../workflow/state');
|
|
11
10
|
const { STAGE_IDS } = require('../workflow/stages');
|
|
11
|
+
const {
|
|
12
|
+
loadState,
|
|
13
|
+
extractWorkflowStateFromComments,
|
|
14
|
+
readWorkflowStateFromBeads,
|
|
15
|
+
} = require('../workflow/state-manager');
|
|
12
16
|
|
|
13
17
|
const WORKFLOW_STAGES = {
|
|
14
18
|
1: { name: 'Fresh Start', nextCommand: 'research' },
|
|
@@ -338,42 +342,22 @@ function parseStatusInputs(args = [], flags = {}) {
|
|
|
338
342
|
issueId: flags.issueId || flags['--issue-id'] || getInlineValue('--issue-id') || getNextValue('--issue-id'),
|
|
339
343
|
workflowState: flags.workflowState || flags['--workflow-state'] || getInlineValue('--workflow-state') || getNextValue('--workflow-state'),
|
|
340
344
|
bdComments: flags.bdComments || flags['--bd-comments'] || getInlineValue('--bd-comments') || getNextValue('--bd-comments'),
|
|
345
|
+
projectRoot: flags.projectRoot || flags['--project-root'] || getInlineValue('--project-root') || getNextValue('--project-root') || null,
|
|
341
346
|
};
|
|
342
347
|
}
|
|
343
348
|
|
|
344
|
-
function extractWorkflowStateFromComments(comments = '') {
|
|
345
|
-
const matches = String(comments).match(/^WorkflowState:\s*(\{.*\})$/gm);
|
|
346
|
-
if (!matches || matches.length === 0) {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const latest = matches.at(-1).replace(/^WorkflowState:\s*/, '');
|
|
351
|
-
return readWorkflowState(latest);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function readWorkflowStateFromBeads(issueId, options = {}) {
|
|
355
|
-
if (!issueId) {
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const comments = options.comments || secureExecFileSync('bd', ['comments', 'list', issueId], {
|
|
360
|
-
encoding: 'utf8',
|
|
361
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
362
|
-
}).trim();
|
|
363
|
-
|
|
364
|
-
if (!comments) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return extractWorkflowStateFromComments(comments);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
349
|
function resolveWorkflowState(inputs) {
|
|
372
350
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
351
|
+
if (inputs.workflowState) {
|
|
352
|
+
return { workflowState: readWorkflowState(inputs.workflowState), fallbackReason: null };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { state } = loadState(inputs.projectRoot, {
|
|
356
|
+
issueId: inputs.issueId,
|
|
357
|
+
comments: inputs.bdComments,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return { workflowState: state, fallbackReason: null };
|
|
377
361
|
} catch (error) {
|
|
378
362
|
return {
|
|
379
363
|
workflowState: null,
|
|
@@ -512,8 +496,11 @@ function formatStatus(result) {
|
|
|
512
496
|
module.exports = {
|
|
513
497
|
name: 'status',
|
|
514
498
|
description: 'Intelligent stage detection with confidence scoring',
|
|
515
|
-
handler: async (args, flags,
|
|
499
|
+
handler: async (args, flags, projectRoot) => {
|
|
516
500
|
const inputs = parseStatusInputs(args, flags);
|
|
501
|
+
if (!inputs.projectRoot && projectRoot) {
|
|
502
|
+
inputs.projectRoot = projectRoot;
|
|
503
|
+
}
|
|
517
504
|
const { workflowState, fallbackReason } = resolveWorkflowState(inputs);
|
|
518
505
|
|
|
519
506
|
if (workflowState) {
|
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
|
+
}
|
|
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
|
+
}
|
|
91
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,76 @@ 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.startsWith('test/') && file.endsWith('.test.js')) {
|
|
156
|
+
return [file];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (file.startsWith('lib/') && file.endsWith('.js')) {
|
|
160
|
+
const relative = file.slice('lib/'.length);
|
|
161
|
+
return [`test/${relative.replace(/\.js$/, '.test.js')}`];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (file.startsWith('scripts/') && file.endsWith('.js')) {
|
|
165
|
+
const relative = file.slice('scripts/'.length).replace(/\.js$/, '.test.js');
|
|
166
|
+
return [
|
|
167
|
+
`test/scripts/${relative}`,
|
|
168
|
+
`test/${relative}`,
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (file.startsWith('.github/workflows/')) {
|
|
173
|
+
const workflowName = path.basename(file, path.extname(file));
|
|
174
|
+
return [
|
|
175
|
+
`test/workflows/${workflowName}.test.js`,
|
|
176
|
+
'test/ci-workflow.test.js',
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (file.startsWith('.claude/commands/') && file.endsWith('.md')) {
|
|
181
|
+
return [
|
|
182
|
+
'test/command-sync-check.test.js',
|
|
183
|
+
'test/structural/command-sync.test.js',
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return [];
|
|
128
188
|
}
|
|
129
189
|
|
|
130
190
|
module.exports = {
|
|
191
|
+
getChangedFiles,
|
|
192
|
+
getAffectedTestFiles,
|
|
193
|
+
getTestCandidatesForChangedFile,
|
|
131
194
|
name: 'test',
|
|
132
195
|
description: 'Run tests with smart defaults (timeout, Beads skip, affected-only)',
|
|
133
196
|
usage: 'forge test [--affected]',
|
|
@@ -181,7 +244,9 @@ module.exports = {
|
|
|
181
244
|
|
|
182
245
|
// 5. --affected flag: find changed test files
|
|
183
246
|
if (flags['--affected'] || flags.affected) {
|
|
184
|
-
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync
|
|
247
|
+
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync, fs, {
|
|
248
|
+
sinceUpstream: flags.sinceUpstream || flags['--since-upstream'],
|
|
249
|
+
});
|
|
185
250
|
if (affectedTests.length > 0) {
|
|
186
251
|
testArgs = ['run', 'test', ...affectedTests];
|
|
187
252
|
}
|
package/lib/commands/validate.js
CHANGED
|
@@ -14,12 +14,69 @@ 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
|
+
|
|
50
|
+
function listConflictScanFiles(rootDir) {
|
|
51
|
+
try {
|
|
52
|
+
const output = execFileSync(
|
|
53
|
+
'git',
|
|
54
|
+
['ls-files', '-z', '--cached', '--others', '--exclude-standard'],
|
|
55
|
+
{ encoding: 'utf8', cwd: rootDir, timeout: 120000, maxBuffer: GIT_CONFLICT_SCAN_MAX_BUFFER },
|
|
56
|
+
);
|
|
57
|
+
return output
|
|
58
|
+
.split('\0')
|
|
59
|
+
.filter(file => file.length > 0)
|
|
60
|
+
.map(file => path.join(rootDir, file));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!isGitListingUnavailableError(error)) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldSkipConflictScanPath(relativePath) {
|
|
70
|
+
const segments = String(relativePath || '')
|
|
71
|
+
.split(/[\\/]+/)
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
const directorySegments = segments.slice(0, -1);
|
|
74
|
+
return directorySegments.some(segment => (
|
|
75
|
+
(segment.startsWith('.') && !CONFLICT_MARKER_ALLOWED_DOT_DIRS.has(segment))
|
|
76
|
+
|| CONFLICT_MARKER_IGNORE_DIRS.has(segment)
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
23
80
|
function getExecOptions() {
|
|
24
81
|
return { encoding: 'utf8', cwd: process.cwd(), timeout: 120000 };
|
|
25
82
|
}
|
|
@@ -27,6 +84,15 @@ function getExecOptions() {
|
|
|
27
84
|
const ERROR_PATTERNS = {
|
|
28
85
|
COMMAND_NOT_FOUND: ['ENOENT', 'not found'],
|
|
29
86
|
NO_LOCK_FILE: ['requires an existing', 'package-lock'],
|
|
87
|
+
GIT_LISTING_UNAVAILABLE: [
|
|
88
|
+
'not a git repository',
|
|
89
|
+
'maxBuffer', // git ls-files output exceeds maxBuffer
|
|
90
|
+
'ENOBUFS', // OS-level buffer overflow
|
|
91
|
+
'ETIMEDOUT', // subprocess timeout
|
|
92
|
+
'EPERM', // permission denied
|
|
93
|
+
'EACCES', // access denied
|
|
94
|
+
'spawnSync', // general spawn failure
|
|
95
|
+
],
|
|
30
96
|
};
|
|
31
97
|
|
|
32
98
|
/**
|
|
@@ -39,6 +105,14 @@ function isCommandNotFound(error) {
|
|
|
39
105
|
);
|
|
40
106
|
}
|
|
41
107
|
|
|
108
|
+
function isGitListingUnavailableError(error) {
|
|
109
|
+
if (!error || typeof error.message !== 'string') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return isCommandNotFound(error)
|
|
113
|
+
|| ERROR_PATTERNS.GIT_LISTING_UNAVAILABLE.some(pattern => error.message.includes(pattern));
|
|
114
|
+
}
|
|
115
|
+
|
|
42
116
|
/**
|
|
43
117
|
* Parse number from regex match
|
|
44
118
|
* @private
|
|
@@ -57,6 +131,131 @@ function getCheckStatus(check) {
|
|
|
57
131
|
return check.success ? 'PASS' : 'FAIL';
|
|
58
132
|
}
|
|
59
133
|
|
|
134
|
+
function scanForConflictMarkers(rootDir = process.cwd()) {
|
|
135
|
+
const filesWithMarkers = [];
|
|
136
|
+
|
|
137
|
+
function inspectFile(absolutePath) {
|
|
138
|
+
const relativePath = path.relative(rootDir, absolutePath) || path.basename(absolutePath);
|
|
139
|
+
if (shouldSkipConflictScanPath(relativePath)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (!fs.lstatSync(absolutePath).isFile()) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!canSkipConflictScanStatError(error)) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const startLine = findConflictMarkerStartLine(readConflictScanFile(absolutePath));
|
|
155
|
+
if (startLine === null) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
filesWithMarkers.push({ path: relativePath, line: startLine });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function walk(currentDir) {
|
|
163
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
164
|
+
if (shouldSkipConflictScanEntry(entry)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (entry.isDirectory()) {
|
|
169
|
+
walk(path.join(currentDir, entry.name));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!entry.isFile()) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
inspectFile(path.join(currentDir, entry.name));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const trackedFiles = listConflictScanFiles(rootDir);
|
|
182
|
+
if (trackedFiles) {
|
|
183
|
+
for (const absolutePath of trackedFiles) {
|
|
184
|
+
inspectFile(absolutePath);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
walk(rootDir);
|
|
188
|
+
}
|
|
189
|
+
filesWithMarkers.sort((left, right) => {
|
|
190
|
+
if (left.path === right.path) {
|
|
191
|
+
return left.line - right.line;
|
|
192
|
+
}
|
|
193
|
+
return left.path.localeCompare(right.path);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
success: filesWithMarkers.length === 0,
|
|
198
|
+
files: filesWithMarkers,
|
|
199
|
+
message: filesWithMarkers.length === 0
|
|
200
|
+
? 'No conflict markers found'
|
|
201
|
+
: `Conflict markers found in ${filesWithMarkers.length} file(s)`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function shouldSkipConflictScanEntry(entry) {
|
|
206
|
+
return entry.isDirectory() && (
|
|
207
|
+
(entry.name.startsWith('.') && !CONFLICT_MARKER_ALLOWED_DOT_DIRS.has(entry.name))
|
|
208
|
+
|| CONFLICT_MARKER_IGNORE_DIRS.has(entry.name)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function canSkipConflictScanStatError(error) {
|
|
213
|
+
return Boolean(error && ['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readConflictScanFile(absolutePath) {
|
|
217
|
+
try {
|
|
218
|
+
return fs.readFileSync(absolutePath, 'utf8');
|
|
219
|
+
} catch (_error) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function findConflictMarkerStartLine(content) {
|
|
225
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
226
|
+
let activeStartLine = null;
|
|
227
|
+
let sawDivider = false;
|
|
228
|
+
|
|
229
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
230
|
+
const line = lines[index];
|
|
231
|
+
const lineNumber = index + 1;
|
|
232
|
+
if (activeStartLine === null) {
|
|
233
|
+
if (CONFLICT_START_PATTERN.test(line)) {
|
|
234
|
+
activeStartLine = lineNumber;
|
|
235
|
+
sawDivider = false;
|
|
236
|
+
} else if (CONFLICT_END_PATTERN.test(line)) {
|
|
237
|
+
return lineNumber;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!sawDivider) {
|
|
243
|
+
if (CONFLICT_DIVIDER_PATTERN.test(line)) {
|
|
244
|
+
sawDivider = true;
|
|
245
|
+
} else if (CONFLICT_START_PATTERN.test(line)) {
|
|
246
|
+
activeStartLine = index + 1;
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (CONFLICT_END_PATTERN.test(line)) {
|
|
252
|
+
return activeStartLine;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return activeStartLine;
|
|
257
|
+
}
|
|
258
|
+
|
|
60
259
|
/**
|
|
61
260
|
* Parse vulnerability counts from audit output
|
|
62
261
|
* @private
|
|
@@ -450,13 +649,29 @@ async function runAllTests() {
|
|
|
450
649
|
* console.log(result.summary);
|
|
451
650
|
*/
|
|
452
651
|
async function executeValidate(options = {}) { // NOSONAR S3776
|
|
453
|
-
const { skip = [], continueOnError = true } = options || {};
|
|
652
|
+
const { skip = [], continueOnError = true, rootDir = process.cwd() } = options || {};
|
|
454
653
|
|
|
455
654
|
const checks = {};
|
|
456
655
|
const failedChecks = [];
|
|
457
656
|
const errors = [];
|
|
458
657
|
const startTime = Date.now();
|
|
459
658
|
|
|
659
|
+
// 0. Conflict markers
|
|
660
|
+
if (!skip.includes(CHECK_TYPES.CONFLICT_MARKERS)) {
|
|
661
|
+
try {
|
|
662
|
+
checks.conflictMarkers = scanForConflictMarkers(rootDir);
|
|
663
|
+
if (!checks.conflictMarkers.success) {
|
|
664
|
+
failedChecks.push(CHECK_TYPES.CONFLICT_MARKERS);
|
|
665
|
+
if (!continueOnError) {
|
|
666
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
errors.push(`Conflict marker scan error: ${error.message}`);
|
|
671
|
+
checks.conflictMarkers = { success: false, message: error.message };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
460
675
|
// 1. Type checking
|
|
461
676
|
if (!skip.includes(CHECK_TYPES.TYPE_CHECK)) {
|
|
462
677
|
try {
|
|
@@ -535,6 +750,7 @@ function buildResult(checks, failedChecks, errors, startTime) {
|
|
|
535
750
|
// Build summary using getCheckStatus helper
|
|
536
751
|
const checkResults = [];
|
|
537
752
|
const checkLabels = {
|
|
753
|
+
conflictMarkers: 'Conflict Markers',
|
|
538
754
|
typeCheck: 'Type',
|
|
539
755
|
lint: 'Lint',
|
|
540
756
|
security: 'Security',
|
|
@@ -610,6 +826,7 @@ module.exports = {
|
|
|
610
826
|
runLint,
|
|
611
827
|
runSecurityScan,
|
|
612
828
|
runAllTests,
|
|
829
|
+
scanForConflictMarkers,
|
|
613
830
|
executeValidate,
|
|
614
831
|
executeDebugMode,
|
|
615
832
|
};
|
package/lib/setup-action-log.js
CHANGED
|
@@ -26,11 +26,11 @@ function capitalize(str) {
|
|
|
26
26
|
* @param {boolean} verbose - Whether to show file-by-file detail
|
|
27
27
|
* @returns {string} The formatted summary output
|
|
28
28
|
*/
|
|
29
|
-
function renderSetupSummary(actionLog, agentNames, verbose) {
|
|
29
|
+
function renderSetupSummary(actionLog, agentNames, verbose, options = {}) {
|
|
30
30
|
if (verbose) {
|
|
31
|
-
return renderVerbose(actionLog, agentNames);
|
|
31
|
+
return renderVerbose(actionLog, agentNames, options);
|
|
32
32
|
}
|
|
33
|
-
return renderDefault(actionLog, agentNames);
|
|
33
|
+
return renderDefault(actionLog, agentNames, options);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -40,10 +40,11 @@ function renderSetupSummary(actionLog, agentNames, verbose) {
|
|
|
40
40
|
* @param {string[]} agentNames
|
|
41
41
|
* @returns {string}
|
|
42
42
|
*/
|
|
43
|
-
function renderDefault(actionLog, agentNames) {
|
|
43
|
+
function renderDefault(actionLog, agentNames, options = {}) {
|
|
44
44
|
const agentCount = agentNames.length;
|
|
45
45
|
const agentLabel = agentCount === 1 ? '1 agent' : `${agentCount} agents`;
|
|
46
46
|
const agentList = agentNames.length > 0 ? ` (${agentNames.join(', ')})` : '';
|
|
47
|
+
const status = options.status === 'partial' ? 'partially complete' : 'complete';
|
|
47
48
|
|
|
48
49
|
const summary = actionLog.getSummary();
|
|
49
50
|
|
|
@@ -63,7 +64,7 @@ function renderDefault(actionLog, agentNames) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
const lines = [];
|
|
66
|
-
lines.push(`Forge setup
|
|
67
|
+
lines.push(`Forge setup ${status} — ${agentLabel} configured${agentList}`);
|
|
67
68
|
|
|
68
69
|
if (parts.length > 0) {
|
|
69
70
|
lines.push(` ${parts.join(' | ')}`);
|
|
@@ -83,10 +84,18 @@ function renderDefault(actionLog, agentNames) {
|
|
|
83
84
|
* @param {string[]} _agentNames - Not used in verbose (agents come from log data)
|
|
84
85
|
* @returns {string}
|
|
85
86
|
*/
|
|
86
|
-
function renderVerbose(actionLog, _agentNames) {
|
|
87
|
+
function renderVerbose(actionLog, _agentNames, options = {}) {
|
|
87
88
|
const agentSummary = actionLog.getAgentSummary();
|
|
89
|
+
const status = options.status === 'partial' ? 'partially complete' : 'complete';
|
|
88
90
|
const lines = [];
|
|
89
91
|
|
|
92
|
+
if (Object.keys(agentSummary).length === 0) {
|
|
93
|
+
return 'No file operations recorded.';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lines.push(`Forge setup ${status}`);
|
|
97
|
+
lines.push('');
|
|
98
|
+
|
|
90
99
|
for (const [agent, actions] of Object.entries(agentSummary)) {
|
|
91
100
|
for (const [action, files] of Object.entries(actions)) {
|
|
92
101
|
const fileCount = files.length;
|
|
@@ -95,11 +104,6 @@ function renderVerbose(actionLog, _agentNames) {
|
|
|
95
104
|
lines.push(`${agent}: ${fileList} (${fileLabel}) [${action}]`);
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
|
-
|
|
99
|
-
if (lines.length === 0) {
|
|
100
|
-
return 'No file operations recorded.';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
107
|
return lines.join('\n');
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
3
|
const { repairWorkflowRuntimeAssets } = require('../commands/setup');
|
|
7
4
|
const { checkRuntimeHealth } = require('../runtime-health');
|
|
8
5
|
const { normalizeStageId } = require('./stages');
|
|
@@ -11,8 +8,7 @@ const {
|
|
|
11
8
|
normalizeOverrideRecord,
|
|
12
9
|
readWorkflowState,
|
|
13
10
|
} = require('./state');
|
|
14
|
-
|
|
15
|
-
const WORKFLOW_STATE_FILENAME = '.forge-state.json';
|
|
11
|
+
const { loadState, WORKFLOW_STATE_FILENAME } = require('./state-manager');
|
|
16
12
|
|
|
17
13
|
function getOverrideInput(flags = {}) {
|
|
18
14
|
if (Object.hasOwn(flags, 'overrideStage')) {
|
|
@@ -67,6 +63,8 @@ function readWorkflowStateFile(projectRoot) {
|
|
|
67
63
|
return null;
|
|
68
64
|
}
|
|
69
65
|
|
|
66
|
+
const fs = require('node:fs');
|
|
67
|
+
const path = require('node:path');
|
|
70
68
|
const statePath = path.join(projectRoot, WORKFLOW_STATE_FILENAME);
|
|
71
69
|
if (!fs.existsSync(statePath)) {
|
|
72
70
|
return null;
|
|
@@ -76,11 +74,17 @@ function readWorkflowStateFile(projectRoot) {
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
function resolveWorkflowStateInput(workflowState, flags = {}, args = [], projectRoot) {
|
|
79
|
-
|
|
77
|
+
const inlineOrFlag = workflowState
|
|
80
78
|
|| flags.workflowState
|
|
81
79
|
|| flags['--workflow-state']
|
|
82
|
-
|| getCliFlagValue('--workflow-state', args)
|
|
83
|
-
|
|
80
|
+
|| getCliFlagValue('--workflow-state', args);
|
|
81
|
+
|
|
82
|
+
if (inlineOrFlag) {
|
|
83
|
+
return inlineOrFlag;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { state } = loadState(projectRoot);
|
|
87
|
+
return state;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
function readWorkflowStateInput(input) {
|