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.
- package/.claude/commands/dev.md +26 -0
- package/.claude/commands/plan.md +48 -5
- package/.claude/commands/premerge.md +0 -3
- package/.claude/commands/rollback.md +4 -4
- package/.claude/commands/ship.md +71 -41
- package/.claude/commands/status.md +9 -38
- package/.claude/commands/validate.md +47 -2
- package/.cline/workflows/dev.md +26 -0
- package/.cline/workflows/plan.md +48 -5
- package/.cline/workflows/premerge.md +0 -3
- package/.cline/workflows/rollback.md +4 -4
- package/.cline/workflows/ship.md +71 -41
- package/.cline/workflows/status.md +9 -38
- package/.cline/workflows/validate.md +47 -2
- package/.codex/skills/dev/SKILL.md +26 -0
- package/.codex/skills/plan/SKILL.md +48 -5
- package/.codex/skills/premerge/SKILL.md +0 -3
- package/.codex/skills/rollback/SKILL.md +4 -4
- package/.codex/skills/ship/SKILL.md +71 -41
- package/.codex/skills/status/SKILL.md +9 -38
- package/.codex/skills/validate/SKILL.md +47 -2
- package/.cursor/commands/dev.md +26 -0
- package/.cursor/commands/plan.md +48 -5
- package/.cursor/commands/premerge.md +0 -3
- package/.cursor/commands/rollback.md +4 -4
- package/.cursor/commands/ship.md +71 -41
- package/.cursor/commands/status.md +9 -38
- package/.cursor/commands/validate.md +47 -2
- package/.cursor/hooks/state/continual-learning-index.json +19 -0
- package/.cursor/hooks/state/continual-learning.json +8 -0
- package/.github/prompts/dev.prompt.md +26 -0
- package/.github/prompts/plan.prompt.md +48 -5
- package/.github/prompts/premerge.prompt.md +0 -3
- package/.github/prompts/rollback.prompt.md +4 -4
- package/.github/prompts/ship.prompt.md +71 -41
- package/.github/prompts/status.prompt.md +9 -38
- package/.github/prompts/validate.prompt.md +47 -2
- package/.kilocode/workflows/dev.md +26 -0
- package/.kilocode/workflows/plan.md +48 -5
- package/.kilocode/workflows/premerge.md +0 -3
- package/.kilocode/workflows/rollback.md +4 -4
- package/.kilocode/workflows/ship.md +71 -41
- package/.kilocode/workflows/status.md +9 -38
- package/.kilocode/workflows/validate.md +47 -2
- package/.opencode/commands/dev.md +26 -0
- package/.opencode/commands/plan.md +48 -5
- package/.opencode/commands/premerge.md +0 -3
- package/.opencode/commands/rollback.md +4 -4
- package/.opencode/commands/ship.md +71 -41
- package/.opencode/commands/status.md +9 -38
- package/.opencode/commands/validate.md +47 -2
- package/.roo/commands/dev.md +26 -0
- package/.roo/commands/plan.md +48 -5
- package/.roo/commands/premerge.md +0 -3
- package/.roo/commands/rollback.md +4 -4
- package/.roo/commands/ship.md +71 -41
- package/.roo/commands/status.md +9 -38
- package/.roo/commands/validate.md +47 -2
- package/AGENTS.md +7 -1
- package/CLAUDE.md +5 -4
- package/LICENSE +21 -21
- package/README.md +21 -19
- package/bin/{forge-validate.js → forge-preflight.js} +21 -15
- package/bin/forge.js +209 -138
- package/docs/AGENT_INSTALL_PROMPT.md +1 -1
- package/docs/BEADS_GITHUB_SYNC.md +251 -0
- package/docs/ENHANCED_ONBOARDING.md +6 -6
- package/docs/EXAMPLES.md +4 -4
- package/docs/GREPTILE_SETUP.md +1 -1
- package/docs/MANUAL_REVIEW_GUIDE.md +1 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SETUP.md +1 -2
- package/docs/VALIDATION.md +11 -11
- package/install.sh +1 -3
- package/lib/agents-config.js +3 -3
- package/lib/detect-agent.js +191 -0
- package/lib/detect-worktree.js +47 -0
- package/lib/file-hash.js +26 -0
- package/lib/setup-action-log.js +139 -0
- package/lib/setup-summary-renderer.js +106 -0
- package/lib/setup.js +75 -1
- package/package.json +3 -4
- 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 };
|
package/lib/file-hash.js
ADDED
|
@@ -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
|
+
"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-
|
|
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",
|