forge-orkes 0.41.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/bin/create-forge.js +245 -43
  2. package/package.json +5 -1
  3. package/template/.claude/agents/doc-reviewer.md +115 -0
  4. package/template/.claude/agents/performance-reviewer.md +138 -0
  5. package/template/.claude/agents/security-reviewer.md +163 -0
  6. package/template/.claude/agents/tester.md +3 -5
  7. package/template/.claude/hooks/README.md +39 -0
  8. package/template/.claude/hooks/block-dangerous-commands.sh +158 -0
  9. package/template/.claude/hooks/forge-active-skill-guard.sh +44 -0
  10. package/template/.claude/hooks/format-on-save.sh +95 -0
  11. package/template/.claude/hooks/protect-files.sh +101 -0
  12. package/template/.claude/hooks/scan-secrets.sh +87 -0
  13. package/template/.claude/hooks/tests/README.md +76 -0
  14. package/template/.claude/hooks/tests/cases/block-dangerous-commands.cases.json +376 -0
  15. package/template/.claude/hooks/tests/cases/protect-files.cases.json +222 -0
  16. package/template/.claude/hooks/tests/cases/scan-secrets.cases.json +218 -0
  17. package/template/.claude/hooks/tests/cases/warn-large-files.cases.json +146 -0
  18. package/template/.claude/hooks/tests/run.sh +118 -0
  19. package/template/.claude/hooks/warn-large-files.sh +71 -0
  20. package/template/.claude/rules/README.md +63 -0
  21. package/template/.claude/rules/agent-discipline.md +14 -0
  22. package/template/.claude/settings.json +69 -1
  23. package/template/.claude/skills/architecting/SKILL.md +2 -0
  24. package/template/.claude/skills/chief-of-staff/SKILL.md +37 -26
  25. package/template/.claude/skills/executing/SKILL.md +16 -0
  26. package/template/.claude/skills/forge/SKILL.md +15 -27
  27. package/template/.claude/skills/forge/desire-paths-review.md +37 -0
  28. package/template/.claude/skills/initializing/SKILL.md +4 -0
  29. package/template/.claude/skills/planning/SKILL.md +17 -3
  30. package/template/.claude/skills/reviewing/SKILL.md +2 -0
  31. package/template/.claude/skills/testing/SKILL.md +3 -3
  32. package/template/.claude/skills/verifying/SKILL.md +20 -9
  33. package/template/.forge/FORGE.md +36 -8
  34. package/template/.forge/migrations/0.20.0-nested-phase-layout.md +10 -2
  35. package/template/.forge/migrations/0.42.0-id-reservation.md +48 -0
  36. package/template/.forge/migrations/0.43.0-safety-policy.md +60 -0
  37. package/template/.forge/migrations/0.44.0-desire-paths.md +57 -0
  38. package/template/.forge/reservations.yml +31 -0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const readline = require('readline');
6
7
  const { execSync } = require('child_process');
@@ -93,6 +94,56 @@ function copyDirRecursive(src, dest) {
93
94
  return count;
94
95
  }
95
96
 
