forge-workflow 0.0.3 → 0.0.4

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 (83) hide show
  1. package/.claude/commands/dev.md +26 -0
  2. package/.claude/commands/plan.md +48 -5
  3. package/.claude/commands/premerge.md +0 -3
  4. package/.claude/commands/rollback.md +4 -4
  5. package/.claude/commands/ship.md +71 -41
  6. package/.claude/commands/status.md +9 -38
  7. package/.claude/commands/validate.md +47 -2
  8. package/.cline/workflows/dev.md +26 -0
  9. package/.cline/workflows/plan.md +48 -5
  10. package/.cline/workflows/premerge.md +0 -3
  11. package/.cline/workflows/rollback.md +4 -4
  12. package/.cline/workflows/ship.md +71 -41
  13. package/.cline/workflows/status.md +9 -38
  14. package/.cline/workflows/validate.md +47 -2
  15. package/.codex/skills/dev/SKILL.md +26 -0
  16. package/.codex/skills/plan/SKILL.md +48 -5
  17. package/.codex/skills/premerge/SKILL.md +0 -3
  18. package/.codex/skills/rollback/SKILL.md +4 -4
  19. package/.codex/skills/ship/SKILL.md +71 -41
  20. package/.codex/skills/status/SKILL.md +9 -38
  21. package/.codex/skills/validate/SKILL.md +47 -2
  22. package/.cursor/commands/dev.md +26 -0
  23. package/.cursor/commands/plan.md +48 -5
  24. package/.cursor/commands/premerge.md +0 -3
  25. package/.cursor/commands/rollback.md +4 -4
  26. package/.cursor/commands/ship.md +71 -41
  27. package/.cursor/commands/status.md +9 -38
  28. package/.cursor/commands/validate.md +47 -2
  29. package/.cursor/hooks/state/continual-learning-index.json +19 -0
  30. package/.cursor/hooks/state/continual-learning.json +8 -0
  31. package/.github/prompts/dev.prompt.md +26 -0
  32. package/.github/prompts/plan.prompt.md +48 -5
  33. package/.github/prompts/premerge.prompt.md +0 -3
  34. package/.github/prompts/rollback.prompt.md +4 -4
  35. package/.github/prompts/ship.prompt.md +71 -41
  36. package/.github/prompts/status.prompt.md +9 -38
  37. package/.github/prompts/validate.prompt.md +47 -2
  38. package/.kilocode/workflows/dev.md +26 -0
  39. package/.kilocode/workflows/plan.md +48 -5
  40. package/.kilocode/workflows/premerge.md +0 -3
  41. package/.kilocode/workflows/rollback.md +4 -4
  42. package/.kilocode/workflows/ship.md +71 -41
  43. package/.kilocode/workflows/status.md +9 -38
  44. package/.kilocode/workflows/validate.md +47 -2
  45. package/.opencode/commands/dev.md +26 -0
  46. package/.opencode/commands/plan.md +48 -5
  47. package/.opencode/commands/premerge.md +0 -3
  48. package/.opencode/commands/rollback.md +4 -4
  49. package/.opencode/commands/ship.md +71 -41
  50. package/.opencode/commands/status.md +9 -38
  51. package/.opencode/commands/validate.md +47 -2
  52. package/.roo/commands/dev.md +26 -0
  53. package/.roo/commands/plan.md +48 -5
  54. package/.roo/commands/premerge.md +0 -3
  55. package/.roo/commands/rollback.md +4 -4
  56. package/.roo/commands/ship.md +71 -41
  57. package/.roo/commands/status.md +9 -38
  58. package/.roo/commands/validate.md +47 -2
  59. package/AGENTS.md +7 -1
  60. package/CLAUDE.md +5 -4
  61. package/LICENSE +21 -21
  62. package/README.md +21 -19
  63. package/bin/{forge-validate.js → forge-preflight.js} +21 -15
  64. package/bin/forge.js +209 -138
  65. package/docs/AGENT_INSTALL_PROMPT.md +1 -1
  66. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  67. package/docs/ENHANCED_ONBOARDING.md +6 -6
  68. package/docs/EXAMPLES.md +4 -4
  69. package/docs/GREPTILE_SETUP.md +1 -1
  70. package/docs/MANUAL_REVIEW_GUIDE.md +1 -1
  71. package/docs/ROADMAP.md +6 -6
  72. package/docs/SETUP.md +1 -2
  73. package/docs/VALIDATION.md +11 -11
  74. package/install.sh +1 -3
  75. package/lib/agents-config.js +3 -3
  76. package/lib/detect-agent.js +191 -0
  77. package/lib/detect-worktree.js +47 -0
  78. package/lib/file-hash.js +26 -0
  79. package/lib/setup-action-log.js +139 -0
  80. package/lib/setup-summary-renderer.js +106 -0
  81. package/lib/setup.js +75 -1
  82. package/package.json +3 -4
  83. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Agent Auto-Detection Module
