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.
Files changed (62) hide show
  1. package/README.md +454 -338
  2. package/dist/cli.js +194 -935
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/loader.d.ts +1 -1
  5. package/dist/config/loader.js +49 -56
  6. package/dist/config/loader.js.map +1 -1
  7. package/dist/config/schema.d.ts +37 -125
  8. package/dist/config/schema.js +13 -28
  9. package/dist/config/schema.js.map +1 -1
  10. package/dist/doctor.d.ts +10 -0
  11. package/dist/doctor.js +148 -0
  12. package/dist/doctor.js.map +1 -0
  13. package/dist/gates/index.d.ts +14 -12
  14. package/dist/gates/index.js +53 -105
  15. package/dist/gates/index.js.map +1 -1
  16. package/dist/gates/lint-gate.d.ts +2 -2
  17. package/dist/gates/lint-gate.js +60 -66
  18. package/dist/gates/lint-gate.js.map +1 -1
  19. package/dist/gates/tests-gate.d.ts +2 -4
  20. package/dist/gates/tests-gate.js +75 -203
  21. package/dist/gates/tests-gate.js.map +1 -1
  22. package/dist/gates/types-gate.d.ts +2 -2
  23. package/dist/gates/types-gate.js +53 -59
  24. package/dist/gates/types-gate.js.map +1 -1
  25. package/dist/linear/client.d.ts +31 -108
  26. package/dist/linear/client.js +88 -388
  27. package/dist/linear/client.js.map +1 -1
  28. package/dist/linear/sync.d.ts +15 -0
  29. package/dist/linear/sync.js +102 -0
  30. package/dist/linear/sync.js.map +1 -0
  31. package/dist/runner/loop.d.ts +4 -0
  32. package/dist/runner/loop.js +168 -0
  33. package/dist/runner/loop.js.map +1 -0
  34. package/dist/runner/prompt.d.ts +14 -0
  35. package/dist/runner/prompt.js +59 -0
  36. package/dist/runner/prompt.js.map +1 -0
  37. package/dist/runner/update.d.ts +1 -0
  38. package/dist/runner/update.js +72 -0
  39. package/dist/runner/update.js.map +1 -0
  40. package/dist/server.d.ts +6 -2
  41. package/dist/server.js +43 -101
  42. package/dist/server.js.map +1 -1
  43. package/dist/setup.d.ts +5 -0
  44. package/dist/setup.js +208 -0
  45. package/dist/setup.js.map +1 -0
  46. package/dist/state/cache.d.ts +3 -0
  47. package/dist/state/cache.js +23 -0
  48. package/dist/state/cache.js.map +1 -0
  49. package/dist/state/status.d.ts +66 -0
  50. package/dist/state/status.js +96 -0
  51. package/dist/state/status.js.map +1 -0
  52. package/dist/types.d.ts +46 -114
  53. package/dist/worktree/manager.d.ts +6 -103
  54. package/dist/worktree/manager.js +25 -296
  55. package/dist/worktree/manager.js.map +1 -1
  56. package/hooks/pre-commit-verify.js +109 -109
  57. package/package.json +3 -2
  58. package/skills/forge-go.md +20 -13
  59. package/skills/forge-setup.md +149 -388
  60. package/skills/forge-spec.md +367 -342
  61. package/skills/forge-triage.md +179 -133
  62. 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 (!cache.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
- }
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.1.41",
4
- "description": "Pre-PR verification harness for Claude Code agents — gate runner + CLI + MCP server",
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"
@@ -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. The execution engine at `src/go/executor.ts` provides the programmatic logic this skill drives the agent orchestration.
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). Use the executor's `readCurrentMilestone()` approach — match `### Milestone N:` header and extract until the next milestone header.
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 (Automatic)
105
+ ### Step 2.5 — Session Isolation (Future Enhancement)
106
106
 
107
- The execution engine automatically creates a git worktree for isolated execution. This happens transparently you don't need to manage it manually.
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
- **What happens behind the scenes:**
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
- **If worktree creation fails:** The engine falls back to running in the main working directory (original behavior). A warning is printed but execution continues.
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/go/executor.ts:42 — Type 'string' is not assignable to type 'number'
324
- - lint: src/go/executor.ts:55 — Unused variable 'foo'
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
- It degrades gracefully if no API key or config, it exits silently.
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". It degrades gracefully if no API key or config, it exits silently.
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). In this case, treat the entire milestone as a single wave with one agent whose task is the milestone's goal.
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:** If the worktree directory already exists (e.g., from a crashed session), the engine attempts `npx forge cleanup` first. If that fails, it falls back to main directory execution.
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.