forge-cc 0.1.0
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/.forge.json +5 -0
- package/AGENTS.md +42 -0
- package/README.md +283 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +148 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +44 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +57 -0
- package/dist/config/schema.js +15 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/gates/index.d.ts +11 -0
- package/dist/gates/index.js +106 -0
- package/dist/gates/index.js.map +1 -0
- package/dist/gates/lint-gate.d.ts +2 -0
- package/dist/gates/lint-gate.js +66 -0
- package/dist/gates/lint-gate.js.map +1 -0
- package/dist/gates/prd-gate.d.ts +7 -0
- package/dist/gates/prd-gate.js +193 -0
- package/dist/gates/prd-gate.js.map +1 -0
- package/dist/gates/runtime-gate.d.ts +5 -0
- package/dist/gates/runtime-gate.js +99 -0
- package/dist/gates/runtime-gate.js.map +1 -0
- package/dist/gates/tests-gate.d.ts +2 -0
- package/dist/gates/tests-gate.js +116 -0
- package/dist/gates/tests-gate.js.map +1 -0
- package/dist/gates/types-gate.d.ts +2 -0
- package/dist/gates/types-gate.js +59 -0
- package/dist/gates/types-gate.js.map +1 -0
- package/dist/gates/visual-gate.d.ts +6 -0
- package/dist/gates/visual-gate.js +118 -0
- package/dist/gates/visual-gate.js.map +1 -0
- package/dist/go/auto-chain.d.ts +107 -0
- package/dist/go/auto-chain.js +303 -0
- package/dist/go/auto-chain.js.map +1 -0
- package/dist/go/executor.d.ts +130 -0
- package/dist/go/executor.js +409 -0
- package/dist/go/executor.js.map +1 -0
- package/dist/go/finalize.d.ts +58 -0
- package/dist/go/finalize.js +200 -0
- package/dist/go/finalize.js.map +1 -0
- package/dist/go/linear-sync.d.ts +75 -0
- package/dist/go/linear-sync.js +239 -0
- package/dist/go/linear-sync.js.map +1 -0
- package/dist/go/verify-loop.d.ts +47 -0
- package/dist/go/verify-loop.js +172 -0
- package/dist/go/verify-loop.js.map +1 -0
- package/dist/hooks/pre-commit.d.ts +5 -0
- package/dist/hooks/pre-commit.js +69 -0
- package/dist/hooks/pre-commit.js.map +1 -0
- package/dist/linear/client.d.ts +108 -0
- package/dist/linear/client.js +388 -0
- package/dist/linear/client.js.map +1 -0
- package/dist/linear/issues.d.ts +20 -0
- package/dist/linear/issues.js +39 -0
- package/dist/linear/issues.js.map +1 -0
- package/dist/linear/milestones.d.ts +11 -0
- package/dist/linear/milestones.js +32 -0
- package/dist/linear/milestones.js.map +1 -0
- package/dist/linear/projects.d.ts +16 -0
- package/dist/linear/projects.js +50 -0
- package/dist/linear/projects.js.map +1 -0
- package/dist/reporter/human.d.ts +2 -0
- package/dist/reporter/human.js +63 -0
- package/dist/reporter/human.js.map +1 -0
- package/dist/reporter/json.d.ts +2 -0
- package/dist/reporter/json.js +4 -0
- package/dist/reporter/json.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +109 -0
- package/dist/server.js.map +1 -0
- package/dist/spec/generator.d.ts +14 -0
- package/dist/spec/generator.js +206 -0
- package/dist/spec/generator.js.map +1 -0
- package/dist/spec/interview.d.ts +104 -0
- package/dist/spec/interview.js +342 -0
- package/dist/spec/interview.js.map +1 -0
- package/dist/spec/linear-sync.d.ts +48 -0
- package/dist/spec/linear-sync.js +125 -0
- package/dist/spec/linear-sync.js.map +1 -0
- package/dist/spec/scanner.d.ts +45 -0
- package/dist/spec/scanner.js +473 -0
- package/dist/spec/scanner.js.map +1 -0
- package/dist/spec/templates.d.ts +345 -0
- package/dist/spec/templates.js +86 -0
- package/dist/spec/templates.js.map +1 -0
- package/dist/state/reader.d.ts +29 -0
- package/dist/state/reader.js +116 -0
- package/dist/state/reader.js.map +1 -0
- package/dist/state/writer.d.ts +60 -0
- package/dist/state/writer.js +222 -0
- package/dist/state/writer.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/browser.d.ts +10 -0
- package/dist/utils/browser.js +89 -0
- package/dist/utils/browser.js.map +1 -0
- package/hooks/pre-commit-verify.js +103 -0
- package/package.json +68 -0
- package/skills/README.md +33 -0
- package/skills/forge-go.md +332 -0
- package/skills/forge-spec.md +251 -0
- package/skills/forge-triage.md +133 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { readRoadmapProgress } from "./reader.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
async function ensureDir(filePath) {
|
|
9
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
function branchSlug(branch) {
|
|
12
|
+
return branch.replace(/\//g, "-").toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// writeStateFile
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
export async function writeStateFile(projectDir, info) {
|
|
18
|
+
const milestoneRows = info.milestoneTable
|
|
19
|
+
.map((m) => `| ${m.number} | ${m.name} | ${m.status} |`)
|
|
20
|
+
.join("\n");
|
|
21
|
+
const nextActions = info.nextActions
|
|
22
|
+
.map((a, i) => `${i + 1}. ${a}`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
const content = `# ${info.project} — Project State
|
|
25
|
+
|
|
26
|
+
## Current Position
|
|
27
|
+
- **Project:** ${info.project} (build phase)
|
|
28
|
+
- **Milestone:** Milestone ${info.milestone.number} — ${info.milestone.name}
|
|
29
|
+
- **Branch:** ${info.branch}
|
|
30
|
+
- **Active PRD:** \`${info.activePrd}\`
|
|
31
|
+
- **Last Session:** ${info.lastSession}
|
|
32
|
+
|
|
33
|
+
## Milestone Progress
|
|
34
|
+
| Milestone | Name | Status |
|
|
35
|
+
|-----------|------|--------|
|
|
36
|
+
${milestoneRows}
|
|
37
|
+
|
|
38
|
+
## Next Actions
|
|
39
|
+
${nextActions}
|
|
40
|
+
`;
|
|
41
|
+
const filePath = join(projectDir, ".planning", "STATE.md");
|
|
42
|
+
await ensureDir(filePath);
|
|
43
|
+
await writeFile(filePath, content, "utf-8");
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// updateRoadmapMilestone
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export async function updateRoadmapMilestone(projectDir, milestoneNumber, status) {
|
|
49
|
+
const filePath = join(projectDir, ".planning", "ROADMAP.md");
|
|
50
|
+
const raw = await readFile(filePath, "utf-8");
|
|
51
|
+
// Match the specific milestone row and replace its status
|
|
52
|
+
const pattern = new RegExp(`^(\\|\\s*${milestoneNumber}\\s*\\|\\s*.+?\\s*\\|)\\s*.+?\\s*\\|`, "m");
|
|
53
|
+
const match = raw.match(pattern);
|
|
54
|
+
if (!match) {
|
|
55
|
+
throw new Error(`Milestone ${milestoneNumber} not found in ROADMAP.md table`);
|
|
56
|
+
}
|
|
57
|
+
const updated = raw.replace(pattern, `$1 ${status} |`);
|
|
58
|
+
await writeFile(filePath, updated, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// writeSessionMemory
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
export async function writeSessionMemory(projectDir, branch, data) {
|
|
64
|
+
const slug = branchSlug(branch);
|
|
65
|
+
const filePath = join(projectDir, ".claude", "memory", `session-${slug}.md`);
|
|
66
|
+
const content = `# Session State
|
|
67
|
+
**Date:** ${data.date}
|
|
68
|
+
**Developer:** ${data.developer}
|
|
69
|
+
**Branch:** ${branch}
|
|
70
|
+
**Working On:** ${data.workingOn}
|
|
71
|
+
**Status:** ${data.status}
|
|
72
|
+
**Next:** ${data.next}
|
|
73
|
+
**Blockers:** ${data.blockers}
|
|
74
|
+
`;
|
|
75
|
+
await ensureDir(filePath);
|
|
76
|
+
await writeFile(filePath, content, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// commitMilestoneWork — commits and optionally pushes milestone work
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
export function commitMilestoneWork(options) {
|
|
82
|
+
const { projectDir, milestoneNumber, milestoneName, filesToStage, push, branch, } = options;
|
|
83
|
+
if (filesToStage.length === 0) {
|
|
84
|
+
throw new Error("filesToStage must contain at least one file — never use git add .");
|
|
85
|
+
}
|
|
86
|
+
// Check if git is available
|
|
87
|
+
try {
|
|
88
|
+
execSync("git --version", { cwd: projectDir, stdio: "pipe" });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("git is not available on this system. Cannot commit milestone work.");
|
|
92
|
+
}
|
|
93
|
+
// Check for detached HEAD
|
|
94
|
+
try {
|
|
95
|
+
const headRef = execSync("git symbolic-ref HEAD", {
|
|
96
|
+
cwd: projectDir,
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
stdio: "pipe",
|
|
99
|
+
}).trim();
|
|
100
|
+
if (!headRef) {
|
|
101
|
+
throw new Error("Detached HEAD detected — cannot commit. Check out a branch first.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof Error && err.message.includes("Detached HEAD")) {
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
// git symbolic-ref fails on detached HEAD with exit code 128
|
|
109
|
+
throw new Error("Detached HEAD detected — cannot commit. Check out a branch first.");
|
|
110
|
+
}
|
|
111
|
+
// Stage only the specified files, skipping files that don't exist
|
|
112
|
+
for (const file of filesToStage) {
|
|
113
|
+
try {
|
|
114
|
+
execSync(`git add ${JSON.stringify(file)}`, {
|
|
115
|
+
cwd: projectDir,
|
|
116
|
+
stdio: "pipe",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// File may not exist or be outside the repo — skip and continue
|
|
121
|
+
console.warn(`Warning: Could not stage file "${file}" — skipping.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Commit with a descriptive message
|
|
125
|
+
const commitMessage = `feat: ${milestoneName} (Milestone ${milestoneNumber})`;
|
|
126
|
+
try {
|
|
127
|
+
execSync(`git commit -m ${JSON.stringify(commitMessage)}`, {
|
|
128
|
+
cwd: projectDir,
|
|
129
|
+
stdio: "pipe",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
134
|
+
throw new Error(`git commit failed: ${msg}`);
|
|
135
|
+
}
|
|
136
|
+
// Read back the commit SHA
|
|
137
|
+
const commitSha = execSync("git rev-parse HEAD", {
|
|
138
|
+
cwd: projectDir,
|
|
139
|
+
encoding: "utf-8",
|
|
140
|
+
}).trim();
|
|
141
|
+
// Optionally push to remote
|
|
142
|
+
let pushed = false;
|
|
143
|
+
if (push && branch) {
|
|
144
|
+
try {
|
|
145
|
+
execSync(`git push origin ${branch}`, {
|
|
146
|
+
cwd: projectDir,
|
|
147
|
+
stdio: "pipe",
|
|
148
|
+
});
|
|
149
|
+
pushed = true;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
console.warn(`Warning: git push failed: ${msg}. Commit was created locally.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { commitSha, pushed };
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// isLastMilestone — detects if this is the final pending milestone
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
export async function isLastMilestone(projectDir, milestoneNumber) {
|
|
162
|
+
const roadmap = await readRoadmapProgress(projectDir);
|
|
163
|
+
if (!roadmap || roadmap.milestones.length === 0) {
|
|
164
|
+
return true; // No roadmap data — treat as last by default
|
|
165
|
+
}
|
|
166
|
+
const maxMilestone = Math.max(...roadmap.milestones.map((m) => m.number));
|
|
167
|
+
// If this IS the highest milestone number, it's the last
|
|
168
|
+
if (milestoneNumber >= maxMilestone) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
// If all milestones after this one are already complete, this is effectively last
|
|
172
|
+
const remaining = roadmap.milestones.filter((m) => m.number > milestoneNumber &&
|
|
173
|
+
!m.status.toLowerCase().startsWith("complete"));
|
|
174
|
+
return remaining.length === 0;
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// updateMilestoneProgress — updates all state docs after milestone completion
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
export async function updateMilestoneProgress(options) {
|
|
180
|
+
const { projectDir, project, milestoneNumber, milestoneName, branch, activePrd, developer, nextMilestone, milestoneTable, } = options;
|
|
181
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
182
|
+
// 1. Mark this milestone as complete in ROADMAP.md
|
|
183
|
+
await updateRoadmapMilestone(projectDir, milestoneNumber, `Complete (${today})`);
|
|
184
|
+
// 2. Build next actions based on whether there's a next milestone
|
|
185
|
+
const nextActions = nextMilestone
|
|
186
|
+
? [
|
|
187
|
+
`Begin Milestone ${nextMilestone.number} — ${nextMilestone.name}`,
|
|
188
|
+
"Read PRD for next milestone scope",
|
|
189
|
+
"Spawn agent team for next milestone",
|
|
190
|
+
]
|
|
191
|
+
: [
|
|
192
|
+
"All milestones complete — final review and cleanup",
|
|
193
|
+
"Merge feature branch to main",
|
|
194
|
+
"Archive planning docs",
|
|
195
|
+
];
|
|
196
|
+
// 3. Update STATE.md with current position
|
|
197
|
+
const stateTarget = nextMilestone ?? {
|
|
198
|
+
number: milestoneNumber,
|
|
199
|
+
name: milestoneName,
|
|
200
|
+
};
|
|
201
|
+
await writeStateFile(projectDir, {
|
|
202
|
+
project,
|
|
203
|
+
milestone: stateTarget,
|
|
204
|
+
branch,
|
|
205
|
+
activePrd,
|
|
206
|
+
lastSession: today,
|
|
207
|
+
milestoneTable,
|
|
208
|
+
nextActions,
|
|
209
|
+
});
|
|
210
|
+
// 4. Write session memory for this branch
|
|
211
|
+
await writeSessionMemory(projectDir, branch, {
|
|
212
|
+
date: today,
|
|
213
|
+
developer,
|
|
214
|
+
workingOn: `Milestone ${milestoneNumber} — ${milestoneName}`,
|
|
215
|
+
status: "Complete",
|
|
216
|
+
next: nextMilestone
|
|
217
|
+
? `Milestone ${nextMilestone.number} — ${nextMilestone.name}`
|
|
218
|
+
: "All milestones complete",
|
|
219
|
+
blockers: "None",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../src/state/writer.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Structured error from a verification gate */
|
|
2
|
+
export interface GateError {
|
|
3
|
+
file?: string;
|
|
4
|
+
line?: number;
|
|
5
|
+
message: string;
|
|
6
|
+
remediation?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Result from a single verification gate */
|
|
9
|
+
export interface GateResult {
|
|
10
|
+
gate: string;
|
|
11
|
+
passed: boolean;
|
|
12
|
+
errors: GateError[];
|
|
13
|
+
warnings: string[];
|
|
14
|
+
duration_ms: number;
|
|
15
|
+
}
|
|
16
|
+
/** Extended result for visual validation with screenshots */
|
|
17
|
+
export interface VisualResult extends GateResult {
|
|
18
|
+
screenshots: Array<{
|
|
19
|
+
page: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}>;
|
|
22
|
+
consoleErrors: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Input for the full verification pipeline */
|
|
25
|
+
export interface PipelineInput {
|
|
26
|
+
projectDir: string;
|
|
27
|
+
gates?: string[];
|
|
28
|
+
prdPath?: string;
|
|
29
|
+
milestoneType?: "ui" | "data" | "mixed";
|
|
30
|
+
pages?: string[];
|
|
31
|
+
apiEndpoints?: string[];
|
|
32
|
+
maxIterations?: number;
|
|
33
|
+
devServerCommand?: string;
|
|
34
|
+
devServerPort?: number;
|
|
35
|
+
baseBranch?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Result from the full verification pipeline */
|
|
38
|
+
export interface PipelineResult {
|
|
39
|
+
passed: boolean;
|
|
40
|
+
iteration: number;
|
|
41
|
+
maxIterations: number;
|
|
42
|
+
gates: GateResult[];
|
|
43
|
+
report: string;
|
|
44
|
+
}
|
|
45
|
+
/** Configuration from .forge.json */
|
|
46
|
+
export interface ForgeConfig {
|
|
47
|
+
gates: string[];
|
|
48
|
+
maxIterations: number;
|
|
49
|
+
verifyFreshness: number;
|
|
50
|
+
devServer?: {
|
|
51
|
+
command: string;
|
|
52
|
+
port: number;
|
|
53
|
+
readyPattern?: string;
|
|
54
|
+
};
|
|
55
|
+
prdPath?: string;
|
|
56
|
+
linearProject?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Verification cache written to .forge/last-verify.json */
|
|
59
|
+
export interface VerifyCache {
|
|
60
|
+
passed: boolean;
|
|
61
|
+
timestamp: string;
|
|
62
|
+
gates: GateResult[];
|
|
63
|
+
branch: string;
|
|
64
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Browser } from "playwright";
|
|
2
|
+
import { type ChildProcess } from "node:child_process";
|
|
3
|
+
export declare function getBrowser(): Promise<Browser>;
|
|
4
|
+
export declare function closeBrowser(): Promise<void>;
|
|
5
|
+
export declare function startDevServer(projectDir: string, command?: string, port?: number): Promise<{
|
|
6
|
+
port: number;
|
|
7
|
+
process: ChildProcess;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function stopDevServer(): Promise<void>;
|
|
10
|
+
export declare function waitForServer(port: number, timeoutMs?: number): Promise<boolean>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { spawn, execSync } from "node:child_process";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
let browserInstance = null;
|
|
5
|
+
let devServerProcess = null;
|
|
6
|
+
export async function getBrowser() {
|
|
7
|
+
if (!browserInstance || !browserInstance.isConnected()) {
|
|
8
|
+
try {
|
|
9
|
+
browserInstance = await chromium.launch({ headless: true });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13
|
+
if (message.includes("Executable doesn't exist") || message.includes("browserType.launch")) {
|
|
14
|
+
throw new Error(`Playwright browsers are not installed. Run "npx playwright install chromium" to fix this. Original error: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return browserInstance;
|
|
20
|
+
}
|
|
21
|
+
export async function closeBrowser() {
|
|
22
|
+
if (browserInstance) {
|
|
23
|
+
await browserInstance.close();
|
|
24
|
+
browserInstance = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function startDevServer(projectDir, command, port) {
|
|
28
|
+
const resolvedCommand = command ?? "npm run dev";
|
|
29
|
+
const resolvedPort = port ?? 3000;
|
|
30
|
+
// Kill any existing dev server before starting a new one
|
|
31
|
+
await stopDevServer();
|
|
32
|
+
devServerProcess = spawn(resolvedCommand, {
|
|
33
|
+
cwd: projectDir,
|
|
34
|
+
shell: true,
|
|
35
|
+
stdio: "pipe",
|
|
36
|
+
});
|
|
37
|
+
// Wait for the server to become reachable
|
|
38
|
+
const ready = await waitForServer(resolvedPort);
|
|
39
|
+
if (!ready) {
|
|
40
|
+
await stopDevServer();
|
|
41
|
+
throw new Error(`Dev server failed to start on port ${resolvedPort} within timeout`);
|
|
42
|
+
}
|
|
43
|
+
return { port: resolvedPort, process: devServerProcess };
|
|
44
|
+
}
|
|
45
|
+
export async function stopDevServer() {
|
|
46
|
+
if (devServerProcess) {
|
|
47
|
+
const proc = devServerProcess;
|
|
48
|
+
devServerProcess = null;
|
|
49
|
+
try {
|
|
50
|
+
if (process.platform === "win32" && proc.pid) {
|
|
51
|
+
// On Windows, proc.kill() doesn't kill the child process tree.
|
|
52
|
+
// Use taskkill with /T (tree) /F (force) to kill the process and its children.
|
|
53
|
+
try {
|
|
54
|
+
execSync(`taskkill /pid ${proc.pid} /T /F`, { stdio: "pipe" });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// taskkill may fail if the process already exited — fall back to proc.kill()
|
|
58
|
+
try {
|
|
59
|
+
proc.kill();
|
|
60
|
+
}
|
|
61
|
+
catch { /* already exited */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
proc.kill();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Process may have already exited — ignore
|
|
70
|
+
}
|
|
71
|
+
// Brief wait for cleanup
|
|
72
|
+
await setTimeout(500);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function waitForServer(port, timeoutMs) {
|
|
76
|
+
const deadline = Date.now() + (timeoutMs ?? 30_000);
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
try {
|
|
79
|
+
await fetch(`http://localhost:${port}`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Server not ready yet — wait and retry
|
|
84
|
+
await setTimeout(1000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/utils/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
// Read hook input from stdin
|
|
8
|
+
let input = "";
|
|
9
|
+
process.stdin.setEncoding("utf-8");
|
|
10
|
+
process.stdin.on("data", (chunk) => {
|
|
11
|
+
input += chunk;
|
|
12
|
+
});
|
|
13
|
+
process.stdin.on("end", () => {
|
|
14
|
+
try {
|
|
15
|
+
const hookData = JSON.parse(input);
|
|
16
|
+
const result = checkPreCommit(hookData);
|
|
17
|
+
console.log(JSON.stringify(result));
|
|
18
|
+
} catch {
|
|
19
|
+
// On any error, allow (don't block the user's work)
|
|
20
|
+
console.log(JSON.stringify({ decision: "allow" }));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function checkPreCommit(hookData) {
|
|
25
|
+
// Only intercept Bash calls with "git commit" in the command
|
|
26
|
+
if (hookData.tool_name !== "Bash") {
|
|
27
|
+
return { decision: "allow" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const command = hookData.tool_input?.command ?? "";
|
|
31
|
+
if (!command.includes("git commit")) {
|
|
32
|
+
return { decision: "allow" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const projectDir = process.cwd();
|
|
36
|
+
|
|
37
|
+
// Check 1: Wrong branch protection
|
|
38
|
+
try {
|
|
39
|
+
const branch = execSync("git branch --show-current", {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
}).trim();
|
|
42
|
+
if (branch === "main" || branch === "master") {
|
|
43
|
+
return {
|
|
44
|
+
decision: "block",
|
|
45
|
+
reason: `Forge: Cannot commit directly to ${branch}. Create a feature branch first.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Can't determine branch — allow
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check 2: Verify cache exists
|
|
53
|
+
const cachePath = join(projectDir, ".forge", "last-verify.json");
|
|
54
|
+
if (!existsSync(cachePath)) {
|
|
55
|
+
return {
|
|
56
|
+
decision: "block",
|
|
57
|
+
reason:
|
|
58
|
+
"Forge: No verification found. Run `npx forge verify` before committing.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
64
|
+
|
|
65
|
+
// Check 3: Did verification pass?
|
|
66
|
+
if (!cache.passed) {
|
|
67
|
+
return {
|
|
68
|
+
decision: "block",
|
|
69
|
+
reason:
|
|
70
|
+
"Forge: Last verification FAILED. Fix errors and run `npx forge verify` again.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check 4: Is it fresh? (default 10 minutes = 600000ms)
|
|
75
|
+
let freshness = 600_000;
|
|
76
|
+
const configPath = join(projectDir, ".forge.json");
|
|
77
|
+
if (existsSync(configPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
80
|
+
if (config.verifyFreshness) freshness = config.verifyFreshness;
|
|
81
|
+
} catch {
|
|
82
|
+
/* use default */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const age = Date.now() - new Date(cache.timestamp).getTime();
|
|
87
|
+
if (age > freshness) {
|
|
88
|
+
const ageMin = Math.round(age / 60_000);
|
|
89
|
+
return {
|
|
90
|
+
decision: "block",
|
|
91
|
+
reason: `Forge: Verification is stale (${ageMin}min old). Run \`npx forge verify\` again.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { decision: "allow" };
|
|
96
|
+
} catch {
|
|
97
|
+
return {
|
|
98
|
+
decision: "block",
|
|
99
|
+
reason:
|
|
100
|
+
"Forge: Could not read verification cache. Run `npx forge verify`.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forge-cc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pre-PR verification harness for Claude Code agents — gate runner + CLI + MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Troy Hoffman",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/troyhoffman/forge-cc.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"forge",
|
|
14
|
+
"verification",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"mcp",
|
|
17
|
+
"pre-commit",
|
|
18
|
+
"lint",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"main": "dist/gates/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./dist/gates/index.js",
|
|
27
|
+
"./cli": "./dist/cli.js",
|
|
28
|
+
"./server": "./dist/server.js"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"forge": "dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist/",
|
|
35
|
+
"skills/",
|
|
36
|
+
"hooks/",
|
|
37
|
+
".forge.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"AGENTS.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"dev": "tsc --watch",
|
|
44
|
+
"start": "node dist/index.js",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"verify": "node dist/cli.js verify",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
52
|
+
"commander": "^13.0.0",
|
|
53
|
+
"zod": "^3.24.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"playwright": ">=1.40.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"playwright": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^22.0.0",
|
|
65
|
+
"typescript": "^5.7.0",
|
|
66
|
+
"vitest": "^3.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# forge-cc Skills
|
|
2
|
+
|
|
3
|
+
Skills are markdown instruction files that Claude Code discovers and executes. When a user types a skill command (e.g., `/forge:triage`), Claude Code reads the corresponding markdown file and follows its instructions using available tools (MCP tools, Bash, file operations, etc.).
|
|
4
|
+
|
|
5
|
+
Skills are prompts, not code. The LLM interprets the instructions and orchestrates tool calls to execute the workflow.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Copy or symlink the skill files into your Claude Code skills directory:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Copy all skills
|
|
13
|
+
cp skills/forge-*.md ~/.claude/skills/
|
|
14
|
+
|
|
15
|
+
# Or symlink (updates automatically with forge-cc)
|
|
16
|
+
ln -s "$(pwd)/skills/forge-triage.md" ~/.claude/skills/forge-triage.md
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Skills are also distributed via `npm install forge-cc` and can be found in `node_modules/forge-cc/skills/`.
|
|
20
|
+
|
|
21
|
+
## Available Skills
|
|
22
|
+
|
|
23
|
+
| Skill | Command | Description |
|
|
24
|
+
|-------|---------|-------------|
|
|
25
|
+
| Triage | `/forge:triage` | Brain dump to Linear projects. Paste unstructured ideas, get organized projects. |
|
|
26
|
+
| Spec | `/forge:spec` | Interview to PRD. Select a project, answer questions, get milestones + issues. *(coming soon)* |
|
|
27
|
+
| Go | `/forge:go` | Execute milestones. Wave-based agents, self-healing verification, auto mode. *(coming soon)* |
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
- Claude Code with MCP tools enabled
|
|
32
|
+
- Linear MCP tools configured (`mcp__linear__*`) for triage and spec skills
|
|
33
|
+
- `forge-cc` installed in the project for verification gates (used by go skill)
|