forge-cc 0.1.5 → 0.1.7
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 +50 -3
- package/dist/cli.js +295 -9
- package/dist/cli.js.map +1 -1
- package/dist/go/auto-chain.d.ts +44 -5
- package/dist/go/auto-chain.js +236 -81
- package/dist/go/auto-chain.js.map +1 -1
- package/dist/go/executor.d.ts +2 -0
- package/dist/go/executor.js.map +1 -1
- package/dist/hooks/pre-commit.js +9 -3
- package/dist/hooks/pre-commit.js.map +1 -1
- package/dist/reporter/human.d.ts +5 -0
- package/dist/reporter/human.js +30 -0
- package/dist/reporter/human.js.map +1 -1
- package/dist/spec/generator.d.ts +20 -0
- package/dist/spec/generator.js +23 -2
- package/dist/spec/generator.js.map +1 -1
- package/dist/spec/interview.d.ts +20 -2
- package/dist/spec/interview.js +8 -0
- package/dist/spec/interview.js.map +1 -1
- package/dist/spec/scanner.d.ts +34 -0
- package/dist/spec/scanner.js +93 -0
- package/dist/spec/scanner.js.map +1 -1
- package/dist/spec/templates.d.ts +34 -12
- package/dist/spec/templates.js +8 -0
- package/dist/spec/templates.js.map +1 -1
- package/dist/utils/platform.d.ts +29 -0
- package/dist/utils/platform.js +90 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/worktree/identity.d.ts +9 -0
- package/dist/worktree/identity.js +32 -0
- package/dist/worktree/identity.js.map +1 -0
- package/dist/worktree/manager.d.ts +70 -0
- package/dist/worktree/manager.js +217 -0
- package/dist/worktree/manager.js.map +1 -0
- package/dist/worktree/parallel.d.ts +87 -0
- package/dist/worktree/parallel.js +328 -0
- package/dist/worktree/parallel.js.map +1 -0
- package/dist/worktree/session.d.ts +64 -0
- package/dist/worktree/session.js +193 -0
- package/dist/worktree/session.js.map +1 -0
- package/dist/worktree/state-merge.d.ts +43 -0
- package/dist/worktree/state-merge.js +162 -0
- package/dist/worktree/state-merge.js.map +1 -0
- package/hooks/pre-commit-verify.js +9 -3
- package/package.json +1 -1
- package/skills/forge-go.md +57 -14
- package/skills/forge-setup.md +28 -2
- package/skills/forge-spec.md +51 -13
- package/skills/forge-update.md +21 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { atomicWriteFileSync } from "../utils/platform.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// mergeSessionState
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Merge state from a completed worktree session back to the main repo.
|
|
9
|
+
*
|
|
10
|
+
* - STATE.md: updates the milestone row with completion status and the
|
|
11
|
+
* Last Session date.
|
|
12
|
+
* - ROADMAP.md: marks the milestone row as complete with the given date.
|
|
13
|
+
*
|
|
14
|
+
* Uses synchronous I/O throughout — consistent with all other worktree modules.
|
|
15
|
+
*
|
|
16
|
+
* @param mainRepoDir - Absolute path to the main repository
|
|
17
|
+
* @param worktreeDir - Absolute path to the completed worktree
|
|
18
|
+
* @param milestoneNumber - The milestone that was completed
|
|
19
|
+
* @param completionDate - Date string (YYYY-MM-DD) for the completion
|
|
20
|
+
*/
|
|
21
|
+
export function mergeSessionState(mainRepoDir, worktreeDir, milestoneNumber, completionDate) {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
let stateUpdated = false;
|
|
24
|
+
let roadmapUpdated = false;
|
|
25
|
+
const completionStatus = `Complete (${completionDate})`;
|
|
26
|
+
// --- Check that the worktree has planning files --------------------------
|
|
27
|
+
const worktreeStatePath = join(worktreeDir, ".planning", "STATE.md");
|
|
28
|
+
const worktreeRoadmapPath = join(worktreeDir, ".planning", "ROADMAP.md");
|
|
29
|
+
if (!existsSync(worktreeStatePath)) {
|
|
30
|
+
warnings.push(`Worktree STATE.md not found at ${worktreeStatePath} — skipping state merge`);
|
|
31
|
+
}
|
|
32
|
+
if (!existsSync(worktreeRoadmapPath)) {
|
|
33
|
+
warnings.push(`Worktree ROADMAP.md not found at ${worktreeRoadmapPath} — skipping roadmap merge`);
|
|
34
|
+
}
|
|
35
|
+
// --- Update main repo STATE.md ------------------------------------------
|
|
36
|
+
const mainStatePath = join(mainRepoDir, ".planning", "STATE.md");
|
|
37
|
+
if (existsSync(mainStatePath)) {
|
|
38
|
+
const milestoneUpdated = updateStateMilestoneRow(mainStatePath, milestoneNumber, completionStatus);
|
|
39
|
+
if (milestoneUpdated) {
|
|
40
|
+
stateUpdated = true;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
warnings.push(`Milestone ${milestoneNumber} row not found in main repo STATE.md — no state update performed`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
warnings.push(`Main repo STATE.md not found at ${mainStatePath} — cannot update state`);
|
|
48
|
+
}
|
|
49
|
+
// --- Update main repo ROADMAP.md ----------------------------------------
|
|
50
|
+
const mainRoadmapPath = join(mainRepoDir, ".planning", "ROADMAP.md");
|
|
51
|
+
if (existsSync(mainRoadmapPath)) {
|
|
52
|
+
const milestoneUpdated = updateRoadmapMilestoneStatus(mainRoadmapPath, milestoneNumber, completionStatus);
|
|
53
|
+
if (milestoneUpdated) {
|
|
54
|
+
roadmapUpdated = true;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
warnings.push(`Milestone ${milestoneNumber} row not found in main repo ROADMAP.md — no roadmap update performed`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
warnings.push(`Main repo ROADMAP.md not found at ${mainRoadmapPath} — cannot update roadmap`);
|
|
62
|
+
}
|
|
63
|
+
return { stateUpdated, roadmapUpdated, warnings };
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// updateRoadmapMilestoneStatus
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Update a specific milestone row in a ROADMAP.md file.
|
|
70
|
+
* Uses structured line-by-line parsing (not regex replace) to safely
|
|
71
|
+
* update the status column of the milestone's table row.
|
|
72
|
+
*
|
|
73
|
+
* @param roadmapPath - Absolute path to ROADMAP.md
|
|
74
|
+
* @param milestoneNumber - Which milestone to update
|
|
75
|
+
* @param newStatus - New status string (e.g., "Complete (2026-02-15)")
|
|
76
|
+
* @returns true if the milestone was found and updated
|
|
77
|
+
*/
|
|
78
|
+
export function updateRoadmapMilestoneStatus(roadmapPath, milestoneNumber, newStatus) {
|
|
79
|
+
const content = readFileSync(roadmapPath, "utf-8");
|
|
80
|
+
const lines = content.split("\n");
|
|
81
|
+
let found = false;
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
// Match table rows: | <number> | <name> | <status> |
|
|
85
|
+
// Cells are separated by |. Split and check the first data cell.
|
|
86
|
+
if (!line.trimStart().startsWith("|"))
|
|
87
|
+
continue;
|
|
88
|
+
const cells = line.split("|");
|
|
89
|
+
// A valid table row split by | produces: ["", " cell1 ", " cell2 ", " cell3 ", ""]
|
|
90
|
+
// We need at least 4 separators (5 segments) for a 3-column table row.
|
|
91
|
+
if (cells.length < 5)
|
|
92
|
+
continue;
|
|
93
|
+
const firstCell = cells[1].trim();
|
|
94
|
+
const parsedNumber = parseInt(firstCell, 10);
|
|
95
|
+
if (isNaN(parsedNumber) || parsedNumber !== milestoneNumber)
|
|
96
|
+
continue;
|
|
97
|
+
// Check if this is already completed — if so, last completer wins with warning.
|
|
98
|
+
const currentStatus = cells[3].trim();
|
|
99
|
+
const alreadyComplete = currentStatus.toLowerCase().startsWith("complete");
|
|
100
|
+
// Update the status cell (index 3), preserving cell padding.
|
|
101
|
+
cells[3] = ` ${newStatus} `;
|
|
102
|
+
lines[i] = cells.join("|");
|
|
103
|
+
found = true;
|
|
104
|
+
if (alreadyComplete) {
|
|
105
|
+
// Last completer wins — overwrite is intentional but callers may
|
|
106
|
+
// want to know. We still return true since the update was applied.
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (found) {
|
|
111
|
+
atomicWriteFileSync(roadmapPath, lines.join("\n"));
|
|
112
|
+
}
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// updateStateMilestoneRow
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Update the milestone progress table in STATE.md.
|
|
120
|
+
* Reads the current STATE.md, finds the milestone row, updates its status.
|
|
121
|
+
* Also updates the `**Last Session:**` date if present.
|
|
122
|
+
* Preserves all other content.
|
|
123
|
+
*
|
|
124
|
+
* @param statePath - Absolute path to STATE.md
|
|
125
|
+
* @param milestoneNumber - Which milestone to update
|
|
126
|
+
* @param newStatus - New status string
|
|
127
|
+
* @returns true if the milestone was found and updated
|
|
128
|
+
*/
|
|
129
|
+
export function updateStateMilestoneRow(statePath, milestoneNumber, newStatus) {
|
|
130
|
+
const content = readFileSync(statePath, "utf-8");
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
let milestoneFound = false;
|
|
133
|
+
// Extract the date from the status string for Last Session update.
|
|
134
|
+
// Status format is typically "Complete (YYYY-MM-DD)".
|
|
135
|
+
const dateMatch = newStatus.match(/\((\d{4}-\d{2}-\d{2})\)/);
|
|
136
|
+
const completionDate = dateMatch ? dateMatch[1] : null;
|
|
137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
const line = lines[i];
|
|
139
|
+
// --- Update milestone table row ----------------------------------------
|
|
140
|
+
if (line.trimStart().startsWith("|")) {
|
|
141
|
+
const cells = line.split("|");
|
|
142
|
+
if (cells.length >= 5) {
|
|
143
|
+
const firstCell = cells[1].trim();
|
|
144
|
+
const parsedNumber = parseInt(firstCell, 10);
|
|
145
|
+
if (!isNaN(parsedNumber) && parsedNumber === milestoneNumber) {
|
|
146
|
+
cells[3] = ` ${newStatus} `;
|
|
147
|
+
lines[i] = cells.join("|");
|
|
148
|
+
milestoneFound = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// --- Update Last Session date ------------------------------------------
|
|
153
|
+
if (completionDate && line.match(/\*\*Last Session:\*\*/)) {
|
|
154
|
+
lines[i] = line.replace(/(\*\*Last Session:\*\*\s*)\S+/, `$1${completionDate}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (milestoneFound) {
|
|
158
|
+
atomicWriteFileSync(statePath, lines.join("\n"));
|
|
159
|
+
}
|
|
160
|
+
return milestoneFound;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=state-merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-merge.js","sourceRoot":"","sources":["../../src/worktree/state-merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -35,8 +35,9 @@ function checkPreCommit(hookData) {
|
|
|
35
35
|
const projectDir = process.cwd();
|
|
36
36
|
|
|
37
37
|
// Check 1: Wrong branch protection
|
|
38
|
+
let branch = "unknown";
|
|
38
39
|
try {
|
|
39
|
-
|
|
40
|
+
branch = execSync("git branch --show-current", {
|
|
40
41
|
encoding: "utf-8",
|
|
41
42
|
}).trim();
|
|
42
43
|
if (branch === "main" || branch === "master") {
|
|
@@ -49,8 +50,13 @@ function checkPreCommit(hookData) {
|
|
|
49
50
|
// Can't determine branch — allow
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
// Check 2: Verify cache exists
|
|
53
|
-
const
|
|
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;
|
|
54
60
|
if (!existsSync(cachePath)) {
|
|
55
61
|
return {
|
|
56
62
|
decision: "block",
|
package/package.json
CHANGED
package/skills/forge-go.md
CHANGED
|
@@ -38,6 +38,8 @@ If all milestones are complete:
|
|
|
38
38
|
|
|
39
39
|
If `--auto` was passed as an argument, set mode to auto and skip the prompt.
|
|
40
40
|
|
|
41
|
+
If `--single` was passed as an argument, set mode to single and skip the prompt.
|
|
42
|
+
|
|
41
43
|
Otherwise: **your very next tool call MUST be AskUserQuestion.** No file reads, no git commands, no exploration — ask the user first. Use exactly these parameters:
|
|
42
44
|
|
|
43
45
|
- question: "How should this project be executed?"
|
|
@@ -65,6 +67,12 @@ Verify the execution environment is ready:
|
|
|
65
67
|
|
|
66
68
|
> You have uncommitted changes. Commit or stash them before running /forge:go.
|
|
67
69
|
|
|
70
|
+
5. **Milestone size:** Count waves and agents in the milestone. If >3 waves or >6 total agents, warn:
|
|
71
|
+
|
|
72
|
+
> Warning: This milestone has {N} waves and {M} agents. Large milestones risk context overflow. Consider splitting before execution.
|
|
73
|
+
|
|
74
|
+
This is a pre-flight warning, not a hard abort — the user can choose to proceed. But the warning should be prominent so they can split the milestone first if needed.
|
|
75
|
+
|
|
68
76
|
Print the pre-flight summary:
|
|
69
77
|
|
|
70
78
|
```
|
|
@@ -78,6 +86,21 @@ Print the pre-flight summary:
|
|
|
78
86
|
Ready to execute Milestone 4.
|
|
79
87
|
```
|
|
80
88
|
|
|
89
|
+
### Step 2.5 — Session Isolation (Automatic)
|
|
90
|
+
|
|
91
|
+
The execution engine automatically creates a git worktree for isolated execution. This happens transparently — you don't need to manage it manually.
|
|
92
|
+
|
|
93
|
+
**What happens behind the scenes:**
|
|
94
|
+
1. A worktree is created at `../.forge-wt/<repo>/<session-id>/` based on the feature branch
|
|
95
|
+
2. A session is registered in `.forge/sessions.json`
|
|
96
|
+
3. All wave execution happens inside the worktree directory
|
|
97
|
+
4. After completion, changes are merged back to the feature branch
|
|
98
|
+
5. The worktree and session are cleaned up
|
|
99
|
+
|
|
100
|
+
**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.
|
|
101
|
+
|
|
102
|
+
**If worktree creation fails:** The engine falls back to running in the main working directory (original behavior). A warning is printed but execution continues.
|
|
103
|
+
|
|
81
104
|
### Step 3 — Execute Waves
|
|
82
105
|
|
|
83
106
|
Parse the milestone section from the PRD. Each milestone contains waves with agent definitions:
|
|
@@ -261,7 +284,7 @@ After milestone completion, determine the next action:
|
|
|
261
284
|
- Verification: PASSED
|
|
262
285
|
- Branch: {branch} (pushed)
|
|
263
286
|
|
|
264
|
-
**Next:** Run `/clear`
|
|
287
|
+
**Next:** Run `/clear` then `/forge:go` for Milestone {N+1}, or exit and run `npx forge run` to auto-chain all remaining milestones.
|
|
265
288
|
```
|
|
266
289
|
|
|
267
290
|
#### If this IS the last milestone:
|
|
@@ -303,27 +326,46 @@ The PR is ready for review.
|
|
|
303
326
|
|
|
304
327
|
### Auto Mode
|
|
305
328
|
|
|
306
|
-
When the user selects "Auto (all milestones)" in Step 1
|
|
329
|
+
When the user selects "Auto (all milestones)" in Step 1 Part B or invokes with `--auto` (e.g., `/forge:go --auto`):
|
|
330
|
+
|
|
331
|
+
Print the following instructions and then **stop** (do not execute a milestone):
|
|
307
332
|
|
|
308
|
-
|
|
333
|
+
```
|
|
334
|
+
## Auto Mode — Fresh Context Execution
|
|
309
335
|
|
|
310
|
-
|
|
311
|
-
2. Print instructions for the context reset:
|
|
336
|
+
Auto mode runs each milestone in a fresh Claude session for maximum quality.
|
|
312
337
|
|
|
313
|
-
|
|
314
|
-
## Context Reset for Milestone {N+1}
|
|
338
|
+
**To start:** Exit this Claude session (Ctrl+C), then run in your terminal:
|
|
315
339
|
|
|
316
|
-
|
|
340
|
+
npx forge run
|
|
317
341
|
|
|
318
|
-
|
|
319
|
-
|
|
342
|
+
**What happens:**
|
|
343
|
+
- Each milestone gets a fresh Claude session (no context rot)
|
|
344
|
+
- Output streams inline to your terminal
|
|
345
|
+
- Stops on completion, failure, or stall
|
|
346
|
+
- Resume after failure: fix the issue, run `npx forge run` again
|
|
320
347
|
|
|
321
|
-
|
|
322
|
-
|
|
348
|
+
**Requires:** claude CLI on PATH, --dangerously-skip-permissions (automatic)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**IMPORTANT:** Auto mode does NOT execute milestones in the current session. It redirects the user to `npx forge run`, which handles spawning fresh Claude sessions per milestone via the Ralph Loop pattern.
|
|
323
352
|
|
|
324
|
-
|
|
353
|
+
### Parallel Milestones (dependsOn)
|
|
354
|
+
|
|
355
|
+
When milestones specify `dependsOn` fields in the PRD, the scheduler can identify which milestones are independent and run them in parallel:
|
|
356
|
+
|
|
357
|
+
- Milestones with no `dependsOn` (or `dependsOn: []`) can run in the first wave
|
|
358
|
+
- Milestones that depend on completed milestones become ready as dependencies finish
|
|
359
|
+
- The scheduler builds a DAG and groups milestones into execution waves
|
|
360
|
+
|
|
361
|
+
Example PRD milestone with dependencies:
|
|
362
|
+
```
|
|
363
|
+
### Milestone 3: Integration Layer
|
|
364
|
+
**dependsOn:** 1, 2
|
|
365
|
+
**Goal:** Combine components from M1 and M2...
|
|
366
|
+
```
|
|
325
367
|
|
|
326
|
-
|
|
368
|
+
If no `dependsOn` fields are present, milestones execute sequentially (backward compatible).
|
|
327
369
|
|
|
328
370
|
### Step 9 — Linear Issue Start (On Milestone Begin)
|
|
329
371
|
|
|
@@ -347,3 +389,4 @@ If Linear is not configured, skip silently.
|
|
|
347
389
|
> Milestone {N} has no wave definitions. Update the PRD with agent assignments before running /forge:go.
|
|
348
390
|
- **Already on correct milestone:** If STATE.md's current milestone matches the target, proceed normally (this is the expected case).
|
|
349
391
|
- **Linear auth fails:** Warn but continue execution. Linear sync is not blocking.
|
|
392
|
+
- **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.
|
package/skills/forge-setup.md
CHANGED
|
@@ -93,7 +93,32 @@ Check if `~/.claude/CLAUDE.md` exists:
|
|
|
93
93
|
- **If it does not exist:** Create it using `globalClaudeMdTemplate()` from the templates.
|
|
94
94
|
- **If it exists:** Leave it alone. Do not overwrite the user's global config.
|
|
95
95
|
|
|
96
|
-
### Step 6 — Install
|
|
96
|
+
### Step 6 — Install Skills
|
|
97
|
+
|
|
98
|
+
Copy all forge skills to `~/.claude/commands/forge/` so they're discoverable via `/forge:*`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
mkdir -p ~/.claude/commands/forge
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Find the installed forge-cc package and copy skill files, stripping the `forge-` prefix:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
SKILLS_DIR="$(dirname "$(which forge)")/../lib/node_modules/forge-cc/skills"
|
|
108
|
+
# Fallback: check local node_modules
|
|
109
|
+
if [ ! -d "$SKILLS_DIR" ]; then
|
|
110
|
+
SKILLS_DIR="node_modules/forge-cc/skills"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
for f in "$SKILLS_DIR"/forge-*.md; do
|
|
114
|
+
name=$(basename "$f" | sed 's/^forge-//')
|
|
115
|
+
cp "$f" ~/.claude/commands/forge/"$name"
|
|
116
|
+
done
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Print: "Installed forge skills to ~/.claude/commands/forge/"
|
|
120
|
+
|
|
121
|
+
### Step 7 — Install Hooks
|
|
97
122
|
|
|
98
123
|
Check if the user has a `.claude/settings.json` or `.claude/settings.local.json` in the project:
|
|
99
124
|
|
|
@@ -126,7 +151,7 @@ If a settings file already exists, inform the user:
|
|
|
126
151
|
> Settings file already exists. To add the version-check hook manually, add this to your hooks config:
|
|
127
152
|
> `"command": "node node_modules/forge-cc/hooks/version-check.js"`
|
|
128
153
|
|
|
129
|
-
### Step
|
|
154
|
+
### Step 8 — Summary
|
|
130
155
|
|
|
131
156
|
Print a summary of everything that was created or updated:
|
|
132
157
|
|
|
@@ -138,6 +163,7 @@ Print a summary of everything that was created or updated:
|
|
|
138
163
|
**Gates:** {comma-separated list}
|
|
139
164
|
|
|
140
165
|
### Files Created/Updated
|
|
166
|
+
- ~/.claude/commands/forge/*.md ✓ (skills)
|
|
141
167
|
- .forge.json ✓
|
|
142
168
|
- CLAUDE.md ✓
|
|
143
169
|
- .planning/STATE.md ✓
|
package/skills/forge-spec.md
CHANGED
|
@@ -83,12 +83,31 @@ Conduct an adaptive interview using the interview engine logic from `src/spec/in
|
|
|
83
83
|
4. **Scope** — what's out, sacred files, boundaries
|
|
84
84
|
5. **Milestones** — phasing, dependencies, delivery chunks
|
|
85
85
|
|
|
86
|
+
**Milestone Sizing Constraint (Hard Rule):**
|
|
87
|
+
|
|
88
|
+
Each milestone MUST be completable in one main agent context window. If a milestone requires more than ~4 agents across 2-3 waves, split it. This is non-negotiable — large milestones cause context overflow and execution failures. When interviewing about milestones, actively recommend splitting any milestone that looks too large.
|
|
89
|
+
|
|
90
|
+
**Milestone Dependencies (dependsOn):**
|
|
91
|
+
|
|
92
|
+
During the milestones phase of the interview, ask about milestone dependencies using AskUserQuestion. For each milestone after the first, ask:
|
|
93
|
+
|
|
94
|
+
- question: "Does Milestone {N} depend on any previous milestones?"
|
|
95
|
+
- options:
|
|
96
|
+
- "No dependencies — can start immediately"
|
|
97
|
+
- "Depends on Milestone {N-1} (sequential)"
|
|
98
|
+
- "Depends on specific milestones (I'll specify)"
|
|
99
|
+
|
|
100
|
+
If milestones have explicit dependencies, include `**dependsOn:** 1, 2` in the milestone section of the PRD. If no dependencies are specified, omit the field (backward compatible — treated as sequential).
|
|
101
|
+
|
|
102
|
+
Independent milestones enable parallel execution via `/forge:go`, which creates separate worktrees for each parallel milestone.
|
|
103
|
+
|
|
86
104
|
**Interview Rules:**
|
|
87
105
|
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **
|
|
91
|
-
- **
|
|
106
|
+
- **NEVER present questions as numbered text — always use AskUserQuestion with 2-4 options per question.** Every interview question MUST be delivered via Claude Code's AskUserQuestion tool with structured multiple-choice options. Do not print numbered lists of questions for the user to answer in free text.
|
|
107
|
+
- **Lead with recommendations.** Every question includes context from the codebase scan as the question text. Never ask a blank "what do you want to build?" question.
|
|
108
|
+
- **Ask 1 question at a time via AskUserQuestion.** Each question gets its own AskUserQuestion call with 2-4 predefined options derived from codebase scan context and common patterns. Always include a final option like "Other (I'll describe)" to allow the user to provide a custom answer.
|
|
109
|
+
- **Follow interesting threads.** If the user's selection mentions migration, breaking changes, multiple user types, or external integrations, follow up with targeted AskUserQuestion calls.
|
|
110
|
+
- **Show progress.** After each answer round, show a compact status as text output:
|
|
92
111
|
|
|
93
112
|
```
|
|
94
113
|
Progress: [##---] Problem & Goals (2/2) | User Stories (0/2) | Technical (0/1) | Scope (0/1) | Milestones (0/1)
|
|
@@ -101,22 +120,35 @@ Progress: [##---] Problem & Goals (2/2) | User Stories (0/2) | Technical (0/1) |
|
|
|
101
120
|
- **Stop when complete.** When all sections have enough info (Problem 2+, Users 2+, Technical 1+, Scope 1+, Milestones 1+), move to Step 4. Don't drag the interview out.
|
|
102
121
|
- **Allow early exit.** If the user says "that's enough", "skip", or "generate it", respect that and move to Step 4 with what you have.
|
|
103
122
|
|
|
104
|
-
**Question Format:**
|
|
123
|
+
**Question Format (AskUserQuestion):**
|
|
124
|
+
|
|
125
|
+
Each interview question MUST use AskUserQuestion. Build the question text from codebase scan context and the section being asked about. Provide 2-4 options that reflect likely answers based on the scan results, plus a free-text escape hatch. Example:
|
|
105
126
|
|
|
106
127
|
```
|
|
107
|
-
|
|
128
|
+
AskUserQuestion:
|
|
129
|
+
question: "[Section] Context from scan or previous answers. Question text here?"
|
|
130
|
+
options:
|
|
131
|
+
- "Option A — a likely answer based on scan findings"
|
|
132
|
+
- "Option B — another plausible direction"
|
|
133
|
+
- "Option C — a third possibility (if applicable)"
|
|
134
|
+
- "Other (I'll describe)"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If the user selects "Other (I'll describe)", prompt them for a free-text answer using a follow-up AskUserQuestion or accept their typed response.
|
|
108
138
|
|
|
109
|
-
|
|
139
|
+
**Do NOT do this (anti-pattern):**
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
```
|
|
142
|
+
### Round N
|
|
113
143
|
|
|
114
|
-
|
|
115
|
-
|
|
144
|
+
1. **[Section]** Question text?
|
|
145
|
+
2. **[Section]** Question text?
|
|
116
146
|
|
|
117
|
-
> Answer by number
|
|
147
|
+
> Answer by number...
|
|
118
148
|
```
|
|
119
149
|
|
|
150
|
+
This numbered-text format is explicitly prohibited. Always use AskUserQuestion.
|
|
151
|
+
|
|
120
152
|
### Step 4 — Generate PRD
|
|
121
153
|
|
|
122
154
|
Using all gathered interview answers and codebase scan results, generate the final PRD. Use the generator module at `src/spec/generator.ts`:
|
|
@@ -154,9 +186,13 @@ The PRD should follow this structure:
|
|
|
154
186
|
- [ ] Issue title — brief description
|
|
155
187
|
|
|
156
188
|
### Milestone 2: {Name}
|
|
189
|
+
**dependsOn:** 1
|
|
190
|
+
**Goal:** {What this delivers}
|
|
157
191
|
...
|
|
158
192
|
```
|
|
159
193
|
|
|
194
|
+
**Milestone sizing check:** Before finalizing, review each milestone against the sizing constraint. Every milestone MUST fit in one agent context window (~4 agents across 2-3 waves max). If any milestone exceeds this, split it into smaller milestones before writing the final PRD. Set `maxContextWindowFit: true` on all milestones — if you cannot make a milestone fit, flag it as `maxContextWindowFit: false` and warn the user.
|
|
195
|
+
|
|
160
196
|
Write the final PRD to `.planning/prds/{project-slug}.md`. Tell the user:
|
|
161
197
|
|
|
162
198
|
> Final PRD written to `.planning/prds/{slug}.md`.
|
|
@@ -234,13 +270,15 @@ After sync, print the handoff prompt:
|
|
|
234
270
|
PRD: `.planning/prds/{slug}.md`
|
|
235
271
|
Linear: {project URL}
|
|
236
272
|
|
|
237
|
-
**Next step:** Run `/forge:go` to
|
|
273
|
+
**Next step:** Run `/forge:go` for one milestone at a time, or exit and run `npx forge run` to execute all milestones autonomously. The execution engine will:
|
|
238
274
|
- Read the PRD and milestone plan
|
|
239
275
|
- Spawn agent teams for each issue
|
|
240
276
|
- Verify each change with forge-mcp gates
|
|
241
277
|
- Open PRs and transition issues automatically
|
|
242
278
|
```
|
|
243
279
|
|
|
280
|
+
**Note:** `/forge:go` now uses git worktrees for session isolation. Multiple users can run `/forge:go` on different milestones simultaneously without conflicts.
|
|
281
|
+
|
|
244
282
|
## Edge Cases
|
|
245
283
|
|
|
246
284
|
- **No Linear connection:** Warn the user. Still generate the PRD locally — skip the Linear sync steps.
|
package/skills/forge-update.md
CHANGED
|
@@ -48,7 +48,27 @@ Verify the update succeeded:
|
|
|
48
48
|
npm list -g forge-cc --json 2>/dev/null | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8');const j=JSON.parse(d);console.log(j.dependencies?.['forge-cc']?.version||'failed')"
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
### Step 4 —
|
|
51
|
+
### Step 4 — Re-sync Skills
|
|
52
|
+
|
|
53
|
+
After updating, copy the latest skills to `~/.claude/commands/forge/`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mkdir -p ~/.claude/commands/forge
|
|
57
|
+
|
|
58
|
+
SKILLS_DIR="$(dirname "$(which forge)")/../lib/node_modules/forge-cc/skills"
|
|
59
|
+
if [ ! -d "$SKILLS_DIR" ]; then
|
|
60
|
+
SKILLS_DIR="node_modules/forge-cc/skills"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
for f in "$SKILLS_DIR"/forge-*.md; do
|
|
64
|
+
name=$(basename "$f" | sed 's/^forge-//')
|
|
65
|
+
cp "$f" ~/.claude/commands/forge/"$name"
|
|
66
|
+
done
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Print: "Synced skills to ~/.claude/commands/forge/"
|
|
70
|
+
|
|
71
|
+
### Step 5 — Post-Update Check
|
|
52
72
|
|
|
53
73
|
After updating, check if the current project's forge files need refreshing:
|
|
54
74
|
|