@codewithjuber/forgekit 0.8.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 (140) hide show
  1. package/.claude-plugin/marketplace.json +12 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.codex-plugin/plugin.json +29 -0
  4. package/.mcp.json +8 -0
  5. package/ARCHITECTURE.md +314 -0
  6. package/CHANGELOG.md +467 -0
  7. package/LICENSE +21 -0
  8. package/ONBOARDING.md +180 -0
  9. package/README.md +286 -0
  10. package/bin/claude-init.sh +90 -0
  11. package/bin/claude-taste.sh +33 -0
  12. package/bin/learn-consolidate.sh +51 -0
  13. package/brand.json +15 -0
  14. package/global/CLAUDE.md +31 -0
  15. package/global/crew/frontend-verifier.md +36 -0
  16. package/global/crew/independent-reviewer.md +29 -0
  17. package/global/crew/scout.md +24 -0
  18. package/global/crew/verifier.md +28 -0
  19. package/global/guards/_guardlib.sh +36 -0
  20. package/global/guards/cortex.sh +14 -0
  21. package/global/guards/cost-budget.sh +41 -0
  22. package/global/guards/doom-loop.sh +25 -0
  23. package/global/guards/format-on-edit.sh +32 -0
  24. package/global/guards/lean-guard.sh +20 -0
  25. package/global/guards/protect-paths.sh +45 -0
  26. package/global/guards/recall-load.sh +22 -0
  27. package/global/guards/secret-redact.sh +18 -0
  28. package/global/guards/session-learner.sh +72 -0
  29. package/global/recall/MEMORY.md +7 -0
  30. package/global/rules/self-correction.md +17 -0
  31. package/global/rules/stack-notes.md +24 -0
  32. package/global/rules/tech-currency.md +19 -0
  33. package/global/settings.template.json +183 -0
  34. package/global/statusline.sh +51 -0
  35. package/global/taste/brutalist.json +9 -0
  36. package/global/taste/brutalist.md +19 -0
  37. package/global/taste/corporate.json +9 -0
  38. package/global/taste/corporate.md +19 -0
  39. package/global/taste/editorial.json +9 -0
  40. package/global/taste/editorial.md +19 -0
  41. package/global/taste/minimalist.json +9 -0
  42. package/global/taste/minimalist.md +20 -0
  43. package/global/taste/playful.json +9 -0
  44. package/global/taste/playful.md +19 -0
  45. package/global/tools/atlas/SKILL.md +27 -0
  46. package/global/tools/code-modernization/SKILL.md +275 -0
  47. package/global/tools/code-modernization/references/cost-impact-preflight.md +54 -0
  48. package/global/tools/code-modernization/references/design-patterns-cheatsheet.md +24 -0
  49. package/global/tools/code-modernization/references/research-protocol.md +42 -0
  50. package/global/tools/code-modernization/scripts/preflight_scan.py +190 -0
  51. package/global/tools/cognitive-substrate/SKILL.md +56 -0
  52. package/global/tools/cognitive-substrate/references/capability-map.md +17 -0
  53. package/global/tools/cost-guard/SKILL.md +50 -0
  54. package/global/tools/design-md/SKILL.md +54 -0
  55. package/global/tools/dev-radar/SKILL.md +56 -0
  56. package/global/tools/explore-plan-code/SKILL.md +24 -0
  57. package/global/tools/lean/SKILL.md +41 -0
  58. package/global/tools/recall/SKILL.md +31 -0
  59. package/global/tools/reuse-first/SKILL.md +64 -0
  60. package/global/tools/self-improve/SKILL.md +44 -0
  61. package/global/tools/taste/SKILL.md +26 -0
  62. package/global/tools/tech-selector/SKILL.md +35 -0
  63. package/global/tools/ui-workflow/SKILL.md +44 -0
  64. package/hooks/hooks.json +107 -0
  65. package/install.sh +88 -0
  66. package/package.json +93 -0
  67. package/public/index.html +45 -0
  68. package/scripts/build-pages.mjs +180 -0
  69. package/scripts/bump.mjs +322 -0
  70. package/skills/cognitive-substrate/SKILL.md +56 -0
  71. package/skills/cognitive-substrate/references/capability-map.md +17 -0
  72. package/source/mcp.json +10 -0
  73. package/source/rules.json +106 -0
  74. package/source/substrate.json +41 -0
  75. package/src/adjudicate.js +84 -0
  76. package/src/anchor.js +210 -0
  77. package/src/atlas.js +487 -0
  78. package/src/brain.js +84 -0
  79. package/src/brand.js +25 -0
  80. package/src/cli.js +1509 -0
  81. package/src/context.js +273 -0
  82. package/src/cortex.js +251 -0
  83. package/src/cortex_distill.js +55 -0
  84. package/src/cortex_features.js +81 -0
  85. package/src/cortex_hook.js +197 -0
  86. package/src/cortex_hook_main.js +139 -0
  87. package/src/cortex_mcp.js +352 -0
  88. package/src/cost_report.js +271 -0
  89. package/src/dash.html +396 -0
  90. package/src/dash.js +220 -0
  91. package/src/diagnose.js +0 -0
  92. package/src/doctor.js +315 -0
  93. package/src/embed.js +244 -0
  94. package/src/emit/_shared.js +39 -0
  95. package/src/emit/aider.js +22 -0
  96. package/src/emit/claude.js +44 -0
  97. package/src/emit/codex.js +17 -0
  98. package/src/emit/continue.js +28 -0
  99. package/src/emit/copilot.js +12 -0
  100. package/src/emit/cursor.js +23 -0
  101. package/src/emit/gemini.js +40 -0
  102. package/src/emit/mcp.js +94 -0
  103. package/src/emit/windsurf.js +22 -0
  104. package/src/emit/zed.js +34 -0
  105. package/src/eval.js +47 -0
  106. package/src/extract.js +82 -0
  107. package/src/harden.js +44 -0
  108. package/src/imagine.js +301 -0
  109. package/src/init.js +178 -0
  110. package/src/lean.js +149 -0
  111. package/src/ledger.js +475 -0
  112. package/src/ledger_bridge.js +279 -0
  113. package/src/ledger_read.js +152 -0
  114. package/src/ledger_store.js +360 -0
  115. package/src/lessons.js +185 -0
  116. package/src/lessons_store.js +137 -0
  117. package/src/metrics.js +54 -0
  118. package/src/model_tiers.js +17 -0
  119. package/src/model_tiers.json +39 -0
  120. package/src/predictor.js +143 -0
  121. package/src/preflight.js +410 -0
  122. package/src/providers.js +320 -0
  123. package/src/recall.js +103 -0
  124. package/src/reuse.js +0 -0
  125. package/src/route.js +323 -0
  126. package/src/scope.js +122 -0
  127. package/src/skillgate.js +89 -0
  128. package/src/speclock.js +64 -0
  129. package/src/substrate.js +492 -0
  130. package/src/sync.js +132 -0
  131. package/src/taste.js +55 -0
  132. package/src/uicheck.js +96 -0
  133. package/src/uifingerprint.js +861 -0
  134. package/src/uivisual.js +334 -0
  135. package/src/util.js +71 -0
  136. package/src/verify.js +117 -0
  137. package/templates/project-layer/.claude/settings.json +22 -0
  138. package/templates/project-layer/.claude/skills/hostlelo-deploy/SKILL.md +38 -0
  139. package/templates/project-layer/AGENTS.md +28 -0
  140. package/templates/project-layer/CLAUDE.md +40 -0
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Zero-dependency version bump for forgekit (node stdlib only).
4
+ *
5
+ * Usage:
6
+ * node scripts/bump.mjs <patch|minor|major|auto> # bump every version field + CHANGELOG
7
+ * node scripts/bump.mjs check # assert all version fields agree (CI guard)
8
+ * node scripts/bump.mjs <kind> --dry-run # compute + print, write nothing
9
+ *
10
+ * "auto" heuristic (deliberately simple):
11
+ * 1. Conventional commits since the last v* tag:
12
+ * BREAKING CHANGE / "type!:" -> major, feat -> minor, anything else -> patch.
13
+ * 2. If git yields no commits, fall back to the CHANGELOG [Unreleased] body:
14
+ * "BREAKING" -> major, a "### Added" section -> minor, any other content -> patch.
15
+ * 3. Nothing found anywhere -> error (nothing to release).
16
+ *
17
+ * Files touched (all version fields in the repo):
18
+ * package.json, package-lock.json (root "version" + packages[""].version),
19
+ * .claude-plugin/plugin.json, .codex-plugin/plugin.json,
20
+ * CITATION.cff (version + date-released), landing/index.html (display string),
21
+ * CHANGELOG.md ([Unreleased] rotated under "## [X.Y.Z] - <date>" + compare links).
22
+ *
23
+ * Prints ONLY the new version on stdout (diagnostics go to stderr) so callers can
24
+ * capture it: NEW="$(node scripts/bump.mjs auto)".
25
+ */
26
+ import { execFileSync } from "node:child_process";
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Pure version math
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export function parseVersion(v) {
36
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(v).trim());
37
+ if (!m) throw new Error(`unsupported version format: ${JSON.stringify(v)} (want X.Y.Z)`);
38
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
39
+ }
40
+
41
+ export function computeNext(current, kind) {
42
+ const { major, minor, patch } = parseVersion(current);
43
+ switch (kind) {
44
+ case "major":
45
+ return `${major + 1}.0.0`;
46
+ case "minor":
47
+ return `${major}.${minor + 1}.0`;
48
+ case "patch":
49
+ return `${major}.${minor}.${patch + 1}`;
50
+ default:
51
+ throw new Error(`unknown bump kind: ${JSON.stringify(kind)} (want patch|minor|major)`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Conventional-commit heuristic. `commits` is [{ subject, body }].
57
+ * BREAKING -> major, feat -> minor, anything else -> patch, no commits -> null.
58
+ */
59
+ export function inferKindFromCommits(commits) {
60
+ if (!commits || commits.length === 0) return null;
61
+ let kind = "patch";
62
+ for (const { subject = "", body = "" } of commits) {
63
+ if (/^[a-z]+(\([^)]*\))?!:/.test(subject) || /BREAKING[ -]CHANGE/.test(`${subject}\n${body}`)) {
64
+ return "major";
65
+ }
66
+ if (/^feat(\([^)]*\))?:/.test(subject)) kind = "minor";
67
+ }
68
+ return kind;
69
+ }
70
+
71
+ /**
72
+ * CHANGELOG fallback heuristic on the [Unreleased] body.
73
+ * "BREAKING" -> major, "### Added" section -> minor, other content -> patch, empty -> null.
74
+ */
75
+ export function inferKindFromChangelog(unreleasedBody) {
76
+ const body = (unreleasedBody || "").trim();
77
+ if (!body) return null;
78
+ if (/\bBREAKING\b/.test(body)) return "major";
79
+ if (/^### Added\b/m.test(body)) return "minor";
80
+ return "patch";
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // CHANGELOG rotation (pure)
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /** Returns the text between "## [Unreleased]" and the next "## [" heading (or EOF/link refs). */
88
+ export function extractUnreleased(changelog) {
89
+ const start = changelog.search(/^## \[Unreleased\][^\n]*\n/m);
90
+ if (start === -1) return null;
91
+ const afterHeading = changelog.indexOf("\n", start) + 1;
92
+ const rest = changelog.slice(afterHeading);
93
+ const next = rest.search(/^(## \[|\[Unreleased\]:)/m);
94
+ return next === -1 ? rest : rest.slice(0, next);
95
+ }
96
+
97
+ /**
98
+ * Moves the [Unreleased] body under "## [newVersion] - date", leaves a fresh empty
99
+ * [Unreleased], and rewrites the keep-a-changelog compare links at the bottom.
100
+ */
101
+ // Complete regex escape (CodeQL js/incomplete-sanitization): versions are semver-shaped
102
+ // by the time main() calls this, but rotateChangelog is exported — escape EVERY
103
+ // metacharacter, not just dots, so a direct caller can't smuggle a pattern in.
104
+ const reEscape = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
105
+
106
+ export function rotateChangelog(changelog, newVersion, prevVersion, date) {
107
+ if (new RegExp(`^## \\[${reEscape(newVersion)}\\]`, "m").test(changelog)) {
108
+ throw new Error(`CHANGELOG.md already has a [${newVersion}] section`);
109
+ }
110
+ const body = extractUnreleased(changelog);
111
+ if (body === null) throw new Error("CHANGELOG.md has no ## [Unreleased] section");
112
+ const start = changelog.search(/^## \[Unreleased\][^\n]*\n/m);
113
+ const afterHeading = changelog.indexOf("\n", start) + 1;
114
+ const head = changelog.slice(0, afterHeading);
115
+ const tail = changelog.slice(afterHeading + body.length);
116
+ const released = `\n## [${newVersion}] - ${date}\n\n${body.trim()}\n\n`;
117
+ let out = head + released + tail;
118
+
119
+ // Rewrite the compare links: [Unreleased] -> vNEW...HEAD, insert [NEW] -> vPREV...vNEW.
120
+ const refRe = /^\[Unreleased\]:\s*(\S+?)\/compare\/\S+\s*$/m;
121
+ const refMatch = refRe.exec(out);
122
+ if (refMatch) {
123
+ const base = refMatch[1];
124
+ out = out.replace(
125
+ refRe,
126
+ `[Unreleased]: ${base}/compare/v${newVersion}...HEAD\n` +
127
+ `[${newVersion}]: ${base}/compare/v${prevVersion}...v${newVersion}`,
128
+ );
129
+ }
130
+ return out;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // File mutation
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export const JSON_VERSION_FILES = [
138
+ "package.json",
139
+ ".claude-plugin/plugin.json",
140
+ ".codex-plugin/plugin.json",
141
+ ];
142
+
143
+ function readIfExists(file) {
144
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
145
+ }
146
+
147
+ /** Replaces the first `"version": "<old>"` in raw JSON text (keeps formatting/escapes). */
148
+ export function setJsonVersionText(text, oldVersion, newVersion) {
149
+ const needle = `"version": "${oldVersion}"`;
150
+ const idx = text.indexOf(needle);
151
+ if (idx === -1) throw new Error(`no "version": "${oldVersion}" field found`);
152
+ const out = `${text.slice(0, idx)}"version": "${newVersion}"${text.slice(idx + needle.length)}`;
153
+ JSON.parse(out); // sanity: still valid JSON
154
+ return out;
155
+ }
156
+
157
+ /** Collects every tracked version field, as { file: version }. Missing files are skipped. */
158
+ export function collectVersions(root) {
159
+ const found = {};
160
+ for (const rel of JSON_VERSION_FILES) {
161
+ const text = readIfExists(path.join(root, rel));
162
+ if (text !== null) found[rel] = JSON.parse(text).version;
163
+ }
164
+ const lockText = readIfExists(path.join(root, "package-lock.json"));
165
+ if (lockText !== null) {
166
+ const lock = JSON.parse(lockText);
167
+ found["package-lock.json (root)"] = lock.version;
168
+ if (lock.packages?.[""]) found['package-lock.json (packages."")'] = lock.packages[""].version;
169
+ }
170
+ const cff = readIfExists(path.join(root, "CITATION.cff"));
171
+ if (cff !== null) {
172
+ const m = /^version:\s*(\S+)\s*$/m.exec(cff);
173
+ if (m) found["CITATION.cff"] = m[1];
174
+ }
175
+ return found;
176
+ }
177
+
178
+ /**
179
+ * Writes `newVersion` (and `date`) into every version field. Returns the list of
180
+ * files changed. Files absent from `root` are skipped so fixtures can be partial.
181
+ */
182
+ export function applyBump(root, currentVersion, newVersion, date) {
183
+ const changed = [];
184
+ const write = (rel, text) => {
185
+ fs.writeFileSync(path.join(root, rel), text);
186
+ changed.push(rel);
187
+ };
188
+
189
+ for (const rel of JSON_VERSION_FILES) {
190
+ const text = readIfExists(path.join(root, rel));
191
+ if (text !== null) write(rel, setJsonVersionText(text, currentVersion, newVersion));
192
+ }
193
+
194
+ const lockRel = "package-lock.json";
195
+ const lockText = readIfExists(path.join(root, lockRel));
196
+ if (lockText !== null) {
197
+ const lock = JSON.parse(lockText);
198
+ lock.version = newVersion;
199
+ if (lock.packages?.[""]) lock.packages[""].version = newVersion;
200
+ write(lockRel, `${JSON.stringify(lock, null, 2)}\n`);
201
+ }
202
+
203
+ const cffRel = "CITATION.cff";
204
+ const cff = readIfExists(path.join(root, cffRel));
205
+ if (cff !== null) {
206
+ write(
207
+ cffRel,
208
+ cff
209
+ .replace(/^version:\s*.*$/m, `version: ${newVersion}`)
210
+ .replace(/^date-released:\s*.*$/m, `date-released: "${date}"`),
211
+ );
212
+ }
213
+
214
+ const landingRel = "landing/index.html";
215
+ const landing = readIfExists(path.join(root, landingRel));
216
+ if (landing !== null && /forgekit v\d+\.\d+\.\d+/.test(landing)) {
217
+ write(landingRel, landing.replace(/forgekit v\d+\.\d+\.\d+/g, `forgekit v${newVersion}`));
218
+ }
219
+
220
+ const clRel = "CHANGELOG.md";
221
+ const changelog = readIfExists(path.join(root, clRel));
222
+ if (changelog !== null) {
223
+ write(clRel, rotateChangelog(changelog, newVersion, currentVersion, date));
224
+ }
225
+
226
+ return changed;
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // CLI
231
+ // ---------------------------------------------------------------------------
232
+
233
+ function today() {
234
+ return new Date().toISOString().slice(0, 10);
235
+ }
236
+
237
+ function gitCommitsSinceLastTag(root) {
238
+ const git = (args) =>
239
+ execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
240
+ try {
241
+ let range;
242
+ try {
243
+ const tag = git(["describe", "--tags", "--abbrev=0", "--match", "v*"]).trim();
244
+ range = [`${tag}..HEAD`];
245
+ } catch {
246
+ range = ["HEAD"]; // no tag yet: consider all commits
247
+ }
248
+ const raw = git(["log", ...range, "--pretty=%s%x1f%b%x1e"]);
249
+ return raw
250
+ .split("\x1e")
251
+ .map((c) => c.trim())
252
+ .filter(Boolean)
253
+ .map((c) => {
254
+ const [subject, body = ""] = c.split("\x1f");
255
+ return { subject: subject.trim(), body: body.trim() };
256
+ });
257
+ } catch {
258
+ return null; // not a git checkout
259
+ }
260
+ }
261
+
262
+ function runCheck(root) {
263
+ const found = collectVersions(root);
264
+ const versions = new Set(Object.values(found));
265
+ if (versions.size === 1) {
266
+ process.stderr.write(`version fields agree: ${[...versions][0]}\n`);
267
+ return 0;
268
+ }
269
+ process.stderr.write("version drift detected:\n");
270
+ for (const [file, v] of Object.entries(found)) process.stderr.write(` ${file}: ${v}\n`);
271
+ return 1;
272
+ }
273
+
274
+ function main(argv) {
275
+ const flags = argv.filter((a) => a.startsWith("--"));
276
+ const args = argv.filter((a) => !a.startsWith("--"));
277
+ const dryRun = flags.includes("--dry-run");
278
+ const cmd = args[0];
279
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
280
+
281
+ if (cmd === "check") return runCheck(root);
282
+
283
+ if (!["patch", "minor", "major", "auto"].includes(cmd)) {
284
+ process.stderr.write(
285
+ "usage: node scripts/bump.mjs <patch|minor|major|auto|check> [--dry-run]\n",
286
+ );
287
+ return 2;
288
+ }
289
+
290
+ const current = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version;
291
+ let kind = cmd;
292
+ if (cmd === "auto") {
293
+ const commits = gitCommitsSinceLastTag(root);
294
+ kind = inferKindFromCommits(commits);
295
+ if (!kind) {
296
+ kind = inferKindFromChangelog(
297
+ extractUnreleased(readIfExists(path.join(root, "CHANGELOG.md")) || ""),
298
+ );
299
+ }
300
+ if (!kind) {
301
+ process.stderr.write(
302
+ "auto: no commits since the last tag and an empty CHANGELOG [Unreleased] — nothing to release\n",
303
+ );
304
+ return 1;
305
+ }
306
+ process.stderr.write(`auto -> ${kind}\n`);
307
+ }
308
+
309
+ const next = computeNext(current, kind);
310
+ if (dryRun) {
311
+ process.stderr.write(`dry-run: ${current} -> ${next} (no files written)\n`);
312
+ } else {
313
+ const changed = applyBump(root, current, next, today());
314
+ process.stderr.write(`bumped ${current} -> ${next}: ${changed.join(", ")}\n`);
315
+ }
316
+ process.stdout.write(`${next}\n`);
317
+ return 0;
318
+ }
319
+
320
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
321
+ process.exit(main(process.argv.slice(2)));
322
+ }
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: cognitive-substrate
3
+ description: >-
4
+ Use before ambiguous, expensive, multi-file, or mutating coding work to run
5
+ Forge's cognitive substrate: assumption gate, model routing, impact
6
+ prediction, scope decomposition, memory/lesson lookup, minimality check, and
7
+ verification planning. Trigger when a user asks to edit/refactor/fix/design
8
+ production code, integrate features, choose model effort, inspect blast
9
+ radius, or avoid agent assumptions.
10
+ ---
11
+
12
+ # Cognitive Substrate
13
+
14
+ Wrap the frozen model in a pre-action check: gate assumptions, route model effort, predict
15
+ blast radius, decompose scope, surface past lessons, and plan verification — **before** editing.
16
+
17
+ ## When to run it
18
+
19
+ Any ambiguous, expensive, multi-file, or mutating task. Skip one-line, well-specified fixes.
20
+ In Claude Code it fires automatically on every prompt (UserPromptSubmit hook); in other
21
+ agents run the CLI yourself, or call the MCP tool `substrate_check`.
22
+
23
+ ## Run
24
+
25
+ ```bash
26
+ forge substrate "<task>" --json # full contract; use the fields below
27
+ ```
28
+
29
+ ## Act on the result
30
+
31
+ 1. **`okToProceed: false`** → ask the returned `assumption.questions` before editing. Do not guess.
32
+ 2. **`route.tier`** → start at that model tier (cheapest capable); escalate only after an external verifier fails.
33
+ 3. **`impact.impactedFiles`** → read these before editing a named symbol/file (the blast radius).
34
+ 4. **`scope.clusters`** → split independent groups into separate sessions; note coupled files you didn't name.
35
+ 5. **`memory.advisory`** → context, not law; tests and human corrections override it.
36
+ 6. **`verification.checklist`** → run it and show output before claiming done.
37
+
38
+ ## Worked example
39
+
40
+ `forge substrate "make the auth better"` → `proceed: ASK FIRST` + clarify
41
+ questions: ask them instead of editing. A clear task returns `proceed: yes` plus
42
+ the impacted files — including importers you didn't name.
43
+
44
+ ## Deeper single checks
45
+
46
+ `forge preflight "<task>"` (assumptions) · `forge route "<task>"` (model tier) ·
47
+ `forge impact <symbol|file>` (blast radius) · `forge scope <file…>` (decomposition) ·
48
+ `forge context "<task>"` (budgeted context assembly; *computes* what's missing) ·
49
+ `forge imagine "<task>" [--run]` (predicted breaks + minimal covering test suite;
50
+ `--run` dry-runs it in a sandboxed worktree) ·
51
+ `forge diagnose "<error>"` (doom-loop: 3× the same failure signature = stop retrying,
52
+ escalate one tier with the minted diagnosis claim at the head of the prompt).
53
+ MCP equivalents: `assumption_gate`, `route_task`, `predict_impact`, `scope_files`.
54
+
55
+ For the full guide (how it works, extending it, the honesty boundary) and the white paper,
56
+ see `docs/cognitive-substrate/README.md`. `references/capability-map.md` maps faculties to commands.
@@ -0,0 +1,17 @@
1
+ # Cognitive substrate capability map
2
+
3
+ | Paper capability | Forge surface | Guarantee |
4
+ | --- | --- | --- |
5
+ | Memory | `forge recall`, `forge cortex`, `forge ledger` | Facts/lessons persist as content-addressed ledger claims; `forge ledger blame` shows provenance. |
6
+ | Learning | `forge cortex`, ledger oracles | External outcomes (tests, CI, human accept/revert) move claim confidence; model weights do not change. |
7
+ | Imagination | `forge imagine [--run]`, `forge impact` | Predicted breaks + minimal covering test suite; `--run` dry-runs it in a sandboxed worktree. |
8
+ | Self-correction | `forge verify`, `forge diagnose` | Tests/builds beat model claims; 3× the same failure signature mints a diagnosis + escalation. |
9
+ | Impact-awareness | `forge atlas`, `forge impact` | Known symbols/files and likely dependents are surfaced. |
10
+ | M1 routing | `forge route` | Transparent model-tier recommendation. |
11
+ | M2 assumption gate | `forge preflight`, `forge context` | Under-specified tasks return *computed* missing-set questions. |
12
+ | M3 decomposition | `forge scope` | Import clusters show independent vs coupled files. |
13
+ | M4 goal anchoring | `forge anchor`, `forge substrate` | Changed files are checked against the stated goal. |
14
+ | M5 anti-over-engineering | `forge lean`, `forge uicheck design` | Footprint vs ask; UI slop-distance + fingerprint conformance gate. |
15
+ | M6 inline verification | `forge verify` | External checks are required before done. |
16
+
17
+ Limits: static graph edges are conservative; memory relevance and model routing are advisory; non-hook tools cannot be forcibly blocked.
@@ -0,0 +1,10 @@
1
+ {
2
+ "context7": {
3
+ "command": "npx",
4
+ "args": ["-y", "@upstash/context7-mcp@3.2.2"]
5
+ },
6
+ "forge-cortex": {
7
+ "command": "forge",
8
+ "args": ["cortex-mcp"]
9
+ }
10
+ }
@@ -0,0 +1,106 @@
1
+ {
2
+ "title": "engineering rules",
3
+ "intro": "Shared rules for every AI coding tool in this repo. Generated into each tool's native format by `forge sync` — edit this source, not the generated files. Keep lean (tools cap the combined size).",
4
+ "sections": [
5
+ {
6
+ "id": "workflow",
7
+ "title": "Workflow",
8
+ "rules": [
9
+ "Explore -> plan -> code -> verify. For multi-file or unfamiliar changes, plan first; skip planning for one-line fixes.",
10
+ "Never call work done without a check you can run: tests, build exit code, lint, or a diff against expected. Show the command + output, don't just assert success.",
11
+ "Fix root causes, not symptoms. Never suppress an error to make a check pass."
12
+ ]
13
+ },
14
+ {
15
+ "id": "substrate",
16
+ "title": "Cognitive substrate (run before acting)",
17
+ "rules": [
18
+ "Before ambiguous, expensive, multi-file, or mutating work, run `forge substrate \"<task>\" --json` (or call the MCP tool `substrate_check`). In Claude Code this fires automatically on each prompt; in other tools invoke it yourself.",
19
+ "If it returns `okToProceed:false`, ask the returned `assumption.questions` before editing — do not guess an under-specified task.",
20
+ "Start at the recommended `route.tier` (cheapest capable model); escalate only after an external verifier fails, never preemptively.",
21
+ "Read `impact.impactedFiles` (predicted blast radius) before editing a named symbol/file, and split `scope.clusters` of independent work into separate sessions.",
22
+ "Treat `memory.advisory` and minimality warnings as advisory; tests, graph membership, and human corrections always override the model.",
23
+ "Run `verification.checklist` and show the output before claiming done."
24
+ ]
25
+ },
26
+ {
27
+ "id": "reuse",
28
+ "title": "Reuse & dependencies",
29
+ "rules": [
30
+ "Reuse an existing util/component/pattern before writing new; the smallest change that fits wins.",
31
+ "Before adding a dependency, verify the current best option from live sources (docs, GitHub health) — prefer what the project already uses.",
32
+ "Only build from scratch when reuse genuinely doesn't fit, and say why."
33
+ ]
34
+ },
35
+ {
36
+ "id": "code",
37
+ "title": "Code style",
38
+ "rules": [
39
+ "Match the surrounding file's conventions over any personal default.",
40
+ "Comment the non-obvious \"why\", never restate the \"what\".",
41
+ "Guard array/object access; wrap fallible async in try/catch; handle errors explicitly (no empty catch).",
42
+ "Keep functions small and single-purpose; separate pure logic from I/O so it's testable."
43
+ ]
44
+ },
45
+ {
46
+ "id": "testing",
47
+ "title": "Testing",
48
+ "rules": [
49
+ "Write tests with the code. For a bug fix, write the failing test first, then fix.",
50
+ "Cover the edge cases the task names: empty, error, boundary. Prefer real inputs over heavy mocking.",
51
+ "Run the suite + typecheck + lint and show the output before claiming done."
52
+ ]
53
+ },
54
+ {
55
+ "id": "security",
56
+ "title": "Security",
57
+ "rules": [
58
+ "Never write secrets, tokens, or keys into code or commits. `.env*` and key files are guard-protected — don't work around it.",
59
+ "Validate and sanitize input at trust boundaries; use parameterized queries, never string-concatenated SQL.",
60
+ "Check paths for traversal before file operations; prefer HTTPS for external requests."
61
+ ]
62
+ },
63
+ {
64
+ "id": "production",
65
+ "title": "Production standards (2026)",
66
+ "rules": [
67
+ "Validate at trust boundaries with a schema (e.g. Zod): parse untrusted input (API, env, file, LLM output) into typed values before use — never trust it raw.",
68
+ "Keep business logic in use-cases/domain, not controllers; hide the DB/ORM behind a repository interface; return DTOs, never raw DB models.",
69
+ "Check ownership/authorization on every object access (OWASP API #1 — never trust a user-supplied ID); paginate every list; make retried mutations idempotent.",
70
+ "Use a typed error model: typed codes, safe public messages, internal detail logged with a requestId. Never swallow errors or leak stack traces to clients.",
71
+ "Emit structured logs (level, message, requestId, service, durationMs); for services add metrics/traces (OpenTelemetry) and track error rate, p95 latency, and AI token cost.",
72
+ "Treat AI/LLM output as untrusted (OWASP LLM Top 10): verify it, never execute it directly, never put secrets/PII in prompts, give agent tools least privilege + human approval for destructive actions.",
73
+ "Prefer boring, typed, maintained tech; contract-first APIs (OpenAPI 3.1) with a new version for every breaking change; don't over-pattern simple code."
74
+ ]
75
+ },
76
+ {
77
+ "id": "git",
78
+ "title": "Git",
79
+ "rules": [
80
+ "Conventional commits: `type(scope): description` (feat, fix, refactor, docs, test, chore, perf).",
81
+ "Stage specific files, not `git add .`. Never force-push shared branches.",
82
+ "Ask before: push, commit, PR merge, history rewrite, `rm -rf`, dropping DB tables, or touching production."
83
+ ]
84
+ },
85
+ {
86
+ "id": "ui",
87
+ "title": "UI",
88
+ "rules": [
89
+ "Build and check mobile AND desktop. Include clear loading/empty/error states; avoid layout shift.",
90
+ "Follow DESIGN.md if present; keep one visual direction per project."
91
+ ]
92
+ },
93
+ {
94
+ "id": "ai-ux",
95
+ "title": "AI interfaces & design quality",
96
+ "rules": [
97
+ "Commit to an aesthetic before coding; avoid AI-slop defaults (Inter/Roboto/Arial/system fonts, purple-gradient-on-white, generic card grids). Pick 2–3 typefaces and a 4–6 color palette with semantic roles.",
98
+ "Meet WCAG: body text ≥4.5:1 contrast (large/UI ≥3:1), visible :focus-visible on every interactive element, tap targets ≥24×24px, alt text + form labels, and wrap animations ≥200ms in prefers-reduced-motion.",
99
+ "Empty states are never dead ends: show a plain-language message + a clear next step. Error messages name the field, explain the problem, and suggest a fix — never 'Something went wrong'.",
100
+ "For AI-driven UIs: show model confidence/uncertainty, make chain-of-thought and tool actions visible, give live preview/feedback over blind prompting, and provide fallback paths when the model fails.",
101
+ "Pick the interaction pattern deliberately — a co-editing canvas or inline controls beat a wall-of-text chatbot when the user is steering structured output.",
102
+ "When AI audits a UI, only ASSERT the deterministic (contrast math, missing focus ring/alt/label) — keep hierarchy/taste/pattern-fit advisory so the audit doesn't hallucinate false positives."
103
+ ]
104
+ }
105
+ ]
106
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": 1,
3
+ "summary": "Forge cognitive substrate wraps a frozen model with deterministic pre-action gates, repo state, memory, routing, impact simulation, and verification discipline.",
4
+ "faculties": [
5
+ { "id": "memory", "forge": "recall + cortex", "status": "partial", "guarantee": "facts and lessons are persisted as auditable files; relevance is advisory" },
6
+ { "id": "learning", "forge": "cortex lessons", "status": "partial", "guarantee": "external outcomes update lesson confidence; no model weights are changed" },
7
+ { "id": "imagination", "forge": "impact graph", "status": "operational-v1", "guarantee": "reverse dependency traversal predicts possible blast radius" },
8
+ { "id": "self-correction", "forge": "verify + doom-loop guard", "status": "partial", "guarantee": "tests/builds are trusted over model claims" },
9
+ { "id": "impact-awareness", "forge": "atlas + impact", "status": "operational-v1", "guarantee": "known symbols/files and likely dependents are surfaced before edits" }
10
+ ],
11
+ "mechanisms": [
12
+ { "id": "M1", "name": "complexity-aware routing", "command": "forge route", "status": "solved-with-transparency-layer" },
13
+ { "id": "M2", "name": "assumption gate", "command": "forge preflight", "status": "operational-v1" },
14
+ { "id": "M3", "name": "task/session decomposition", "command": "forge scope", "status": "solved-with-boundary-helper" },
15
+ { "id": "M4", "name": "goal anchoring", "command": "forge substrate", "status": "partial" },
16
+ { "id": "M5", "name": "scope minimality", "command": "forge substrate", "status": "operational-v1-advisory" },
17
+ { "id": "M6", "name": "inline verification", "command": "forge verify", "status": "partial" }
18
+ ],
19
+ "defaults": { "askThreshold": 0.6, "impactThreshold": 0.1 },
20
+ "llm": {
21
+ "enabled": false,
22
+ "model": "haiku",
23
+ "timeoutMs": 20000,
24
+ "bidirectional": true,
25
+ "band": 0.25,
26
+ "routingBand": 0.2,
27
+ "signalFloor": 0.4,
28
+ "note": "Opt-in (FORGE_LLM=1). The model PROPOSES; the deterministic rubric + repo grounding + tests VERIFY. Any failure falls back to the deterministic path — never blind, never blocking. `bidirectional` (default true) lets a verified reading also clear a false ask / route a tier down, within `band`/`routingBand` and never past the hard floors (`signalFloor`, repo grounding, no-anchor). Set false for the conservative tighten/raise-only mode."
29
+ },
30
+ "policies": {
31
+ "preAction": "Run substrate before ambiguous, expensive, multi-file, or mutating work.",
32
+ "escalation": "Start with the cheapest capable tier and escalate only after an external verifier failure.",
33
+ "hookEnforcement": "Block or warn only on deterministic risks; keep research-edge judgments advisory.",
34
+ "honesty": "Label memory relevance, routing fit, and minimality as advisory unless verified by tests, graph membership, or explicit user scope."
35
+ },
36
+ "limits": [
37
+ "Graph edges are static and conservative; dynamic dispatch and generated code may be missed.",
38
+ "Memory and learning are external file-backed lessons, not weight-level learning.",
39
+ "Non-hook editors receive advisory MCP/context only; Forge cannot enforce hooks where the host has no hook surface."
40
+ ]
41
+ }
@@ -0,0 +1,84 @@
1
+ // forge adjudicate — the substrate's ONE shared "LLM proposes, checks verify" primitive.
2
+ // The whitepaper's load-bearing rule (Panickssery et al. C12; tabayyun, 49:6) is that a
3
+ // model is never its own judge: it may PROPOSE, but an external check arbitrates. This module
4
+ // supplies only the proposer half — a cheap, opt-in, fail-safe model call — while each faculty
5
+ // keeps its deterministic rubric as the judge and reconciles the two (verify, don't trust).
6
+ //
7
+ // Design contract (identical to src/cortex_distill.js, generalized so all faculties share it):
8
+ // - OPT-IN. Off by default (FORGE_LLM!=1) → callers keep their deterministic result and this
9
+ // module is never invoked. Behavior is byte-identical to the pre-LLM substrate.
10
+ // - FAIL-SAFE. Any error/timeout/garble/secret → returns null. A null NEVER changes a verdict.
11
+ // - ZERO-DEP. Access is a `claude -p` CLI shell-out; the runner is injectable so the pure
12
+ // prompt/parse/verify logic is fully testable without the CLI or the network.
13
+ import { execFileSync } from "node:child_process";
14
+ import { SECRET_RE } from "./recall.js";
15
+
16
+ /**
17
+ * Is the LLM proposer layer active for this call? Explicit opt-in wins; env is the default.
18
+ * @param {{llm?:boolean}} [opts]
19
+ */
20
+ export function llmEnabled(opts = {}) {
21
+ if (typeof opts.llm === "boolean") return opts.llm;
22
+ return process.env.FORGE_LLM === "1";
23
+ }
24
+
25
+ /** Build an injectable `claude -p` runner. Pure config in, a (prompt)->string runner out. */
26
+ export function buildRunner({ model = "haiku", timeoutMs = 20000 } = {}) {
27
+ return (prompt) =>
28
+ execFileSync("claude", ["-p", "--model", model], {
29
+ input: prompt,
30
+ encoding: "utf8",
31
+ timeout: timeoutMs,
32
+ stdio: ["pipe", "pipe", "ignore"],
33
+ });
34
+ }
35
+
36
+ /** Extract the first balanced-ish JSON object from model output, or null. */
37
+ export function extractJson(text) {
38
+ if (!text) return null;
39
+ const match = String(text).match(/\{[\s\S]*\}/);
40
+ if (!match) return null;
41
+ try {
42
+ return JSON.parse(match[0]);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Run one adjudication: send `prompt`, parse+validate the reply, refuse secrets both ways.
50
+ * The proposer half only — the caller must still verify the returned value against ground truth.
51
+ * @template T
52
+ * @param {{prompt:string, parse:(obj:any)=>(T|null), run?:(p:string)=>string}} spec
53
+ * @returns {T|null} the validated proposal, or null on ANY failure (caller keeps deterministic).
54
+ */
55
+ export function adjudicate({ prompt, parse, run = buildRunner() }) {
56
+ try {
57
+ if (SECRET_RE.test(String(prompt))) return null; // never send a secret to the model
58
+ const raw = run(prompt);
59
+ if (SECRET_RE.test(String(raw))) return null; // never trust a reply that leaked one back
60
+ const obj = extractJson(raw);
61
+ if (obj == null) return null;
62
+ const parsed = parse(obj);
63
+ return parsed ?? null;
64
+ } catch (err) {
65
+ if (process.env.FORGE_DEBUG === "1")
66
+ process.stderr.write(`forge adjudicate: ${err?.message ?? err}\n`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ import { clamp01 } from "./util.js";
72
+
73
+ /** Coerce an unknown model field to a number in [0,1], or null if it isn't one. */
74
+ export function asUnit(v) {
75
+ const n = typeof v === "number" ? v : Number(v);
76
+ return Number.isFinite(n) ? clamp01(n) : null;
77
+ }
78
+
79
+ /** Coerce an unknown model field to a trimmed, length-capped string (empty → ""). */
80
+ export function asText(v, cap = 200) {
81
+ return String(v ?? "")
82
+ .trim()
83
+ .slice(0, cap);
84
+ }