@izkac/forgekit 0.3.0 → 0.3.2
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 +83 -36
- package/package.json +4 -4
- package/src/integrity.mjs +49 -6
- package/src/integrity.test.mjs +64 -0
package/README.md
CHANGED
|
@@ -1,36 +1,83 @@
|
|
|
1
|
-
# @izkac/forgekit
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
# @izkac/forgekit
|
|
2
|
+
|
|
3
|
+
**A disciplined development workflow for AI coding agents — portable across Claude Code, Cursor, Codex, Copilot, Gemini, Windsurf, and opencode.**
|
|
4
|
+
|
|
5
|
+
  
|
|
6
|
+
|
|
7
|
+
AI agents are fast but undisciplined: they claim success on stubs, skip verification, over-build, and quietly leave the human behind. Forgekit installs a **gated workflow** that holds an agent to the standards a senior engineer would — and gives you, the operator, the visibility and control to stay in charge of a whole fleet of them.
|
|
8
|
+
|
|
9
|
+
One package, three command-line tools:
|
|
10
|
+
|
|
11
|
+
| Bin | What it does |
|
|
12
|
+
|-----|--------------|
|
|
13
|
+
| **`forgekit`** | Install / update the Forge skills into any supported agent |
|
|
14
|
+
| **`forge`** | Run the Forge workflow — sessions, integrity gates, fleet control |
|
|
15
|
+
| **`review`** | Standalone multi-pass thorough code review |
|
|
16
|
+
|
|
17
|
+
## Why Forge (the workflow)
|
|
18
|
+
|
|
19
|
+
Forge moves every substantial change through explicit phases — **triage → brainstorm → plan → implement → verify → review → done** — and enforces them with **executable gates**, not honor-system promises:
|
|
20
|
+
|
|
21
|
+
- **Operator brief** — before any code is written, the agent produces a plain-language HTML explainer of what's about to be built (with diagrams). It opens in your browser and *gates implementation*: you approve what you actually understand, not a wall of spec markdown.
|
|
22
|
+
- **Runtime spine** — every change declares how each capability wires up: library → runtime owner → what it writes → the evidence it works. No orphaned code that's "done" but never called.
|
|
23
|
+
- **Executable product loop** — for jobs, workers, and pipelines the end-to-end acceptance is a *command that must exit green*, not a paragraph claiming it would.
|
|
24
|
+
- **Registered deferrals** — "wire it up later" is only allowed when tracked; the finish gate refuses unresolved ones.
|
|
25
|
+
- **Session scorecard** — every session leaves a graded, measurable trail (A–F).
|
|
26
|
+
|
|
27
|
+
The result: agents that can't fake completion, and a workflow you can trust to run with less babysitting.
|
|
28
|
+
|
|
29
|
+
## Fleet control — command every session from one terminal
|
|
30
|
+
|
|
31
|
+
Running agents across three IDEs and two terminals? `forge fleet` is a single control surface over all of them, across every project and engine:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
forge fleet list # every session: phase, task progress, engine, activity
|
|
35
|
+
forge fleet watch # live-refreshing dashboard
|
|
36
|
+
forge fleet view <s> # detail + live transcript tail (Claude Code)
|
|
37
|
+
forge fleet send <s> "pause and report" # message any session; --all broadcasts
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Sessions auto-register the moment they touch Forge — including ones running inside Claude Desktop or Cursor. You see where everything stands at a glance and steer any of them without switching windows.
|
|
41
|
+
|
|
42
|
+
## Portable — one workflow, every agent
|
|
43
|
+
|
|
44
|
+
The same skills and the same discipline, installed into each agent's native skills directory:
|
|
45
|
+
|
|
46
|
+
**Claude Code · Cursor · Codex CLI · GitHub Copilot · Gemini CLI · Windsurf · opencode**
|
|
47
|
+
|
|
48
|
+
Planning is engine-flexible too: use the **OpenSpec** CLI if you have it, or the **built-in specs engine** (plain markdown, same layout) if you don't. Optional **ADRs** archive each finished change into an Architecture Decision Record.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm i -g @izkac/forgekit
|
|
54
|
+
forgekit install # interactive: pick skills + agents (space toggles, `a` = all)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Non-interactive:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
forgekit install --skills forge,thorough-code-review --agents claude,cursor --force
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then wire a project and start working:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd your-project
|
|
67
|
+
forge init --claude --cursor # slash commands, rules, hooks, .forge/
|
|
68
|
+
|
|
69
|
+
# in the agent chat:
|
|
70
|
+
/forge add a health endpoint that returns { ok: true }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The agent triages, brainstorms with you, plans a tracked change, writes the operator brief for your approval, then implements task-by-task with TDD and per-task review — refusing to call itself done until the gates are green.
|
|
74
|
+
|
|
75
|
+
## Docs
|
|
76
|
+
|
|
77
|
+
- **[How to use Forgekit](https://github.com/izkac/forgekit/blob/main/docs/usage.md)** — full tutorial: install, project wiring, worked examples, integrity gates, fleet, cheat sheet
|
|
78
|
+
- **[Full Forge reference](https://github.com/izkac/forgekit/blob/main/skills/forge/docs/forge.md)** — phases, pace matrix, integrity rules (also installed at `~/.claude/skills/forge/docs/forge.md`)
|
|
79
|
+
- **[Repository](https://github.com/izkac/forgekit#readme)**
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT. The Forge skill and several of its workflows are adapted from [Superpowers](https://github.com/obra/superpowers) (MIT).
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izkac/forgekit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"forgekit": "
|
|
8
|
-
"forge": "
|
|
9
|
-
"review": "
|
|
7
|
+
"forgekit": "bin/forgekit.mjs",
|
|
8
|
+
"forge": "bin/forge.mjs",
|
|
9
|
+
"review": "bin/review.mjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
package/src/integrity.mjs
CHANGED
|
@@ -58,14 +58,52 @@ function isNonEmptyString(value) {
|
|
|
58
58
|
*
|
|
59
59
|
* @param {{ cwd?: string, session?: Record<string, unknown> | null }} opts
|
|
60
60
|
*/
|
|
61
|
+
/**
|
|
62
|
+
* The archived copy of a change, when the live dir is gone. Archiving moves
|
|
63
|
+
* `changes/<change>/` to `changes/archive/<YYYY-MM-DD>-<change>/`, so the
|
|
64
|
+
* done-gate integrity check (which runs *after* archive in the finish flow)
|
|
65
|
+
* would otherwise never find spine.json/e2e.json. Returns null if no match.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} changesDir absolute path to `<root>/changes`
|
|
68
|
+
* @param {string} change change name
|
|
69
|
+
*/
|
|
70
|
+
function findArchivedChangeDir(changesDir, change) {
|
|
71
|
+
const archiveDir = path.join(changesDir, 'archive');
|
|
72
|
+
if (!fs.existsSync(archiveDir)) return null;
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = fs.readdirSync(archiveDir, { withFileTypes: true });
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const matches = entries
|
|
80
|
+
.filter((e) => e.isDirectory())
|
|
81
|
+
.map((e) => e.name)
|
|
82
|
+
// CLI + documented manual archive both name dirs `YYYY-MM-DD-<change>`;
|
|
83
|
+
// slice(11) drops the `YYYY-MM-DD-` prefix so the suffix must equal the
|
|
84
|
+
// change exactly (no false match on `…-other-<change>`).
|
|
85
|
+
.filter((name) => name === change || (/^\d{4}-\d{2}-\d{2}-/.test(name) && name.slice(11) === change))
|
|
86
|
+
.sort();
|
|
87
|
+
// Date-prefixed names sort lexically by date — newest archive wins.
|
|
88
|
+
return matches.length ? path.join(archiveDir, matches[matches.length - 1]) : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Live change dir if present, else its archived copy, else the live path. */
|
|
92
|
+
function liveOrArchived(changesDir, change) {
|
|
93
|
+
const liveDir = path.join(changesDir, change);
|
|
94
|
+
if (fs.existsSync(liveDir)) return liveDir;
|
|
95
|
+
return findArchivedChangeDir(changesDir, change) ?? liveDir;
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
export function resolveChangeDir(opts = {}) {
|
|
62
99
|
const cwd = opts.cwd ?? process.cwd();
|
|
63
100
|
const session = opts.session ?? null;
|
|
64
101
|
const change = session && isNonEmptyString(session.openspecChange) ? session.openspecChange : null;
|
|
65
102
|
if (!change) return null;
|
|
66
103
|
|
|
67
|
-
const
|
|
68
|
-
|
|
104
|
+
const openspecChanges = path.join(cwd, 'openspec', 'changes');
|
|
105
|
+
const openspecDir = path.join(openspecChanges, change);
|
|
106
|
+
if (session.planType === 'openspec') return liveOrArchived(openspecChanges, change);
|
|
69
107
|
|
|
70
108
|
let specsRoot = DEFAULT_SPECS_DIR;
|
|
71
109
|
try {
|
|
@@ -73,13 +111,18 @@ export function resolveChangeDir(opts = {}) {
|
|
|
73
111
|
} catch {
|
|
74
112
|
// keep default
|
|
75
113
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
114
|
+
const specsChanges = path.join(cwd, specsRoot, 'changes');
|
|
115
|
+
const specsDir = path.join(specsChanges, change);
|
|
116
|
+
if (session.planType === 'specs') return liveOrArchived(specsChanges, change);
|
|
78
117
|
|
|
79
|
-
// planType unknown — prefer whichever exists
|
|
118
|
+
// planType unknown — prefer whichever exists (live first, then archived)
|
|
80
119
|
if (fs.existsSync(openspecDir)) return openspecDir;
|
|
81
120
|
if (fs.existsSync(specsDir)) return specsDir;
|
|
82
|
-
return
|
|
121
|
+
return (
|
|
122
|
+
findArchivedChangeDir(openspecChanges, change) ??
|
|
123
|
+
findArchivedChangeDir(specsChanges, change) ??
|
|
124
|
+
openspecDir
|
|
125
|
+
);
|
|
83
126
|
}
|
|
84
127
|
|
|
85
128
|
/**
|
package/src/integrity.test.mjs
CHANGED
|
@@ -105,6 +105,70 @@ test('resolveChangeDir: openspec plan type and session-dir fallback', () => {
|
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test('resolveChangeDir: falls back to the archived copy after archive', () => {
|
|
109
|
+
for (const [planType, root] of [
|
|
110
|
+
['openspec', ['openspec', 'changes']],
|
|
111
|
+
['specs', ['specs', 'changes']],
|
|
112
|
+
]) {
|
|
113
|
+
const cwd = tmp('forge-changedir-arch-');
|
|
114
|
+
try {
|
|
115
|
+
const changesDir = path.join(cwd, ...root);
|
|
116
|
+
const liveDir = path.join(changesDir, 'add-customer-registry');
|
|
117
|
+
fs.mkdirSync(liveDir, { recursive: true });
|
|
118
|
+
const session = { planType, openspecChange: 'add-customer-registry' };
|
|
119
|
+
|
|
120
|
+
// Live dir present → live path.
|
|
121
|
+
assert.equal(resolveChangeDir({ cwd, session }), liveDir);
|
|
122
|
+
|
|
123
|
+
// Archive moves it → resolve follows into changes/archive/<date>-<name>.
|
|
124
|
+
const archived = path.join(changesDir, 'archive', '2026-07-20-add-customer-registry');
|
|
125
|
+
fs.mkdirSync(path.dirname(archived), { recursive: true });
|
|
126
|
+
fs.renameSync(liveDir, archived);
|
|
127
|
+
assert.equal(resolveChangeDir({ cwd, session }), archived);
|
|
128
|
+
|
|
129
|
+
// Newest archive wins when a change name recurs.
|
|
130
|
+
const older = path.join(changesDir, 'archive', '2025-01-01-add-customer-registry');
|
|
131
|
+
fs.mkdirSync(older, { recursive: true });
|
|
132
|
+
assert.equal(resolveChangeDir({ cwd, session }), archived);
|
|
133
|
+
|
|
134
|
+
// No false match on a different change that ends with the same words.
|
|
135
|
+
fs.mkdirSync(path.join(changesDir, 'archive', '2026-07-20-extra-add-customer-registry'), {
|
|
136
|
+
recursive: true,
|
|
137
|
+
});
|
|
138
|
+
assert.equal(resolveChangeDir({ cwd, session }), archived);
|
|
139
|
+
} finally {
|
|
140
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('runIntegrityChecks: passes after archive (spine resolves in archive dir)', () => {
|
|
146
|
+
const cwd = tmp('forge-int-archived-');
|
|
147
|
+
try {
|
|
148
|
+
const sessionDir = makeSessionDir(cwd);
|
|
149
|
+
const session = { planType: 'openspec', openspecChange: 'add-customer-registry', slug: 'add-customer-registry' };
|
|
150
|
+
|
|
151
|
+
// Green while live: spine in the change dir (sync-only notApplicable).
|
|
152
|
+
const liveDir = path.join(cwd, 'openspec', 'changes', 'add-customer-registry');
|
|
153
|
+
fs.mkdirSync(liveDir, { recursive: true });
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(liveDir, 'spine.json'),
|
|
156
|
+
`${JSON.stringify({ rows: [], notApplicable: 'sync HTTP only' }, null, 2)}\n`,
|
|
157
|
+
'utf8',
|
|
158
|
+
);
|
|
159
|
+
assert.equal(runIntegrityChecks({ cwd, sessionDir, session }).ok, true);
|
|
160
|
+
|
|
161
|
+
// Archive the change — the mechanical gate must STILL pass (the bug: it
|
|
162
|
+
// used to look only at the vanished live path and fail).
|
|
163
|
+
const archived = path.join(cwd, 'openspec', 'changes', 'archive', '2026-07-20-add-customer-registry');
|
|
164
|
+
fs.mkdirSync(path.dirname(archived), { recursive: true });
|
|
165
|
+
fs.renameSync(liveDir, archived);
|
|
166
|
+
assert.equal(runIntegrityChecks({ cwd, sessionDir, session }).ok, true);
|
|
167
|
+
} finally {
|
|
168
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
108
172
|
test('deferrals: add, list, resolve lifecycle', () => {
|
|
109
173
|
const dir = tmp('forge-defer-');
|
|
110
174
|
try {
|