@izkac/forgekit 0.3.6 → 0.3.8

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 CHANGED
@@ -32,7 +32,7 @@ Running agents across three IDEs and two terminals? `forge fleet` is a single co
32
32
 
33
33
  ```bash
34
34
  forge fleet list # every session: phase, task progress, engine, activity
35
- forge fleet watch # live-refreshing dashboard
35
+ forge fleet watch # live-refreshing dashboard (active sessions only; --all shows everything)
36
36
  forge fleet view <s> # detail + live transcript tail (Claude Code)
37
37
  forge fleet send <s> "pause and report" # message any session; --all broadcasts
38
38
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izkac/forgekit",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/brief-cli.mjs CHANGED
@@ -3,9 +3,9 @@
3
3
  * Operator-brief CLI (core in brief.mjs).
4
4
  *
5
5
  * Usage:
6
- * forge brief stamp [--session <id>] [--no-open] stamp freshness hash + open in browser
7
- * forge brief check [--session <id>] exit 1 when missing/stale/unstamped
8
- * forge brief open [--session <id>] open in default browser
6
+ * forge brief stamp [--session <id>] stamp freshness hash (never auto-opens)
7
+ * forge brief check [--session <id>] exit 1 when missing/stale/unstamped
8
+ * forge brief open [--session <id>] open in default browser (explicit only)
9
9
  */
10
10
 
11
11
  import fs from 'node:fs';
@@ -16,7 +16,7 @@ import { BRIEF_FILE, briefPath, checkBrief, openInBrowser, stampBrief } from './
16
16
  const [cmd, ...rest] = process.argv.slice(2);
17
17
  if (!cmd || !['stamp', 'check', 'open'].includes(cmd)) {
18
18
  process.stderr.write(
19
- 'Usage: forge brief stamp [--session <id>] [--no-open] | check [--session <id>] | open [--session <id>]\n',
19
+ 'Usage: forge brief stamp [--session <id>] | check [--session <id>] | open [--session <id>]\n',
20
20
  );
21
21
  process.exit(1);
22
22
  }
@@ -45,12 +45,13 @@ if (!changeDir) {
45
45
  }
46
46
 
47
47
  if (cmd === 'stamp') {
48
+ // Never auto-open: re-stamps happen several times a session and each open
49
+ // stole the operator's focus. `forge brief open` is the explicit opt-in.
48
50
  const hash = stampBrief(changeDir);
49
51
  process.stdout.write(`Stamped ${briefPath(changeDir)} (specs hash ${hash})\n`);
50
- if (!rest.includes('--no-open')) {
51
- openInBrowser(briefPath(changeDir));
52
- process.stdout.write('Opened in default browser.\n');
53
- }
52
+ process.stdout.write(
53
+ `Operator: review the brief at ${briefPath(changeDir)} — open it with \`forge brief open\`.\n`,
54
+ );
54
55
  } else {
55
56
  const file = briefPath(changeDir);
56
57
  if (!fs.existsSync(file)) {
package/src/fleet.mjs CHANGED
@@ -5,11 +5,13 @@
5
5
  *
6
6
  * Usage:
7
7
  * forge fleet list [--json]
8
- * forge fleet watch [--interval <sec>]
8
+ * forge fleet watch [--interval <sec>] [--all]
9
9
  * forge fleet view <session> [--transcript [N]]
10
10
  * forge fleet send <session>|--all <message...>
11
11
  *
12
12
  * <session> matches by sessionId, slug, or project name; must be unique.
13
+ * watch shows only active sessions (not done, session dir present); --all
14
+ * includes done/missing ones. list always shows everything.
13
15
  */
14
16
 
15
17
  import fs from 'node:fs';
@@ -27,7 +29,7 @@ function usage() {
27
29
  process.stderr.write(
28
30
  `Usage:
29
31
  forge fleet list [--json]
30
- forge fleet watch [--interval <sec>]
32
+ forge fleet watch [--interval <sec>] [--all]
31
33
  forge fleet view <session> [--transcript [N]]
32
34
  forge fleet send <session>|--all <message...>
33
35
  `,
@@ -78,7 +80,7 @@ function renderTable(entries) {
78
80
  `${pad(e.projectName, 18)} ${pad(e.slug, 26)} ${pad(e.engine ?? '—', 7)} ${pad(
79
81
  e.missing ? 'missing' : phaseBar(e.phase),
80
82
  18,
81
- )} ${pad(tasksCell(e), 13)} ${pad(e.pace ?? '—', 9)} ${pad(relTime(e.updatedAt), 4)} ${
83
+ )} ${pad(tasksCell(e), 13)} ${pad(e.pace ?? '—', 9)} ${pad(relTime(e.lastSeen ?? e.updatedAt), 4)} ${
82
84
  pending > 0 ? `✉ ${pending}` : ''
83
85
  }`,
84
86
  );
@@ -121,10 +123,14 @@ function cmdList(args) {
121
123
  function cmdWatch(args) {
122
124
  const i = args.indexOf('--interval');
123
125
  const interval = Math.max(1, Number(i >= 0 ? args[i + 1] : 3) || 3) * 1000;
126
+ const all = args.includes('--all');
124
127
  const render = () => {
128
+ const entries = listFleet().filter((e) => all || (!e.missing && e.phase !== 'done'));
125
129
  process.stdout.write('\x1b[2J\x1b[H');
126
- process.stdout.write(`forge fleet — ${new Date().toLocaleTimeString()} (Ctrl+C to exit)\n\n`);
127
- process.stdout.write(renderTable(listFleet()));
130
+ process.stdout.write(
131
+ `forge fleet — ${new Date().toLocaleTimeString()} (Ctrl+C to exit${all ? '' : ', --all for done/missing'})\n\n`,
132
+ );
133
+ process.stdout.write(renderTable(entries));
128
134
  };
129
135
  render();
130
136
  setInterval(render, interval);
@@ -9,10 +9,13 @@ import {
9
9
  drainInbox,
10
10
  entryFile,
11
11
  listFleet,
12
+ LIVE_WINDOW_MS,
13
+ liveOverlaps,
12
14
  peekInbox,
13
15
  queueMessage,
14
16
  registerSession,
15
17
  sanitizePath,
18
+ touchSession,
16
19
  unregisterSession,
17
20
  } from './lib/fleet.mjs';
18
21
  import { saveSession } from './lib.mjs';
@@ -132,6 +135,47 @@ function registerSessionIn(fleetDir, project, session) {
132
135
  }
133
136
  }
134
137
 
138
+ test('registerSession stamps lastSeen; touchSession refreshes it', () => {
139
+ process.env.FORGEKIT_FLEET_DIR = path.join(tmp('fleet-hb-'), 'sessions');
140
+ const project = tmp('fleet-proj-');
141
+ makeProject(project, 's5');
142
+
143
+ registerSession(project, makeSession('s5'));
144
+ const before = listFleet()[0].lastSeen;
145
+ assert.ok(before);
146
+
147
+ const file = entryFile(project, 's5');
148
+ const entry = JSON.parse(fs.readFileSync(file, 'utf8'));
149
+ entry.lastSeen = '2000-01-01T00:00:00.000Z';
150
+ fs.writeFileSync(file, JSON.stringify(entry));
151
+
152
+ touchSession(project, 's5');
153
+ const after = listFleet()[0].lastSeen;
154
+ assert.ok(after > '2000-01-01T00:00:00.000Z');
155
+ });
156
+
157
+ test('liveOverlaps flags only live sessions in the same project', () => {
158
+ process.env.FORGEKIT_FLEET_DIR = path.join(tmp('fleet-ovl-'), 'sessions');
159
+ const project = tmp('fleet-proj-');
160
+ const other = tmp('fleet-proj2-');
161
+ for (const id of ['me', 'peer', 'finished']) makeProject(project, id);
162
+ makeProject(other, 'elsewhere');
163
+
164
+ registerSession(project, makeSession('me'));
165
+ registerSession(project, makeSession('peer'));
166
+ registerSession(project, makeSession('finished', { phase: 'done' }));
167
+ registerSession(other, makeSession('elsewhere'));
168
+
169
+ const overlaps = liveOverlaps(project, 'me');
170
+ assert.deepEqual(
171
+ overlaps.map((e) => e.sessionId),
172
+ ['peer'],
173
+ );
174
+
175
+ // Stale heartbeat falls outside the liveness window.
176
+ assert.equal(liveOverlaps(project, 'me', Date.now() + LIVE_WINDOW_MS + 1000).length, 0);
177
+ });
178
+
135
179
  test('sanitizePath matches Claude Code project-dir naming', () => {
136
180
  assert.equal(sanitizePath('S:\\Projects\\forgekit'), 'S--Projects-forgekit');
137
181
  });
package/src/lib/fleet.mjs CHANGED
@@ -82,6 +82,7 @@ export function registerSession(projectRoot, session) {
82
82
  engine: detectEngine() ?? prev.engine ?? null,
83
83
  createdAt: session.createdAt,
84
84
  updatedAt: session.updatedAt,
85
+ lastSeen: new Date().toISOString(),
85
86
  };
86
87
  fs.mkdirSync(fleetDir(), { recursive: true });
87
88
  fs.writeFileSync(file, `${JSON.stringify(entry, null, 2)}\n`, 'utf8');
@@ -90,6 +91,40 @@ export function registerSession(projectRoot, session) {
90
91
  }
91
92
  }
92
93
 
94
+ /**
95
+ * Heartbeat: refresh lastSeen on an existing registry entry. Called from the
96
+ * reminder hook, which fires on every agent turn — so lastSeen ≈ "agent is
97
+ * actually running", unlike updatedAt which only moves on saveSession.
98
+ */
99
+ export function touchSession(projectRoot, sessionId) {
100
+ try {
101
+ const file = entryFile(projectRoot, sessionId);
102
+ const entry = JSON.parse(fs.readFileSync(file, 'utf8'));
103
+ entry.lastSeen = new Date().toISOString();
104
+ fs.writeFileSync(file, `${JSON.stringify(entry, null, 2)}\n`, 'utf8');
105
+ } catch {
106
+ /* advisory */
107
+ }
108
+ }
109
+
110
+ // ponytail: fixed 30-min liveness window; make configurable if hooks ever fire slower.
111
+ export const LIVE_WINDOW_MS = 30 * 60 * 1000;
112
+
113
+ /**
114
+ * Other live sessions in the same project — the overlap signal for "two
115
+ * agents editing one working tree". Live = not done, session dir present,
116
+ * heartbeat (lastSeen, falling back to updatedAt) within LIVE_WINDOW_MS.
117
+ */
118
+ export function liveOverlaps(projectRoot, sessionId, now = Date.now()) {
119
+ const root = path.resolve(projectRoot);
120
+ return listFleet().filter((e) => {
121
+ if (e.sessionId === sessionId || e.missing || e.phase === 'done') return false;
122
+ if (path.resolve(e.project) !== root) return false;
123
+ const seen = new Date(e.lastSeen ?? e.updatedAt).getTime();
124
+ return !Number.isNaN(seen) && now - seen < LIVE_WINDOW_MS;
125
+ });
126
+ }
127
+
93
128
  export function unregisterSession(projectRoot, sessionId) {
94
129
  try {
95
130
  fs.rmSync(entryFile(projectRoot, sessionId), { force: true });
@@ -20,6 +20,7 @@ import {
20
20
  } from './lib.mjs';
21
21
  import { resolveSessionPaceFields } from './preferences.mjs';
22
22
  import { warnIfDoctorFails } from './doctor.mjs';
23
+ import { liveOverlaps, queueMessage, sessionDirFor } from './lib/fleet.mjs';
23
24
 
24
25
  function usage() {
25
26
  process.stderr.write(
@@ -64,19 +65,35 @@ Object.assign(session, paceFields);
64
65
  saveSession(dir, session);
65
66
  writeActive(sessionId);
66
67
 
67
- process.stdout.write(
68
- `${JSON.stringify(
69
- {
70
- sessionId,
71
- dir,
72
- session: defaultStatus(session),
73
- pace: {
74
- requested: session.pace,
75
- resolved: session.resolvedPace,
76
- reason: session.paceReason,
77
- },
78
- },
79
- null,
80
- 2,
81
- )}\n`,
82
- );
68
+ // Fleet coordination: another live session in this working tree risks
69
+ // conflicting edits — surface it here and notify the other sessions' inboxes.
70
+ const overlaps = liveOverlaps(process.cwd(), sessionId);
71
+ for (const o of overlaps) {
72
+ queueMessage(
73
+ sessionDirFor(o),
74
+ `Fleet overlap: session "${session.slug}" (${sessionId}) just started in this project. Coordinate with the user to avoid conflicting edits.`,
75
+ );
76
+ }
77
+
78
+ const out = {
79
+ sessionId,
80
+ dir,
81
+ session: defaultStatus(session),
82
+ pace: {
83
+ requested: session.pace,
84
+ resolved: session.resolvedPace,
85
+ reason: session.paceReason,
86
+ },
87
+ };
88
+ if (overlaps.length > 0) {
89
+ out.overlaps = overlaps.map((o) => ({
90
+ sessionId: o.sessionId,
91
+ slug: o.slug,
92
+ phase: o.phase,
93
+ engine: o.engine,
94
+ lastSeen: o.lastSeen ?? o.updatedAt,
95
+ }));
96
+ out.overlapAdvice =
97
+ 'Other live sessions are working in this project. Tell the user and ask: continue anyway, use a git worktree, or pause one session.';
98
+ }
99
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
package/src/score.mjs CHANGED
@@ -475,7 +475,52 @@ export function formatScorecardMarkdown(card) {
475
475
  }
476
476
 
477
477
  /**
478
- * Write scorecard.json + scorecard.md into the session dir.
478
+ * Durable one-line-per-session ledger at `.forge/scorecards.jsonl`. Sessions
479
+ * are pruned after RETENTION_DAYS and scorecards die with them; the ledger
480
+ * survives — it is the history `/forge:analyze` reads for trends. Re-scoring
481
+ * a session replaces its line (latest score wins). Never throws.
482
+ *
483
+ * @param {string} sessionDir
484
+ * @param {ReturnType<typeof scoreSession>} card
485
+ * @param {Record<string, unknown>} session
486
+ */
487
+ export function appendScorecardLedger(sessionDir, card, session = {}) {
488
+ try {
489
+ const file = path.join(path.resolve(sessionDir, '..', '..'), 'scorecards.jsonl');
490
+ const line = {
491
+ scoredAt: card.scoredAt,
492
+ sessionId: card.sessionId,
493
+ slug: card.slug,
494
+ change: card.openspecChange,
495
+ score: card.score,
496
+ grade: card.grade,
497
+ integrityOk: card.integrityOk,
498
+ pace: session.resolvedPace ?? null,
499
+ incompleteReason: session.incompleteReason ?? null,
500
+ caps: card.caps,
501
+ deductions: card.checks
502
+ .filter((c) => c.points < c.max)
503
+ .map((c) => ({ id: c.id, points: c.points, max: c.max, notes: c.notes })),
504
+ };
505
+ const kept = (fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n') : [])
506
+ .filter(Boolean)
507
+ .filter((l) => {
508
+ try {
509
+ return JSON.parse(l).sessionId !== card.sessionId;
510
+ } catch {
511
+ return false;
512
+ }
513
+ });
514
+ kept.push(JSON.stringify(line));
515
+ fs.writeFileSync(file, `${kept.join('\n')}\n`, 'utf8');
516
+ } catch {
517
+ /* ledger is advisory — never block a scorecard write */
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Write scorecard.json + scorecard.md into the session dir, and mirror a
523
+ * summary line into the durable `.forge/scorecards.jsonl` ledger.
479
524
  *
480
525
  * @param {{ cwd?: string, sessionDir: string, session: Record<string, unknown> }} opts
481
526
  */
@@ -485,5 +530,6 @@ export function writeSessionScorecard(opts) {
485
530
  const mdPath = path.join(opts.sessionDir, 'scorecard.md');
486
531
  writeJson(jsonPath, card);
487
532
  fs.writeFileSync(mdPath, formatScorecardMarkdown(card), 'utf8');
533
+ appendScorecardLedger(opts.sessionDir, card, opts.session);
488
534
  return { card, jsonPath, mdPath };
489
535
  }
@@ -191,6 +191,17 @@ test('writeSessionScorecard writes json and md', () => {
191
191
  assert.equal(fs.existsSync(jsonPath), true);
192
192
  assert.equal(fs.existsSync(mdPath), true);
193
193
  assert.equal(JSON.parse(fs.readFileSync(jsonPath, 'utf8')).grade, card.grade);
194
+
195
+ // Durable ledger: one line per session, re-scoring replaces (not appends).
196
+ const ledger = path.join(root, '.forge', 'scorecards.jsonl');
197
+ assert.equal(fs.existsSync(ledger), true);
198
+ writeSessionScorecard({ cwd: root, sessionDir, session });
199
+ const lines = fs.readFileSync(ledger, 'utf8').split('\n').filter(Boolean);
200
+ assert.equal(lines.length, 1);
201
+ const entry = JSON.parse(lines[0]);
202
+ assert.equal(entry.sessionId, session.id);
203
+ assert.equal(entry.grade, card.grade);
204
+ assert.ok(Array.isArray(entry.deductions));
194
205
  } finally {
195
206
  fs.rmSync(root, { recursive: true, force: true });
196
207
  }
@@ -8,8 +8,14 @@
8
8
  * forge reminder --format plain
9
9
  */
10
10
 
11
- import { FORGE_DIR, loadSession, readActive } from './lib.mjs';
12
- import { drainInbox } from './lib/fleet.mjs';
11
+ import { FORGE_DIR, loadSession, readActive, REPO_ROOT } from './lib.mjs';
12
+ import {
13
+ drainInbox,
14
+ liveOverlaps,
15
+ queueMessage,
16
+ sessionDirFor,
17
+ touchSession,
18
+ } from './lib/fleet.mjs';
13
19
  import { resolveEffectivePreferences } from './preferences.mjs';
14
20
 
15
21
  function getActiveSessionInfo() {
@@ -149,10 +155,32 @@ if (!info) {
149
155
  process.exit(0);
150
156
  }
151
157
 
158
+ // Fleet heartbeat: this hook fires on every agent turn, so lastSeen in the
159
+ // registry tracks "agent actually running", not just the last saveSession.
160
+ touchSession(REPO_ROOT, info.session.id);
161
+
152
162
  let message = prompt
153
163
  ? buildForgePromptMessage(info, prompt)
154
164
  : buildForgeMessage(info);
155
165
 
166
+ // Overlap check on resume: another live session in this same working tree
167
+ // means potentially conflicting edits. Warn this agent and drop a note in the
168
+ // other sessions' inboxes. Session-start only — per-turn would spam.
169
+ if (format === 'claude-session-start') {
170
+ const overlaps = liveOverlaps(REPO_ROOT, info.session.id);
171
+ if (overlaps.length > 0) {
172
+ message += `\n\nFleet overlap — other live session(s) in this project:\n${overlaps
173
+ .map((o) => `- ${o.slug} (${o.sessionId}) · ${o.engine ?? 'unknown'} · phase ${o.phase}`)
174
+ .join('\n')}\nTell the user and ask how to proceed: continue anyway, move this work to a git worktree, or pause one session.`;
175
+ for (const o of overlaps) {
176
+ queueMessage(
177
+ sessionDirFor(o),
178
+ `Fleet overlap: session "${info.session.slug}" (${info.session.id}) just resumed in this project. Coordinate with the user to avoid conflicting edits.`,
179
+ );
180
+ }
181
+ }
182
+ }
183
+
156
184
  // Deliver queued `forge fleet send` messages exactly once (drain moves them
157
185
  // to inbox/delivered/). This is the fleet command bus: control terminal →
158
186
  // inbox file → injected into the agent's next turn via this hook.
@@ -246,6 +246,8 @@ required for correctness.
246
246
  | `/forge:apply` | **Tracked-change implement** — subagent TDD + verify + review (preferred over `/opsx:apply`) |
247
247
  | `/forge:build` | Implement phase (`tasks.md` from either engine) |
248
248
  | `/forge:status` | Show active session progress |
249
+ | `/forge:harness` | Ensure a working, recorded project e2e harness (build proactively) |
250
+ | `/forge:analyze` | Agent-written improvement report over recent sessions |
249
251
  | `/forge:skip` | **Explicit** opt-out of Forge for this task |
250
252
 
251
253
  OpenSpec commands remain available standalone (OpenSpec-engine projects):
@@ -624,6 +626,8 @@ per machine with `forgekit install`; wire project commands/hooks with `forge ini
624
626
  | `/forge:apply` | Tracked-change implement + verify + review (preferred) |
625
627
  | `/forge:build` | Implement phase (`tasks.md` from either engine) |
626
628
  | `/forge:status` | Session progress |
629
+ | `/forge:harness` | Ensure a working, recorded project e2e harness |
630
+ | `/forge:analyze` | Improvement report over recent sessions |
627
631
  | `/forge:skip` | Explicit skip for this task |
628
632
 
629
633
  ### Codex CLI
@@ -37,7 +37,7 @@ Thin wrapper around the project **`openspec-propose`** skill (or `/opsx:propose`
37
37
  where helpful), then:
38
38
 
39
39
  ```bash
40
- forge brief stamp # records specs hash + opens it in the operator's browser
40
+ forge brief stamp # records specs hash (does NOT auto-open)
41
41
  ```
42
42
 
43
43
  `forge phase implement` hard-refuses while the brief is missing or stale
@@ -51,7 +51,8 @@ Thin wrapper around the project **`openspec-propose`** skill (or `/opsx:propose`
51
51
  Count tasks from `tasks.md` checkboxes.
52
52
 
53
53
  7. Get user approval to proceed to implement (unless they already said "go").
54
- The brief (opened by `forge brief stamp`) is what the operator reviews.
54
+ The brief is what the operator reviews tell them its path and that
55
+ `forge brief open` launches it; never open it for them.
55
56
 
56
57
  ## Session tracking
57
58
 
@@ -85,7 +85,7 @@ OpenSpec propose flow without the vendor CLI. Change lives under
85
85
  where helpful), then:
86
86
 
87
87
  ```bash
88
- forge brief stamp # records specs hash + opens it in the operator's browser
88
+ forge brief stamp # records specs hash (does NOT auto-open)
89
89
  ```
90
90
 
91
91
  `forge phase implement` hard-refuses while the brief is missing or stale
@@ -102,7 +102,8 @@ OpenSpec propose flow without the vendor CLI. Change lives under
102
102
  name for both engines.)
103
103
 
104
104
  7. Get user approval on the artefacts before implementing (unless they already said "go").
105
- The brief (opened by `forge brief stamp`) is what the operator reviews.
105
+ The brief is what the operator reviews tell them its path and that
106
+ `forge brief open` launches it; never open it for them.
106
107
 
107
108
  ## Compatibility
108
109
 
@@ -21,7 +21,8 @@ freshness and opens the file.
21
21
  ```bash
22
22
  # 1. specs are final (proposal.md / design.md / tasks.md)
23
23
  # 2. write <changeDir>/brief.html (structure below)
24
- forge brief stamp # records specs hash + opens it in the operator's browser
24
+ forge brief stamp # records specs hash tell the operator where the brief is
25
+ # (it is NOT auto-opened; they can run `forge brief open`)
25
26
  forge phase implement --tasks-total <N> # hard-gated on a fresh stamped brief
26
27
  ```
27
28
 
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: /forge:analyze
3
+ description: Forge — analyze recent sessions and write an improvement report
4
+ category: Workflow
5
+ tags: [workflow, forge, retrospective]
6
+ ---
7
+
8
+ **Forge-owned command.** Read the evidence recent Forge sessions left behind and write an honest improvement report — what went well, what keeps going wrong, and what to change. The analysis is yours: look for patterns, not single events.
9
+
10
+ ## 1. Gather
11
+
12
+ - `.forge/scorecards.jsonl` — durable one-line-per-session ledger (score, grade, deductions, caps, pace, incomplete reasons). Survives session cleanup; this is your history.
13
+ - `.forge/sessions/*/` for sessions still on disk: `scorecard.md`, `verify-evidence.md`, `reviews/final-review.md`, `deferrals.json`, `session.json`.
14
+
15
+ If both are empty, tell the user there is nothing to analyze yet and stop.
16
+
17
+ ## 2. Analyze
18
+
19
+ Patterns worth hunting (not a checklist — follow what the data shows):
20
+
21
+ - **Recurring deductions** — the same check losing points across sessions is a process problem, not a session problem.
22
+ - **`--allow-incomplete` usage** — legitimate deferrals, or the gate being routinely dodged? Read the reasons.
23
+ - **Pace vs outcome** — do brisk/lite sessions score worse here? Are task-count escalations firing when they should?
24
+ - **Evidence honesty** — ceremony-only tests, evidence with non-zero exits, verify phases that re-ran nothing.
25
+ - **Deferrals** — raised vs resolved; anything raised repeatedly for the same area?
26
+ - **Grade trend** — improving, flat, or decaying over time?
27
+
28
+ For each pattern found: name the root cause and one concrete fix — a pace pref, a missing harness, a rule, a habit. No generic advice.
29
+
30
+ ## 3. Report
31
+
32
+ Write `.forge/reports/analysis-<YYYY-MM-DD>.md`:
33
+
34
+ - **TL;DR** — 3 bullets max
35
+ - **Trend table** — session · date · grade · top deduction
36
+ - **What's working** — keep doing
37
+ - **What's broken** — each item with its concrete fix
38
+ - **Next actions** — ranked, smallest-effective first
39
+
40
+ Then summarize the TL;DR to the user in chat.
41
+
42
+ Reference: `~/.claude/skills/forge/docs/forge.md`
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: /forge:harness
3
+ description: Forge — ensure the project has a working, recorded e2e harness
4
+ category: Workflow
5
+ tags: [workflow, forge, e2e]
6
+ ---
7
+
8
+ **Forge-owned command.** Ensure this project has a working, recorded e2e harness — the environment `forge e2e run` steps execute against, and the base for the project's own end-to-end testing. Building it proactively here means later sessions never stall at the integrity gate waiting for one.
9
+
10
+ ## 1. Check what's recorded
11
+
12
+ ```bash
13
+ forge e2e harness
14
+ ```
15
+
16
+ - **Harness shown** → verify it still works: run its start command, then one real probe against it. Working → report to the user and stop. Broken → continue to step 2, treating the existing harness as the starting point (fix, don't rebuild).
17
+ - **"No harness recorded"** → step 2.
18
+
19
+ ## 2. Design it with the operator
20
+
21
+ Explore the project first: how the app starts, what backing services it needs, and what a real user-visible probe looks like (HTTP endpoint, CLI invocation, UI route). Then propose to the user:
22
+
23
+ - what the harness starts (app + backing services, isolated ports/data so it can't touch dev state)
24
+ - how a test asserts *through the product* (the probe `forge e2e` steps will use — not internal function calls)
25
+ - where it lives (e.g. `scripts/e2e/`, a compose file, a test config)
26
+
27
+ **Get explicit approval before building.** A harness is committed project infrastructure, not session scratch.
28
+
29
+ ## 3. Build and prove
30
+
31
+ Build the approved harness. Prove it end-to-end: start it, run one real probe, show the user the output. A harness that has never gone green is not done.
32
+
33
+ ## 4. Record it
34
+
35
+ ```bash
36
+ forge e2e harness --set "<what/where>" --start "<command>" [--dir <path>]
37
+ ```
38
+
39
+ Then commit `.forge/config.json`. Every future session sees the harness on `forge e2e init` and reuses it instead of rebuilding or asking again.
40
+
41
+ Reference: `~/.claude/skills/forge/docs/forge.md`
@@ -13,4 +13,4 @@ Read and follow the Forge skill (`~/.claude/skills/forge/SKILL.md`) and `~/.clau
13
13
  2. Resume from `.forge/active.json` or `forge new <slug>`
14
14
  3. Continue from current `phase` in `session.json`
15
15
 
16
- Subcommands: `/forge:brainstorm`, `/forge:plan`, `/forge:apply`, `/forge:build`, `/forge:status`, `/forge:skip`
16
+ Subcommands: `/forge:brainstorm`, `/forge:plan`, `/forge:apply`, `/forge:build`, `/forge:status`, `/forge:harness`, `/forge:analyze`, `/forge:skip`
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: /forge:analyze
3
+ id: forge-analyze
4
+ category: Workflow
5
+ description: Forge — analyze recent sessions and write an improvement report
6
+ ---
7
+
8
+ **Forge-owned command.** Read the evidence recent Forge sessions left behind and write an honest improvement report — what went well, what keeps going wrong, and what to change. The analysis is yours: look for patterns, not single events.
9
+
10
+ ## 1. Gather
11
+
12
+ - `.forge/scorecards.jsonl` — durable one-line-per-session ledger (score, grade, deductions, caps, pace, incomplete reasons). Survives session cleanup; this is your history.
13
+ - `.forge/sessions/*/` for sessions still on disk: `scorecard.md`, `verify-evidence.md`, `reviews/final-review.md`, `deferrals.json`, `session.json`.
14
+
15
+ If both are empty, tell the user there is nothing to analyze yet and stop.
16
+
17
+ ## 2. Analyze
18
+
19
+ Patterns worth hunting (not a checklist — follow what the data shows):
20
+
21
+ - **Recurring deductions** — the same check losing points across sessions is a process problem, not a session problem.
22
+ - **`--allow-incomplete` usage** — legitimate deferrals, or the gate being routinely dodged? Read the reasons.
23
+ - **Pace vs outcome** — do brisk/lite sessions score worse here? Are task-count escalations firing when they should?
24
+ - **Evidence honesty** — ceremony-only tests, evidence with non-zero exits, verify phases that re-ran nothing.
25
+ - **Deferrals** — raised vs resolved; anything raised repeatedly for the same area?
26
+ - **Grade trend** — improving, flat, or decaying over time?
27
+
28
+ For each pattern found: name the root cause and one concrete fix — a pace pref, a missing harness, a rule, a habit. No generic advice.
29
+
30
+ ## 3. Report
31
+
32
+ Write `.forge/reports/analysis-<YYYY-MM-DD>.md`:
33
+
34
+ - **TL;DR** — 3 bullets max
35
+ - **Trend table** — session · date · grade · top deduction
36
+ - **What's working** — keep doing
37
+ - **What's broken** — each item with its concrete fix
38
+ - **Next actions** — ranked, smallest-effective first
39
+
40
+ Then summarize the TL;DR to the user in chat.
41
+
42
+ Reference: `~/.cursor/skills/forge/docs/forge.md`
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: /forge:harness
3
+ id: forge-harness
4
+ category: Workflow
5
+ description: Forge — ensure the project has a working, recorded e2e harness
6
+ ---
7
+
8
+ **Forge-owned command.** Ensure this project has a working, recorded e2e harness — the environment `forge e2e run` steps execute against, and the base for the project's own end-to-end testing. Building it proactively here means later sessions never stall at the integrity gate waiting for one.
9
+
10
+ ## 1. Check what's recorded
11
+
12
+ ```bash
13
+ forge e2e harness
14
+ ```
15
+
16
+ - **Harness shown** → verify it still works: run its start command, then one real probe against it. Working → report to the user and stop. Broken → continue to step 2, treating the existing harness as the starting point (fix, don't rebuild).
17
+ - **"No harness recorded"** → step 2.
18
+
19
+ ## 2. Design it with the operator
20
+
21
+ Explore the project first: how the app starts, what backing services it needs, and what a real user-visible probe looks like (HTTP endpoint, CLI invocation, UI route). Then propose to the user:
22
+
23
+ - what the harness starts (app + backing services, isolated ports/data so it can't touch dev state)
24
+ - how a test asserts *through the product* (the probe `forge e2e` steps will use — not internal function calls)
25
+ - where it lives (e.g. `scripts/e2e/`, a compose file, a test config)
26
+
27
+ **Get explicit approval before building.** A harness is committed project infrastructure, not session scratch.
28
+
29
+ ## 3. Build and prove
30
+
31
+ Build the approved harness. Prove it end-to-end: start it, run one real probe, show the user the output. A harness that has never gone green is not done.
32
+
33
+ ## 4. Record it
34
+
35
+ ```bash
36
+ forge e2e harness --set "<what/where>" --start "<command>" [--dir <path>]
37
+ ```
38
+
39
+ Then commit `.forge/config.json`. Every future session sees the harness on `forge e2e init` and reuses it instead of rebuilding or asking again.
40
+
41
+ Reference: `~/.cursor/skills/forge/docs/forge.md`
@@ -13,4 +13,4 @@ Read and follow the Forge skill (`~/.cursor/skills/forge/SKILL.md`) and `~/.curs
13
13
  2. Resume from `.forge/active.json` or `forge new <slug>`
14
14
  3. Continue from current `phase` in `session.json`
15
15
 
16
- Subcommands: `/forge:brainstorm`, `/forge:plan`, `/forge:apply`, `/forge:build`, `/forge:status`, `/forge:skip`
16
+ Subcommands: `/forge:brainstorm`, `/forge:plan`, `/forge:apply`, `/forge:build`, `/forge:status`, `/forge:harness`, `/forge:analyze`, `/forge:skip`