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.
- package/bin/create-forge.js +245 -43
- package/package.json +5 -1
- package/template/.claude/agents/doc-reviewer.md +115 -0
- package/template/.claude/agents/performance-reviewer.md +138 -0
- package/template/.claude/agents/security-reviewer.md +163 -0
- package/template/.claude/agents/tester.md +3 -5
- package/template/.claude/hooks/README.md +39 -0
- package/template/.claude/hooks/block-dangerous-commands.sh +158 -0
- package/template/.claude/hooks/forge-active-skill-guard.sh +44 -0
- package/template/.claude/hooks/format-on-save.sh +95 -0
- package/template/.claude/hooks/protect-files.sh +101 -0
- package/template/.claude/hooks/scan-secrets.sh +87 -0
- package/template/.claude/hooks/tests/README.md +76 -0
- package/template/.claude/hooks/tests/cases/block-dangerous-commands.cases.json +376 -0
- package/template/.claude/hooks/tests/cases/protect-files.cases.json +222 -0
- package/template/.claude/hooks/tests/cases/scan-secrets.cases.json +218 -0
- package/template/.claude/hooks/tests/cases/warn-large-files.cases.json +146 -0
- package/template/.claude/hooks/tests/run.sh +118 -0
- package/template/.claude/hooks/warn-large-files.sh +71 -0
- package/template/.claude/rules/README.md +63 -0
- package/template/.claude/rules/agent-discipline.md +14 -0
- package/template/.claude/settings.json +69 -1
- package/template/.claude/skills/architecting/SKILL.md +2 -0
- package/template/.claude/skills/chief-of-staff/SKILL.md +37 -26
- package/template/.claude/skills/executing/SKILL.md +16 -0
- package/template/.claude/skills/forge/SKILL.md +15 -27
- package/template/.claude/skills/forge/desire-paths-review.md +37 -0
- package/template/.claude/skills/initializing/SKILL.md +4 -0
- package/template/.claude/skills/planning/SKILL.md +17 -3
- package/template/.claude/skills/reviewing/SKILL.md +2 -0
- package/template/.claude/skills/testing/SKILL.md +3 -3
- package/template/.claude/skills/verifying/SKILL.md +20 -9
- package/template/.forge/FORGE.md +36 -8
- package/template/.forge/migrations/0.20.0-nested-phase-layout.md +10 -2
- package/template/.forge/migrations/0.42.0-id-reservation.md +48 -0
- package/template/.forge/migrations/0.43.0-safety-policy.md +60 -0
- package/template/.forge/migrations/0.44.0-desire-paths.md +57 -0
- package/template/.forge/reservations.yml +31 -0
package/bin/create-forge.js
CHANGED
|
@@ -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
|
-
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
*
|
|
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:
|
|
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 (
|
|
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
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|