3
+ *
4
+ * 4-layer detection strategy:
5
+ * Layer 1: AI_AGENT env var (universal standard)
6
+ * Layer 2: Agent-specific env vars (high confidence)
7
+ * Layer 3: VSCode path parsing (medium confidence)
8
+ * Layer 4: Config file signatures (medium-low confidence)
9
+ *
10
+ * @module detect-agent
11
+ */
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+
16
+ /**
17
+ * Layer 2 mapping: env var → agent name.
18
+ * Order matters — first match wins within this layer.
19
+ * Each entry: [envVarName, agentName]
20
+ * @type {Array<[string, string]>}
21
+ */
22
+ const AGENT_ENV_VARS = [
23
+ // Forge-supported agents only (7 agents)
24
+ // Claude Code (with cowork sub-check handled separately)
25
+ ['CLAUDECODE', 'claude'],
26
+ ['CLAUDE_CODE', 'claude'],
27
+ // Cursor
28
+ ['CURSOR_TRACE_ID', 'cursor'],
29
+ ['CURSOR_AGENT', 'cursor'],
30
+ // Codex (OpenAI)
31
+ ['CODEX_SANDBOX', 'codex'],
32
+ ['CODEX_CI', 'codex'],
33
+ ['CODEX_THREAD_ID', 'codex'],
34
+ // OpenCode
35
+ ['OPENCODE_CLIENT', 'opencode'],
36
+ // GitHub Copilot
37
+ ['COPILOT_MODEL', 'github-copilot'],
38
+ ['COPILOT_ALLOW_ALL', 'github-copilot'],
39
+ ['COPILOT_GITHUB_TOKEN', 'github-copilot'],
40
+ // Cline, Roo Code, Kilocode — no env vars (VSCode extensions, config-file-only)
41
+ ];
42
+
43
+ /**
44
+ * Layer 4 mapping: config file/dir paths → agent name.
45
+ * Paths are relative to project root.
46
+ * @type {Array<[string, string]>}
47
+ */
48
+ const CONFIG_SIGNATURES = [
49
+ // Forge-supported agents only (7 agents)
50
+ [path.join('.claude', 'settings.json'), 'claude'],
51
+ ['.cursorrules', 'cursor'],
52
+ [path.join('.cursor', 'rules'), 'cursor'],
53
+ ['.clinerules', 'cline'],
54
+ ['.cline', 'cline'],
55
+ [path.join('.roo', 'rules'), 'roo-code'],
56
+ ['.roo', 'roo-code'],
57
+ ['.kilocode', 'kilocode'],
58
+ ['codex.md', 'codex'],
59
+ ['.codex', 'codex'],
60
+ [path.join('.opencode', 'commands'), 'opencode'],
61
+ ['.opencode', 'opencode'],
62
+ [path.join('.github', 'copilot-instructions.md'), 'github-copilot'],
63
+ ];
64
+
65
+ /**
66
+ * Detect the actively running AI agent from environment signals.
67
+ *
68
+ * Covers layers 1-3 of the 4-layer detection strategy:
69
+ * 1. AI_AGENT env var (universal, highest priority)
70
+ * 2. Agent-specific env vars (high confidence)
71
+ * 3. VSCode path parsing (medium confidence)
72
+ * Layer 4 (config file signatures) is in detectConfiguredAgents().
73
+ *
74
+ * @param {Record<string, string>} [env=process.env] - Environment variables
75
+ * @returns {{ name: string, source: 'env'|'path', confidence: 'high'|'medium' } | null}
76
+ */
77
+ function detectActiveAgent(env = process.env) {
78
+ // Layer 1: AI_AGENT universal env var
79
+ if (env.AI_AGENT) {
80
+ return { name: env.AI_AGENT, source: 'env', confidence: 'high' };
81
+ }
82
+
83
+ // Layer 2: Agent-specific env vars
84
+ // Special sub-check: Claude Code cowork mode
85
+ if ((env.CLAUDECODE || env.CLAUDE_CODE) && env.CLAUDE_CODE_IS_COWORK) {
86
+ return { name: 'cowork', source: 'env', confidence: 'high' };
87
+ }
88
+
89
+ for (const [varName, agentName] of AGENT_ENV_VARS) {
90
+ if (env[varName]) {
91
+ return { name: agentName, source: 'env', confidence: 'high' };
92
+ }
93
+ }
94
+
95
+ // Layer 3: VSCode path parsing
96
+ const pathResult = _detectFromVSCodePaths(env);
97
+ if (pathResult && pathResult.agent) {
98
+ return { name: pathResult.agent, source: 'path', confidence: 'medium' };
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Parse VSCode-related env vars for editor/agent identification.
106
+ *
107
+ * @param {Record<string, string>} env - Environment variables
108
+ * @returns {{ agent: string|null, editor: string|null } | null}
109
+ * @private
110
+ */
111
+ function _detectFromVSCodePaths(env) {
112
+ const pathsToCheck = [
113
+ env.VSCODE_CODE_CACHE_PATH,
114
+ env.VSCODE_NLS_CONFIG,
115
+ ].filter(Boolean);
116
+
117
+ if (pathsToCheck.length === 0) return null;
118
+
119
+ const combined = pathsToCheck.join(' ').toLowerCase();
120
+
121
+ // Only detect Forge-supported agents via VSCode paths
122
+ if (combined.includes('cursor')) {
123
+ return { agent: 'cursor', editor: 'cursor' };
124
+ }
125
+
126
+ // Generic VSCode (or unsupported VSCode forks) — not a specific agent
127
+ if (combined.includes('code')) {
128
+ return { agent: null, editor: 'vscode' };
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Detect all agents with config files present in the project root (Layer 4).
136
+ *
137
+ * @param {string} projectRoot - Absolute path to project root
138
+ * @returns {string[]} Array of unique agent names detected from config files
139
+ */
140
+ function detectConfiguredAgents(projectRoot) {
141
+ const detected = new Set();
142
+
143
+ for (const [relativePath, agentName] of CONFIG_SIGNATURES) {
144
+ const fullPath = path.join(projectRoot, relativePath);
145
+ try {
146
+ if (fs.existsSync(fullPath)) {
147
+ detected.add(agentName);
148
+ }
149
+ } catch (_err) {
150
+ // Permission error or other fs issue — skip silently
151
+ }
152
+ }
153
+
154
+ return [...detected];
155
+ }
156
+
157
+ /**
158
+ * Full environment detection combining all 4 layers.
159
+ *
160
+ * @param {string} projectRoot - Absolute path to project root
161
+ * @param {Record<string, string>} [env=process.env] - Environment variables
162
+ * @returns {{
163
+ * activeAgent: string|null,
164
+ * activeAgentSource: 'env'|'path'|null,
165
+ * confidence: 'high'|'medium'|null,
166
+ * configuredAgents: string[],
167
+ * editor: string|null
168
+ * }}
169
+ */
170
+ function detectEnvironment(projectRoot, env = process.env) {
171
+ const active = detectActiveAgent(env);
172
+ const configuredAgents = detectConfiguredAgents(projectRoot);
173
+
174
+ // Get editor from VSCode paths (_detectFromVSCodePaths is cheap and deterministic)
175
+ const vscodePaths = _detectFromVSCodePaths(env);
176
+ const editor = vscodePaths ? vscodePaths.editor : null;
177
+
178
+ return {
179
+ activeAgent: active ? active.name : null,
180
+ activeAgentSource: active ? active.source : null,
181
+ confidence: active ? active.confidence : null,
182
+ configuredAgents,
183
+ editor,
184
+ };
185
+ }
186
+
187
+ module.exports = {
188
+ detectActiveAgent,
189
+ detectConfiguredAgents,
190
+ detectEnvironment,
191
+ };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Detect if the current directory is inside a git worktree.
8
+ * Uses git rev-parse --git-dir vs --git-common-dir — they differ in worktrees.
9
+ *
10
+ * @param {string} [cwd=process.cwd()] - Directory to check
11
+ * @returns {{ inWorktree: boolean, branch?: string, mainWorktree?: string }}
12
+ */
13
+ function detectWorktree(cwd = process.cwd()) {
14
+ try {
15
+ const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
16
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe']
17
+ }).trim();
18
+
19
+ const gitCommonDir = execFileSync('git', ['rev-parse', '--git-common-dir'], {
20
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe']
21
+ }).trim();
22
+
23
+ // Resolve to absolute paths for reliable comparison
24
+ const absGitDir = path.resolve(cwd, gitDir);
25
+ const absCommonDir = path.resolve(cwd, gitCommonDir);
26
+
27
+ // In a worktree, git-dir is like .git/worktrees/<name>
28
+ // while git-common-dir is the main .git directory
29
+ if (absGitDir !== absCommonDir) {
30
+ const branch = execFileSync('git', ['branch', '--show-current'], {
31
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe']
32
+ }).trim();
33
+
34
+ // Main worktree is one level above the common .git dir
35
+ const mainWorktree = path.resolve(absCommonDir, '..');
36
+
37
+ return { inWorktree: true, branch, mainWorktree };
38
+ }
39
+
40
+ return { inWorktree: false };
41
+ } catch (_err) {
42
+ // Not in a git repo or git not available
43
+ return { inWorktree: false };
44
+ }
45
+ }
46
+
47
+ module.exports = { detectWorktree };
@@ -0,0 +1,26 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+
4
+ /**
5
+ * Compute SHA-256 hex digest of a string.
6
+ * @param {string} content
7
+ * @returns {string} 64-char lowercase hex hash
8
+ */
9
+ function contentHash(content) {
10
+ return crypto.createHash('sha256').update(content).digest('hex');
11
+ }
12
+
13
+ /**
14
+ * Check whether an existing file's content matches the given string.
15
+ * Returns false if the file does not exist.
16
+ * @param {string} filePath
17
+ * @param {string} newContent
18
+ * @returns {boolean}
19
+ */
20
+ function fileMatchesContent(filePath, newContent) {
21
+ if (!fs.existsSync(filePath)) return false;
22
+ const existing = fs.readFileSync(filePath, 'utf8');
23
+ return contentHash(existing) === contentHash(newContent);
24
+ }
25
+
26
+ module.exports = { contentHash, fileMatchesContent };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Centralized action log for setup operations.
3
+ *
4
+ * Collects file-level actions (created, skipped, merged, etc.) during
5
+ * `forge setup` so the CLI can display a structured summary at the end.
6
+ *
7
+ * @module setup-action-log
8
+ */
9
+
10
+ /** Map of directory prefixes to human-readable agent names. */
11
+ const AGENT_PREFIXES = {
12
+ '.claude/': 'Claude Code',
13
+ '.cursor/': 'Cursor',
14
+ '.windsurf/': 'Windsurf',
15
+ '.cline/': 'Cline',
16
+ '.codex/': 'Codex',
17
+ '.opencode/': 'OpenCode',
18
+ '.kilocode/': 'Kilocode',
19
+ '.roo/': 'Roo Code',
20
+ '.github/prompts/': 'GitHub Copilot'
21
+ };
22
+
23
+ /**
24
+ * Detect the agent name from a file path.
25
+ *
26
+ * @param {string} filePath - Relative file path (e.g. `.claude/settings.json`)
27
+ * @returns {string} Agent name or `'General'` if no agent prefix matches
28
+ */
29
+ function detectAgent(filePath) {
30
+ const normalized = filePath.replace(/\\/g, '/');
31
+ for (const [prefix, name] of Object.entries(AGENT_PREFIXES)) {
32
+ if (normalized.startsWith(prefix)) {
33
+ return name;
34
+ }
35
+ }
36
+ return 'General';
37
+ }
38
+
39
+ /**
40
+ * Strip the agent directory prefix from a file path.
41
+ *
42
+ * @param {string} filePath - Relative file path
43
+ * @returns {string} Path with the leading agent directory removed
44
+ */
45
+ function stripAgentPrefix(filePath) {
46
+ const normalized = filePath.replace(/\\/g, '/');
47
+ for (const prefix of Object.keys(AGENT_PREFIXES)) {
48
+ if (normalized.startsWith(prefix)) {
49
+ return normalized.slice(prefix.length);
50
+ }
51
+ }
52
+ return normalized;
53
+ }
54
+
55
+ class SetupActionLog {
56
+ constructor() {
57
+ /** @type {Array<{file: string, action: string, detail: string|null}>} */
58
+ this.actions = [];
59
+ }
60
+
61
+ /**
62
+ * Record a setup action.
63
+ *
64
+ * @param {string} file - Relative file path that was acted on
65
+ * @param {string} action - One of: created, skipped, merged, conflict, removed, force-created
66
+ * @param {string|null} [detail=null] - Optional human-readable detail
67
+ */
68
+ add(file, action, detail = null) {
69
+ this.actions.push({ file, action, detail });
70
+ }
71
+
72
+ /**
73
+ * Get counts grouped by action type.
74
+ *
75
+ * @returns {Record<string, number>} e.g. `{ created: 4, skipped: 2 }`
76
+ */
77
+ getSummary() {
78
+ const counts = {};
79
+ for (const { action } of this.actions) {
80
+ counts[action] = (counts[action] || 0) + 1;
81
+ }
82
+ return counts;
83
+ }
84
+
85
+ /**
86
+ * Get the full ordered list of actions.
87
+ *
88
+ * @returns {Array<{file: string, action: string, detail: string|null}>}
89
+ */
90
+ getVerbose() {
91
+ return this.actions;
92
+ }
93
+
94
+ /**
95
+ * Group files by detected agent name.
96
+ *
97
+ * Each agent key maps to an object whose keys are action types and whose
98
+ * values are arrays of file paths (with the agent prefix stripped).
99
+ *
100
+ * @returns {Record<string, Record<string, string[]>>}
101
+ */
102
+ getAgentSummary() {
103
+ const agents = {};
104
+ for (const { file, action } of this.actions) {
105
+ const agent = detectAgent(file);
106
+ const stripped = stripAgentPrefix(file);
107
+
108
+ if (!agents[agent]) {
109
+ agents[agent] = {};
110
+ }
111
+ if (!agents[agent][action]) {
112
+ agents[agent][action] = [];
113
+ }
114
+ agents[agent][action].push(stripped);
115
+ }
116
+ return agents;
117
+ }
118
+
119
+ /**
120
+ * Filter actions by type.
121
+ *
122
+ * @param {string} action - The action type to filter on
123
+ * @returns {Array<{file: string, action: string, detail: string|null}>}
124
+ */
125
+ getByAction(action) {
126
+ return this.actions.filter(a => a.action === action);
127
+ }
128
+
129
+ /**
130
+ * Total number of recorded actions.
131
+ *
132
+ * @returns {number}
133
+ */
134
+ get length() {
135
+ return this.actions.length;
136
+ }
137
+ }
138
+
139
+ module.exports = { SetupActionLog };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Renders a clean summary from SetupActionLog data.
3
+ *
4
+ * Default mode: 3-line concise output.
5
+ * Verbose mode: file-by-file detail grouped by agent.
6
+ *
7
+ * @module setup-summary-renderer
8
+ */
9
+
10
+ /**
11
+ * Capitalize the first letter of a string.
12
+ *
13
+ * @param {string} str
14
+ * @returns {string}
15
+ */
16
+ function capitalize(str) {
17
+ if (!str) return str;
18
+ return str.charAt(0).toUpperCase() + str.slice(1);
19
+ }
20
+
21
+ /**
22
+ * Render setup summary as a string.
23
+ *
24
+ * @param {import('./setup-action-log').SetupActionLog} actionLog - The populated action log
25
+ * @param {string[]} agentNames - List of configured agent slugs (e.g. ['claude', 'cursor'])
26
+ * @param {boolean} verbose - Whether to show file-by-file detail
27
+ * @returns {string} The formatted summary output
28
+ */
29
+ function renderSetupSummary(actionLog, agentNames, verbose) {
30
+ if (verbose) {
31
+ return renderVerbose(actionLog, agentNames);
32
+ }
33
+ return renderDefault(actionLog, agentNames);
34
+ }
35
+
36
+ /**
37
+ * Render the default 3-line concise summary.
38
+ *
39
+ * @param {import('./setup-action-log').SetupActionLog} actionLog
40
+ * @param {string[]} agentNames
41
+ * @returns {string}
42
+ */
43
+ function renderDefault(actionLog, agentNames) {
44
+ const agentCount = agentNames.length;
45
+ const agentLabel = agentCount === 1 ? '1 agent' : `${agentCount} agents`;
46
+ const agentList = agentNames.length > 0 ? ` (${agentNames.join(', ')})` : '';
47
+
48
+ const summary = actionLog.getSummary();
49
+
50
+ // Build counts line — only include non-zero actions
51
+ const displayOrder = ['created', 'skipped', 'merged', 'force-created', 'updated', 'conflict', 'removed'];
52
+ const parts = [];
53
+ for (const action of displayOrder) {
54
+ if (summary[action] && summary[action] > 0) {
55
+ parts.push(`${capitalize(action)}: ${summary[action]} ${summary[action] === 1 ? 'file' : 'files'}`);
56
+ }
57
+ }
58
+ // Include any actions not in displayOrder
59
+ for (const [action, count] of Object.entries(summary)) {
60
+ if (!displayOrder.includes(action) && count > 0) {
61
+ parts.push(`${capitalize(action)}: ${count} ${count === 1 ? 'file' : 'files'}`);
62
+ }
63
+ }
64
+
65
+ const lines = [];
66
+ lines.push(`Forge setup complete — ${agentLabel} configured${agentList}`);
67
+
68
+ if (parts.length > 0) {
69
+ lines.push(` ${parts.join(' | ')}`);
70
+ } else {
71
+ lines.push(' 0 files changed');
72
+ }
73
+
74
+ lines.push(' Run forge setup --verbose to see all files');
75
+
76
+ return lines.join('\n');
77
+ }
78
+
79
+ /**
80
+ * Render verbose file-by-file output grouped by agent.
81
+ *
82
+ * @param {import('./setup-action-log').SetupActionLog} actionLog
83
+ * @param {string[]} _agentNames - Not used in verbose (agents come from log data)
84
+ * @returns {string}
85
+ */
86
+ function renderVerbose(actionLog, _agentNames) {
87
+ const agentSummary = actionLog.getAgentSummary();
88
+ const lines = [];
89
+
90
+ for (const [agent, actions] of Object.entries(agentSummary)) {
91
+ for (const [action, files] of Object.entries(actions)) {
92
+ const fileCount = files.length;
93
+ const fileLabel = fileCount === 1 ? '1 file' : `${fileCount} files`;
94
+ const fileList = files.join(', ');
95
+ lines.push(`${agent}: ${fileList} (${fileLabel}) [${action}]`);
96
+ }
97
+ }
98
+
99
+ if (lines.length === 0) {
100
+ return 'No file operations recorded.';
101
+ }
102
+
103
+ return lines.join('\n');
104
+ }
105
+
106
+ module.exports = { renderSetupSummary };
package/lib/setup.js CHANGED
@@ -109,10 +109,84 @@ async function markStepComplete(projectPath, stepName) {
109
109
  await saveSetupState(projectPath, state);
110
110
  }
111
111
 
112
+ /**
113
+ * Scaffold GitHub-Beads sync files into a target project.
114
+ * Copies workflow, config, and mapping template — never overwrites existing files.
115
+ *
116
+ * @param {string} projectPath - Target project root
117
+ * @param {string} pkgDir - Forge package directory (source of template files)
118
+ * @returns {Promise<{created: string[], skipped: string[]}>}
119
+ */
120
+ async function scaffoldGithubBeadsSync(projectPath, pkgDir) {
121
+ const created = [];
122
+ const skipped = [];
123
+
124
+ // Sync script modules that workflows depend on at runtime
125
+ const syncScripts = [
126
+ 'config.mjs', 'mapping.mjs', 'comment.mjs', 'github-api.mjs',
127
+ 'sanitize.mjs', 'run-bd.mjs', 'label-mapper.mjs', 'index.mjs',
128
+ 'reverse-sync.mjs', 'reverse-sync-cli.mjs',
129
+ ];
130
+
131
+ const files = [
132
+ // Phase 1: GitHub → Beads
133
+ {
134
+ src: path.join(pkgDir, '.github', 'workflows', 'github-to-beads.yml'),
135
+ dest: path.join('.github', 'workflows', 'github-to-beads.yml'),
136
+ },
137
+ // Phase 2: Beads → GitHub
138
+ {
139
+ src: path.join(pkgDir, '.github', 'workflows', 'beads-to-github.yml'),
140
+ dest: path.join('.github', 'workflows', 'beads-to-github.yml'),
141
+ },
142
+ {
143
+ src: path.join(pkgDir, 'scripts', 'github-beads-sync.config.json'),
144
+ dest: path.join('scripts', 'github-beads-sync.config.json'),
145
+ },
146
+ {
147
+ src: null, // generated in-place (empty mapping template)
148
+ dest: path.join('.github', 'beads-mapping.json'),
149
+ content: '{}',
150
+ },
151
+ // Sync script modules
152
+ ...syncScripts.map((name) => ({
153
+ src: path.join(pkgDir, 'scripts', 'github-beads-sync', name),
154
+ dest: path.join('scripts', 'github-beads-sync', name),
155
+ })),
156
+ ];
157
+
158
+ for (const file of files) {
159
+ const destPath = path.join(projectPath, file.dest);
160
+
161
+ // Never overwrite existing files — preserve user customizations
162
+ if (fs.existsSync(destPath)) {
163
+ skipped.push(file.dest);
164
+ continue;
165
+ }
166
+
167
+ // Ensure parent directory exists
168
+ const destDir = path.dirname(destPath);
169
+ await fs.promises.mkdir(destDir, { recursive: true });
170
+
171
+ if (file.src) {
172
+ // Copy from template
173
+ await fs.promises.copyFile(file.src, destPath);
174
+ } else {
175
+ // Write generated content
176
+ await fs.promises.writeFile(destPath, file.content, 'utf-8');
177
+ }
178
+
179
+ created.push(file.dest);
180
+ }
181
+
182
+ return { created, skipped };
183
+ }
184
+
112
185
  module.exports = {
113
186
  saveSetupState,
114
187
  loadSetupState,
115
188
  isSetupComplete,
116
189
  getNextStep,
117
- markStepComplete
190
+ markStepComplete,
191
+ scaffoldGithubBeadsSync
118
192
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "forge-workflow",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
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",
7
- "forge-validate": "bin/forge-validate.js"
7
+ "forge-preflight": "bin/forge-preflight.js"
8
8
  },
9
9
  "workspaces": [
10
10
  "packages/*"
@@ -15,9 +15,8 @@
15
15
  "test:all": "bun run test:setup && bun test",
16
16
  "test:coverage": "c8 --check-coverage bun test",
17
17
  "typecheck": "echo 'No TypeScript in project - skipping type check'",
18
- "lint": "eslint .",
18
+ "lint": "eslint . --max-warnings 0",
19
19
  "check": "bash scripts/validate.sh",
20
- "postinstall": "node ./bin/forge.js",
21
20
  "prepare": "lefthook install || echo 'Note: lefthook not installed. Git hooks disabled. Run: bun add -d lefthook'",
22
21
  "setup": "node ./bin/forge.js setup",
23
22
  "help": "node ./bin/forge.js --help",