forge-workflow 0.0.6 → 0.0.8
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/.cursorrules +149 -0
- package/bin/forge.js +43 -3
- 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 +127 -79
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_issue.js +172 -0
- package/lib/commands/_registry.js +40 -1
- package/lib/commands/claim.js +5 -0
- package/lib/commands/close.js +5 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/create.js +5 -0
- package/lib/commands/dev.js +26 -0
- package/lib/commands/issue.js +5 -0
- package/lib/commands/list.js +5 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/ready.js +5 -0
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/show.js +5 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +19 -1
- package/lib/commands/update.js +5 -0
- package/lib/commands/validate.js +13 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/frontmatter.js +79 -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/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -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 +15 -5
- package/scripts/beads-context.sh +12 -4
- package/scripts/check-agents.js +103 -0
- package/scripts/lib/eval-runner.js +50 -0
- package/scripts/pr-coordinator.sh +71 -21
- package/scripts/smart-status.sh +21 -11
- package/scripts/sync-commands.js +49 -20
- package/scripts/test.js +16 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module commands-reset
|
|
5
|
+
*
|
|
6
|
+
* Reset agent command files to match the canonical source (commands/*.md or .claude/commands/*.md).
|
|
7
|
+
* Rebuilds adapted files via sync-commands and optionally writes them.
|
|
8
|
+
*
|
|
9
|
+
* Used by `forge commands reset [--dry-run] [--all] [command-name]`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const {
|
|
15
|
+
syncCommands,
|
|
16
|
+
contentHash,
|
|
17
|
+
resolveCanonicalCommandsDir,
|
|
18
|
+
writeSyncManifest,
|
|
19
|
+
} = require('../../scripts/sync-commands.js');
|
|
20
|
+
|
|
21
|
+
function validateCommandName(commandName) {
|
|
22
|
+
if (commandName && !/^[a-z0-9-]+$/.test(commandName)) {
|
|
23
|
+
return [`Invalid command name: "${commandName}" - only lowercase letters, numbers, and hyphens allowed`];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureCanonicalCommandExists(repoRoot, canonicalDir, commandName) {
|
|
30
|
+
if (!commandName) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(path.join(canonicalDir, `${commandName}.md`))) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const canonicalLabel = path.relative(repoRoot, canonicalDir).replaceAll('\\', '/');
|
|
39
|
+
return [`Command not found: ${canonicalLabel}/${commandName}.md`];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function filterPlannedEntries(entries, commandName, all) {
|
|
43
|
+
if (commandName && all !== true) {
|
|
44
|
+
return entries.filter(entry =>
|
|
45
|
+
entry.filename.replaceAll('.prompt.md', '').replaceAll('.md', '') === commandName ||
|
|
46
|
+
entry.dir.includes(`/${commandName}/`)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recordEntryResult(entry, reset, skipped, dryRun) {
|
|
54
|
+
const relativeFile = path.join(entry.dir, entry.filename);
|
|
55
|
+
|
|
56
|
+
if (dryRun) {
|
|
57
|
+
if (fs.existsSync(entry.filePath)) {
|
|
58
|
+
const existing = fs.readFileSync(entry.filePath, 'utf8');
|
|
59
|
+
if (contentHash(existing) === contentHash(entry.content)) {
|
|
60
|
+
skipped.push({ agent: entry.agent, file: relativeFile });
|
|
61
|
+
} else {
|
|
62
|
+
reset.push({ agent: entry.agent, file: relativeFile });
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
reset.push({ agent: entry.agent, file: relativeFile });
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const targetDir = path.dirname(entry.filePath);
|
|
71
|
+
if (!fs.existsSync(targetDir)) {
|
|
72
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(entry.filePath)) {
|
|
76
|
+
const existing = fs.readFileSync(entry.filePath, 'utf8');
|
|
77
|
+
if (contentHash(existing) === contentHash(entry.content)) {
|
|
78
|
+
skipped.push({ agent: entry.agent, file: relativeFile });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(entry.filePath, entry.content);
|
|
84
|
+
reset.push({ agent: entry.agent, file: relativeFile });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resetCommands({ repoRoot, commandName, all, dryRun }) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
const reset = [];
|
|
90
|
+
const skipped = [];
|
|
91
|
+
|
|
92
|
+
errors.push(...validateCommandName(commandName));
|
|
93
|
+
if (errors.length > 0) {
|
|
94
|
+
return { reset, skipped, errors };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const canonicalDir = resolveCanonicalCommandsDir(repoRoot);
|
|
98
|
+
if (!canonicalDir) {
|
|
99
|
+
errors.push('Canonical source directory not found: commands/ or .claude/commands/');
|
|
100
|
+
return { reset, skipped, errors };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
errors.push(...ensureCanonicalCommandExists(repoRoot, canonicalDir, commandName));
|
|
104
|
+
if (errors.length > 0) {
|
|
105
|
+
return { reset, skipped, errors };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = syncCommands({ dryRun: true, check: false, repoRoot, canonicalDir });
|
|
109
|
+
if (!result.planned || result.planned.length === 0) {
|
|
110
|
+
return { reset, skipped, errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entries = filterPlannedEntries(result.planned, commandName, all);
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
recordEntryResult(entry, reset, skipped, dryRun);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!dryRun) {
|
|
119
|
+
// Refresh the manifest using the full expected sync set without rewriting
|
|
120
|
+
// unrelated command files that were intentionally excluded from this reset.
|
|
121
|
+
writeSyncManifest(repoRoot, result.planned);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { reset, skipped, errors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
name: 'commands-reset',
|
|
129
|
+
description: 'Reset generated agent command files to canonical content',
|
|
130
|
+
handler: async (args, flags, repoRoot) => {
|
|
131
|
+
const commandName = args.find(arg => !arg.startsWith('-'));
|
|
132
|
+
const result = resetCommands({
|
|
133
|
+
repoRoot,
|
|
134
|
+
commandName,
|
|
135
|
+
all: Boolean(flags.all),
|
|
136
|
+
dryRun: Boolean(flags.dryRun),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (result.errors.length > 0) {
|
|
140
|
+
return { success: false, error: result.errors.join('; ') };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { success: true, ...result };
|
|
144
|
+
},
|
|
145
|
+
resetCommands,
|
|
146
|
+
resolveCanonicalCommandsDir,
|
|
147
|
+
};
|
package/lib/commands/dev.js
CHANGED
|
@@ -558,6 +558,32 @@ function verifyTaskCompletion(taskTitle, ownedFiles, opts = {}) {
|
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
module.exports = {
|
|
561
|
+
name: 'dev',
|
|
562
|
+
description: 'Run the TDD development stage with phase guidance',
|
|
563
|
+
handler: async (args, flags = {}) => {
|
|
564
|
+
const featureName = args[0] || 'feature';
|
|
565
|
+
const phaseInput = flags.phase || args[1];
|
|
566
|
+
const phase = typeof phaseInput === 'string' ? phaseInput.toUpperCase() : undefined;
|
|
567
|
+
if (phase && !['RED', 'GREEN', 'REFACTOR'].includes(phase)) {
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
error: `Invalid phase '${phaseInput}'. Valid phases: red, green, refactor`,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const result = await executeDev(featureName, phase ? { phase } : {});
|
|
575
|
+
if (!result.success) {
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const lines = [`TDD Phase: ${result.phase || result.detectedPhase}`];
|
|
580
|
+
if (result.guidance) lines.push('', result.guidance);
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
...result,
|
|
584
|
+
output: lines.join('\n'),
|
|
585
|
+
};
|
|
586
|
+
},
|
|
561
587
|
detectTDDPhase,
|
|
562
588
|
identifyFilePairs,
|
|
563
589
|
runTests,
|
package/lib/commands/plan.js
CHANGED
|
@@ -680,6 +680,24 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
680
680
|
}
|
|
681
681
|
|
|
682
682
|
module.exports = {
|
|
683
|
+
name: 'plan',
|
|
684
|
+
description: 'Create implementation plan from researched feature context',
|
|
685
|
+
handler: async (args) => {
|
|
686
|
+
const result = await executePlan(args[0]);
|
|
687
|
+
if (!result.success) {
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const lines = [`Plan created: ${result.summary || result.branchName || args[0]}`];
|
|
692
|
+
if (result.beadsIssueId) lines.push(`Beads: ${result.beadsIssueId}`);
|
|
693
|
+
if (result.branchName) lines.push(`Branch: ${result.branchName}`);
|
|
694
|
+
if (result.nextCommand) lines.push(`Next: ${result.nextCommand}`);
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
...result,
|
|
698
|
+
output: lines.join('\n'),
|
|
699
|
+
};
|
|
700
|
+
},
|
|
683
701
|
readResearchDoc,
|
|
684
702
|
detectScope,
|
|
685
703
|
createBeadsIssue,
|