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.
Files changed (55) hide show
  1. package/.cursorrules +149 -0
  2. package/bin/forge.js +43 -3
  3. package/lib/agents/README.md +46 -1
  4. package/lib/agents/cline.plugin.json +11 -4
  5. package/lib/agents/codex.plugin.json +2 -2
  6. package/lib/agents/copilot.plugin.json +5 -5
  7. package/lib/agents/cursor.plugin.json +1 -1
  8. package/lib/agents/kilocode.plugin.json +1 -1
  9. package/lib/agents/opencode.plugin.json +7 -4
  10. package/lib/agents/roo.plugin.json +10 -3
  11. package/lib/agents-config.js +127 -79
  12. package/lib/codex-skills.js +50 -0
  13. package/lib/commands/_issue.js +172 -0
  14. package/lib/commands/_registry.js +40 -1
  15. package/lib/commands/claim.js +5 -0
  16. package/lib/commands/close.js +5 -0
  17. package/lib/commands/commands-reset.js +147 -0
  18. package/lib/commands/create.js +5 -0
  19. package/lib/commands/dev.js +26 -0
  20. package/lib/commands/issue.js +5 -0
  21. package/lib/commands/list.js +5 -0
  22. package/lib/commands/plan.js +18 -0
  23. package/lib/commands/ready.js +5 -0
  24. package/lib/commands/setup.js +4295 -0
  25. package/lib/commands/ship.js +20 -0
  26. package/lib/commands/show.js +5 -0
  27. package/lib/commands/status.js +210 -44
  28. package/lib/commands/sync.js +19 -1
  29. package/lib/commands/update.js +5 -0
  30. package/lib/commands/validate.js +13 -0
  31. package/lib/detect-agent.js +38 -8
  32. package/lib/detection-utils.js +405 -0
  33. package/lib/file-utils.js +260 -0
  34. package/lib/forge-context.js +42 -0
  35. package/lib/frontmatter.js +79 -0
  36. package/lib/husky-migration.js +113 -12
  37. package/lib/lefthook-check.js +27 -6
  38. package/lib/plugin-manager.js +225 -72
  39. package/lib/project-discovery.js +39 -5
  40. package/lib/runtime-health.js +305 -0
  41. package/lib/shell-utils.js +50 -0
  42. package/lib/ui-utils.js +43 -0
  43. package/lib/validation-utils.js +163 -0
  44. package/lib/workflow/enforce-stage.js +179 -0
  45. package/lib/workflow/stages.js +201 -0
  46. package/lib/workflow/state.js +332 -0
  47. package/opencode.json +67 -0
  48. package/package.json +15 -5
  49. package/scripts/beads-context.sh +12 -4
  50. package/scripts/check-agents.js +103 -0
  51. package/scripts/lib/eval-runner.js +50 -0
  52. package/scripts/pr-coordinator.sh +71 -21
  53. package/scripts/smart-status.sh +21 -11
  54. package/scripts/sync-commands.js +49 -20
  55. 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
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('create');
@@ -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,
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { createIssueCommand } = require('./_issue');
4
+
5
+ module.exports = createIssueCommand();
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('list');
@@ -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,
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('ready');