forge-workflow 0.0.9 → 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/ship.md +2 -2
- package/.claude/commands/status.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/ship.md +2 -2
- 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/ship/SKILL.md +2 -2
- 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/ship.md +2 -2
- 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/ship.prompt.md +2 -2
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +43 -10
- package/.github/workflows/github-to-beads.yml +10 -7
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/ship.md +2 -2
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/ship.md +2 -2
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/ship.md +2 -2
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +1 -0
- package/CLAUDE.md +12 -0
- package/bin/forge.js +10 -5
- package/docs/BEADS_GITHUB_SYNC.md +26 -0
- package/docs/TOOLCHAIN.md +130 -148
- 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/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +16 -10
- package/lib/commands/status.js +181 -0
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +37 -2
- package/lib/commands/validate.js +14 -8
- 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/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 +3 -1
- package/lib/workflow/state-manager.js +164 -8
- 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 -232
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- 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/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 +74 -329
- package/scripts/sync-agentic-workflow.js +48 -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 +219 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
|
@@ -116,4 +116,25 @@ function handleRecommend(flags, projectPath) {
|
|
|
116
116
|
return { budgetMode, recommendations };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
async function handler(_args = [], flags = {}, projectRoot) {
|
|
120
|
+
const result = handleRecommend(flags, projectRoot);
|
|
121
|
+
if (result.error) {
|
|
122
|
+
return { success: false, error: result.error };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const output = [
|
|
126
|
+
'',
|
|
127
|
+
`🔧 Forge Tool Recommendations (Budget: ${result.budgetMode})`,
|
|
128
|
+
formatRecommendations(result.recommendations),
|
|
129
|
+
].join('\n');
|
|
130
|
+
|
|
131
|
+
return { success: true, output };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
name: 'recommend',
|
|
136
|
+
description: 'Show tool recommendations for the current project',
|
|
137
|
+
handler,
|
|
138
|
+
formatRecommendations,
|
|
139
|
+
handleRecommend
|
|
140
|
+
};
|
package/lib/commands/setup.js
CHANGED
|
@@ -36,7 +36,7 @@ const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require(path.join(packag
|
|
|
36
36
|
const beadsSetupLib = require(path.join(packageDir, 'lib', 'beads-setup'));
|
|
37
37
|
const { beadsHealthCheck } = require(path.join(packageDir, 'lib', 'beads-health-check'));
|
|
38
38
|
const { setupPAT } = require(path.join(packageDir, 'lib', 'pat-setup'));
|
|
39
|
-
const {
|
|
39
|
+
const { DEFAULT_BEADS_VERSION, detectDefaultBranch, templateWorkflows, scaffoldBeadsSync } = require(path.join(packageDir, 'lib', 'beads-sync-scaffold'));
|
|
40
40
|
|
|
41
41
|
// Load incremental setup modules
|
|
42
42
|
const { detectEnvironment } = require('../detect-agent');
|
|
@@ -2734,13 +2734,15 @@ function installGitHooks() { // NOSONAR — Extracted as-is from bin/forge.js; c
|
|
|
2734
2734
|
execFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2735
2735
|
console.log(' ✓ Lefthook hooks installed (global)');
|
|
2736
2736
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2737
|
+
} catch (err) {
|
|
2738
|
+
console.warn('Lefthook installation failed:', err.message);
|
|
2739
|
+
console.warn(' ⚠ Lefthook hooks were not installed; raw git push remains unsafe in this worktree.');
|
|
2740
|
+
console.log(' ℹ Lefthook not found. Install it:');
|
|
2741
|
+
console.log(' bun add -d lefthook (recommended)');
|
|
2742
|
+
console.log(' OR: bun add -g lefthook (global)');
|
|
2743
|
+
console.log(' Then run: bunx lefthook install');
|
|
2744
|
+
console.log(` Run ${PKG_MANAGER} install in this worktree, then rerun setup.`);
|
|
2745
|
+
}
|
|
2744
2746
|
|
|
2745
2747
|
console.log('');
|
|
2746
2748
|
|
|
@@ -2785,7 +2787,9 @@ function repairDeclaredLefthookDependency(selectedAgents) {
|
|
|
2785
2787
|
return { attempted: true, repaired: true };
|
|
2786
2788
|
} catch (err) {
|
|
2787
2789
|
console.warn('Lefthook install failed:', err.message);
|
|
2790
|
+
console.warn(' ⚠ Lefthook repair failed; raw git push remains unsafe in this worktree.');
|
|
2788
2791
|
console.log(` ⚠ ${status.message}`);
|
|
2792
|
+
console.log(` Run ${PKG_MANAGER} install in this worktree, then rerun setup.`);
|
|
2789
2793
|
console.log('');
|
|
2790
2794
|
return { attempted: true, repaired: false, error: err };
|
|
2791
2795
|
}
|
|
@@ -3314,7 +3318,9 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3314
3318
|
console.log(' ✓ Lefthook binary restored');
|
|
3315
3319
|
} catch (err) {
|
|
3316
3320
|
console.warn('Lefthook install failed:', err.message);
|
|
3321
|
+
console.warn(' ⚠ Lefthook repair failed; raw git push remains unsafe in this worktree.');
|
|
3317
3322
|
console.log(` ⚠ ${status.message}`);
|
|
3323
|
+
console.log(` Run ${PKG_MANAGER} install in this worktree, then rerun setup.`);
|
|
3318
3324
|
}
|
|
3319
3325
|
console.log('');
|
|
3320
3326
|
return;
|
|
@@ -4087,9 +4093,9 @@ async function handleSyncScaffold() {
|
|
|
4087
4093
|
console.log(` Skipped: ${f} (already exists)`);
|
|
4088
4094
|
}
|
|
4089
4095
|
|
|
4090
|
-
// Detect default branch and
|
|
4096
|
+
// Detect default branch and pin forge-managed workflows to the repo baseline Beads version.
|
|
4091
4097
|
const branch = detectDefaultBranch(projectRoot);
|
|
4092
|
-
const beadsVersion =
|
|
4098
|
+
const beadsVersion = DEFAULT_BEADS_VERSION;
|
|
4093
4099
|
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
4094
4100
|
templateWorkflows(workflowDir, branch, beadsVersion, result.filesCreated || []);
|
|
4095
4101
|
console.log(` Branch: ${branch}, Beads version: ${beadsVersion}`);
|
package/lib/commands/status.js
CHANGED
|
@@ -8,11 +8,15 @@ const {
|
|
|
8
8
|
getAllowedTransitionsForWorkflowState,
|
|
9
9
|
} = require('../workflow/state');
|
|
10
10
|
const { STAGE_IDS } = require('../workflow/stages');
|
|
11
|
+
const { detectWorktree } = require('../detect-worktree');
|
|
12
|
+
const { readBeadsSnapshot } = require('../status/beads-snapshot');
|
|
13
|
+
const { formatZeroArgStatus } = require('../status/presenter');
|
|
11
14
|
const {
|
|
12
15
|
loadState,
|
|
13
16
|
extractWorkflowStateFromComments,
|
|
14
17
|
readWorkflowStateFromBeads,
|
|
15
18
|
} = require('../workflow/state-manager');
|
|
19
|
+
const { secureExecFileSync } = require('../shell-utils.js');
|
|
16
20
|
|
|
17
21
|
const WORKFLOW_STAGES = {
|
|
18
22
|
1: { name: 'Fresh Start', nextCommand: 'research' },
|
|
@@ -346,6 +350,138 @@ function parseStatusInputs(args = [], flags = {}) {
|
|
|
346
350
|
};
|
|
347
351
|
}
|
|
348
352
|
|
|
353
|
+
function runGitCommand(projectRoot, args) {
|
|
354
|
+
if (!projectRoot) {
|
|
355
|
+
return '';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
return secureExecFileSync('git', args, {
|
|
360
|
+
encoding: 'utf8',
|
|
361
|
+
cwd: projectRoot,
|
|
362
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
363
|
+
}).trim();
|
|
364
|
+
} catch (_error) {
|
|
365
|
+
return '';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function detectRepoContext(projectRoot = process.cwd()) {
|
|
370
|
+
const worktree = detectWorktree(projectRoot);
|
|
371
|
+
const branch = worktree.branch || runGitCommand(projectRoot, ['branch', '--show-current']) || 'unknown';
|
|
372
|
+
const statusOutput = runGitCommand(projectRoot, ['status', '--short']);
|
|
373
|
+
const changedEntries = statusOutput ? statusOutput.split(/\r?\n/).filter(Boolean) : [];
|
|
374
|
+
const clean = changedEntries.length === 0;
|
|
375
|
+
const summary = clean ? 'clean' : `${changedEntries.length} uncommitted change${changedEntries.length === 1 ? '' : 's'}`;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
branch,
|
|
379
|
+
inWorktree: worktree.inWorktree === true,
|
|
380
|
+
worktreePath: worktree.currentWorktree || projectRoot,
|
|
381
|
+
mainWorktree: worktree.mainWorktree || projectRoot,
|
|
382
|
+
workingTree: {
|
|
383
|
+
clean,
|
|
384
|
+
summary,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readIssueDetails(issueId, projectRoot = process.cwd()) {
|
|
390
|
+
if (!issueId) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const raw = secureExecFileSync('bd', ['show', issueId, '--json'], {
|
|
396
|
+
encoding: 'utf8',
|
|
397
|
+
cwd: projectRoot,
|
|
398
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
399
|
+
}).trim();
|
|
400
|
+
if (!raw) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const parsed = JSON.parse(raw);
|
|
405
|
+
return Array.isArray(parsed) ? parsed[0] || null : parsed;
|
|
406
|
+
} catch (_error) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function extractDesignSlugs(design) {
|
|
412
|
+
if (typeof design !== 'string' || design.trim() === '') {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const slugs = new Set();
|
|
417
|
+
const pattern = /docs[\\/]+plans[\\/]+\d{4}-\d{2}-\d{2}-([a-z0-9-]+?)-(?:design|tasks|decisions)\.md/g;
|
|
418
|
+
let match = pattern.exec(design);
|
|
419
|
+
while (match) {
|
|
420
|
+
slugs.add(match[1]);
|
|
421
|
+
match = pattern.exec(design);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return [...slugs];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function designMatchesFeatureSlug(issue, featureSlug) {
|
|
428
|
+
if (!issue || !featureSlug) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return extractDesignSlugs(issue.design).includes(featureSlug);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function discoverCurrentIssue(snapshot, context, projectRoot = process.cwd()) {
|
|
436
|
+
const activeAssigned = snapshot.activeAssigned || [];
|
|
437
|
+
if (activeAssigned.length === 0) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const branchInfo = analyzeBranch(context.branch || 'master');
|
|
442
|
+
const hydratedIssues = new Map();
|
|
443
|
+
|
|
444
|
+
function hydrateIssue(issue) {
|
|
445
|
+
if (!issue || !issue.id) {
|
|
446
|
+
return issue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (hydratedIssues.has(issue.id)) {
|
|
450
|
+
return hydratedIssues.get(issue.id);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const details = issue.design && issue.comments ? issue : readIssueDetails(issue.id, projectRoot);
|
|
454
|
+
const hydratedIssue = details ? { ...issue, ...details } : issue;
|
|
455
|
+
hydratedIssues.set(issue.id, hydratedIssue);
|
|
456
|
+
return hydratedIssue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (branchInfo.featureSlug) {
|
|
460
|
+
const slugMatches = [];
|
|
461
|
+
|
|
462
|
+
for (const issue of activeAssigned) {
|
|
463
|
+
const candidate = issue.design ? issue : hydrateIssue(issue);
|
|
464
|
+
if (designMatchesFeatureSlug(candidate, branchInfo.featureSlug)) {
|
|
465
|
+
slugMatches.push(candidate);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (slugMatches.length === 1) {
|
|
470
|
+
return hydrateIssue(slugMatches[0]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (slugMatches.length > 1) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (activeAssigned.length === 1) {
|
|
479
|
+
return hydrateIssue(activeAssigned[0]);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
349
485
|
function resolveWorkflowState(inputs) {
|
|
350
486
|
try {
|
|
351
487
|
if (inputs.workflowState) {
|
|
@@ -354,7 +490,9 @@ function resolveWorkflowState(inputs) {
|
|
|
354
490
|
|
|
355
491
|
const { state } = loadState(inputs.projectRoot, {
|
|
356
492
|
issueId: inputs.issueId,
|
|
493
|
+
issue: inputs.issue,
|
|
357
494
|
comments: inputs.bdComments,
|
|
495
|
+
preferBeads: inputs.preferBeads,
|
|
358
496
|
});
|
|
359
497
|
|
|
360
498
|
return { workflowState: state, fallbackReason: null };
|
|
@@ -498,11 +636,51 @@ module.exports = {
|
|
|
498
636
|
description: 'Intelligent stage detection with confidence scoring',
|
|
499
637
|
handler: async (args, flags, projectRoot) => {
|
|
500
638
|
const inputs = parseStatusInputs(args, flags);
|
|
639
|
+
const isZeroArgStatus = !inputs.issueId && !inputs.workflowState && !inputs.bdComments;
|
|
640
|
+
inputs.preferBeads = !isZeroArgStatus && Boolean(inputs.issueId || inputs.bdComments);
|
|
501
641
|
if (!inputs.projectRoot && projectRoot) {
|
|
502
642
|
inputs.projectRoot = projectRoot;
|
|
503
643
|
}
|
|
644
|
+
const effectiveProjectRoot = inputs.projectRoot || process.cwd();
|
|
645
|
+
let context = null;
|
|
646
|
+
let snapshot = null;
|
|
647
|
+
|
|
648
|
+
if (isZeroArgStatus) {
|
|
649
|
+
context = detectRepoContext(effectiveProjectRoot);
|
|
650
|
+
snapshot = readBeadsSnapshot(effectiveProjectRoot);
|
|
651
|
+
const discoveredIssue = discoverCurrentIssue(snapshot, context, effectiveProjectRoot);
|
|
652
|
+
if (discoveredIssue) {
|
|
653
|
+
inputs.issueId = discoveredIssue.id;
|
|
654
|
+
inputs.issue = discoveredIssue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
504
658
|
const { workflowState, fallbackReason } = resolveWorkflowState(inputs);
|
|
505
659
|
|
|
660
|
+
if (isZeroArgStatus) {
|
|
661
|
+
if (workflowState) {
|
|
662
|
+
const result = buildAuthoritativeStatus(workflowState);
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
context,
|
|
666
|
+
snapshot,
|
|
667
|
+
issueId: inputs.issueId,
|
|
668
|
+
output: formatZeroArgStatus({ context, snapshot, workflowResult: result }),
|
|
669
|
+
...result,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
success: true,
|
|
675
|
+
context,
|
|
676
|
+
snapshot,
|
|
677
|
+
issueId: inputs.issueId,
|
|
678
|
+
missingWorkflowState: true,
|
|
679
|
+
output: formatZeroArgStatus({ context, snapshot }),
|
|
680
|
+
...(fallbackReason ? { fallbackReason } : {}),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
506
684
|
if (workflowState) {
|
|
507
685
|
const result = buildAuthoritativeStatus(workflowState);
|
|
508
686
|
return { success: true, output: formatStatus(result), ...result };
|
|
@@ -521,6 +699,8 @@ module.exports = {
|
|
|
521
699
|
parseStatusInputs,
|
|
522
700
|
readWorkflowStateFromBeads,
|
|
523
701
|
detectStage,
|
|
702
|
+
detectRepoContext,
|
|
703
|
+
discoverCurrentIssue,
|
|
524
704
|
analyzeBranch,
|
|
525
705
|
analyzeFiles,
|
|
526
706
|
analyzePR,
|
|
@@ -528,4 +708,5 @@ module.exports = {
|
|
|
528
708
|
analyzeBeads,
|
|
529
709
|
calculateConfidence,
|
|
530
710
|
formatStatus,
|
|
711
|
+
extractDesignSlugs,
|
|
531
712
|
};
|
package/lib/commands/team.js
CHANGED
|
@@ -34,4 +34,14 @@ function handleTeam(args) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
async function handler(args = []) {
|
|
38
|
+
handleTeam(args);
|
|
39
|
+
return { success: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
name: 'team',
|
|
44
|
+
description: 'Team coordination workflows and sync utilities',
|
|
45
|
+
handler,
|
|
46
|
+
handleTeam
|
|
47
|
+
};
|
package/lib/commands/test.js
CHANGED
|
@@ -152,6 +152,18 @@ function getAffectedTestFiles(projectRoot, execFileSync, fs = defaultFs, options
|
|
|
152
152
|
function getTestCandidatesForChangedFile(file) {
|
|
153
153
|
if (!file) return [];
|
|
154
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
|
+
|
|
155
167
|
if (file.startsWith('test/') && file.endsWith('.test.js')) {
|
|
156
168
|
return [file];
|
|
157
169
|
}
|
|
@@ -161,8 +173,8 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
161
173
|
return [`test/${relative.replace(/\.js$/, '.test.js')}`];
|
|
162
174
|
}
|
|
163
175
|
|
|
164
|
-
if (file.startsWith('scripts/') && file.endsWith('.js')) {
|
|
165
|
-
const relative = file.slice('scripts/'.length).replace(/\.js$/, '.test.js');
|
|
176
|
+
if (file.startsWith('scripts/') && (file.endsWith('.js') || file.endsWith('.sh'))) {
|
|
177
|
+
const relative = file.slice('scripts/'.length).replace(/\.(js|sh)$/, '.test.js');
|
|
166
178
|
return [
|
|
167
179
|
`test/scripts/${relative}`,
|
|
168
180
|
`test/${relative}`,
|
|
@@ -177,6 +189,13 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
177
189
|
];
|
|
178
190
|
}
|
|
179
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
|
+
|
|
180
199
|
if (file.startsWith('.claude/commands/') && file.endsWith('.md')) {
|
|
181
200
|
return [
|
|
182
201
|
'test/command-sync-check.test.js',
|
|
@@ -184,6 +203,22 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
184
203
|
];
|
|
185
204
|
}
|
|
186
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
|
+
|
|
187
222
|
return [];
|
|
188
223
|
}
|
|
189
224
|
|
package/lib/commands/validate.js
CHANGED
|
@@ -46,6 +46,12 @@ const CONFLICT_START_PATTERN = /^<<<<<<<(?: .*)?$/;
|
|
|
46
46
|
const CONFLICT_DIVIDER_PATTERN = /^=======$/;
|
|
47
47
|
const CONFLICT_END_PATTERN = /^>>>>>>>.*$/;
|
|
48
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
|
+
}
|
|
49
55
|
|
|
50
56
|
function listConflictScanFiles(rootDir) {
|
|
51
57
|
try {
|
|
@@ -78,7 +84,7 @@ function shouldSkipConflictScanPath(relativePath) {
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
function getExecOptions() {
|
|
81
|
-
return { encoding: 'utf8', cwd: process.cwd(), timeout:
|
|
87
|
+
return { encoding: 'utf8', cwd: process.cwd(), timeout: VALIDATION_COMMAND_TIMEOUT_MS };
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
const ERROR_PATTERNS = {
|
|
@@ -308,7 +314,7 @@ async function runTypeCheck() {
|
|
|
308
314
|
return {
|
|
309
315
|
success: false,
|
|
310
316
|
duration: Date.now() - startTime,
|
|
311
|
-
message: 'Type check
|
|
317
|
+
message: validationTimeoutMessage('Type check'),
|
|
312
318
|
};
|
|
313
319
|
}
|
|
314
320
|
|
|
@@ -365,7 +371,7 @@ async function runLint() {
|
|
|
365
371
|
return {
|
|
366
372
|
success: false,
|
|
367
373
|
duration: Date.now() - startTime,
|
|
368
|
-
message: 'Lint check
|
|
374
|
+
message: validationTimeoutMessage('Lint check'),
|
|
369
375
|
};
|
|
370
376
|
}
|
|
371
377
|
|
|
@@ -437,7 +443,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
437
443
|
return {
|
|
438
444
|
success: false,
|
|
439
445
|
duration: Date.now() - startTime,
|
|
440
|
-
message: 'Security audit
|
|
446
|
+
message: validationTimeoutMessage('Security audit'),
|
|
441
447
|
};
|
|
442
448
|
}
|
|
443
449
|
// bun audit exits non-zero when vulnerabilities are found; stdout still contains the data
|
|
@@ -466,7 +472,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
466
472
|
return {
|
|
467
473
|
success: false,
|
|
468
474
|
duration: Date.now() - startTime,
|
|
469
|
-
message: 'Security audit
|
|
475
|
+
message: validationTimeoutMessage('Security audit'),
|
|
470
476
|
};
|
|
471
477
|
}
|
|
472
478
|
// npm audit exits non-zero when it finds any vulnerability - stdout still has JSON
|
|
@@ -521,7 +527,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
521
527
|
return {
|
|
522
528
|
success: false,
|
|
523
529
|
duration: Date.now() - startTime,
|
|
524
|
-
message: 'Security audit
|
|
530
|
+
message: validationTimeoutMessage('Security audit'),
|
|
525
531
|
};
|
|
526
532
|
}
|
|
527
533
|
|
|
@@ -564,7 +570,7 @@ async function runAllTests() {
|
|
|
564
570
|
const startTime = Date.now();
|
|
565
571
|
|
|
566
572
|
try {
|
|
567
|
-
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
|
|
568
574
|
|
|
569
575
|
// Parse bun test output
|
|
570
576
|
const passed = parseNumber(/(\d+) pass/.exec(result)); // NOSONAR S5852 - bounded \d+ pattern
|
|
@@ -591,7 +597,7 @@ async function runAllTests() {
|
|
|
591
597
|
passed: 0,
|
|
592
598
|
failed: 0,
|
|
593
599
|
total: 0,
|
|
594
|
-
message: 'Test execution
|
|
600
|
+
message: validationTimeoutMessage('Test execution'),
|
|
595
601
|
};
|
|
596
602
|
}
|
|
597
603
|
|
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);
|