From 292d19d7b297f2268256ece0a374c25f2d9b6cd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 14:18:58 +0000 Subject: [PATCH 1/3] fix: batch A remediation (import cycle, library guard, release loop, helpers, cleanups) Addresses safe, independently-verified findings from the deep review, keeping the full suite (1069 pass), typecheck, biome, and `docs check` green. HIGH - H4: break the ESM import cycle recall->ledger_read->lessons_store->recall by importing hasSecret directly from secrets.js in lessons_store/cortex_distill/ adjudicate and dropping recall.js's re-export. - H2: test/init.test.js pins settingsPath under a temp dir so the hook-guard merge never touches the developer's real ~/.claude/settings.json. - H1: release.yml is now closed-loop -- per-ref concurrency group, idempotent npm publish + GitHub Release creation, and a final step that fails the job when a tag did not produce both artifacts. Orphan tags v0.22.2/v0.23.2/v0.24.0 documented in docs/RELEASING.md. MEDIUM/LOW - M1: guard the top-level run() in src/cli.js behind a symlink-resolving main-module check and export run, so importing the package root no longer executes the CLI; package.json sideEffects now names the CLI entry. - M4: consolidate four byte-identical git() helpers and the ledger store's readJson into src/util.js (git, readJsonSafe). - Remove dead package.json files "plugin" entry, vestigial .gitlab-ci.yml, and the dangling @AGENTS.md import in CLAUDE.md; fix off-palette Codex brandColor; replace literal NUL bytes with \0 escapes in reuse.js/diagnose.js; make the unimplemented-command stub exit non-zero. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01C7htTiesXKLvAUtv6ET2jz --- .codex-plugin/plugin.json | 7 ++-- .github/workflows/release.yml | 66 ++++++++++++++++++++++++++++++++-- .gitlab-ci.yml | 26 -------------- CHANGELOG.md | 34 ++++++++++++++++++ CLAUDE.md | 5 +-- docs/RELEASING.md | 13 +++++++ package.json | 5 +-- src/adjudicate.js | 2 +- src/cli.js | 34 +++++++++++++----- src/cortex_distill.js | 2 +- src/diagnose.js | Bin 7130 -> 7132 bytes src/docs_check.js | 14 +------- src/handoff.js | 24 +++++-------- src/ledger_store.js | 16 +++------ src/lessons_store.js | 2 +- src/recall.js | 16 ++++----- src/reuse.js | Bin 15630 -> 15631 bytes src/session.js | 13 +------ src/update.js | 13 +------ src/util.js | 29 +++++++++++++++ test/init.test.js | 7 +++- 21 files changed, 208 insertions(+), 120 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 3fee837..33bfa4b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -2,7 +2,10 @@ "name": "forgekit", "version": "0.25.0", "description": "One config, every AI coding tool — cognitive substrate, MCP tools, guards, atlas, recall, and routing from one source.", - "author": { "name": "CodeWithJuber", "url": "https://github.com/CodeWithJuber" }, + "author": { + "name": "CodeWithJuber", + "url": "https://github.com/CodeWithJuber" + }, "homepage": "https://github.com/CodeWithJuber/forgekit#readme", "repository": "https://github.com/CodeWithJuber/forgekit", "license": "MIT", @@ -24,6 +27,6 @@ "Predict impact for this symbol.", "Route this task to the right model tier." ], - "brandColor": "#137A72" + "brandColor": "#f26430" } } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16ea546..e77e4a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,13 @@ on: tags: ["v*"] workflow_dispatch: # dispatched by bump.yml with --ref vX.Y.Z; must target a tag +# A tag can arrive twice for the same version — the push (tags: v*) AND bump.yml's +# explicit `gh workflow run release.yml --ref vX.Y.Z` dispatch. Serialize per-ref so +# the two never race a double publish; the loser queues (never cancels a mid-publish). +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + permissions: contents: read @@ -56,9 +63,19 @@ jobs: fi - name: Publish to npm (with provenance) if: env.HAS_NPM_TOKEN == 'true' - run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + # Idempotent: skip when the version is already on the registry, so re-running a + # wedged release (or the serialized second tag event) never dies on "cannot + # publish over the previously published version". + run: | + PKG=$(node -p "require('./package.json').name") + VER=$(node -p "require('./package.json').version") + if npm view "$PKG@$VER" version >/dev/null 2>&1; then + echo "::notice::$PKG@$VER is already on npm — skipping publish (idempotent re-run)." + else + npm publish --provenance --access public + fi - name: Skip npm publish (NPM_TOKEN not set) if: env.HAS_NPM_TOKEN != 'true' run: | @@ -69,4 +86,49 @@ jobs: TAG: ${{ github.ref_name }} # --generate-notes builds notes from merged PRs/commits since the previous tag, # so it works with lightweight OR annotated tags (unlike --notes-from-tag). - run: gh release create "$TAG" --title "$TAG" --generate-notes --verify-tag + # Idempotent: skip if the Release already exists (re-run recovery). + run: | + if gh release view "$TAG" >/dev/null 2>&1; then + echo "::notice::GitHub Release $TAG already exists — skipping creation." + else + gh release create "$TAG" --title "$TAG" --generate-notes --verify-tag + fi + - name: Verify the release loop closed (fail loudly on a wedge) + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + # H1 closed-loop check: a tag must produce BOTH a GitHub Release and (when + # publishing is enabled) an npm version. Historically release.yml could wedge + # after the tag landed and nothing alerted — orphan tags v0.22.2/v0.23.2/v0.24.0. + # This step turns that silent wedge into a red, notified job failure; re-running + # the workflow (steps above are idempotent) then completes whatever is missing. + run: | + fail=0 + if gh release view "$TAG" >/dev/null 2>&1; then + echo "GitHub Release $TAG: present." + else + echo "::error::No GitHub Release exists for $TAG after the release job." + fail=1 + fi + if [ "$HAS_NPM_TOKEN" = "true" ]; then + PKG=$(node -p "require('./package.json').name") + VER=$(node -p "require('./package.json').version") + ok=0 + for _ in 1 2 3 4 5 6; do + if npm view "$PKG@$VER" version >/dev/null 2>&1; then ok=1; break; fi + sleep 10 # let the registry reflect a just-published version + done + if [ "$ok" = "1" ]; then + echo "npm $PKG@$VER: present." + else + echo "::error::npm does not serve $PKG@$VER after publish (registry wedge?)." + fail=1 + fi + else + echo "::warning::NPM_TOKEN not set — verified the GitHub Release only, not npm." + fi + if [ "$fail" != "0" ]; then + echo "::error::Release loop did NOT close for $TAG — do not treat it as shipped. Re-run this workflow to complete the missing artifact." + exit 1 + fi + echo "Release loop verified for $TAG." diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 668d87f..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,26 +0,0 @@ -# GitLab Pages pipeline for the generated Forgekit landing page. -# Set BUILD_PAGES_LIVE=1 in CI variables to include no-auth GitHub API counters. -image: node:20 - -stages: - - test - - deploy - -cache: - key: "$CI_COMMIT_REF_SLUG" - paths: - - .npm/ - - .cache/pages/ - -before_script: - - npm ci --cache .npm --prefer-offline - -pages: - stage: deploy - script: - - npm run pages:build - artifacts: - paths: - - public - rules: - - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c87bbd..808664e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,40 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- **Broke a static ESM import cycle in the memory layer.** `lessons_store.js`, + `cortex_distill.js`, and `adjudicate.js` now import `hasSecret` directly from its + source of truth (`secrets.js`) instead of a re-export in `recall.js`, eliminating the + `recall → ledger_read → lessons_store → recall` cycle (a top-level-eval TDZ hazard). +- **Importing the package as a library no longer executes the CLI.** The package root + (`exports["."] → src/cli.js`) now guards its top-level `run()` behind a main-module + check (symlink-resolving, so the global `forge` bin still runs), and exports `run`. + `package.json` `sideEffects` now names the CLI entry accurately instead of `false`. +- **Hermetic tests.** `test/init.test.js` no longer merges hook guards into the + developer's real `~/.claude/settings.json`; it pins `settingsPath` under a temp dir. +- **The unimplemented-command stub now exits non-zero**, so scripts and CI no longer + read a "not wired yet" command as success. +- Replaced literal NUL bytes with `\0` escapes in the `reuse.js`/`diagnose.js` + content-hash separators (byte-identical runtime output; the source is now clean text). + +### Changed + +- **The release pipeline is now closed-loop.** `release.yml` gains a per-ref + `concurrency` group, makes npm publish and GitHub Release creation idempotent (so a + wedged release can simply be re-run), and adds a final verification step that FAILS the + job when a tag did not produce both a GitHub Release and (when publishing is enabled) an + npm version — the silent wedge that orphaned tags v0.22.2/v0.23.2/v0.24.0 now surfaces + as a red, notified failure. See `docs/RELEASING.md` for the orphan-tag note. +- **Consolidated duplicated helpers into `src/util.js`.** One read-only, trimmed `git()` + replaces four byte-identical copies (session/handoff/update/docs_check); a shared + `readJsonSafe()` replaces the ledger store's local copy. Deliberately-divergent git + helpers (verify.js's arg order + `FORGE_DEBUG` logging; the un-trimmed docs-sync + variants) are documented rather than force-merged. +- Removed the dead `plugin` entry from `package.json` `files`, the vestigial + `.gitlab-ci.yml`, and the dangling `@AGENTS.md` import in `CLAUDE.md`; fixed the + off-palette Codex plugin `brandColor`. + ## [0.25.0] - 2026-07-20 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 9b5ab32..0cf48d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,13 +1,13 @@ -@AGENTS.md - # forgekit — contributor instructions ## Stack + - Node.js >=20, pure ESM (`"type": "module"`), zero runtime dependencies. - Linter/formatter: Biome 2.5.2 (dev dependency). - Types: TypeScript via JSDoc annotations — no `.ts` files, checked by `tsc`. ## Commands + - Install: `npm ci` - Test: `npm test` (node:test, 600+ tests) - Lint + format: `npx biome check` (or `npm run check`) @@ -15,6 +15,7 @@ - Build pages: `npm run pages:build` ## Rules + - **Zero runtime dependencies** — CI enforces this. Everything uses Node.js built-ins. - ESM only — use `import`, never `require`. - Match existing patterns: dynamic `await import()` for optional modules, brand diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 1d3fbdb..d4d9d13 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -106,6 +106,19 @@ git push origin master "$V" - `scripts/bump.mjs` refuses to rotate the CHANGELOG onto a version that already has a section. When `auto` finds nothing shippable it exits `3` (a graceful skip the auto-release workflow keys off), not a hard error — so a no-op merge never fails CI. +- **Closed-loop verification**: `release.yml`'s final step asserts the tag produced BOTH + a GitHub Release and (when `NPM_TOKEN` is set) an npm version, and **fails the job** if + either is missing. The publish and release-create steps are idempotent, so recovering a + wedged release is just re-running the workflow on the tag. + +## Orphan tags (v0.22.2, v0.23.2, v0.24.0) + +These three tags were cut before the closed-loop guard above existed and have **no +GitHub Release and no npm version** — the release workflow wedged after the tag landed +and nothing alerted. They are recorded here for transparency. To resolve one, either +re-run `release.yml` on the tag (the idempotent steps will backfill the missing Release +and, if `NPM_TOKEN` is set, the npm version) or delete the tag and note it here. This is +a repo-admin action, intentionally not automated. ## Related workflow secrets diff --git a/package.json b/package.json index be4392e..3bee85f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ }, "homepage": "https://github.com/CodeWithJuber/forgekit#readme", "funding": "https://github.com/sponsors/CodeWithJuber", - "sideEffects": false, + "sideEffects": [ + "./src/cli.js" + ], "publishConfig": { "access": "public", "provenance": true @@ -35,7 +37,6 @@ "source", "global", "templates", - "plugin", "hooks", ".claude-plugin", ".codex-plugin", diff --git a/src/adjudicate.js b/src/adjudicate.js index 4689aa0..c308bcc 100644 --- a/src/adjudicate.js +++ b/src/adjudicate.js @@ -15,7 +15,7 @@ import { gatewayModelId } from "./gateway_model_map.js"; import { buildHttpRunner as httpRunner } from "./llm.js"; import { MODELS } from "./model_tiers.js"; import { envModelOverride } from "./providers.js"; -import { hasSecret } from "./recall.js"; +import { hasSecret } from "./secrets.js"; /** * Is the LLM proposer layer active for this call? Explicit opt-in wins; env is the default. diff --git a/src/cli.js b/src/cli.js index a69cc54..f4237f8 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1,7 +1,8 @@ #!/usr/bin/env node -import { readFileSync } from "node:fs"; +import { readFileSync, realpathSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; // forge — zero-dependency dispatcher. Works identically whether installed via the // npm bin, the hardened install.sh symlink, or the Claude Code plugin. import { BRAND } from "./brand.js"; @@ -2477,12 +2478,29 @@ async function run(argv) { process.exitCode = 1; return; } - // ponytail: remaining subcommands land in their build phases; the stub keeps the - // command surface honest and testable now. - console.log(`${BRAND.cli} ${cmd}: not wired yet — coming in a later build phase.`); + // A command is registered in COMMANDS but has no handler wired yet. Exit non-zero + // so scripts (and CI) treat "recognized but unimplemented" as a failure, not success. + console.error(`${BRAND.cli} ${cmd}: not wired yet — coming in a later build phase.`); + process.exitCode = 1; } -run(process.argv.slice(2)).catch((err) => { - console.error(err.message); - process.exitCode = 1; -}); +// Auto-run only as the CLI entry (npm/global bin, install.sh symlink, `node src/cli.js`). +// realpathSync resolves the bin symlink so the global `forge` still runs; importing the +// package root must NOT execute the CLI (package.json marks this file's side effect). +const entryPath = (() => { + const invoked = process.argv[1]; + if (!invoked) return ""; + try { + return realpathSync(invoked); + } catch { + return invoked; + } +})(); +if (entryPath && entryPath === realpathSync(fileURLToPath(import.meta.url))) { + run(process.argv.slice(2)).catch((err) => { + console.error(err.message); + process.exitCode = 1; + }); +} + +export { run }; diff --git a/src/cortex_distill.js b/src/cortex_distill.js index 8c1225a..0887d96 100644 --- a/src/cortex_distill.js +++ b/src/cortex_distill.js @@ -4,7 +4,7 @@ // shells out to the `claude` CLI via the shared adjudicate runner (same primitive every other // faculty uses); the runner is injectable so the pure prompt/parse logic is testable without it. import { buildRunner } from "./adjudicate.js"; -import { hasSecret } from "./recall.js"; +import { hasSecret } from "./secrets.js"; /** * Pure: build the distillation prompt from an episode. diff --git a/src/diagnose.js b/src/diagnose.js index 3dd736c0e77889657cc52e5f8192dfc27302cfe7..d231a6502c43e827b034b3e50479d54a70efe5b4 100644 GIT binary patch delta 24 fcmca*e#d;nUT*Ff1C{Eu%$(HP7=z8nxJ3j2et!uh delta 22 dcmca(e#?BrUT#hXmFl$2oYY!|%_q1;1OR9)2n+xK diff --git a/src/docs_check.js b/src/docs_check.js index 01519d1..6a1316c 100644 --- a/src/docs_check.js +++ b/src/docs_check.js @@ -4,13 +4,13 @@ // a feature can no longer ship without its docs and the gap silently accumulate — the // exact failure that let a whole command family go undocumented. Self-check: it runs // against the forge package root (BRAND.root), not the host repo. -import { execFileSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join, normalize } from "node:path"; import { BRAND } from "./brand.js"; import { COMMANDS, HIDDEN_COMMANDS } from "./commands.js"; import { TOOLS } from "./mcp_tools.js"; import { allPricePairs } from "./model_tiers.js"; +import { git } from "./util.js"; /** The user-facing prose docs every claim is reconciled against. */ const DOC_FILES = ["README.md", "docs/GUIDE.md", "ARCHITECTURE.md", "ROADMAP.md"]; @@ -159,18 +159,6 @@ function checkMcpTools(docs, issues) { } } -const git = (root, args) => { - try { - return execFileSync("git", args, { - cwd: root, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return ""; - } -}; - /** Every tracked Markdown file, so diagram checks cover the WHOLE doc set — not just the * four prose docs. Falls back to a recursive walk when git is unavailable (tmp fixtures). */ function markdownFiles(root) { diff --git a/src/handoff.js b/src/handoff.js index 4acb7ba..b08af0f 100644 --- a/src/handoff.js +++ b/src/handoff.js @@ -4,27 +4,15 @@ // (never appended) snapshot injected at every session start: bounded compression, so the // loader's token cost stays constant over the project's life while total knowledge grows. -import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { BRAND } from "./brand.js"; import { getGoal } from "./goal.js"; import { hasSecret } from "./secrets.js"; +import { git } from "./util.js"; export const statePath = (root) => join(root, ".forge", "state.md"); -function git(root, args) { - try { - return execFileSync("git", args, { - cwd: root, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return ""; - } -} - /** Branch, dirty files (capped), recent commits — empty-safe outside a git repo. */ export function gatherGitFacts(root, { statusCap = 20 } = {}) { const branch = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]); @@ -93,12 +81,18 @@ export function writeState(root, fields = {}, { t = Date.now(), maxLines = 150 } const gotchas = arr(fields.gotchas); const criteria = arr(fields.criteria); if (!done.length && !next.length && !gotchas.length && !criteria.length) - return { ok: false, reason: "empty handoff — say what was done and what comes next" }; + return { + ok: false, + reason: "empty handoff — say what was done and what comes next", + }; const supplied = [...done, ...next, ...gotchas, ...criteria, fields.goal, fields.phase] .filter(Boolean) .join("\n"); if (hasSecret(supplied)) - return { ok: false, reason: "refused: handoff looks like it contains a secret/credential" }; + return { + ok: false, + reason: "refused: handoff looks like it contains a secret/credential", + }; const goal = fields.goal || getGoal(root) || `(none set — \`${BRAND.cli} anchor set "…"\`)`; const facts = gatherGitFacts(root); const assumptions = gatherAssumptions(root).map( diff --git a/src/ledger_store.js b/src/ledger_store.js index 56efd58..53c8f65 100644 --- a/src/ledger_store.js +++ b/src/ledger_store.js @@ -33,7 +33,7 @@ import { validOutcome, } from "./ledger.js"; import { redactSecrets } from "./secrets.js"; -import { contentHash } from "./util.js"; +import { contentHash, readJsonSafe } from "./util.js"; /** The canonical repo ledger. (recall's global store keeps its own sibling ledger.) */ export const repoLedger = (root = process.cwd()) => join(root, ".forge", "ledger"); @@ -84,14 +84,6 @@ const logPath = (dir, log, id) => join(dir, log, `${id}.log`); const claimBytes = (claim) => `${canonicalize({ body: claim.body, kind: claim.kind, scope: claim.scope ?? {}, v: claim.v ?? 1 })}\n`; -const readJson = (path) => { - try { - return JSON.parse(readFileSync(path, "utf8")); - } catch { - return null; // one corrupt file must never take down the whole ledger - } -}; - /** Parse an append-only log: one canonical-JSON record per line, deduped by content * hash, corrupt lines skipped. The single reader every log goes through — and the * single choke point where every line must PROVE its content hash (re-sealing the @@ -161,7 +153,7 @@ function* walkClaimFiles(dir) { .sort()) { const path = join(claimsRoot, shard, f); const id = f.replace(/\.json$/, ""); - const parsed = readJson(path); + const parsed = readJsonSafe(path); // Verify the address: a tampered/corrupt claim is surfaced as claim:null. const valid = parsed && claimId(parsed.kind, parsed.body, parsed.scope) === id; yield { @@ -195,7 +187,7 @@ export function putClaim(dir, claim) { }; const path = claimPath(dir, claim.id); const already = existsSync(path); - const healthy = already && readJson(path) !== null && readFileSync(path, "utf8") === text; + const healthy = already && readJsonSafe(path) !== null && readFileSync(path, "utf8") === text; if (!healthy) { mkdirSync(join(dir, "claims", claim.id.slice(0, 2)), { recursive: true }); writeFileSync(path, text); @@ -299,7 +291,7 @@ export function getClaimByPrefix(dir, prefix) { .sort()[0]; if (!f) return null; const id = f.replace(/\.json$/, ""); - const claim = readJson(join(shardDir, f)); + const claim = readJsonSafe(join(shardDir, f)); if (!claim || claimId(claim.kind, claim.body, claim.scope) !== id) return null; const state = emptyState(); state.claims[id] = { ...claim, id }; diff --git a/src/lessons_store.js b/src/lessons_store.js index 43e10d2..351de44 100644 --- a/src/lessons_store.js +++ b/src/lessons_store.js @@ -11,7 +11,7 @@ import { writeFileSync, } from "node:fs"; import { join } from "node:path"; -import { hasSecret } from "./recall.js"; +import { hasSecret } from "./secrets.js"; import { ledgerOnly } from "./util.js"; export const lessonsDir = (root = process.cwd()) => join(root, ".forge", "lessons"); diff --git a/src/recall.js b/src/recall.js index 6e38877..2685a19 100644 --- a/src/recall.js +++ b/src/recall.js @@ -4,17 +4,13 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -// The merged read helper (P2 read flip). Import cycle note: ledger_read → lessons_store -// → recall is function-level only (no module-eval use on either side), so ESM resolves -// it safely — same pattern as lessons_store's own SECRET_RE import from here. +// The merged read helper (P2 read flip). import { ledgerFacts, mergeFactSlugs } from "./ledger_read.js"; -// Secret-refusal now lives in secrets.js (format grammars + entropy gate) so no -// store — and no shell guard — can disagree; re-exported here because recall is where -// callers historically imported it from (lessons_store, guards, tests). See secrets.js -// for the precision rationale (never refuse a bare English mention like "password"). -import { hasSecret, SECRET_RE } from "./secrets.js"; - -export { hasSecret, SECRET_RE }; +// Secret-refusal lives in secrets.js (format grammars + entropy gate) so no store — +// and no shell guard — can disagree. Callers import hasSecret directly from secrets.js +// (it is the one source of truth); recall no longer re-exports it, which previously +// created a needless ledger_read → lessons_store → recall import cycle. +import { hasSecret } from "./secrets.js"; export function defaultStore() { // Mutable personal memory. FORGE_HOME override wins (tests + recall-load.sh rely on it); diff --git a/src/reuse.js b/src/reuse.js index 63df5d468b7c9c50add910975de2e6a8ee3ed2c2..3e3ec04845388d1ae11c1e432c3f6230e2db7204 100644 GIT binary patch delta 15 WcmeCH>aW`1#m5w5u-TjMupR(0HwC2t delta 14 VcmeCL>Z{t|#mC67*@y429snwK1sMPU diff --git a/src/session.js b/src/session.js index a1c87ed..d5fc66f 100644 --- a/src/session.js +++ b/src/session.js @@ -17,18 +17,7 @@ import { import { join } from "node:path"; import { BRAND } from "./brand.js"; import { sessionPath } from "./cortex_hook.js"; - -function git(root, args) { - try { - return execFileSync("git", args, { - cwd: root, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return ""; - } -} +import { git } from "./util.js"; // Raw NUL-separated porcelain — the ONLY quote-proof status format (paths with // spaces/unicode/quotes arrive verbatim, no C-quoting to undo). diff --git a/src/update.js b/src/update.js index adc444a..f08abca 100644 --- a/src/update.js +++ b/src/update.js @@ -7,6 +7,7 @@ import { execFileSync, execSync } from "node:child_process"; import { existsSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { BRAND } from "./brand.js"; +import { git } from "./util.js"; // The real npm name (scope included) for the `npm i -g` instruction — read, never guessed. function npmName(root) { @@ -17,18 +18,6 @@ function npmName(root) { } } -function git(root, args) { - try { - return execFileSync("git", args, { - cwd: root, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return ""; - } -} - const isGitCheckout = (root) => git(root, ["rev-parse", "--is-inside-work-tree"]) === "true"; // Fetch is the only network step — do it at most once an hour (FETCH_HEAD mtime), so a diff --git a/src/util.js b/src/util.js index 4e8fbc7..c89ca5e 100644 --- a/src/util.js +++ b/src/util.js @@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; export const slug = (s) => String(s) @@ -44,6 +45,34 @@ export function contentHash(text) { return createHash("sha256").update(text).digest("hex"); } +/** Run a read-only git command in `root`; return trimmed stdout, or "" on ANY failure + * (stderr silenced). Consolidated from four byte-identical copies (session/handoff/ + * update/docs_check). Deliberately-divergent variants stay local: verify.js uses a + * different arg order and logs stderr under FORGE_DEBUG; docs_sync/docs_impact keep + * raw (un-trimmed) output on purpose. */ +export function git(root, args) { + try { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return ""; + } +} + +/** Parse a JSON file, returning null on a missing/corrupt file instead of throwing — + * one bad file must never take down the caller. (Distinct from a strict parse that + * should surface a bad config; those callers keep their own throwing readJson.) */ +export function readJsonSafe(path) { + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch { + return null; + } +} + let cachedAuthor; /** The identity stamped on ledger provenance/evidence — FORGE_AUTHOR env override, * else the git identity, else "" (attribution is best-effort, never a hard fail). diff --git a/test/init.test.js b/test/init.test.js index 1908669..becc920 100644 --- a/test/init.test.js +++ b/test/init.test.js @@ -33,7 +33,12 @@ const effectiveCmd = (h) => (Array.isArray(h.args) ? [h.command, ...h.args].join test("init emits the shared config for a fresh repo in one call", () => { const root = mkdtempSync(join(tmpdir(), "forge-init-")); - init({ targetRoot: root }); + // Hermeticity: pin settingsPath under the temp root so the hook-guard merge never + // touches the developer's real ~/.claude/settings.json (mergeSettings defaults there). + init({ + targetRoot: root, + settingsPath: join(root, ".claude", "settings.json"), + }); assert.ok(existsSync(join(root, "AGENTS.md")), "AGENTS.md"); assert.ok(existsSync(join(root, "CLAUDE.md")), "CLAUDE.md"); assert.ok(existsSync(join(root, ".aider.conf.yml")), ".aider.conf.yml"); From 35770e746740ffc12088d4b530e6fd623897d0aa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 14:22:12 +0000 Subject: [PATCH 2/3] feat(skills): add the problem-solver skill A universal, framework-driven problem-solving cycle bundled through the plugin's skills directory (plugin.json "skills": "./global/tools"): Clarify -> Classify -> Diagnose -> Generate -> Decide -> Act & Sustain, with a frameworks reference (5 Whys, Fishbone, First Principles, TRIZ, Cynefin, DMAIC/PDCA/8D/A3, Design Thinking, Nine Windows, weighted decision matrix, pre-mortem), a disciplines reference, and a fill-in canvas. Naming is English throughout. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01C7htTiesXKLvAUtv6ET2jz --- CHANGELOG.md | 9 ++ global/tools/problem-solver/SKILL.md | 108 ++++++++++++++++++ .../assets/problem-solving-canvas.md | 47 ++++++++ .../problem-solver/references/frameworks.md | 68 +++++++++++ .../problem-solver/references/principles.md | 38 ++++++ 5 files changed, 270 insertions(+) create mode 100644 global/tools/problem-solver/SKILL.md create mode 100644 global/tools/problem-solver/assets/problem-solving-canvas.md create mode 100644 global/tools/problem-solver/references/frameworks.md create mode 100644 global/tools/problem-solver/references/principles.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 808664e..0ff618b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- **`problem-solver` skill** (`global/tools/problem-solver/`) — a universal, + framework-driven problem-solving cycle (Clarify → Classify → Diagnose → Generate → + Decide → Act & Sustain) bundled through the plugin's `skills` directory. Ships a + frameworks reference (5 Whys, Fishbone, First Principles, TRIZ, Cynefin, DMAIC/PDCA/8D/ + A3, Design Thinking, Nine Windows, weighted decision matrix, pre-mortem), a disciplines + reference, and a fill-in canvas. + ### Fixed - **Broke a static ESM import cycle in the memory layer.** `lessons_store.js`, diff --git a/global/tools/problem-solver/SKILL.md b/global/tools/problem-solver/SKILL.md new file mode 100644 index 0000000..e02d9f4 --- /dev/null +++ b/global/tools/problem-solver/SKILL.md @@ -0,0 +1,108 @@ +--- +name: problem-solver +description: Universal problem-solving engine that fuses proven frameworks (ASQ 4-step cycle, DMAIC/PDCA, 5 Whys, Fishbone, First Principles, TRIZ, Cynefin, Design Thinking, weighted decision matrix) into one staged loop. Use when the user asks to solve, analyze, or break down a problem, find root causes, choose between options, innovate, make a hard decision, handle a crisis, or wants structured/step-by-step problem solving. +--- + +# Problem Solver + +This skill solves any problem — technical, business, personal, or complex — through a +staged loop. Each analytical stage carries a discipline that keeps the analysis honest +(e.g. verify facts before diagnosing root cause). Match depth to the problem: a trivial +problem gets two sentences of method, not all six stages. + +## Core Workflow: The Problem-Solving Cycle + +Run these 6 stages in order. Loop back when a stage fails its exit check. Always state +which stage you are in. + +``` +[Problem] --> 1. CLARIFY -> Define + verify facts + --> 2. CLASSIFY -> Pick the right mode of attack (Cynefin) + --> 3. DIAGNOSE -> Root cause, not symptoms + --> 4. GENERATE -> Multiple options, wide consultation + --> 5. DECIDE -> Weighted choice + ethics screen + --> 6. ACT & SUSTAIN -> Plan, pilot, review, persist +``` + +### Stage 1 — CLARIFY + +- Separate **fact from opinion**: verify information before acting on it. +- Answer 5W2H: What happened? Where? When? How much/many? Who detected it? Why is it a + problem? (Who is for detection only, never blame — blame corrupts the data.) +- Write a one-sentence problem statement. Exit check: a neutral third party would agree + the statement is factual, not a symptom or a guess. + +### Stage 2 — CLASSIFY (Cynefin) + +Classify the problem's context to choose the strategy: + +- **Clear/Obvious** -> apply best practice directly; skip to Stage 6. +- **Complicated** -> expert analysis needed; go to Stage 3 with analytical tools (5 Whys, + Fishbone, First Principles). +- **Complex** -> cause-and-effect only visible in retrospect; probe with small + safe-to-fail experiments (PDCA loops) instead of one big plan. +- **Chaotic** -> act first to stabilize (stop the bleeding), then reclassify. +- **Disorder** -> gather more information; return to Clarify. + Never apply linear fixes to complex systems. + +### Stage 3 — DIAGNOSE (root cause) + +- Use **5 Whys** for linear chains, **Fishbone (6M)** for multi-cause problems, **First + Principles** to strip assumptions to unarguable truths, **TRIZ** when the problem is a + contradiction ("improving X worsens Y"). +- Reflect deeply: reconcile apparent contradictions before concluding; reject + surface-level answers. +- Exit check: fixing this cause prevents recurrence. If the "root cause" is a person, dig + one level deeper — it's usually a system or process. + +### Stage 4 — GENERATE (options + consultation) + +- Generate **at least 3** genuinely different options before evaluating. Never run with + the first idea. +- Consult diverse perspectives deliberately — frontline people, skeptics, domain experts, + and those affected. Ask the user who should be consulted. +- Use brainstorming, working backward, Nine Windows (past/present/future × + sub/system/supersystem), or analogy from other domains. If stuck, recommend "sleep on + it" — incubation is a legitimate step, not delay. + +### Stage 5 — DECIDE (weighted matrix + ethics screen) + +- Build a **weighted decision matrix**: effectiveness, feasibility, cost, risk, + reversibility, alignment with goals — plus a mandatory **ethics column** (does it harm + anyone? is it honest? does it betray a trust?). +- Screen out any option that fails the ethical screen regardless of score: the ends never + justify unjust means, and no option may be built on deceit or wrongful gain. +- After due diligence, commit with calm and stay unattached to the ego's preferred option. + Document the decision and its rationale. + +### Stage 6 — ACT & SUSTAIN + +- Plan with who/what/when; run a **small pilot** first when stakes are high (PDCA: + Plan-Do-Check-Act). +- Do everything within your power, then stay calm about the outcome — this kills both + paralysis and recklessness. +- Monitor with leading indicators and feedback channels. If targets are missed: bad + execution -> redo Stage 6; poor solution -> redo Stages 4-5; wrong cause -> redo Stage 3. +- Persist through slow results; do not abandon a sound plan at the first setback. +- Standardize and document the fix so the problem cannot recur; extract lessons learned. + +## Reference Files + +Load only when needed: + +- **references/frameworks.md** — Detailed tool guide: 5 Whys, Fishbone, First Principles, + TRIZ (40 principles + contradiction method), Cynefin, DMAIC, PDCA, 8D, A3, Design + Thinking, Nine Windows, decision matrix, potential problem analysis. Read when a stage + needs a specific tool's full procedure. +- **references/principles.md** — The disciplines behind each stage (verify, reflect, + consult, screen ethics, effort-then-acceptance, persist) with application prompts and + boundaries. Read when a problem is ethical, personal, or high-stakes. +- **assets/problem-solving-canvas.md** — Fill-in canvas template covering all 6 stages. + Offer to fill it for substantial problems. + +## Behavior Rules + +- Match depth to problem size: a trivial problem gets 2 sentences of method, not 6 stages. +- Be generic: the same cycle serves code bugs, business strategy, relationship conflict, + or research blockers — only the tools per stage change. +- Prefer evidence over assertion at every stage; a confident guess is still a guess. diff --git a/global/tools/problem-solver/assets/problem-solving-canvas.md b/global/tools/problem-solver/assets/problem-solving-canvas.md new file mode 100644 index 0000000..9767f19 --- /dev/null +++ b/global/tools/problem-solver/assets/problem-solving-canvas.md @@ -0,0 +1,47 @@ +# Problem-Solving Canvas + +**Problem owner:** | **Date:** | **Stage reached:** + +## 1. Clarify + +- Problem statement (one factual sentence): +- 5W2H — What / Where / When / How much / Who detected / Why it matters: +- Verified facts vs. assumptions: + +## 2. Classify (Cynefin) + +- Domain: ☐ Clear ☐ Complicated ☐ Complex ☐ Chaotic ☐ Disorder +- Response mode chosen: + +## 3. Diagnose + +- Tool used: ☐ 5 Whys ☐ Fishbone ☐ First Principles ☐ TRIZ ☐ other: +- Root cause(s) (verified with evidence): +- Contradiction (if TRIZ): "I want **_, but it causes _**" + +## 4. Generate + +- Option A: | Option B: | Option C: +- People consulted / to consult: + +## 5. Decide (weighted matrix) + +| Criterion | Weight | A | B | C | +| ---------------------- | ------ | --- | --- | --- | +| Effectiveness | | | | | +| Feasibility / cost | | | | | +| Risk & reversibility | | | | | +| Goal alignment | | | | | +| **Ethics (pass/fail)** | — | | | | +| **Total** | 100% | | | | + +- Decision & rationale: +- Final check: analysis complete, ego released, committed calmly? ☐ + +## 6. Act & Sustain + +- Action plan (who / what / when): +- Pilot scope & success metric: +- Contingencies (pre-mortem top risks): +- Review date & leading indicators: +- Standardization / lessons learned: diff --git a/global/tools/problem-solver/references/frameworks.md b/global/tools/problem-solver/references/frameworks.md new file mode 100644 index 0000000..8dd8571 --- /dev/null +++ b/global/tools/problem-solver/references/frameworks.md @@ -0,0 +1,68 @@ +# Frameworks Reference + +TOC: 1. 5 Whys · 2. Fishbone · 3. First Principles · 4. TRIZ · 5. Cynefin · 6. PDCA/DMAIC/8D/A3 · 7. Design Thinking · 8. Nine Windows · 9. Decision Matrix · 10. Potential Problem Analysis · 11. Quick creative techniques + +## 1. Five Whys + +Ask "Why?" iteratively (typically 5×) from the problem statement until reaching a cause that, if fixed, prevents recurrence. Stop when the answer points to a process/system/policy, not a person. Verify each link with evidence, not assumption. + +## 2. Fishbone (Ishikawa) — 6M categories + +Draw the problem as the head; branch causes under: Man (people), Method, Machine, Material, Measurement, Mother Nature (environment). Brainstorm within each category, then validate candidates with data. + +## 3. First Principles + +1. List every assumption about the problem. +2. Challenge each: "Is this a physical/mathematical truth, or convention/analogy?" +3. Keep only unarguable fundamentals. +4. Rebuild a solution upward from those fundamentals, ignoring how it "has always been done." + Best for: innovation blockers, cost ceilings, "impossible" constraints. + +## 4. TRIZ (Theory of Inventive Problem Solving) + +Use when the core difficulty is a **contradiction**: improving parameter A worsens parameter B. + +1. Restate the problem as a contradiction ("I want X, but it causes Y"). +2. Find analogous contradictions; apply the relevant **Inventive Principles**. Most-used of the 40: Segmentation (1), Taking out (2), Asymmetry (4), Merging (5), Universality (6), Nesting (7), Prior action (10), Blessing in disguise (22), Intermediary (24), Self-service (25), Copying (26), Cheap short-lived objects (27), Mechanics substitution (28), Flexible shells (30), The other way round (13), Partial/excessive action (16), Another dimension (17), Dynamics (15), Preliminary anti-action (9), Skip/rushing through (21). +3. Goal: the **Ideal Final Result** — the function achieved with the system itself disappearing or the contradiction resolved without compromise. + +## 5. Cynefin Framework + +Five domains → response mode: + +- Clear: sense → categorize → respond (best practice). +- Complicated: sense → analyze → respond (expertise, good practice). +- Complex: probe → sense → respond (parallel safe-to-fail experiments; amplify what works, damp what fails). +- Chaotic: act → sense → respond (stabilize first: containment, authority, triage). +- Disorder/Confused: gather more information. + Warning: the "cliff" between Clear and Chaotic is complacency — over-simplified systems snap. + +## 6. PDCA · DMAIC · 8D · A3 + +- **PDCA**: Plan (hypothesis + small test) → Do (pilot) → Check (measure vs. expectation) → Act (standardize or iterate). Repeat. +- **DMAIC**: Define → Measure → Analyze → Improve → Control. Use for data-rich process problems (Six Sigma). +- **8D**: D1 team → D2 describe problem → D3 interim containment → D4 root cause → D5 permanent corrective action → D6 implement & validate → D7 prevent recurrence → D8 recognize the team. Use for major incidents. +- **A3**: one-page structured report: background, current condition, target, root-cause analysis, countermeasures, plan, follow-up. Use to communicate a problem-solving effort. + +## 7. Design Thinking + +Empathize → Define → Ideate → Prototype → Test. Use when the problem is human-centered and requirements are fuzzy. Iterate with real users; fall in love with the problem, not the first solution. + +## 8. Nine Windows + +Grid of time (past / present / future) × scale (subsystem / system / supersystem). Fill all nine cells to escape present-tense, system-level tunnel vision. Ask: what did this look like before? What will it become? What would the supersystem do for free? + +## 9. Weighted Decision Matrix + +1. Define 4–7 criteria; assign weights summing to 100%. +2. Score each option 1–5 per criterion; compute weighted totals. +3. Mandatory extra criterion: **Ethics** — any option scoring "fails" here is eliminated regardless of total. +4. Sanity-check the winner: sensitivity analysis (change a weight ±10%; does the winner change?), reversibility, worst-case. + +## 10. Potential Problem Analysis (pre-mortem) + +Before implementing: list what could go wrong; rate probability × severity; design preventive actions for high-probability items and contingency plans for high-severity items. Ask: "It's 6 months later and this failed — why?" + +## 11. Quick Creative Techniques + +Brainstorming · Work backward from the solved state · Kipling 6 questions (What/Why/When/How/Where/Who) · Draw the problem/process · Pareto 80/20 · Divide and conquer · SWOT · Analogies from other industries · Sleep on it (incubation) · Read the problem aloud. diff --git a/global/tools/problem-solver/references/principles.md b/global/tools/problem-solver/references/principles.md new file mode 100644 index 0000000..1061820 --- /dev/null +++ b/global/tools/problem-solver/references/principles.md @@ -0,0 +1,38 @@ +# Disciplines for Problem Solving + +The disciplines behind each stage of the cycle. They keep the analysis honest; they never +replace evidence. + +## Discipline map by stage + +| Stage | Discipline | What it demands practically | +| ----------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- | +| 1. Clarify | Verify before acting | Treat reports, data, and accusations as unverified until checked; separate fact from opinion | +| 1. Clarify | Truthfulness | No flattering the problem statement to please stakeholders | +| 3. Diagnose | Deep reflection | Reflect until contradictions resolve; reject surface-level answers | +| 3. Diagnose | No-blame, fairness even against yourself | Diagnose systems, not scapegoats | +| 4. Generate | Consultation | Decisions on shared matters are made by consultation; seek the people who actually know | +| 5. Decide | Detachment after diligence | Do the analysis, then release ego-attachment to a preferred outcome | +| 5. Decide | Trust and justice | Ethical screen: no option built on injustice, deceit, or wrongful gain | +| 5. Decide | Honest weighing | Weigh options justly; don't rig the scales for the answer you want | +| 6. Act | Effort, then acceptance | Full diligence first; then calm, not anxiety, about outcomes | +| 6. Act | Excellence | Execute with excellence; a sloppy rollout wastes a sound plan | +| 6. Sustain | Patience | Persist through slow results; don't abandon sound plans at the first setback | +| All | Humility | Acknowledge the limits of your knowledge; stay teachable | +| All | Care for those affected | Solutions must not crush the vulnerable to optimize a metric | + +## Application prompts (use in conversation) + +- Clarify: "Which parts of what you've told me are verified facts, and which are reports or assumptions?" +- Diagnose: "If we remove this symptom, does the problem recur? Have we reflected one level deeper?" +- Generate: "Who is affected by this who hasn't been consulted yet?" +- Decide: "If every option were scored honestly, which would you _hope_ wins — and is that hope distorting the weights?" Then commit calmly. +- Act: "Have we done everything within our power?" Then let the outcome be. + +## Boundaries + +- The disciplines complement, never replace, evidence and analysis. +- Keep the framing secular and universal; apply the substance (verification, consultation, + ethics screen, effort-then-acceptance) for any user, in any domain. +- Stay within your competence: for specialist judgments (legal, medical, financial), name + the limit and recommend a qualified expert rather than guessing. From d7792734483eb770975d19cbce5ea29b9b43cc3d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 14:33:35 +0000 Subject: [PATCH 3/3] refactor: phase 4 cleanups (M5 layering cycle, M12 architecture, cat-file guard) - M5: applyPrimaryTool (repo_config.js) no longer dynamically imports the sync compiler; the sync runner is injected by cli.js (the orchestration layer), keeping the config-leaf module acyclic. syncFn is now required at runtime. - M12: refresh ARCHITECTURE.md for the v0.20-v0.24 modules that were absent from the reference -- commit_gate (forge precommit), consensus (forge verify --deep), knowledge_router, deja, and docs_impact (forge docs impact). - LOW: ledger_store.js git cat-file -e ref resolver passes `--` before the ref. Full suite (1069 pass), typecheck, biome, and docs check all green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01C7htTiesXKLvAUtv6ET2jz --- ARCHITECTURE.md | 37 +++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 8 ++++++++ src/cli.js | 7 ++++++- src/ledger_store.js | 4 +++- src/repo_config.js | 13 ++++++++----- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0cf4cec..e4e3238 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -299,6 +299,43 @@ the series and a sustained alarm rides the gate's block reason. Proceeding under assumptions appends a record the advisory names and the next handoff surfaces — a guess can never silently become a fact. +**Commit-boundary gate (`src/commit_gate.js`, `forge precommit`).** The commit rung of +the gate lattice (turn ⊂ commit ⊂ PR): the Stop hook gates the turn and CI's `docs check` +gates the PR, so this runs the SAME registry-derived completeness classifier +(`classifyPath` from `gate.js`) plus `hasSecret` over staged added lines at the commit +boundary — code staged without its doc/state artifact, or a staged secret, is caught +while the fix is still one `git add` away. Each rung is an independent catch layer, so +the silent-miss probability falls multiplicatively. + +**Deep verification (`src/consensus.js`, `forge verify --deep`).** Where plain `verify` +asks one oracle (the tests) plus one heuristic, this runs a table of independent lenses +and aggregates them with the same noisy-OR risk score `lessons.js` uses, behind a +cross-family gate so correlated structural signals can't block alone. Deep `ok` is a +conjunction: the core verifier must PASS **and** the lens consensus must not block; an +unconfigured core can never yield `ok:true`. The score is a calibrated heuristic, not a +proof. + +**Knowledge routing (`src/knowledge_router.js`).** The third routing leg beside `route.js` +(model tiers) and `intent.js` (intent classes), using the same exemplar k-NN math: a fact +is routed to its storage home (decisions vs. the ledger vs. …) tuned by adding example +rows, never regexes. It is TOTAL by construction — a fact resembling nothing falls back to +the ledger (whose decay semantics make an unsure placement safe), never "nowhere". + +**Anti-repetition memory (`src/deja.js`).** Closes the "why do I keep re-solving solved +tasks" gap: a clean first-try success used to leave no durable trace (cortex only mints on +correction). At Stop it mints one `summary` claim (a deterministic, secret-redacted gist), +attaching a `test.run` confirm when the session's tests passed; `dejaLookup` then ranks +prior summary/lesson/diagnosis claims for a new task with the same `retrieve()` (rel × rec +× val) the ledger query uses. No new protocol — it reuses the shipped PCM machinery. + +**The documentation-impact graph (`src/docs_impact.js`, `forge docs impact`).** Where +`docs check` reconciles fixed registries and `docs sync` scans a diff for identifiers, +this answers "I changed X — which documented surfaces mention X and are now potentially +stale?" via a data-driven `EXTRACTORS` registry (commands, flags, env vars, MCP tools, +exported symbols, brand tokens, version, package.json fields) reused from `docs_check.js`, +an inverted entity → `file:line` index over every doc surface, and a diff-scoped impact +query ranked by confidence. Advisory by default; `--strict` exits non-zero for CI. + **Deliberately not wired:** `checkpointCadence` (optimal-stopping check spacing) still has no runtime step-loop to consume it — wiring it would mean inventing one. It stays library math with tests until a real consumer exists. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ff618b..edbe316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,14 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Removed the dead `plugin` entry from `package.json` `files`, the vestigial `.gitlab-ci.yml`, and the dangling `@AGENTS.md` import in `CLAUDE.md`; fixed the off-palette Codex plugin `brandColor`. +- **Broke a layering cycle in `repo_config.js`.** `applyPrimaryTool` no longer reaches + back into the sync compiler via a dynamic `import("./sync.js")`; the sync runner is now + injected by the caller (`cli.js`), keeping the config leaf acyclic. +- `ledger_store.js`'s `git cat-file -e` ref resolver now passes `--` before the ref + (defense in depth against a ref that begins with `-`). +- **Refreshed `ARCHITECTURE.md`** for the v0.20–v0.24 modules that were missing from the + reference: `commit_gate` (`forge precommit`), `consensus` (`forge verify --deep`), + `knowledge_router`, `deja`, and `docs_impact` (`forge docs impact`). ## [0.25.0] - 2026-07-20 diff --git a/src/cli.js b/src/cli.js index f4237f8..40e3ad6 100755 --- a/src/cli.js +++ b/src/cli.js @@ -2439,7 +2439,12 @@ async function run(argv) { process.exitCode = 1; return; } - const r = await applyPrimaryTool(root, name); + // Inject the sync runner from here (the orchestration layer) so repo_config — + // a config-leaf module — no longer reaches back into the sync compiler. + const { sync } = await import("./sync.js"); + const r = await applyPrimaryTool(root, name, { + syncFn: (r2) => sync({ targetRoot: r2 }), + }); if (json) return console.log(JSON.stringify(r, null, 2)); heading(`${BRAND.brand} tools — primary set\n`); console.log(` primary tool ${paint(r.primaryTool, "ok")}`); diff --git a/src/ledger_store.js b/src/ledger_store.js index 53c8f65..4895c83 100644 --- a/src/ledger_store.js +++ b/src/ledger_store.js @@ -55,7 +55,9 @@ const repoRootOf = (dir) => dirname(dirname(dir)); // (and non-git repos) never spawn git. const gitResolver = (root) => (sha) => { try { - execFileSync("git", ["cat-file", "-e", sha], { + // `--` guards against a ref that begins with "-" being read as a flag (defense in + // depth; execFileSync already avoids the shell, and refs here are validated). + execFileSync("git", ["cat-file", "-e", "--", sha], { cwd: root, stdio: "ignore", }); diff --git a/src/repo_config.js b/src/repo_config.js index 26909c5..abc6b42 100644 --- a/src/repo_config.js +++ b/src/repo_config.js @@ -311,11 +311,11 @@ export function nonPrimaryTargets(report, primary) { /** * Set `name` as the repo's primary tool and gitignore every other tool's emitted - * artifacts. Runs a real sync to learn the emit targets (injectable via `syncFn` for - * tests), so the gitignore block always reflects what Forge actually emits. + * artifacts. The caller injects `syncFn` (the sync runner) so this config-leaf module + * never imports the sync compiler; the gitignore block reflects what Forge actually emits. * @param {string} root * @param {string} name canonical primary-tool key (must be in KNOWN_TOOLS) - * @param {{syncFn?:(root:string)=>Promise<{report:any[]}>|{report:any[]}}} [opts] + * @param {{syncFn?:(root:string)=>Promise<{report:any[]}>|{report:any[]}}} [opts] syncFn is required at runtime * @returns {Promise<{primaryTool:string, configPath:string, targets:string[], * gitignore:string, gitignorePath:string}>} */ @@ -323,8 +323,11 @@ export async function applyPrimaryTool(root, name, { syncFn } = {}) { if (!KNOWN_TOOLS.includes(name)) throw new Error(`unknown tool: ${name} (known: ${KNOWN_TOOLS.join(", ")})`); const cfgFile = setPrimaryTool(root, name); - const runSync = syncFn || (async (r) => (await import("./sync.js")).sync({ targetRoot: r })); - const { report } = await runSync(root); + // syncFn is injected by the caller (cli.js / tests). Required so this config-leaf + // module never imports the sync compiler — keeping the layering acyclic. + if (typeof syncFn !== "function") + throw new Error("applyPrimaryTool requires an injected syncFn(root) -> { report }"); + const { report } = await syncFn(root); const targets = nonPrimaryTargets(report, name); const gi = ensureGitignoreBlock(root, targets); return {