forge-cc 0.1.4 → 0.1.6
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 +96 -10
- package/dist/cli.js +225 -7
- package/dist/cli.js.map +1 -1
- package/dist/go/auto-chain.d.ts +37 -5
- package/dist/go/auto-chain.js +220 -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/setup/templates.js +97 -122
- package/dist/setup/templates.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 +22 -0
- 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/hooks/version-check.js +78 -78
- package/package.json +1 -1
- package/skills/forge-go.md +39 -0
- package/skills/forge-setup.md +183 -157
- package/skills/forge-spec.md +50 -12
- package/skills/forge-update.md +92 -72
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { openSync, closeSync, unlinkSync, mkdirSync, existsSync, } from "node:fs";
|
|
3
|
+
import { readJsonFileSync, writeJsonFileSync, generateSessionId, } from "../utils/platform.js";
|
|
4
|
+
/**
|
|
5
|
+
* Get the path to the session registry file.
|
|
6
|
+
* Located at <repoRoot>/.forge/sessions.json
|
|
7
|
+
*/
|
|
8
|
+
export function getRegistryPath(repoRoot) {
|
|
9
|
+
return join(repoRoot, ".forge", "sessions.json");
|
|
10
|
+
}
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// File-based lock for registry writes
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const LOCK_RETRIES = 10;
|
|
15
|
+
const LOCK_RETRY_MS = 100;
|
|
16
|
+
function getLockPath(repoRoot) {
|
|
17
|
+
return join(repoRoot, ".forge", "sessions.lock");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Acquire an exclusive lock file. Retries with backoff on contention.
|
|
21
|
+
* Uses O_CREAT|O_EXCL which atomically fails if the file exists.
|
|
22
|
+
*/
|
|
23
|
+
function acquireLock(repoRoot) {
|
|
24
|
+
const lockPath = getLockPath(repoRoot);
|
|
25
|
+
const dir = join(repoRoot, ".forge");
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
for (let attempt = 0; attempt < LOCK_RETRIES; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
// O_CREAT | O_EXCL | O_WRONLY — fails if file already exists
|
|
32
|
+
const fd = openSync(lockPath, "wx");
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
if (attempt === LOCK_RETRIES - 1) {
|
|
38
|
+
// Last attempt — force-remove stale lock and try once more
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(lockPath);
|
|
41
|
+
const fd = openSync(lockPath, "wx");
|
|
42
|
+
closeSync(fd);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`Failed to acquire session registry lock at ${lockPath} after ${LOCK_RETRIES} attempts`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Busy-wait (sync context)
|
|
50
|
+
const waitUntil = Date.now() + LOCK_RETRY_MS;
|
|
51
|
+
while (Date.now() < waitUntil) {
|
|
52
|
+
// spin
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Release the lock file.
|
|
59
|
+
*/
|
|
60
|
+
function releaseLock(repoRoot) {
|
|
61
|
+
try {
|
|
62
|
+
unlinkSync(getLockPath(repoRoot));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Lock already removed — non-fatal
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Execute a callback while holding the registry lock.
|
|
70
|
+
* Ensures read-modify-write operations are serialized.
|
|
71
|
+
*/
|
|
72
|
+
function withRegistryLock(repoRoot, fn) {
|
|
73
|
+
acquireLock(repoRoot);
|
|
74
|
+
try {
|
|
75
|
+
return fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
releaseLock(repoRoot);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Load the session registry. Returns empty registry if file doesn't exist.
|
|
83
|
+
*/
|
|
84
|
+
export function loadRegistry(repoRoot) {
|
|
85
|
+
const data = readJsonFileSync(getRegistryPath(repoRoot));
|
|
86
|
+
if (data === null) {
|
|
87
|
+
return { sessions: [] };
|
|
88
|
+
}
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Save the session registry atomically.
|
|
93
|
+
*/
|
|
94
|
+
export function saveRegistry(repoRoot, registry) {
|
|
95
|
+
writeJsonFileSync(getRegistryPath(repoRoot), registry);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Register a new session. Returns the created session.
|
|
99
|
+
* Uses file-based locking to prevent lost updates from concurrent writes.
|
|
100
|
+
*/
|
|
101
|
+
export function registerSession(repoRoot, params) {
|
|
102
|
+
return withRegistryLock(repoRoot, () => {
|
|
103
|
+
const registry = loadRegistry(repoRoot);
|
|
104
|
+
const session = {
|
|
105
|
+
id: generateSessionId(),
|
|
106
|
+
user: params.user.name,
|
|
107
|
+
email: params.user.email,
|
|
108
|
+
skill: params.skill,
|
|
109
|
+
milestone: params.milestone,
|
|
110
|
+
branch: params.branch,
|
|
111
|
+
worktreePath: params.worktreePath,
|
|
112
|
+
startedAt: new Date().toISOString(),
|
|
113
|
+
pid: process.pid,
|
|
114
|
+
status: "active",
|
|
115
|
+
};
|
|
116
|
+
registry.sessions.push(session);
|
|
117
|
+
saveRegistry(repoRoot, registry);
|
|
118
|
+
return session;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Deregister (remove) a session by ID.
|
|
123
|
+
* Uses file-based locking to prevent lost updates.
|
|
124
|
+
*/
|
|
125
|
+
export function deregisterSession(repoRoot, sessionId) {
|
|
126
|
+
withRegistryLock(repoRoot, () => {
|
|
127
|
+
const registry = loadRegistry(repoRoot);
|
|
128
|
+
registry.sessions = registry.sessions.filter((s) => s.id !== sessionId);
|
|
129
|
+
saveRegistry(repoRoot, registry);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Update a session's status.
|
|
134
|
+
* Uses file-based locking to prevent lost updates.
|
|
135
|
+
*/
|
|
136
|
+
export function updateSessionStatus(repoRoot, sessionId, status) {
|
|
137
|
+
withRegistryLock(repoRoot, () => {
|
|
138
|
+
const registry = loadRegistry(repoRoot);
|
|
139
|
+
const session = registry.sessions.find((s) => s.id === sessionId);
|
|
140
|
+
if (session) {
|
|
141
|
+
session.status = status;
|
|
142
|
+
saveRegistry(repoRoot, registry);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if a process with the given PID is still running.
|
|
148
|
+
*/
|
|
149
|
+
function isPidAlive(pid) {
|
|
150
|
+
try {
|
|
151
|
+
process.kill(pid, 0);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Find and mark stale sessions.
|
|
160
|
+
* A session is stale if its PID is no longer running.
|
|
161
|
+
* Uses file-based locking to prevent lost updates.
|
|
162
|
+
*/
|
|
163
|
+
export function detectStaleSessions(repoRoot) {
|
|
164
|
+
return withRegistryLock(repoRoot, () => {
|
|
165
|
+
const registry = loadRegistry(repoRoot);
|
|
166
|
+
const newlyStale = [];
|
|
167
|
+
for (const session of registry.sessions) {
|
|
168
|
+
if (session.status === "active" && !isPidAlive(session.pid)) {
|
|
169
|
+
session.status = "stale";
|
|
170
|
+
newlyStale.push(session);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (newlyStale.length > 0) {
|
|
174
|
+
saveRegistry(repoRoot, registry);
|
|
175
|
+
}
|
|
176
|
+
return newlyStale;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get all active sessions.
|
|
181
|
+
*/
|
|
182
|
+
export function getActiveSessions(repoRoot) {
|
|
183
|
+
const registry = loadRegistry(repoRoot);
|
|
184
|
+
return registry.sessions.filter((s) => s.status === "active");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get a session by ID.
|
|
188
|
+
*/
|
|
189
|
+
export function getSession(repoRoot, sessionId) {
|
|
190
|
+
const registry = loadRegistry(repoRoot);
|
|
191
|
+
return registry.sessions.find((s) => s.id === sessionId) ?? null;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/worktree/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface MergeResult {
|
|
2
|
+
stateUpdated: boolean;
|
|
3
|
+
roadmapUpdated: boolean;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Merge state from a completed worktree session back to the main repo.
|
|
8
|
+
*
|
|
9
|
+
* - STATE.md: updates the milestone row with completion status and the
|
|
10
|
+
* Last Session date.
|
|
11
|
+
* - ROADMAP.md: marks the milestone row as complete with the given date.
|
|
12
|
+
*
|
|
13
|
+
* Uses synchronous I/O throughout — consistent with all other worktree modules.
|
|
14
|
+
*
|
|
15
|
+
* @param mainRepoDir - Absolute path to the main repository
|
|
16
|
+
* @param worktreeDir - Absolute path to the completed worktree
|
|
17
|
+
* @param milestoneNumber - The milestone that was completed
|
|
18
|
+
* @param completionDate - Date string (YYYY-MM-DD) for the completion
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeSessionState(mainRepoDir: string, worktreeDir: string, milestoneNumber: number, completionDate: string): MergeResult;
|
|
21
|
+
/**
|
|
22
|
+
* Update a specific milestone row in a ROADMAP.md file.
|
|
23
|
+
* Uses structured line-by-line parsing (not regex replace) to safely
|
|
24
|
+
* update the status column of the milestone's table row.
|
|
25
|
+
*
|
|
26
|
+
* @param roadmapPath - Absolute path to ROADMAP.md
|
|
27
|
+
* @param milestoneNumber - Which milestone to update
|
|
28
|
+
* @param newStatus - New status string (e.g., "Complete (2026-02-15)")
|
|
29
|
+
* @returns true if the milestone was found and updated
|
|
30
|
+
*/
|
|
31
|
+
export declare function updateRoadmapMilestoneStatus(roadmapPath: string, milestoneNumber: number, newStatus: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Update the milestone progress table in STATE.md.
|
|
34
|
+
* Reads the current STATE.md, finds the milestone row, updates its status.
|
|
35
|
+
* Also updates the `**Last Session:**` date if present.
|
|
36
|
+
* Preserves all other content.
|
|
37
|
+
*
|
|
38
|
+
* @param statePath - Absolute path to STATE.md
|
|
39
|
+
* @param milestoneNumber - Which milestone to update
|
|
40
|
+
* @param newStatus - New status string
|
|
41
|
+
* @returns true if the milestone was found and updated
|
|
42
|
+
*/
|
|
43
|
+
export declare function updateStateMilestoneRow(statePath: string, milestoneNumber: number, newStatus: string): boolean;
|
|
@@ -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/hooks/version-check.js
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { join, dirname } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
// Read hook input from stdin
|
|
9
|
-
let input = "";
|
|
10
|
-
process.stdin.setEncoding("utf-8");
|
|
11
|
-
process.stdin.on("data", (chunk) => {
|
|
12
|
-
input += chunk;
|
|
13
|
-
});
|
|
14
|
-
process.stdin.on("end", () => {
|
|
15
|
-
try {
|
|
16
|
-
checkForUpdate();
|
|
17
|
-
} catch {
|
|
18
|
-
// Silent failure — never crash, never block
|
|
19
|
-
}
|
|
20
|
-
// Always exit cleanly with no output (allow session to proceed)
|
|
21
|
-
process.exit(0);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function checkForUpdate() {
|
|
25
|
-
// Get installed version from forge-cc's own package.json
|
|
26
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
const pkgPath = join(__dirname, "..", "package.json");
|
|
28
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
29
|
-
const currentVersion = pkg.version;
|
|
30
|
-
|
|
31
|
-
if (!currentVersion) return;
|
|
32
|
-
|
|
33
|
-
// Query npm registry for latest version (5 second timeout)
|
|
34
|
-
let latestVersion;
|
|
35
|
-
try {
|
|
36
|
-
const result = execSync("npm view forge-cc version --json", {
|
|
37
|
-
encoding: "utf-8",
|
|
38
|
-
timeout: 5000,
|
|
39
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
-
});
|
|
41
|
-
latestVersion = JSON.parse(result);
|
|
42
|
-
} catch {
|
|
43
|
-
// Offline, npm slow, or package not published yet — skip silently
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (typeof latestVersion !== "string" || !latestVersion) return;
|
|
48
|
-
|
|
49
|
-
// Compare versions using semver-compatible numeric comparison
|
|
50
|
-
if (isOutdated(currentVersion, latestVersion)) {
|
|
51
|
-
process.stderr.write(
|
|
52
|
-
`[forge] Update available: v${currentVersion} → v${latestVersion}. Run /forge:update to upgrade.\n`
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Returns true if `current` is older than `latest`.
|
|
59
|
-
* Splits on ".", compares major/minor/patch numerically.
|
|
60
|
-
*/
|
|
61
|
-
function isOutdated(current, latest) {
|
|
62
|
-
const parseParts = (v) =>
|
|
63
|
-
v
|
|
64
|
-
.replace(/^v/, "")
|
|
65
|
-
.split(".")
|
|
66
|
-
.map((n) => parseInt(n, 10));
|
|
67
|
-
|
|
68
|
-
const cur = parseParts(current);
|
|
69
|
-
const lat = parseParts(latest);
|
|
70
|
-
|
|
71
|
-
for (let i = 0; i < 3; i++) {
|
|
72
|
-
const c = cur[i] || 0;
|
|
73
|
-
const l = lat[i] || 0;
|
|
74
|
-
if (l > c) return true;
|
|
75
|
-
if (c > l) return false;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// Read hook input from stdin
|
|
9
|
+
let input = "";
|
|
10
|
+
process.stdin.setEncoding("utf-8");
|
|
11
|
+
process.stdin.on("data", (chunk) => {
|
|
12
|
+
input += chunk;
|
|
13
|
+
});
|
|
14
|
+
process.stdin.on("end", () => {
|
|
15
|
+
try {
|
|
16
|
+
checkForUpdate();
|
|
17
|
+
} catch {
|
|
18
|
+
// Silent failure — never crash, never block
|
|
19
|
+
}
|
|
20
|
+
// Always exit cleanly with no output (allow session to proceed)
|
|
21
|
+
process.exit(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function checkForUpdate() {
|
|
25
|
+
// Get installed version from forge-cc's own package.json
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
28
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
29
|
+
const currentVersion = pkg.version;
|
|
30
|
+
|
|
31
|
+
if (!currentVersion) return;
|
|
32
|
+
|
|
33
|
+
// Query npm registry for latest version (5 second timeout)
|
|
34
|
+
let latestVersion;
|
|
35
|
+
try {
|
|
36
|
+
const result = execSync("npm view forge-cc version --json", {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
timeout: 5000,
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
latestVersion = JSON.parse(result);
|
|
42
|
+
} catch {
|
|
43
|
+
// Offline, npm slow, or package not published yet — skip silently
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof latestVersion !== "string" || !latestVersion) return;
|
|
48
|
+
|
|
49
|
+
// Compare versions using semver-compatible numeric comparison
|
|
50
|
+
if (isOutdated(currentVersion, latestVersion)) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`[forge] Update available: v${currentVersion} → v${latestVersion}. Run /forge:update to upgrade.\n`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns true if `current` is older than `latest`.
|
|
59
|
+
* Splits on ".", compares major/minor/patch numerically.
|
|
60
|
+
*/
|
|
61
|
+
function isOutdated(current, latest) {
|
|
62
|
+
const parseParts = (v) =>
|
|
63
|
+
v
|
|
64
|
+
.replace(/^v/, "")
|
|
65
|
+
.split(".")
|
|
66
|
+
.map((n) => parseInt(n, 10));
|
|
67
|
+
|
|
68
|
+
const cur = parseParts(current);
|
|
69
|
+
const lat = parseParts(latest);
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < 3; i++) {
|
|
72
|
+
const c = cur[i] || 0;
|
|
73
|
+
const l = lat[i] || 0;
|
|
74
|
+
if (l > c) return true;
|
|
75
|
+
if (c > l) return false;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|