97
+ /**
98
+ * Make every *.sh under a hooks dir executable (ADR-017). The npm tarball usually
99
+ * preserves the bit, but a checkout/copy can strip it — the guard hooks must be
100
+ * runnable or Claude Code silently skips them. Best-effort: never throws.
101
+ */
102
+ function makeHookScriptsExecutable(hooksDir) {
103
+ if (!fs.existsSync(hooksDir)) return;
104
+ const walk = (dir) => {
105
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
106
+ const p = path.join(dir, entry.name);
107
+ if (entry.isDirectory()) walk(p);
108
+ else if (entry.name.endsWith('.sh')) {
109
+ try { fs.chmodSync(p, 0o755); } catch { /* best-effort */ }
110
+ }
111
+ }
112
+ };
113
+ try { walk(hooksDir); } catch { /* best-effort */ }
114
+ }
115
+
116
+ /**
117
+ * Additively sync a framework-managed `.claude/` dir into an existing project on
118
+ * upgrade (ADR-017/018). Copies/updates the template's files, but NEVER deletes
119
+ * project-local extras — so experimental hooks (m10 `forge-claim-check`) and
120
+ * pack-installed path-scoped rules (laravel-*, vue-inertia, …) survive. These dirs
121
+ * are intentionally NOT in FRAMEWORK_OWNED_DIRS, whose auto-clean would flag those
122
+ * extras as "stale" and offer to remove them.
123
+ */
124
+ function upgradeAdditiveDir(relDir, results) {
125
+ const srcDir = path.join(templateDir, relDir);
126
+ const destDir = path.join(targetDir, relDir);
127
+ if (!fs.existsSync(srcDir)) return;
128
+
129
+ for (const rel of collectFiles(srcDir, '')) {
130
+ const srcPath = path.join(srcDir, rel);
131
+ const destPath = path.join(destDir, rel);
132
+ const displayPath = path.join(relDir, rel);
133
+ if (!fs.existsSync(destPath)) {
134
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
135
+ fs.copyFileSync(srcPath, destPath);
136
+ results.added.push(displayPath);
137
+ } else if (Buffer.compare(fs.readFileSync(srcPath), fs.readFileSync(destPath)) !== 0) {
138
+ fs.copyFileSync(srcPath, destPath);
139
+ results.updated.push(displayPath);
140
+ } else {
141
+ results.unchanged.push(displayPath);
142
+ }
143
+ }
144
+ makeHookScriptsExecutable(destDir); // no-op for dirs with no *.sh
145
+ }
146
+
96
147
  /**
97
148
  * npm strips files literally named `.gitignore` from a published tarball (it
98
149
  * treats them as ignore-rules, not content), so the template ships its forge
@@ -155,7 +206,7 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
155
206
  const srcDir = path.join(templateDir, relDir);
156
207
  const destDir = path.join(targetDir, relDir);
157
208
 
158
- const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [] };
209
+ const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [], removeCandidates: [] };
159
210
 
160
211
  if (!fs.existsSync(srcDir)) return result;
161
212
 
@@ -195,15 +246,13 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
195
246
  }
196
247
  const destPath = path.join(destDir, rel);
197
248
  if (autoClean) {
198
- fs.unlinkSync(destPath);
199
- // Remove empty parent dirs up to relDir
200
- let dir = path.dirname(destPath);
201
- while (dir !== destDir && fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
202
- fs.rmdirSync(dir);
203
- dir = path.dirname(dir);
204
- }
249
+ // Defer deletion — collect the candidate; upgrade() confirms before
250
+ // removing (R066a). Empty-parent cleanup needs destDir, carried along.
251
+ result.removeCandidates.push({ destPath, destDir, displayPath });
252
+ } else {
253
+ // Template-only dirs: report-only, no deletion here (existing behavior).
254
+ result.removed.push(displayPath);
205
255
  }
206
- result.removed.push(path.join(relDir, rel));
207
256
  }
208
257
  }
209
258
 
@@ -271,7 +320,80 @@ function ensureClaudeMdImport() {
271
320
  }
272
321
 
273
322
  /**
274
- * Smart-merge settings.json: overwrite forge.* keys from template, preserve user hooks.
323
+ * Additively install the template's framework-managed hooks into an existing
324
+ * settings.json (ADR-017). Never removes a user's hooks; dedups by deep-equality
325
+ * so re-running upgrade is idempotent. New groups (the safety guardrails) get
326
+ * appended; groups the project already has (identical) are skipped.
327
+ */
328
+ function mergeManagedHooks(destSettings, srcSettings) {
329
+ if (!srcSettings.hooks) return;
330
+ destSettings.hooks = destSettings.hooks || {};
331
+ for (const event of Object.keys(srcSettings.hooks)) {
332
+ const srcGroups = srcSettings.hooks[event] || [];
333
+ const destGroups = destSettings.hooks[event] || (destSettings.hooks[event] = []);
334
+ for (const group of srcGroups) {
335
+ const groupJson = JSON.stringify(group);
336
+ if (!destGroups.some((d) => JSON.stringify(d) === groupJson)) {
337
+ destGroups.push(group);
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Union the template's secret/key deny globs into the project's permissions.deny
345
+ * (ADR-017). Additive — a user's own allow/deny entries are untouched.
346
+ */
347
+ function mergeManagedDeny(destSettings, srcSettings) {
348
+ const srcDeny = (srcSettings.permissions && srcSettings.permissions.deny) || [];
349
+ if (!srcDeny.length) return;
350
+ destSettings.permissions = destSettings.permissions || {};
351
+ const destDeny = destSettings.permissions.deny || (destSettings.permissions.deny = []);
352
+ for (const glob of srcDeny) {
353
+ if (!destDeny.includes(glob)) destDeny.push(glob);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Strip the legacy *inline* active-skill guard from PreToolUse (forge#12). It was
359
+ * an inline `if [ ! -f .../.forge/.active-skill ]; then ... exit 2; fi` one-liner;
360
+ * it is replaced by the forge-active-skill-guard.sh script (which also honors a
361
+ * milestone's executing status). The additive hook merge can only ADD the new
362
+ * script group — it can't remove the superseded inline one, so an un-migrated
363
+ * project would run BOTH and the old inline check would still block edits when the
364
+ * marker is cleared. This removes only the inline form (the script call — which
365
+ * carries neither `if [ ! -f` nor the message — is left intact).
366
+ */
367
+ function migrateActiveSkillHook(destSettings) {
368
+ const pre = destSettings.hooks && destSettings.hooks.PreToolUse;
369
+ if (!Array.isArray(pre)) return false;
370
+ let changed = false;
371
+ for (const group of pre) {
372
+ if (!Array.isArray(group.hooks)) continue;
373
+ const kept = group.hooks.filter(
374
+ (h) =>
375
+ !(
376
+ typeof h.command === 'string' &&
377
+ h.command.includes('.forge/.active-skill') &&
378
+ h.command.includes('No active skill') &&
379
+ h.command.includes('if [ ! -f')
380
+ )
381
+ );
382
+ if (kept.length !== group.hooks.length) {
383
+ group.hooks = kept;
384
+ changed = true;
385
+ }
386
+ }
387
+ // Drop any group left with no hooks by the strip.
388
+ destSettings.hooks.PreToolUse = pre.filter(
389
+ (g) => !Array.isArray(g.hooks) || g.hooks.length > 0
390
+ );
391
+ return changed;
392
+ }
393
+
394
+ /**
395
+ * Smart-merge settings.json: overwrite forge.* keys from template, additively
396
+ * install framework-managed safety hooks + secret-deny globs, preserve user hooks.
275
397
  */
276
398
  function upgradeSettings() {
277
399
  const srcPath = path.join(templateDir, SETTINGS_FILE);
@@ -289,6 +411,12 @@ function upgradeSettings() {
289
411
  destSettings.forge = { ...destSettings.forge, ...srcSettings.forge };
290
412
  // Always stamp current package version
291
413
  destSettings.forge.version = pkgVersion;
414
+ // Remove the superseded inline active-skill guard before the additive merge
415
+ // installs its script-based replacement (forge#12).
416
+ migrateActiveSkillHook(destSettings);
417
+ // Additively install framework-managed safety guardrails (ADR-017)
418
+ mergeManagedHooks(destSettings, srcSettings);
419
+ mergeManagedDeny(destSettings, srcSettings);
292
420
 
293
421
  const after = JSON.stringify(destSettings);
294
422
  if (before === after) return 'unchanged';
@@ -321,6 +449,7 @@ async function install() {
321
449
  const srcClaude = path.join(templateDir, '.claude');
322
450
  const destClaude = path.join(targetDir, '.claude');
323
451
  const claudeCount = copyDirRecursive(srcClaude, destClaude);
452
+ makeHookScriptsExecutable(path.join(destClaude, 'hooks'));
324
453
  console.log(` Installed .claude/ (${claudeCount} files)`);
325
454
 
326
455
  // Copy .forge/templates/ directory
@@ -358,7 +487,9 @@ async function install() {
358
487
 
359
488
  console.log(`\n Forge v${pkgVersion} is ready. Start with: /forge\n`);
360
489
 
361
- // Presence-gated: if a Codex adapter is present, note it may need regeneration.
490
+ // Presence-gated: a fresh install normally has no Codex adapter, so this is a
491
+ // no-op — but if the target repo already carries a .agents/ / .codex/ tree
492
+ // (e.g. migrating in from another setup), note it may need regeneration.
362
493
  noticeCodexAdapter();
363
494
  }
364
495
 
@@ -442,14 +573,19 @@ function runPostUpgradeMigrationChecks(installedVersion) {
442
573
 
443
574
  let stdout = '';
444
575
  try {
576
+ // Trust model: guides were re-synced from the (trusted) npm package in
577
+ // step 2 above before this runs, so the on-disk content is not
578
+ // attacker-controlled here; the only residual risk is a runaway/buggy
579
+ // detection block, bounded by `timeout`.
445
580
  stdout = execSync(script, {
446
581
  cwd: targetDir,
447
582
  shell: '/bin/bash',
448
583
  encoding: 'utf-8',
449
584
  stdio: ['ignore', 'pipe', 'ignore'],
585
+ timeout: 10000, // 10s — a pathological guide throws (caught below) → no-op, loop continues
450
586
  });
451
587
  } catch {
452
- // A non-zero exit or detection error is a no-op, not a failure — keep going.
588
+ // A non-zero exit, timeout, or detection error is a no-op, not a failure — keep going.
453
589
  continue;
454
590
  }
455
591
 
@@ -476,6 +612,26 @@ function runPostUpgradeMigrationChecks(installedVersion) {
476
612
  }
477
613
  }
478
614
 
615
+ /**
616
+ * Sync a single standalone framework-owned file, overwriting when content
617
+ * differs. Records the outcome in `results` (added/updated/unchanged). Used for
618
+ * framework files that live outside the recursively-synced dirs (.forge/.gitignore,
619
+ * .forge/FORGE.md).
620
+ */
621
+ function syncFrameworkFile(srcPath, destPath, displayName, results) {
622
+ if (!fs.existsSync(srcPath)) return;
623
+ const newContent = fs.readFileSync(srcPath, 'utf-8');
624
+ const existed = fs.existsSync(destPath);
625
+ const oldContent = existed ? fs.readFileSync(destPath, 'utf-8') : null;
626
+ if (oldContent !== newContent) {
627
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
628
+ fs.writeFileSync(destPath, newContent);
629
+ results[existed ? 'updated' : 'added'].push(displayName);
630
+ } else {
631
+ results.unchanged.push(displayName);
632
+ }
633
+ }
634
+
479
635
  async function upgrade() {
480
636
  console.log('\n Forge Upgrade\n');
481
637
 
@@ -524,7 +680,9 @@ async function upgrade() {
524
680
  preserved: [],
525
681
  };
526
682
 
527
- // 1. Process framework-owned directories (auto-clean stale files)
683
+ // 1. Process framework-owned directories (stale files are collected for a
684
+ // confirmed removal pass below, not deleted inline — R066a).
685
+ const pendingRemovals = [];
528
686
  for (const dir of FRAMEWORK_OWNED_DIRS) {
529
687
  const dirResult = upgradeDir(dir, { autoClean: true });
530
688
  results.updated.push(...dirResult.updated);
@@ -532,6 +690,7 @@ async function upgrade() {
532
690
  results.unchanged.push(...dirResult.unchanged);
533
691
  results.removed.push(...dirResult.removed);
534
692
  results.preserved.push(...dirResult.preserved);
693
+ pendingRemovals.push(...dirResult.removeCandidates);
535
694
  }
536
695
 
537
696
  // 2. Process template-only directories
@@ -544,41 +703,66 @@ async function upgrade() {
544
703
  results.preserved.push(...dirResult.preserved);
545
704
  }
546
705
 
547
- // 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
548
- // named .gitignore); materialize/refresh it as `.forge/.gitignore`.
549
- const giSrc = path.join(templateDir, '.forge', 'gitignore');
550
- const giDest = path.join(targetDir, '.forge', '.gitignore');
551
- if (fs.existsSync(giSrc)) {
552
- const newContent = fs.readFileSync(giSrc, 'utf-8');
553
- const existed = fs.existsSync(giDest);
554
- const oldContent = existed ? fs.readFileSync(giDest, 'utf-8') : null;
555
- if (oldContent !== newContent) {
556
- fs.mkdirSync(path.dirname(giDest), { recursive: true });
557
- fs.writeFileSync(giDest, newContent);
558
- results[existed ? 'updated' : 'added'].push('.forge/.gitignore');
559
- } else {
560
- results.unchanged.push('.forge/.gitignore');
706
+ // 2-hooks. Sync the safety-guardrail hooks (ADR-017) and the always-on rules
707
+ // layer (ADR-018). Additive — preserves project-local/experimental hooks and
708
+ // pack-installed path-scoped rules, chmod +x's any scripts. Settings wiring is
709
+ // merged separately by upgradeSettings().
710
+ upgradeAdditiveDir('.claude/hooks', results);
711
+ upgradeAdditiveDir('.claude/rules', results);
712
+
713
+ // 2a. Confirm stale framework-file removals before deleting (R066a). Defaults
714
+ // to yes (cleanup is the normal case); auto-confirms when non-interactive
715
+ // (CI / piped stdin) or with --force/--yes, preserving prior behavior.
716
+ if (pendingRemovals.length > 0) {
717
+ const auto =
718
+ process.argv.includes('--force') ||
719
+ process.argv.includes('--yes') ||
720
+ !process.stdin.isTTY;
721
+ console.log(` ${pendingRemovals.length} stale framework file(s) no longer in the template:`);
722
+ for (const c of pendingRemovals) console.log(` ${c.displayPath}`);
723
+ let go = auto;
724
+ if (!auto) {
725
+ const ans = await prompt(' Remove these stale framework files? [Y/n] ');
726
+ go = ans === '' || ans === 'y' || ans === 'yes';
561
727
  }
562
- }
563
-
564
- // 2c. Sync the framework prose file .forge/FORGE.md (framework-owned single file,
565
- // same overwrite-when-different pattern as 2b). MUST run before the CLAUDE.md pass
566
- // so a migrated project's import line never points at a missing file.
567
- const fmSrc = path.join(templateDir, '.forge', 'FORGE.md');
568
- const fmDest = path.join(targetDir, '.forge', 'FORGE.md');
569
- if (fs.existsSync(fmSrc)) {
570
- const newContent = fs.readFileSync(fmSrc, 'utf-8');
571
- const existed = fs.existsSync(fmDest);
572
- const oldContent = existed ? fs.readFileSync(fmDest, 'utf-8') : null;
573
- if (oldContent !== newContent) {
574
- fs.mkdirSync(path.dirname(fmDest), { recursive: true });
575
- fs.writeFileSync(fmDest, newContent);
576
- results[existed ? 'updated' : 'added'].push('.forge/FORGE.md');
728
+ if (go) {
729
+ for (const c of pendingRemovals) {
730
+ if (fs.existsSync(c.destPath)) fs.unlinkSync(c.destPath);
731
+ // Remove now-empty parent dirs up to the synced root.
732
+ let dir = path.dirname(c.destPath);
733
+ while (dir !== c.destDir && fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
734
+ fs.rmdirSync(dir);
735
+ dir = path.dirname(dir);
736
+ }
737
+ results.removed.push(c.displayPath);
738
+ }
577
739
  } else {
578
- results.unchanged.push('.forge/FORGE.md');
740
+ console.log(' Keeping stale files (skipped removal).');
741
+ for (const c of pendingRemovals) {
742
+ results.preserved.push(`${c.displayPath} (stale — kept by choice)`);
743
+ }
579
744
  }
745
+ console.log();
580
746
  }
581
747
 
748
+ // 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
749
+ // named .gitignore); materialize/refresh it as `.forge/.gitignore`.
750
+ syncFrameworkFile(
751
+ path.join(templateDir, '.forge', 'gitignore'),
752
+ path.join(targetDir, '.forge', '.gitignore'),
753
+ '.forge/.gitignore',
754
+ results
755
+ );
756
+
757
+ // 2c. Sync the framework prose file .forge/FORGE.md. MUST run before the
758
+ // CLAUDE.md pass so a migrated project's import line never points at a missing file.
759
+ syncFrameworkFile(
760
+ path.join(templateDir, '.forge', 'FORGE.md'),
761
+ path.join(targetDir, '.forge', 'FORGE.md'),
762
+ '.forge/FORGE.md',
763
+ results
764
+ );
765
+
582
766
  // 3. Guarantee the CLAUDE.md import line; auto-migrate any legacy embedded section.
583
767
  const claudeStatus = ensureClaudeMdImport();
584
768
  if (claudeStatus === 'migrated') {
@@ -721,6 +905,24 @@ function noticeWorktreeLag() {
721
905
 
722
906
  // --- Entry point ---
723
907
 
908
+ // cwd sanity (R066c): Forge writes .claude/, .forge/, and CLAUDE.md into the
909
+ // current directory. Refuse the clearly-wrong locations (home dir, filesystem
910
+ // root) unless --force, so an accidental run from the wrong place can't litter
911
+ // framework files across $HOME or /.
912
+ function assertSaneTargetDir() {
913
+ if (process.argv.includes('--force')) return;
914
+ const root = path.parse(targetDir).root;
915
+ if (targetDir === root || targetDir === os.homedir()) {
916
+ const where = targetDir === os.homedir() ? 'your home directory' : 'the filesystem root';
917
+ console.error(` ✖ Refusing to run in ${where} (${targetDir}).`);
918
+ console.error(' Forge writes .claude/, .forge/, and CLAUDE.md into the current directory —');
919
+ console.error(' run it from a project root. To override: forge-orkes --force\n');
920
+ process.exit(1);
921
+ }
922
+ }
923
+
924
+ assertSaneTargetDir();
925
+
724
926
  const subcommand = process.argv[2];
725
927
 
726
928
  if (subcommand === 'upgrade') {
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.41.0",
3
+ "version": "0.44.0",
4
4
  "description": "Set up the Forge meta-prompting framework for Claude Code in your project",
5
5
  "bin": {
6
6
  "create-forge": "./bin/create-forge.js"
7
7
  },
8
+ "scripts": {
9
+ "release": "node scripts/release.js",
10
+ "release:audit": "node scripts/release.js audit"
11
+ },
8
12
  "files": [
9
13
  "bin/",
10
14
  "template/"
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: doc-reviewer
3
+ description: Confidence-gated documentation specialist. Verifies docs against source code — signatures, examples, paths, config — not vibe-checks. Spawnable from the `reviewing` skill or invoked directly when docs change materially. Read-only.
4
+ tools:
5
+ - Read
6
+ - Grep
7
+ - Glob
8
+ - Bash
9
+ ---
10
+
11
+ You review documentation for quality. Focus on whether docs are **accurate**, **complete**, and **useful** — not whether they're pretty. A reader who follows wrong docs hits a wall; your job is to catch that before they do. The discipline that makes this agent worth running: **verify every claim against source before flagging it.**
12
+
13
+ ## Your Sources of Truth
14
+
15
+ The source code is the source of truth — docs are checked against it, never the reverse. Know which bucket a changed doc falls in; the bar differs.
16
+
17
+ 1. **Agent-instruction docs** — `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/**`, `.claude/skills/**/SKILL.md`, `.claude/agents/**`. Wrong content here cascades into every agent session. **Bar is highest.**
18
+ 2. **Process/standards docs** — contributing guides, workflow/convention docs, the project's `.forge/` governance files. Affect every contributor. Bar is high.
19
+ 3. **Architecture docs / ADRs** — `.forge/decisions/**`, `docs/architecture/**`, design docs. Verify against actual code and check consistency with sibling ADRs.
20
+ 4. **Code-adjacent docs** — `README.md`, per-directory `README.md`, inline docstrings, JSDoc/TSDoc, package docs. Verify against the code they describe.
21
+ 5. **Feature specs / plans** — verify acceptance criteria are testable and match the implementation when the work has landed.
22
+
23
+ If the project has no `.forge/` governance docs, just classify by what's present (READMEs, ADRs, agent docs).
24
+
25
+ ## Confidence Gating — Report Threshold
26
+
27
+ Every finding requires:
28
+
29
+ 1. **Severity** — Blocker (wrong info that will make a reader take a wrong action) / Major (missing a critical step, or outdated but not dangerous) / Minor (clarity/polish)
30
+ 2. **Confidence** — 1-10. 10 = verified against source code/config. 5 = reads oddly, unverified. <6 = drop.
31
+ 3. **Concrete impact** — one sentence: "A reader following this doc will `<do X>` and hit `<Y>`." If you cannot name the wrong action the reader will take, drop the finding.
32
+ 4. **Fix** — the exact rewrite, not "clarify this" or "improve phrasing."
33
+
34
+ Report only Blocker + Major findings at **Confidence ≥ 8**. Minor findings at 6-7 confidence collapse into a single "Nice-to-fix" list. Drop everything below 6. If nothing meets the bar, one sentence, stop.
35
+
36
+ ## How to Review
37
+
38
+ 1. Run `git diff --name-only` filtered to doc formats: `*.md`, `*.rst`, `*.txt`, and inline docstring/JSDoc changes (use `git diff` to see comment/docstring diffs).
39
+ 2. Classify each changed file into one of the buckets above.
40
+ 3. For each doc change, **read the source code it references**. Verify before flagging.
41
+ 4. Apply the categories below, in order.
42
+
43
+ ## Accuracy — Cross-Reference With Code
44
+
45
+ This is the core of the job. Every factual claim gets checked against source.
46
+
47
+ - **Function/command signatures**: read the actual function or CLI definition. Parameter names, types, defaults, return types must match.
48
+ - **Code examples**: trace every import path (does it exist? use Glob), every function call (does the signature match? use Grep), every CLI flag (does it exist? check the command definition). Examples that don't run waste readers.
49
+ - **Config options**: grep the codebase for the option name. Is it still used? Has the default changed?
50
+ - **File/directory references**: use Glob or `ls` to verify referenced paths exist.
51
+ - **Version numbers**: check the manifest (`package.json`, `composer.json`, `pyproject.toml`, `go.mod`, etc.). Outdated version claims are a Blocker when they drive install instructions.
52
+ - **URLs**: internal URLs (`http://localhost:...`, repo-relative links) — verify spelling, port, and that the target exists. External URLs — trust, but flag an obviously defunct domain.
53
+
54
+ If you can't verify something, say so: "Could not verify X — requires runtime testing." Do not flag it as a defect.
55
+
56
+ ## Completeness
57
+
58
+ - **Prerequisites** named (e.g. "requires `jq`" — flag anything that relies on a binary not guaranteed installed)
59
+ - **Steps in order** — no "configure X" before "install X"
60
+ - **Required inputs and expected outputs** for each step
61
+ - **Error cases**: at minimum, "if you see X, it means Y"
62
+ - **Cross-links**: if this doc references another, the link points somewhere real
63
+
64
+ ## Clarity
65
+
66
+ - **Pronouns with clear antecedents** ("it" referring to something three paragraphs up is a Minor)
67
+ - **No jargon without definition** on first use in onboarding docs (skill/agent files may assume their audience)
68
+ - **Imperative voice** for instructions ("Run …" not "You should run …")
69
+ - **Consistent terminology** — don't switch between two words for the same concept within a single doc
70
+
71
+ ## Consistency Within the Project
72
+
73
+ - **Conventions** — flag contradictions between a stated convention doc and what the code/commands actually do
74
+ - **ADRs** — a new architectural doc that contradicts an existing ADR without marking the old one superseded is a Blocker
75
+ - **Terminology drift** — if the docs call it one thing and the code calls it another, the docs are wrong; flag it
76
+
77
+ ## Agent-Docs Specific Checks
78
+
79
+ When reviewing `CLAUDE.md`, `AGENTS.md`, `.claude/skills/**`, `.claude/agents/**`, or `.cursor/rules/**`:
80
+
81
+ - **File references**: if the doc tells an agent to read `.claude/skills/foo/bar.md`, verify `foo/bar.md` exists
82
+ - **Skill/command references**: if the doc says "run the `reviewing` skill" or "`/review`", verify the referenced skill/command exists and matches the described behaviour
83
+ - **Agent references**: if the doc mentions an agent (e.g. `security-reviewer`), verify `.claude/agents/security-reviewer.md` exists
84
+ - **Stale skill triggers**: a skill's `description:` field must accurately describe when it should fire — a wrong description causes skill-invocation drift
85
+
86
+ ## Calibrated Exclusions — Do Not Flag
87
+
88
+ 1. Grammar/typos that don't change meaning (unless in a headline, title, or first-line `description:`)
89
+ 2. Style preferences the project hasn't agreed on (Oxford comma, sentence vs title case) — unless there's a stated style rule this violates
90
+ 3. "This could be explained better" without a concrete proposed rewrite
91
+ 4. Archive/historical docs (`**/archive/**`, ADRs marked Superseded) unless the change itself targets them
92
+ 5. Comments that are self-evident from the code they describe being removed — that's cleanup, not a finding
93
+
94
+ ## Output Format
95
+
96
+ ```
97
+ ## Docs changed
98
+ <list, one line per doc, e.g. ".claude/skills/reviewing/SKILL.md, README.md">
99
+
100
+ ## Findings (Confidence ≥ 8)
101
+
102
+ ### 1. [Blocker] <short title>
103
+ - File: `README.md:28`
104
+ - Confidence: 10/10
105
+ - Impact: "A reader following the quick start will run `chmod +x ./.claude/hooks/*.sh` before copying the hooks in, hit `No such file or directory`, and skip the step."
106
+ - Fix: Move the `chmod +x` line to run after step 1 (the `cp -r .claude` line), not before.
107
+
108
+ ## Nice-to-fix
109
+ - `docs/WORKFLOW.md:88` — terminology drift: uses "task" and "unit" interchangeably; pick one
110
+
111
+ ## Summary
112
+ <one sentence — e.g. "One Blocker in README quick start; rest is clean.">
113
+ ```
114
+
115
+ If no findings at Confidence ≥ 8: "No findings at Confidence ≥ 8. Docs reviewed: <list>." Stop.