forge-workflow 0.0.7 → 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/bin/forge.js CHANGED
@@ -2541,6 +2541,13 @@ function parseFlags() {
2541
2541
  sync: false, // Scaffold Beads GitHub sync workflows (--sync)
2542
2542
  };
2543
2543
 
2544
+ // Issue passthrough commands delegate all flags to bd.
2545
+ // Skip global parsing so flags like --type, -p, --help reach the handler intact.
2546
+ const issuePassthroughCommands = ['create', 'update', 'claim', 'close', 'show', 'list', 'ready', 'issue'];
2547
+ if (issuePassthroughCommands.includes(args[0])) {
2548
+ return flags;
2549
+ }
2550
+
2544
2551
  for (let i = 0; i < args.length;) {
2545
2552
  const arg = args[i];
2546
2553
 
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('node:child_process');
4
+
5
+ const SUBCOMMANDS = {
6
+ create: {
7
+ description: 'Create a Beads issue via Forge',
8
+ usage: 'forge create [title] [bd-create-flags]',
9
+ helpCommand: 'create',
10
+ buildBdArgs: (args) => ['create', ...args],
11
+ },
12
+ update: {
13
+ description: 'Update a Beads issue via Forge',
14
+ usage: 'forge update <id...> [bd-update-flags]',
15
+ helpCommand: 'update',
16
+ buildBdArgs: (args) => ['update', ...args],
17
+ },
18
+ claim: {
19
+ description: 'Claim a Beads issue via Forge',
20
+ usage: 'forge claim <id> [bd-update-flags]',
21
+ helpCommand: 'update',
22
+ buildBdArgs: (args) => {
23
+ const [issueId, ...rest] = args;
24
+ if (!issueId) {
25
+ return { error: 'Missing issue id. Usage: forge claim <id> [bd-update-flags]' };
26
+ }
27
+ return ['update', issueId, '--claim', ...rest];
28
+ },
29
+ },
30
+ close: {
31
+ description: 'Close a Beads issue via Forge',
32
+ usage: 'forge close <id...> [bd-close-flags]',
33
+ helpCommand: 'close',
34
+ buildBdArgs: (args) => ['close', ...args],
35
+ },
36
+ show: {
37
+ description: 'Show a Beads issue via Forge',
38
+ usage: 'forge show <id> [bd-show-flags]',
39
+ helpCommand: 'show',
40
+ buildBdArgs: (args) => ['show', ...args],
41
+ },
42
+ list: {
43
+ description: 'List Beads issues via Forge',
44
+ usage: 'forge list [bd-list-flags]',
45
+ helpCommand: 'list',
46
+ buildBdArgs: (args) => ['list', ...args],
47
+ },
48
+ ready: {
49
+ description: 'Show ready Beads issues via Forge',
50
+ usage: 'forge ready [bd-ready-flags]',
51
+ helpCommand: 'ready',
52
+ buildBdArgs: (args) => ['ready', ...args],
53
+ },
54
+ };
55
+
56
+ function normalizeArgs(args = []) {
57
+ return args.filter(arg => arg !== '--');
58
+ }
59
+
60
+ function getExecOptions(projectRoot) {
61
+ return {
62
+ cwd: projectRoot,
63
+ stdio: 'inherit',
64
+ };
65
+ }
66
+
67
+ function formatIssueHelp() {
68
+ const lines = [
69
+ 'Usage: forge issue <subcommand> [...]',
70
+ '',
71
+ 'Supported subcommands:',
72
+ ];
73
+
74
+ for (const [name, spec] of Object.entries(SUBCOMMANDS)) {
75
+ lines.push(` ${name.padEnd(6)} ${spec.description}`);
76
+ }
77
+
78
+ lines.push('');
79
+ lines.push('Examples:');
80
+ lines.push(' forge create --title "Add feature" --type feature');
81
+ lines.push(' forge claim forge-abc');
82
+ lines.push(' forge update forge-abc --priority 1');
83
+ lines.push(' forge close forge-abc --reason "Done"');
84
+ lines.push(' forge issue show forge-abc --json');
85
+
86
+ return lines.join('\n');
87
+ }
88
+
89
+ function extractErrorMessage(error) {
90
+ if (error?.code === 'ENOENT') {
91
+ return 'Beads (bd) command not found. Install or initialize Beads before using Forge issue commands.';
92
+ }
93
+
94
+ // With stdio: 'inherit', error.stderr and error.stdout are always null.
95
+ // Only error.message is available for diagnostics.
96
+ return error?.message?.trim() || 'Beads command failed';
97
+ }
98
+
99
+ function buildBdArgs(subcommand, rawArgs) {
100
+ const spec = SUBCOMMANDS[subcommand];
101
+ if (!spec) {
102
+ return { error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
103
+ }
104
+
105
+ const args = normalizeArgs(rawArgs);
106
+ if (args.includes('--help') || args.includes('-h')) {
107
+ return [spec.helpCommand, '--help'];
108
+ }
109
+
110
+ return spec.buildBdArgs(args);
111
+ }
112
+
113
+ async function runIssueSubcommand(subcommand, args, projectRoot, opts = {}) {
114
+ const exec = opts._exec || execFileSync;
115
+ const bdArgs = buildBdArgs(subcommand, args);
116
+
117
+ if (!Array.isArray(bdArgs)) {
118
+ return { success: false, error: bdArgs.error };
119
+ }
120
+
121
+ try {
122
+ exec('bd', bdArgs, getExecOptions(projectRoot));
123
+ return { success: true, subcommand };
124
+ } catch (error) {
125
+ return {
126
+ success: false,
127
+ error: extractErrorMessage(error),
128
+ };
129
+ }
130
+ }
131
+
132
+ function makeAliasCommand(subcommand) {
133
+ const spec = SUBCOMMANDS[subcommand];
134
+ if (!spec) {
135
+ throw new Error(`Unknown issue subcommand '${subcommand}'`);
136
+ }
137
+
138
+ return {
139
+ name: subcommand,
140
+ description: spec.description,
141
+ usage: spec.usage,
142
+ flags: {},
143
+ handler: async (args, _flags, projectRoot, opts = {}) =>
144
+ runIssueSubcommand(subcommand, args, projectRoot, opts),
145
+ };
146
+ }
147
+
148
+ function createIssueCommand() {
149
+ return {
150
+ name: 'issue',
151
+ description: 'Manage Beads issues through the Forge command surface',
152
+ usage: 'forge issue <create|update|claim|close|show|list|ready> [...]',
153
+ flags: {},
154
+ handler: async (args, _flags, projectRoot, opts = {}) => {
155
+ const [subcommand, ...rest] = normalizeArgs(args);
156
+
157
+ if (!subcommand || subcommand === '--help' || subcommand === '-h') {
158
+ return { success: true, output: formatIssueHelp() };
159
+ }
160
+
161
+ return runIssueSubcommand(subcommand, rest, projectRoot, opts);
162
+ },
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ SUBCOMMANDS,
168
+ buildBdArgs,
169
+ createIssueCommand,
170
+ makeAliasCommand,
171
+ runIssueSubcommand,
172
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('claim');
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('close');
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('create');
@@ -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');
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('ready');
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('show');
@@ -7,7 +7,9 @@ function isRecoverableBeadsSyncError(error) {
7
7
  return (
8
8
  message.includes('failed to open database') ||
9
9
  message.includes('database not found') ||
10
- message.includes('no beads configuration found')
10
+ message.includes('no beads configuration found') ||
11
+ message.includes('remote \'origin\' not found') ||
12
+ message.includes('remote "origin" not found')
11
13
  );
12
14
  }
13
15
 
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { makeAliasCommand } = require('./_issue');
4
+
5
+ module.exports = makeAliasCommand('update');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-workflow",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "7-stage TDD workflow for ALL AI coding agents (Claude, Cursor, Cline, OpenCode, Copilot, Kilo Code, Roo Code, Codex)",
5
5
  "bin": {
6
6
  "forge": "bin/forge.js",
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const path = require('path');
12
+ const fs = require('fs');
12
13
  const { execSync } = require('node:child_process');
13
14
 
14
15
  // ── active worktree tracking (cleanup on crash) ─────────────────────
@@ -17,6 +18,17 @@ const { execSync } = require('node:child_process');
17
18
  // Note: execSync is safe here — all paths are internally generated, never user input.
18
19
  const activeEvalWorktrees = new Map(); // path -> branch
19
20
 
21
+ /**
22
+ * Force-remove a directory that git worktree remove may leave behind (Windows).
23
+ */
24
+ function forceRemoveDir(dirPath) {
25
+ try {
26
+ if (fs.existsSync(dirPath)) {
27
+ fs.rmSync(dirPath, { recursive: true, force: true });
28
+ }
29
+ } catch (_err) { /* best-effort */ }
30
+ }
31
+
20
32
  function cleanupActiveWorktrees() {
21
33
  if (activeEvalWorktrees.size === 0) return;
22
34
  let repoRoot;
@@ -25,6 +37,7 @@ function cleanupActiveWorktrees() {
25
37
  try {
26
38
  execSync(`git worktree remove --force "${wtPath}"`, { cwd: repoRoot, stdio: 'pipe' });
27
39
  } catch (_err) { /* already removed */ }
40
+ forceRemoveDir(wtPath);
28
41
  if (branch && branch.startsWith('eval-')) {
29
42
  try {
30
43
  execSync(`git branch -D "${branch}"`, { cwd: repoRoot, stdio: 'pipe' });
@@ -35,6 +48,37 @@ function cleanupActiveWorktrees() {
35
48
  activeEvalWorktrees.clear();
36
49
  }
37
50
 
51
+ /**
52
+ * Remove stale eval-* directories left behind by crashed runs.
53
+ * Git has already forgotten them (worktree prune), but the directories persist on Windows.
54
+ */
55
+ function cleanupStaleEvalWorktrees() {
56
+ try {
57
+ const worktreesDir = getWorktreesDir();
58
+ if (!fs.existsSync(worktreesDir)) return;
59
+
60
+ // Get the list of paths git still knows about
61
+ const repoRoot = getRepoRoot();
62
+ const knownRaw = execSync('git worktree list --porcelain', {
63
+ cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
64
+ });
65
+ const knownPaths = new Set(
66
+ knownRaw.split('\n')
67
+ .filter((l) => l.startsWith('worktree '))
68
+ .map((l) => l.slice('worktree '.length).replace(/\\/g, '/'))
69
+ );
70
+
71
+ const entries = fs.readdirSync(worktreesDir);
72
+ for (const entry of entries) {
73
+ if (!entry.startsWith('eval-')) continue;
74
+ const fullPath = path.join(worktreesDir, entry).replace(/\\/g, '/');
75
+ if (!knownPaths.has(fullPath)) {
76
+ forceRemoveDir(path.join(worktreesDir, entry));
77
+ }
78
+ }
79
+ } catch (_err) { /* best-effort — don't block eval creation */ }
80
+ }
81
+
38
82
  process.on('exit', cleanupActiveWorktrees);
39
83
  process.on('SIGINT', () => {
40
84
  const hadWork = activeEvalWorktrees.size > 0;
@@ -78,6 +122,9 @@ function getWorktreesDir() {
78
122
  * @returns {Promise<{ path: string, branch: string }>}
79
123
  */
80
124
  async function createEvalWorktree() {
125
+ // Self-heal: remove stale eval dirs from previous crashed runs
126
+ cleanupStaleEvalWorktrees();
127
+
81
128
  const timestamp = Date.now();
82
129
  const pid = process.pid;
83
130
  const name = `eval-${timestamp}-${pid}`;
@@ -128,6 +175,9 @@ async function destroyEvalWorktree(worktreePath) {
128
175
  stdio: ['pipe', 'pipe', 'pipe'],
129
176
  });
130
177
 
178
+ // Windows: git worktree remove often leaves the directory behind
179
+ forceRemoveDir(worktreePath);
180
+
131
181
  // Prune to clean up references
132
182
  execSync('git worktree prune', {
133
183
  cwd: repoRoot,