forge-workflow 0.0.5 → 0.0.7
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 +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.cursorrules +149 -0
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +211 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +18 -1
package/lib/commands/ship.js
CHANGED
|
@@ -367,6 +367,26 @@ async function executeShip(options) {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
module.exports = {
|
|
370
|
+
name: 'ship',
|
|
371
|
+
description: 'Create a pull request from validated feature work',
|
|
372
|
+
handler: async (args, flags = {}) => {
|
|
373
|
+
const result = await executeShip({
|
|
374
|
+
featureSlug: args[0],
|
|
375
|
+
title: args[1],
|
|
376
|
+
dryRun: Boolean(flags.dryRun || flags['--dry-run']),
|
|
377
|
+
});
|
|
378
|
+
if (!result.success) {
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const lines = [result.message];
|
|
383
|
+
if (result.prUrl) lines.push(`PR: ${result.prUrl}`);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
...result,
|
|
387
|
+
output: lines.join('\n'),
|
|
388
|
+
};
|
|
389
|
+
},
|
|
370
390
|
extractKeyDecisions,
|
|
371
391
|
extractTestScenarios,
|
|
372
392
|
getTestCoverage,
|
package/lib/commands/status.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* Detects workflow stage (1-9) with confidence scoring
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { secureExecFileSync } = require('../shell-utils');
|
|
7
|
+
const {
|
|
8
|
+
readWorkflowState,
|
|
9
|
+
getAllowedTransitionsForWorkflowState,
|
|
10
|
+
} = require('../workflow/state');
|
|
11
|
+
const { STAGE_IDS } = require('../workflow/stages');
|
|
12
|
+
|
|
6
13
|
const WORKFLOW_STAGES = {
|
|
7
14
|
1: { name: 'Fresh Start', nextCommand: 'research' },
|
|
8
15
|
2: { name: 'Research', nextCommand: 'research' },
|
|
@@ -15,6 +22,16 @@ const WORKFLOW_STAGES = {
|
|
|
15
22
|
9: { name: 'Verification', nextCommand: 'verify' },
|
|
16
23
|
};
|
|
17
24
|
|
|
25
|
+
const AUTHORITATIVE_STAGE_NAMES = {
|
|
26
|
+
plan: 'Planning',
|
|
27
|
+
dev: 'Development',
|
|
28
|
+
validate: 'Validation',
|
|
29
|
+
ship: 'Shipping',
|
|
30
|
+
review: 'Review',
|
|
31
|
+
premerge: 'Premerge',
|
|
32
|
+
verify: 'Verification',
|
|
33
|
+
};
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* Analyze branch state
|
|
20
37
|
* @param {string} branch - Current branch name
|
|
@@ -302,63 +319,175 @@ function detectStage(context) {
|
|
|
302
319
|
};
|
|
303
320
|
}
|
|
304
321
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
function parseStatusInputs(args = [], flags = {}) {
|
|
323
|
+
const statusArgs = Array.isArray(args) ? args : [];
|
|
324
|
+
const getNextValue = (flagName) => {
|
|
325
|
+
const index = statusArgs.indexOf(flagName);
|
|
326
|
+
if (index !== -1 && index + 1 < statusArgs.length) {
|
|
327
|
+
return statusArgs[index + 1];
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
};
|
|
331
|
+
const getInlineValue = (flagName) => {
|
|
332
|
+
const prefix = `${flagName}=`;
|
|
333
|
+
const match = statusArgs.find(arg => typeof arg === 'string' && arg.startsWith(prefix));
|
|
334
|
+
return match ? match.slice(prefix.length) : null;
|
|
335
|
+
};
|
|
312
336
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
return {
|
|
338
|
+
issueId: flags.issueId || flags['--issue-id'] || getInlineValue('--issue-id') || getNextValue('--issue-id'),
|
|
339
|
+
workflowState: flags.workflowState || flags['--workflow-state'] || getInlineValue('--workflow-state') || getNextValue('--workflow-state'),
|
|
340
|
+
bdComments: flags.bdComments || flags['--bd-comments'] || getInlineValue('--bd-comments') || getNextValue('--bd-comments'),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
318
343
|
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
if (result.factors.files.hasPlan) {
|
|
325
|
-
completed.push('✓ Plan created');
|
|
326
|
-
}
|
|
327
|
-
if (result.factors.branch.onFeatureBranch) {
|
|
328
|
-
completed.push('✓ Feature branch created');
|
|
344
|
+
function extractWorkflowStateFromComments(comments = '') {
|
|
345
|
+
const matches = String(comments).match(/^WorkflowState:\s*(\{.*\})$/gm);
|
|
346
|
+
if (!matches || matches.length === 0) {
|
|
347
|
+
return null;
|
|
329
348
|
}
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
332
357
|
}
|
|
333
|
-
|
|
334
|
-
|
|
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;
|
|
335
366
|
}
|
|
336
|
-
|
|
337
|
-
|
|
367
|
+
|
|
368
|
+
return extractWorkflowStateFromComments(comments);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function resolveWorkflowState(inputs) {
|
|
372
|
+
try {
|
|
373
|
+
const workflowState = inputs.workflowState
|
|
374
|
+
? readWorkflowState(inputs.workflowState)
|
|
375
|
+
: readWorkflowStateFromBeads(inputs.issueId, { comments: inputs.bdComments });
|
|
376
|
+
return { workflowState, fallbackReason: null };
|
|
377
|
+
} catch (error) {
|
|
378
|
+
return {
|
|
379
|
+
workflowState: null,
|
|
380
|
+
fallbackReason: error instanceof Error ? error.message : String(error),
|
|
381
|
+
};
|
|
338
382
|
}
|
|
339
|
-
|
|
340
|
-
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildAuthoritativeStatus(workflowState) {
|
|
386
|
+
const currentStage = workflowState.currentStage;
|
|
387
|
+
const stageIndex = STAGE_IDS.indexOf(currentStage);
|
|
388
|
+
const nextStages = getAllowedTransitionsForWorkflowState(workflowState);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
stage: stageIndex === -1 ? null : stageIndex + 1,
|
|
392
|
+
stageId: currentStage,
|
|
393
|
+
stageName: AUTHORITATIVE_STAGE_NAMES[currentStage] || currentStage,
|
|
394
|
+
confidence: 'high',
|
|
395
|
+
confidenceScore: 100,
|
|
396
|
+
runCommand: currentStage,
|
|
397
|
+
nextCommand: nextStages[0] || null,
|
|
398
|
+
nextStages,
|
|
399
|
+
authoritative: true,
|
|
400
|
+
workflowState,
|
|
401
|
+
factors: {
|
|
402
|
+
files: {
|
|
403
|
+
hasResearch: workflowState.completedStages.includes('plan'),
|
|
404
|
+
hasPlan: workflowState.completedStages.includes('plan') || currentStage !== 'plan',
|
|
405
|
+
testsPass: workflowState.completedStages.includes('validate'),
|
|
406
|
+
},
|
|
407
|
+
branch: {},
|
|
408
|
+
pr: {},
|
|
409
|
+
checks: { allChecksPass: workflowState.completedStages.includes('validate') },
|
|
410
|
+
beads: { hasActiveIssue: true },
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildMissingWorkflowStateStatus() {
|
|
416
|
+
return {
|
|
417
|
+
stage: null,
|
|
418
|
+
stageId: null,
|
|
419
|
+
stageName: 'Unknown',
|
|
420
|
+
confidence: 'low',
|
|
421
|
+
confidenceScore: 0,
|
|
422
|
+
authoritative: false,
|
|
423
|
+
missingWorkflowState: true,
|
|
424
|
+
output: '\nNo authoritative workflow state available.\nProvide --workflow-state or --issue-id so /status can read recorded stage state.\n',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildHeaderLines(title, detailLines = []) {
|
|
429
|
+
return ['', title, ...detailLines, ''];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildSectionLines(title, items = []) {
|
|
433
|
+
if (items.length === 0) {
|
|
434
|
+
return [];
|
|
341
435
|
}
|
|
342
|
-
|
|
343
|
-
|
|
436
|
+
|
|
437
|
+
return [title, ...items.map(item => ` ${item}`), ''];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function collectCompletedChecks(result) {
|
|
441
|
+
const completed = [];
|
|
442
|
+
|
|
443
|
+
if (result.factors.files.hasResearch) completed.push('Research doc exists');
|
|
444
|
+
if (result.factors.files.hasPlan) completed.push('Plan created');
|
|
445
|
+
if (result.factors.branch.onFeatureBranch) completed.push('Feature branch created');
|
|
446
|
+
if (result.factors.files.hasTests) completed.push('Tests written');
|
|
447
|
+
if (result.factors.files.testsPass) completed.push('Tests passing');
|
|
448
|
+
if (result.factors.checks.allChecksPass) completed.push('All checks passing');
|
|
449
|
+
if (result.factors.pr.hasPR) completed.push(`PR created (#${result.factors.pr.prNumber})`);
|
|
450
|
+
if (result.factors.pr.prApproved) completed.push('PR approved');
|
|
451
|
+
if (result.factors.pr.prMerged) completed.push('PR merged');
|
|
452
|
+
|
|
453
|
+
return completed;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function formatAuthoritativeStatus(result) {
|
|
457
|
+
const completedStages = result.workflowState.completedStages.map(stageId => stageId);
|
|
458
|
+
const allowedCommands = result.nextStages.map(stageId => '/' + stageId).join(', ');
|
|
459
|
+
const nextLines = [`Run now: /${result.runCommand}`];
|
|
460
|
+
|
|
461
|
+
if (result.nextCommand) {
|
|
462
|
+
nextLines.push(`Next after this: /${result.nextCommand}`);
|
|
344
463
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
464
|
+
if (allowedCommands) {
|
|
465
|
+
nextLines.push(`Allowed after this: ${allowedCommands}`);
|
|
347
466
|
}
|
|
348
467
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
468
|
+
return [
|
|
469
|
+
...buildHeaderLines(`Current Stage: ${result.stageId} - ${result.stageName}`, [
|
|
470
|
+
' Source: authoritative workflow state',
|
|
471
|
+
` Classification: ${result.workflowState.workflowDecisions.classification}`,
|
|
472
|
+
]),
|
|
473
|
+
...buildSectionLines('Completed stages:', completedStages),
|
|
474
|
+
...nextLines,
|
|
475
|
+
'',
|
|
476
|
+
].join('\n');
|
|
477
|
+
}
|
|
354
478
|
|
|
355
|
-
|
|
356
|
-
lines
|
|
357
|
-
|
|
479
|
+
function formatHeuristicStatus(result) {
|
|
480
|
+
const lines = [
|
|
481
|
+
...buildHeaderLines(`Current Stage: ${result.stage} - ${result.stageName}`, [
|
|
482
|
+
` Confidence: ${result.confidence.toUpperCase()} (${result.confidenceScore}%)`,
|
|
483
|
+
]),
|
|
484
|
+
...buildSectionLines('Completed:', collectCompletedChecks(result)),
|
|
485
|
+
`Next: /${result.nextCommand}`,
|
|
486
|
+
'',
|
|
487
|
+
];
|
|
358
488
|
|
|
359
|
-
// Low confidence warning
|
|
360
489
|
if (result.confidence === 'low') {
|
|
361
|
-
lines.push('
|
|
490
|
+
lines.push('Low confidence - Manual verification suggested');
|
|
362
491
|
lines.push(' Conflicting signals detected. Please verify current stage.');
|
|
363
492
|
lines.push('');
|
|
364
493
|
}
|
|
@@ -366,7 +495,44 @@ function formatStatus(result) {
|
|
|
366
495
|
return lines.join('\n');
|
|
367
496
|
}
|
|
368
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Format status output
|
|
500
|
+
* @param {object} result - Detection result
|
|
501
|
+
* @returns {string} Formatted output
|
|
502
|
+
*/
|
|
503
|
+
function formatStatus(result) {
|
|
504
|
+
if (result.authoritative && result.workflowState) {
|
|
505
|
+
return formatAuthoritativeStatus(result);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return formatHeuristicStatus(result);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
369
512
|
module.exports = {
|
|
513
|
+
name: 'status',
|
|
514
|
+
description: 'Intelligent stage detection with confidence scoring',
|
|
515
|
+
handler: async (args, flags, _projectRoot) => {
|
|
516
|
+
const inputs = parseStatusInputs(args, flags);
|
|
517
|
+
const { workflowState, fallbackReason } = resolveWorkflowState(inputs);
|
|
518
|
+
|
|
519
|
+
if (workflowState) {
|
|
520
|
+
const result = buildAuthoritativeStatus(workflowState);
|
|
521
|
+
return { success: true, output: formatStatus(result), ...result };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
success: true,
|
|
526
|
+
...buildMissingWorkflowStateStatus(),
|
|
527
|
+
...(fallbackReason ? { fallbackReason } : {}),
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
buildAuthoritativeStatus,
|
|
531
|
+
buildMissingWorkflowStateStatus,
|
|
532
|
+
extractWorkflowStateFromComments,
|
|
533
|
+
resolveWorkflowState,
|
|
534
|
+
parseStatusInputs,
|
|
535
|
+
readWorkflowStateFromBeads,
|
|
370
536
|
detectStage,
|
|
371
537
|
analyzeBranch,
|
|
372
538
|
analyzeFiles,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
function isRecoverableBeadsSyncError(error) {
|
|
6
|
+
const message = error?.message ?? String(error ?? '');
|
|
7
|
+
return (
|
|
8
|
+
message.includes('failed to open database') ||
|
|
9
|
+
message.includes('database not found') ||
|
|
10
|
+
message.includes('no beads configuration found')
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Forge Sync Command
|
|
16
|
+
* Syncs Beads issue data by running dolt pull + push.
|
|
17
|
+
* Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
|
|
18
|
+
*
|
|
19
|
+
* @module commands/sync
|
|
20
|
+
*/
|
|
21
|
+
module.exports = {
|
|
22
|
+
name: 'sync',
|
|
23
|
+
description: 'Sync Beads issue data (dolt pull + push)',
|
|
24
|
+
usage: 'forge sync',
|
|
25
|
+
flags: {},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run beads dolt pull + push to sync issue data.
|
|
29
|
+
* @param {string[]} _args - Positional arguments (unused)
|
|
30
|
+
* @param {object} _flags - CLI flags (unused)
|
|
31
|
+
* @param {string} _projectRoot - Project root path (unused)
|
|
32
|
+
* @param {object} [opts] - Options for dependency injection
|
|
33
|
+
* @param {Function} [opts._exec] - Override for execFileSync (testing)
|
|
34
|
+
* @returns {Promise<{success: boolean, synced: boolean, message?: string, error?: string}>}
|
|
35
|
+
*/
|
|
36
|
+
handler: async (_args, _flags, _projectRoot, opts = {}) => {
|
|
37
|
+
const exec = opts._exec || execFileSync;
|
|
38
|
+
|
|
39
|
+
// Step 1: Check if bd binary exists
|
|
40
|
+
try {
|
|
41
|
+
exec('bd', ['--version'], { stdio: 'pipe' });
|
|
42
|
+
} catch { /* intentional: bd not installed, skip sync gracefully */ // NOSONAR S2486
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
synced: false,
|
|
46
|
+
message: 'Beads not installed — nothing to sync',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Step 2: Run dolt pull + push
|
|
51
|
+
try {
|
|
52
|
+
exec('bd', ['dolt', 'pull'], { stdio: 'pipe' });
|
|
53
|
+
exec('bd', ['dolt', 'push'], { stdio: 'pipe' });
|
|
54
|
+
} catch (syncErr) {
|
|
55
|
+
if (isRecoverableBeadsSyncError(syncErr)) {
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
synced: false,
|
|
59
|
+
message: 'Beads is installed but not initialized for sync in this worktree — skipping sync',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
synced: false,
|
|
65
|
+
error: syncErr.message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { success: true, synced: true };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { existsSync } = require('node:fs');
|
|
2
|
+
const { execFileSync } = require('node:child_process');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
// Resolve bash binary from fixed, known locations only (no PATH search).
|
|
6
|
+
// SonarCloud S4036: PATH must not be searched for OS commands.
|
|
7
|
+
function _resolveBash() {
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
// Fixed locations for Git Bash on Windows (no PATH search)
|
|
10
|
+
const candidates = [
|
|
11
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
12
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
13
|
+
`${process.env.LOCALAPPDATA || ''}\\Programs\\Git\\bin\\bash.exe`,
|
|
14
|
+
];
|
|
15
|
+
for (const p of candidates) {
|
|
16
|
+
if (p && existsSync(p)) return p;
|
|
17
|
+
}
|
|
18
|
+
// Fallback: assume Git Bash is in PATH (less secure but functional)
|
|
19
|
+
return 'bash';
|
|
20
|
+
}
|
|
21
|
+
return '/usr/bin/bash';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleTeam(args) {
|
|
25
|
+
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'forge-team', 'index.sh');
|
|
26
|
+
const bashPath = _resolveBash();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
execFileSync(bashPath, [scriptPath, ...args], {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
process.exit(err.status || 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { handleTeam };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test Command — Smart Test Runner
|
|
5
|
+
*
|
|
6
|
+
* Wraps test execution with smart defaults:
|
|
7
|
+
* - Auto-detects package manager from lockfiles
|
|
8
|
+
* - Checks Beads (Dolt) connectivity, sets BEADS_SKIP_TESTS if unavailable
|
|
9
|
+
* - Supports --affected flag to run only tests for changed files
|
|
10
|
+
*
|
|
11
|
+
* Security: Uses execFileSync for subprocess calls (OWASP A03)
|
|
12
|
+
*
|
|
13
|
+
* @module commands/test
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execFileSync: defaultExecFileSync, spawnSync: defaultSpawnSync } = require('node:child_process');
|
|
17
|
+
const defaultFs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
/** @type {Array<[string, string]>} Lockfile → package manager mapping (order matters) */
|
|
21
|
+
const LOCKFILE_MAP = [
|
|
22
|
+
['bun.lockb', 'bun'],
|
|
23
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
24
|
+
['yarn.lock', 'yarn'],
|
|
25
|
+
['package-lock.json', 'npm'],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIMEOUT = 120000;
|
|
29
|
+
const BEADS_CHECK_TIMEOUT = 3000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect the package manager by checking which lockfile exists.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
35
|
+
* @param {Object} fs - Injected fs module
|
|
36
|
+
* @returns {string} Package manager command ('bun' | 'pnpm' | 'yarn' | 'npm')
|
|
37
|
+
*/
|
|
38
|
+
function detectPackageManager(projectRoot, fs) {
|
|
39
|
+
for (const [lockfile, manager] of LOCKFILE_MAP) {
|
|
40
|
+
if (fs.existsSync(path.join(projectRoot, lockfile))) {
|
|
41
|
+
return manager;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return 'npm';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if Beads (bd CLI) is reachable.
|
|
49
|
+
*
|
|
50
|
+
* @param {Function} execFileSync - Injected execFileSync
|
|
51
|
+
* @returns {boolean} true if bd is available
|
|
52
|
+
*/
|
|
53
|
+
function checkBeadsConnectivity(execFileSync) {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync('bd', ['list', '--limit=1'], { timeout: BEADS_CHECK_TIMEOUT });
|
|
56
|
+
return true;
|
|
57
|
+
} catch (_e) { // NOSONAR S2486
|
|
58
|
+
/* intentional: bd not installed or unreachable */
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
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
|
|
76
|
+
let baseBranch = 'main';
|
|
77
|
+
try {
|
|
78
|
+
baseBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'origin/HEAD'], {
|
|
79
|
+
encoding: 'utf8', timeout: 3000,
|
|
80
|
+
}).trim().replace('origin/', '');
|
|
81
|
+
} catch (_e) { // NOSONAR S2486
|
|
82
|
+
/* intentional: origin/HEAD not set, detect base branch manually */
|
|
83
|
+
for (const name of ['main', 'master']) {
|
|
84
|
+
try {
|
|
85
|
+
execFileSync('git', ['rev-parse', '--verify', name], { stdio: 'pipe', timeout: 3000 });
|
|
86
|
+
baseBranch = name;
|
|
87
|
+
break;
|
|
88
|
+
} catch (_e2) { /* intentional: branch doesn't exist, try next name */ } // NOSONAR S2486
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const mergeBase = execFileSync('git', ['merge-base', 'HEAD', baseBranch], {
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
timeout: 5000,
|
|
96
|
+
}).trim();
|
|
97
|
+
diffRef = `${mergeBase}...HEAD`;
|
|
98
|
+
} catch (_e) { /* intentional: merge-base failed, fallback to diff against HEAD */ // NOSONAR S2486
|
|
99
|
+
diffRef = 'HEAD';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let output;
|
|
103
|
+
try {
|
|
104
|
+
output = execFileSync('git', ['diff', '--name-only', diffRef], {
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
timeout: 5000,
|
|
107
|
+
}).trim();
|
|
108
|
+
} catch (_e) { // NOSONAR S2486
|
|
109
|
+
/* intentional: git diff failed, no affected files to report */
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!output) return [];
|
|
114
|
+
|
|
115
|
+
const changedFiles = output.split('\n').filter(Boolean);
|
|
116
|
+
|
|
117
|
+
// Map lib/*.js files to test/*.test.js
|
|
118
|
+
const testFiles = [];
|
|
119
|
+
for (const file of changedFiles) {
|
|
120
|
+
if (file.startsWith('lib/') && file.endsWith('.js')) {
|
|
121
|
+
const relative = file.slice('lib/'.length);
|
|
122
|
+
const testFile = `test/${relative.replace(/\.js$/, '.test.js')}`;
|
|
123
|
+
testFiles.push(testFile);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return testFiles;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
name: 'test',
|
|
132
|
+
description: 'Run tests with smart defaults (timeout, Beads skip, affected-only)',
|
|
133
|
+
usage: 'forge test [--affected]',
|
|
134
|
+
flags: {
|
|
135
|
+
'--affected': 'Run only tests for changed files',
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run tests with smart defaults.
|
|
140
|
+
*
|
|
141
|
+
* @param {string[]} _args - Positional arguments (unused)
|
|
142
|
+
* @param {Object} flags - Parsed flags ({ affected?: boolean })
|
|
143
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
144
|
+
* @param {Object} [deps] - Dependency injection for testability
|
|
145
|
+
* @param {Object} [deps.fs] - fs module
|
|
146
|
+
* @param {Function} [deps.execFileSync] - child_process.execFileSync
|
|
147
|
+
* @param {Function} [deps.spawnSync] - child_process.spawnSync
|
|
148
|
+
* @returns {Promise<{ success: boolean, exitCode: number, beadsSkipped: boolean }>}
|
|
149
|
+
*/
|
|
150
|
+
async handler(_args, flags, projectRoot, deps = {}) {
|
|
151
|
+
const fs = deps.fs || defaultFs;
|
|
152
|
+
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
153
|
+
const spawnSync = deps.spawnSync || defaultSpawnSync;
|
|
154
|
+
|
|
155
|
+
// 1. Detect package manager
|
|
156
|
+
const pkgManager = detectPackageManager(projectRoot, fs);
|
|
157
|
+
|
|
158
|
+
// 2. Read timeout from package.json or use default
|
|
159
|
+
let timeout = DEFAULT_TIMEOUT;
|
|
160
|
+
try {
|
|
161
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
163
|
+
const testScript = pkg.scripts?.test || '';
|
|
164
|
+
const timeoutMatch = testScript.match(/--timeout\s+(\d+)/);
|
|
165
|
+
if (timeoutMatch) {
|
|
166
|
+
timeout = Number.parseInt(timeoutMatch[1], 10);
|
|
167
|
+
}
|
|
168
|
+
} catch (_e) { /* intentional: package.json missing or unreadable, use default timeout */ } // NOSONAR S2486
|
|
169
|
+
|
|
170
|
+
// 3. Check Beads connectivity
|
|
171
|
+
const beadsAvailable = checkBeadsConnectivity(execFileSync);
|
|
172
|
+
const beadsSkipped = !beadsAvailable;
|
|
173
|
+
|
|
174
|
+
const extraEnv = {};
|
|
175
|
+
if (beadsSkipped) {
|
|
176
|
+
extraEnv.BEADS_SKIP_TESTS = '1';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 4. Build test command args
|
|
180
|
+
let testArgs = ['run', 'test'];
|
|
181
|
+
|
|
182
|
+
// 5. --affected flag: find changed test files
|
|
183
|
+
if (flags['--affected'] || flags.affected) {
|
|
184
|
+
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync);
|
|
185
|
+
if (affectedTests.length > 0) {
|
|
186
|
+
testArgs = ['run', 'test', ...affectedTests];
|
|
187
|
+
}
|
|
188
|
+
// If no affected tests found, fall back to running all tests
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 6. Run tests
|
|
192
|
+
const result = spawnSync(pkgManager, testArgs, {
|
|
193
|
+
env: { ...process.env, ...extraEnv },
|
|
194
|
+
timeout,
|
|
195
|
+
stdio: 'inherit',
|
|
196
|
+
shell: process.platform === 'win32',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const exitCode = result.status ?? 1;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
success: exitCode === 0,
|
|
203
|
+
exitCode,
|
|
204
|
+
beadsSkipped,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
};
|
package/lib/commands/validate.js
CHANGED
|
@@ -593,6 +593,19 @@ function executeDebugMode({ fixAttempts = 0, claim } = {}) {
|
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
module.exports = {
|
|
596
|
+
name: 'validate',
|
|
597
|
+
description: 'Run validation checks across type, lint, security, and tests',
|
|
598
|
+
handler: async () => {
|
|
599
|
+
const result = await executeValidate();
|
|
600
|
+
if (!result.success) {
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
...result,
|
|
606
|
+
output: result.summary,
|
|
607
|
+
};
|
|
608
|
+
},
|
|
596
609
|
runTypeCheck,
|
|
597
610
|
runLint,
|
|
598
611
|
runSecurityScan,
|