forge-cc 0.1.41 → 1.0.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/README.md +454 -338
- package/dist/cli.js +194 -935
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +49 -56
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +37 -125
- package/dist/config/schema.js +13 -28
- package/dist/config/schema.js.map +1 -1
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +148 -0
- package/dist/doctor.js.map +1 -0
- package/dist/gates/index.d.ts +14 -12
- package/dist/gates/index.js +53 -105
- package/dist/gates/index.js.map +1 -1
- package/dist/gates/lint-gate.d.ts +2 -2
- package/dist/gates/lint-gate.js +60 -66
- package/dist/gates/lint-gate.js.map +1 -1
- package/dist/gates/tests-gate.d.ts +2 -4
- package/dist/gates/tests-gate.js +75 -203
- package/dist/gates/tests-gate.js.map +1 -1
- package/dist/gates/types-gate.d.ts +2 -2
- package/dist/gates/types-gate.js +53 -59
- package/dist/gates/types-gate.js.map +1 -1
- package/dist/linear/client.d.ts +31 -108
- package/dist/linear/client.js +88 -388
- package/dist/linear/client.js.map +1 -1
- package/dist/linear/sync.d.ts +15 -0
- package/dist/linear/sync.js +102 -0
- package/dist/linear/sync.js.map +1 -0
- package/dist/runner/loop.d.ts +4 -0
- package/dist/runner/loop.js +168 -0
- package/dist/runner/loop.js.map +1 -0
- package/dist/runner/prompt.d.ts +14 -0
- package/dist/runner/prompt.js +59 -0
- package/dist/runner/prompt.js.map +1 -0
- package/dist/runner/update.d.ts +1 -0
- package/dist/runner/update.js +72 -0
- package/dist/runner/update.js.map +1 -0
- package/dist/server.d.ts +6 -2
- package/dist/server.js +43 -101
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +208 -0
- package/dist/setup.js.map +1 -0
- package/dist/state/cache.d.ts +3 -0
- package/dist/state/cache.js +23 -0
- package/dist/state/cache.js.map +1 -0
- package/dist/state/status.d.ts +66 -0
- package/dist/state/status.js +96 -0
- package/dist/state/status.js.map +1 -0
- package/dist/types.d.ts +46 -114
- package/dist/worktree/manager.d.ts +6 -103
- package/dist/worktree/manager.js +25 -296
- package/dist/worktree/manager.js.map +1 -1
- package/hooks/pre-commit-verify.js +109 -109
- package/package.json +3 -2
- package/skills/forge-go.md +20 -13
- package/skills/forge-setup.md +149 -388
- package/skills/forge-spec.md +367 -342
- package/skills/forge-triage.md +179 -133
- package/skills/forge-update.md +87 -93
|
@@ -1,109 +1,109 @@
|
|
|
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
|
-
let branch = "unknown";
|
|
39
|
-
try {
|
|
40
|
-
branch = execSync("git branch --show-current", {
|
|
41
|
-
encoding: "utf-8",
|
|
42
|
-
}).trim();
|
|
43
|
-
if (branch === "main" || branch === "master") {
|
|
44
|
-
return {
|
|
45
|
-
decision: "block",
|
|
46
|
-
reason: `Forge: Cannot commit directly to ${branch}. Create a feature branch first.`,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
// Can't determine branch — allow
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check 2: Verify cache exists — per-branch first, fall back to legacy path
|
|
54
|
-
const slug = branch.replace(/\//g, "-").toLowerCase();
|
|
55
|
-
const perBranchCachePath = join(projectDir, ".forge", "verify-cache", `${slug}.json`);
|
|
56
|
-
const legacyCachePath = join(projectDir, ".forge", "last-verify.json");
|
|
57
|
-
const cachePath = existsSync(perBranchCachePath)
|
|
58
|
-
? perBranchCachePath
|
|
59
|
-
: legacyCachePath;
|
|
60
|
-
if (!existsSync(cachePath)) {
|
|
61
|
-
return {
|
|
62
|
-
decision: "block",
|
|
63
|
-
reason:
|
|
64
|
-
"Forge: No verification found. Run `npx forge verify` before committing.",
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
70
|
-
|
|
71
|
-
// Check 3: Did verification pass?
|
|
72
|
-
if (
|
|
73
|
-
return {
|
|
74
|
-
decision: "block",
|
|
75
|
-
reason:
|
|
76
|
-
"Forge: Last verification FAILED. Fix errors and run `npx forge verify` again.",
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check 4: Is it fresh? (default 10 minutes = 600000ms)
|
|
81
|
-
let freshness = 600_000;
|
|
82
|
-
const configPath = join(projectDir, ".forge.json");
|
|
83
|
-
if (existsSync(configPath)) {
|
|
84
|
-
try {
|
|
85
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
86
|
-
if (config.verifyFreshness) freshness = config.verifyFreshness;
|
|
87
|
-
} catch {
|
|
88
|
-
/* use default */
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const age = Date.now() - new Date(cache.timestamp).getTime();
|
|
93
|
-
if (age > freshness) {
|
|
94
|
-
const ageMin = Math.round(age / 60_000);
|
|
95
|
-
return {
|
|
96
|
-
decision: "block",
|
|
97
|
-
reason: `Forge: Verification is stale (${ageMin}min old). Run \`npx forge verify\` again.`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { decision: "allow" };
|
|
102
|
-
} catch {
|
|
103
|
-
return {
|
|
104
|
-
decision: "block",
|
|
105
|
-
reason:
|
|
106
|
-
"Forge: Could not read verification cache. Run `npx forge verify`.",
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
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
|
+
let branch = "unknown";
|
|
39
|
+
try {
|
|
40
|
+
branch = execSync("git branch --show-current", {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
}).trim();
|
|
43
|
+
if (branch === "main" || branch === "master") {
|
|
44
|
+
return {
|
|
45
|
+
decision: "block",
|
|
46
|
+
reason: `Forge: Cannot commit directly to ${branch}. Create a feature branch first.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Can't determine branch — allow
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check 2: Verify cache exists — per-branch first, fall back to legacy path
|
|
54
|
+
const slug = branch.replace(/\//g, "-").toLowerCase();
|
|
55
|
+
const perBranchCachePath = join(projectDir, ".forge", "verify-cache", `${slug}.json`);
|
|
56
|
+
const legacyCachePath = join(projectDir, ".forge", "last-verify.json");
|
|
57
|
+
const cachePath = existsSync(perBranchCachePath)
|
|
58
|
+
? perBranchCachePath
|
|
59
|
+
: legacyCachePath;
|
|
60
|
+
if (!existsSync(cachePath)) {
|
|
61
|
+
return {
|
|
62
|
+
decision: "block",
|
|
63
|
+
reason:
|
|
64
|
+
"Forge: No verification found. Run `npx forge verify` before committing.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
70
|
+
|
|
71
|
+
// Check 3: Did verification pass? (v2 format: cache.result === 'PASSED')
|
|
72
|
+
if (cache.result !== 'PASSED') {
|
|
73
|
+
return {
|
|
74
|
+
decision: "block",
|
|
75
|
+
reason:
|
|
76
|
+
"Forge: Last verification FAILED. Fix errors and run `npx forge verify` again.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check 4: Is it fresh? (default 10 minutes = 600000ms)
|
|
81
|
+
let freshness = 600_000;
|
|
82
|
+
const configPath = join(projectDir, ".forge.json");
|
|
83
|
+
if (existsSync(configPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
86
|
+
if (config.verifyFreshness) freshness = config.verifyFreshness;
|
|
87
|
+
} catch {
|
|
88
|
+
/* use default */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const age = Date.now() - new Date(cache.timestamp).getTime();
|
|
93
|
+
if (age > freshness) {
|
|
94
|
+
const ageMin = Math.round(age / 60_000);
|
|
95
|
+
return {
|
|
96
|
+
decision: "block",
|
|
97
|
+
reason: `Forge: Verification is stale (${ageMin}min old). Run \`npx forge verify\` again.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { decision: "allow" };
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
decision: "block",
|
|
105
|
+
reason:
|
|
106
|
+
"Forge: Could not read verification cache. Run `npx forge verify`.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-cc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Forge — verification harness for Claude Code agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Troy Hoffman",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"prepublishOnly": "npm run build"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@linear/sdk": "^75.0.0",
|
|
53
54
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
54
55
|
"commander": "^13.0.0",
|
|
55
56
|
"zod": "^3.24.0"
|
package/skills/forge-go.md
CHANGED
|
@@ -4,7 +4,7 @@ Execute milestones from your PRD with real agent teams (TeamCreate/SendMessage),
|
|
|
4
4
|
|
|
5
5
|
## Instructions
|
|
6
6
|
|
|
7
|
-
Follow these steps exactly.
|
|
7
|
+
Follow these steps exactly. This skill drives the full agent orchestration — there is no backing TypeScript engine. The forge CLI provides verification (`forge verify`), state management (`forge status`), and Linear sync (`forge linear-sync`) as deterministic commands.
|
|
8
8
|
|
|
9
9
|
### Step 1 — Orient + Choose Mode
|
|
10
10
|
|
|
@@ -62,7 +62,7 @@ Verify the execution environment is ready:
|
|
|
62
62
|
|
|
63
63
|
> You're on the main branch. Switch to your feature branch first: `git checkout {branch}`
|
|
64
64
|
|
|
65
|
-
2. **Milestone exists:** Read ONLY the current milestone section from the PRD (progressive disclosure — NOT the full PRD).
|
|
65
|
+
2. **Milestone exists:** Read ONLY the current milestone section from the PRD (progressive disclosure — NOT the full PRD). Match the `### Milestone N:` header and extract until the next milestone header.
|
|
66
66
|
|
|
67
67
|
3. **Not already complete:** Check the status file to confirm this milestone is not already marked complete. If it is, advance to the next pending milestone.
|
|
68
68
|
|
|
@@ -102,20 +102,20 @@ This establishes the visual baseline for regression detection.
|
|
|
102
102
|
|
|
103
103
|
**The visual gate MUST run if configured — do not skip based on your assessment of whether changes affect the UI.** Parser changes, data fixes, and test-only changes can surface unexpected visual regressions. The only valid reason to skip is: `.forge.json` does NOT include `visual` in gates AND does NOT specify a `devServerUrl`.
|
|
104
104
|
|
|
105
|
-
### Step 2.5 — Session Isolation (
|
|
105
|
+
### Step 2.5 — Session Isolation (Future Enhancement)
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
> **Note:** Session isolation via git worktrees is not yet implemented in the skill flow. The current implementation runs in the main working directory. The design below describes the planned behavior for a future version.
|
|
108
108
|
|
|
109
|
-
**
|
|
109
|
+
**Planned behavior:**
|
|
110
110
|
1. A worktree is created at `../.forge-wt/<repo>/<session-id>/` based on the feature branch
|
|
111
111
|
2. A session is registered in `.forge/sessions.json`
|
|
112
112
|
3. All wave execution happens inside the worktree directory
|
|
113
113
|
4. After completion, changes are merged back to the feature branch
|
|
114
114
|
5. The worktree and session are cleaned up
|
|
115
115
|
|
|
116
|
-
**Why:** Multiple users or terminals can run `/forge:go` simultaneously without corrupting each other's work. Each session gets an isolated copy of the codebase.
|
|
116
|
+
**Why (when implemented):** Multiple users or terminals can run `/forge:go` simultaneously without corrupting each other's work. Each session gets an isolated copy of the codebase.
|
|
117
117
|
|
|
118
|
-
**
|
|
118
|
+
**Current behavior:** Execution runs in the main working directory. Avoid running multiple `/forge:go` sessions concurrently until worktree isolation is implemented.
|
|
119
119
|
|
|
120
120
|
### Step 3 — Create Agent Team + Execute Waves
|
|
121
121
|
|
|
@@ -157,6 +157,8 @@ Parse the milestone section from the PRD. Each milestone contains waves with age
|
|
|
157
157
|
- Modifies: file3
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
**If no wave definitions exist:** For milestones with fewer than 3 issues, treat the entire milestone as a single wave with one agent. For milestones with 3+ distinct issues, design waves that group related issues and respect dependencies (e.g., foundational work in Wave 1, dependent work in Wave 2, integration/testing in Wave 3).
|
|
161
|
+
|
|
160
162
|
#### 3c. Execute Each Wave
|
|
161
163
|
|
|
162
164
|
For each wave, in order:
|
|
@@ -320,8 +322,8 @@ When mechanical verification fails after a wave (or after reviewer fixes):
|
|
|
320
322
|
## Verification Failed After {N} Iterations
|
|
321
323
|
|
|
322
324
|
### Remaining Errors:
|
|
323
|
-
- types: src/
|
|
324
|
-
- lint: src/
|
|
325
|
+
- types: src/gates/types.ts:42 — Type 'string' is not assignable to type 'number'
|
|
326
|
+
- lint: src/gates/lint.ts:55 — Unused variable 'foo'
|
|
325
327
|
|
|
326
328
|
The self-healing loop could not resolve all errors.
|
|
327
329
|
Please fix the remaining issues manually, then run `/forge:go` again.
|
|
@@ -385,7 +387,12 @@ Pass `--last` if this is the last milestone. Pass `--pr-url {url}` if a PR was c
|
|
|
385
387
|
- If NOT last milestone: adds progress comments to milestone issues
|
|
386
388
|
- If last milestone: transitions all project issues and the project to "In Review", adds PR link comments
|
|
387
389
|
|
|
388
|
-
|
|
390
|
+
The CLI prints informational output about what it did or why it skipped:
|
|
391
|
+
- Missing API key: `[forge] LINEAR_API_KEY not set, skipping Linear sync`
|
|
392
|
+
- Missing teamId: `[forge] No linearTeamId in status file, skipping Linear sync`
|
|
393
|
+
- On success: `[forge] Transitioning N issues to {state}`, `[forge] Updated project {id} to {state}`
|
|
394
|
+
|
|
395
|
+
These messages are informational, not errors. The skill should continue execution regardless of the output.
|
|
389
396
|
|
|
390
397
|
If no Linear project ID is found in either location, skip this step silently.
|
|
391
398
|
|
|
@@ -554,7 +561,7 @@ If a Linear project ID is found:
|
|
|
554
561
|
npx forge linear-sync start --slug {slug} --milestone {number}
|
|
555
562
|
```
|
|
556
563
|
|
|
557
|
-
This command programmatically transitions milestone issues to "In Progress" and the project to "In Progress".
|
|
564
|
+
This command programmatically transitions milestone issues to "In Progress" and the project to "In Progress". The CLI prints informational output (e.g., `[forge] Transitioning N issues to In Progress` or `[forge] LINEAR_API_KEY not set, skipping Linear sync`). These are informational messages, not errors — continue execution regardless.
|
|
558
565
|
|
|
559
566
|
If no Linear project ID is found, skip silently.
|
|
560
567
|
|
|
@@ -562,14 +569,14 @@ If no Linear project ID is found, skip silently.
|
|
|
562
569
|
|
|
563
570
|
- **No PRD:** Abort with message to run `/forge:spec` first.
|
|
564
571
|
- **No status files:** Same as no PRD — abort with message to run `/forge:spec` first.
|
|
565
|
-
- **No waves in milestone:** The milestone section may not have structured wave definitions (e.g., it was written by hand without the spec engine).
|
|
572
|
+
- **No waves in milestone:** The milestone section may not have structured wave definitions (e.g., it was written by hand without the spec engine). For milestones with fewer than 3 issues, treat the entire milestone as a single wave with one agent whose task is the milestone's goal. For milestones with 3+ distinct issues, design waves that group related issues and respect dependencies.
|
|
566
573
|
- **Agent failure:** If an agent in a wave fails (exits with error, times out), record the failure, include the error in the wave result, and proceed to verification. The self-healing loop may fix the issue.
|
|
567
574
|
- **Branch diverged:** If `git push` fails due to divergence, attempt `git pull --rebase` first. If that fails, stop and ask the user.
|
|
568
575
|
- **Interrupted execution:** If execution is interrupted mid-wave, the status files are NOT updated. Running `/forge:go` again will retry the same milestone from the beginning. Completed agents' work will be in the working tree — the new run's verification will detect what's already working. Shut down any remaining team members before retrying.
|
|
569
576
|
- **Empty milestone section:** If the PRD has a milestone header but no content, abort with:
|
|
570
577
|
> Milestone {N} has no wave definitions. Update the PRD with agent assignments before running /forge:go.
|
|
571
578
|
- **Already on correct milestone:** If the status file's current milestone matches the target, proceed normally (this is the expected case).
|
|
572
|
-
- **Worktree conflict:**
|
|
579
|
+
- **Worktree conflict (future):** When session isolation is implemented, if the worktree directory already exists (e.g., from a crashed session), run `npx forge cleanup` first. If that fails, fall back to main directory execution.
|
|
573
580
|
- **Linear auth fails:** Warn but continue execution. Linear sync is not blocking — the milestone should still execute and complete without Linear.
|
|
574
581
|
- **Team creation fails:** If TeamCreate fails, fall back to the legacy Task-based agent spawning (fire-and-forget without SendMessage). Log a warning that review and consensus will be skipped.
|
|
575
582
|
- **Reviewer timeout:** If the reviewer does not respond within 5 minutes, log a warning and proceed without review findings. Do not block wave progression on a stalled reviewer.
|