# `lib/` — Installer Internals (detailed reference)

This is the on-demand detail tier for `lib/`. Root `CLAUDE.md` § Key Files
carries the one-line current contract for each file; this file carries the
full per-file contract **and** the layered `act:`-by-`act:` evolution history.
Read this when working in `lib/`; skim root `CLAUDE.md` otherwise.

`lib/` is the installer core plus one setup engine per heavier subsystem.
No build step; CommonJS.

## Installer core

- `lib/cli.js` — main orchestration and all CLI logic. The `MODULES`
  registry MOVED to `lib/modules.js` (act:e82baead, architecture-0005);
  `cli.js` re-exports it for back-compat (`require('../lib/cli').MODULES`
  still works). `cli.js` keeps orchestration, arg parsing, the postInstall
  pipeline, and generators.
  The `--reindex` flag (act:98d41fa2) regenerates
  `.claude/skills/_index.json` in place and exits (no install body, no
  global side effects; no-op without `.claude/skills/`) — so a consumer
  editing its `directives-project.yaml` overlay (a new session-close
  mandate or routine, both read from `_index.json`) can pick it up
  without a full reinstall. `generateSkillIndex` is exported.
  The `--freeze`/`--unfreeze`/`--list-frozen` flags (act:c1013967) edit
  `.ccrc.json` `frozenModules` and exit; the install body then expands
  the declared list by its `requires` closure (`expandFrozenClosure` —
  freezing engagement also pins work-tracking), intersects with what's
  actually installed (`effectiveFrozen` — never the raw declared list),
  and HOLDS those modules' CC-owned files at the current version (loud
  per-module frozen report; an auto pib.db backup runs first when a
  frozen install still has a pending schema migration). The
  upstream-cleanup classifier is extracted to a pure `computeRemovals`;
  it, `expandFrozenClosure`, and the hoisted top-level `manifestPath`
  are exported + unit-tested. `generateAgentWrappers()` (see root
  § Generated artifacts) also lives here.
  Also home of `seedAdvisoriesGitignore()` (act:c008862c,
  grp:wt-noise-immunity), run in the install body: on FRESH installs
  only (no prior `.ccrc.json`) it seeds a `.gitignore` entry for
  `.claude/cabinet/advisories-state.json` (machine-written runtime
  state — tracked, it churns every commit and reads as authored work in
  mux worktrees) unless some rule already ignores it (`git
  check-ignore`); existing installs are left untouched — untracking a
  live consumer repo is operator-gated — but a TRACKED copy is reported
  loudly so the consumer walk surfaces it. No-git and dry-run aware,
  injectable exec, exported for tests.
- `lib/modules.js` — the `MODULES` manifest: every module → the template
  paths it copies into `.claude/` (architecture-0005; extracted from
  `cli.js` in act:e82baead). `cli.js` requires + re-exports it for
  back-compat. This is the SINGLE source for "which files does module X
  ship" — the module template arrays that older docs said "live in
  `lib/cli.js`" now live here. Exports `{ MODULES }`. Unit-tested that
  each module ships its declared scripts (e.g. the audit module ships
  `patterns-scope-check.mjs`).
- `lib/installer-gate.js` — shared version/hash gating for the setup
  engines (Wave 1, act:a6377c82). Exports `sha256`, `compareVersions`,
  `hashSourceDir`, `tarballIsStale`, and — since act:9a0af01a —
  `readGlobalManifest`/`writeGlobalManifest` (+ `ccHome`/
  `globalManifestPath`), hoisted here from mux-setup, watchtower-setup, and
  engagement-server-setup, which each carried a BYTE-IDENTICAL copy of the
  pair: the same duplication condition that created this module in the first
  place. All four user-level engines (the fourth being output-register) now
  delegate; the shape-drift guard (a non-object top level or non-object
  `files` reads as empty) lives in the one copy, since every caller indexes
  `manifest.files[dest]` unconditionally. Before it, `sha256()` was
  copy-pasted three times and `compareVersions()` twice, and the
  equal-version "don't hard-skip, fall through to a per-file hash-compare"
  rule was re-implemented per installer. Per-file-manifest installers
  (mux, watchtower, engagement-server) use `sha256` + `compareVersions`;
  versioned-tarball installers (verify, site-audit) use `tarballIsStale`
  + `hashSourceDir` for the equal-version source-hash re-pack. All five
  setup engines delegate here.
- `lib/copy.js` — template copying with conflict detection; exports
  `recordSkip()`, the single skip-ownership helper used by every skip
  site in both `lib/cli.js` and `lib/copy.js` (skipped project-created
  files are omitted from the `.ccrc.json` manifest — omission means
  "not ours", so a reinstall can't claim them). Also exports
  `isProjectOwnedSeed()` (act:2001bf54): the basename rule for files
  shipped as STARTER content but PROJECT-OWNED by declaration the moment
  they land — today `directives-project.yaml`. The installer seeds them
  if absent, NEVER overwrites them, and ALWAYS omits them from the
  manifest (a recorded seed false-blocks the very first edit via
  cc-upstream-guard and a `--yes` reinstall would clobber it). The
  cli.js upstream-cleanup loop exempts these too — dropping the key
  means "project-owned", NOT "removed upstream", so the freeing
  reinstall can't delete the consumer's overlay. Also exports
  `classifyFileOwnership()` (Wave 1, act:a6377c82): the single-file
  ownership cascade used by cli.js's single-file branch — it closes the
  missing `isProjectOwnedSeed` guard on that path, so a project-owned
  seed reached via the single-file route is classified the same as via
  the directory walk. Also exports `freezeRetain()` (act:c1013967): the
  canonical FREEZE rule — a THIRD ownership state between CC-owned and
  project-owned. When a module is frozen (`.ccrc.json` `frozenModules`,
  threaded as `copyTemplates({frozen})`), a differing CC-managed file is
  HELD instead of overwritten: an ALREADY-CC-owned file (in the prior
  manifest) is RETAINED in the new manifest at its ON-DISK hash (keeps
  cc-drift-check/reset green and the cleanup loop from deleting it, while
  cc-upstream-guard keeps it edit-blocked), recorded in `results.frozen[]`;
  a project-owned file FALLS THROUGH to skip-and-omit (retaining it would
  adopt + clobber it on unfreeze). A held-back NEW file in a frozen module
  is reported but gets no manifest entry (no on-disk content to hash).
  Single source of truth for the rule across both copy paths (the
  directory walk and cli.js's single-file branch). 23 tests in `test/freeze/`.
- `lib/metadata.js` — `.ccrc.json` read/write/merge/create. `create()`
  accepts + persists `dbSchemaVersion` (top-level field, set only when a
  real version is confirmed — undefined rides the prior value through via
  `...existing`); `lib/cli.js` threads `setupDb`'s returned `schemaVersion`
  into it. Also exposes `readFrozenModules`/`setFrozenModules`
  (act:c1013967): the `.ccrc.json` `frozenModules` accessors for the
  freeze-module mechanism (a non-array reads as empty — shape-drift
  defensive; `create()` preserves the field via `...existing`).
  Also exposes `readMcpOptOut` (act:b770c323): the `.ccrc.json`
  `mcpOptOut` accessor — the CC-shipped MCP server keys this consumer
  declines (flow's `no-pib-db.md` is the live specimen; its validator
  requires the pib-db registration absent while the engagement→
  work-tracking closure keeps shipping the files). Same defensive shape
  as `readFrozenModules`; the key survives reinstalls via `create()`'s
  `...existing` spread, so a consumer sets it once by hand. Honored by
  `mergeMcpServers` in `lib/cli.js` (opted-out keys are never added and
  are REMOVED if present — self-heal toward declared intent; removal is
  scoped to keys CC ships, so hand-added servers are never touched;
  nothing-to-contribute installs don't rewrite `.mcp.json`). Tests in
  `test/mcp-opt-out/`.
  `normalize()` guards shape drift (non-object top level reads as
  'no metadata').
- `lib/settings-merge.js` — merges CC hooks into `.claude/settings.json`.
  CC-owned hook commands ship with a `$CLAUDE_PROJECT_DIR/` prefix so they
  resolve from any cwd (a bare-relative `.claude/hooks/X.sh` breaks when a
  hook fires from a non-root directory; commit 81693f8). A reinstall
  migration REPLACES (not duplicates) any superseded bare-relative form
  with the prefixed one — `bareForm()` derives the legacy spelling,
  `dropHookCommands()` removes it, then the prefixed form is added;
  exact-match on CC-owned command strings only, so consumer-custom hooks
  are never touched. `mergeSettings` ALSO migrates the stale pib action
  gate matchers to their `mcp__pib-db__*` MCP tool-name form (act:ff693d4c —
  the old bare matchers never matched the real MCP tool names, so the
  gates were dead since shipping); the fired-at-least-once telemetry record
  in `.claude/state/hooks-fired.jsonl` is written by the gate SCRIPTS
  themselves (`action-quality-gate.sh`, `action-completion-gate.sh`), NOT by
  settings-merge. Also heals `~/.claude/settings.json` by
  stripping CC hook entries with project-relative paths (runs
  unconditionally on every install). Exports `mergeBashCompressHooks()`
  for the opt-in bash-compress module; `mergeSkillOverrides()` — the
  installer sets the 34 cabinet-* skills to `skillOverrides: name-only`
  so their long descriptions don't overflow the skill-listing budget
  (act:bee0fa16); and `mergeCcDevHooks()` + `CC_DEV_HOOKS` — the
  repo-local `node --check` PostToolUse dev hook
  (`$CLAUDE_PROJECT_DIR/scripts/node-check-hook.sh`), gated on
  package.json name === `create-claude-cabinet` so it installs only in
  the source repo, never in consumers (act:ff693d4c). 10 tests in
  `test/settings-merge/settings-merge.test.js`.
- `lib/reset.js` — manifest-safe uninstall/removal. Exports `reset()`:
  removes CC-managed files per the `.ccrc.json` manifest and strips
  CC-managed hook entries from `.claude/settings.json` (matched against
  `CC_HOOK_PATTERNS` / `DEFAULT_HOOKS`), never touching project-owned files.

## Work-tracking / DB

- `lib/db-setup.js` — pib-db init step (runs after the copy loop). Exports
  the pure helpers `isAbiMismatch`, `readBundledSchemaVersion`,
  `shouldSkipInit`, `verifyInstalledSchema` plus `setupDb(projectDir, opts)`
  (injectable
  `opts.exec`/`opts.log` for tests). `setupDb` returns `{results,
  schemaVersion}` (a bare array before act:f5dab71f). LAZY-INIT: skips DB
  init when the recorded `.ccrc.json` `dbSchemaVersion` already equals the
  bundled `SCHEMA_VERSION` (read as TEXT from `pib-db-lib.mjs`, never
  imported — so a no-migration reinstall never loads the native module).
  SELF-HEALS a better-sqlite3 ABI mismatch (`npm rebuild` + retry once,
  else one actionable line — never a throw or silent skip; act:e7c69b5c).
  `schemaVersion` is null when init couldn't confirm a version, so the
  caller never records a false 'schema current' claim.
  `verifyInstalledSchema(projectDir, opts)` is the POST-INSTALL SELF-CHECK
  (act:b7d81b52): it shells `pib-db.mjs verify-schema` and returns
  `{status, results}` where status is `ok` | `mismatch` | `skipped` |
  `unknown`. It exists because the installer reported SUCCESS on both
  consumers the migration-lineage collision killed — the copy loop exiting
  zero is a claim about FILES, this is a claim about the SCHEMA those files
  will run against. Three placement/ shape rules are load-bearing:
  cli.js calls it AFTER the postInstall loop (engagement-setup REPLACES the
  installed pib-db library, so checking earlier grades the db against a
  library about to be swapped — and false-alarms on every engagement
  install); it never throws and never gates the install (a self-check that
  can wall an install is worse than the defect it detects); and `unknown` is
  a distinct register from `mismatch`, because an older installed CLI with no
  `verify-schema` command exits nonzero with usage text — reporting that as a
  mismatch cries wolf on every pre-upgrade consumer, reporting it as `ok`
  restores the exact silence the guard exists to end. A `mismatch` sets
  cli.js's `schemaSelfCheckFailed`, which REPLACES the "Cabinet assembled!"
  banner with an explicit "the work tracker is not usable" close. 14 tests in
  `test/db-setup/db-setup.test.js`.
- `lib/engagement-setup.js` — extends the pib-db files with the engagement
  schema when the engagement module is installed. Dispatched from cli.js's
  postInstall pipeline. ORDERING NOTE (act:b7d81b52): the overlay library
  refuses to adopt a base-lineage database until `engagement_events` exists
  (its v5 `expect` post-condition), so the table must be ensured BEFORE
  migrate runs on the add-engagement-to-an-existing-consumer path. It is —
  the schema-ensure step shells `pib-db.mjs init`, and `init()` execs the
  (already overlaid) schema file, which declares `engagement_events`, before
  calling migrate. That ordering is a real dependency now, not a
  coincidence; `test/pib-db-lineage/` pins both directions.
  Two duties: (1) a marker-aware FILE OVERLAY —
  copies the engagement-inclusive pib-db files over the base versions,
  deciding by whether the installed file already contains engagement code
  (not just its `SCHEMA_VERSION` number), so an equal-version reinstall
  still upgrades a base→engagement file; (2) a postInstall schema-ensure.
  Exports `setupEngagement()`.

## Project identity

- `lib/project-context.js` — a THIN RE-EXPORT of
  `templates/scripts/project-context.cjs` (Wave 1, act:a6377c82). The
  logic lives in the template copy (co-located with the memory scripts,
  which `require` it as a sibling; consumers have no `lib/`); this file
  re-exports it so CC-source-repo callers (`lib/` code and the
  memory/orient SKILL.md best-effort `require('./lib/project-context')`)
  resolve the exact same implementation. Collapsed the byte-identical
  twins that had already drifted once ("one concept, one place").

## Setup engines (per opt-in module)

- `lib/verify-setup.js` — cabinet-verify runtime installer (creates
  `~/.claude-cabinet/verify/<version>/dist/cabinet-verify-<version>.tgz`
  via `npm pack`; writes `~/.claude-cabinet/verify/current/VERSION`
  pointer; equal-version source-hash re-pack via `installer-gate.js`).
- `lib/site-audit-setup.js` — site-audit runtime installer (mirrors
  verify-setup; packs `templates/site-audit-runtime/` to
  `~/.claude-cabinet/site-audit/<version>/`; equal-version source-hash
  re-pack via `installer-gate.js`).
- `lib/engagement-server-setup.js` — engagement-server runtime installer
  (copies server files to `~/.claude-cabinet/engagement-server/`;
  follows mux-setup.js pattern with global manifest SHA256 tracking,
  incl. the equal-version hash-compare fall-through of act:df1a1dc3 —
  equal versions copy only changed files, `'unchanged'` when none do).
- `lib/output-register.js` — Cabinet Register output-style installer
  (act:9a0af01a). Seeds `templates/output-styles/*.md` (GLOBBED, never
  hand-enumerated — the orphan test only scans `templates/skills/`) into the
  user-level `~/.claude/output-styles/`, then seeds `outputStyle` into the
  PROJECT's `.claude/settings.json`. Unlike the other four engines this one
  is SEED-ONLY: `isProjectOwnedSeed()` semantics (never overwrite — the
  operator hand-tunes their register) PLUS loud validation of the INSTALLED
  copy, because neither sibling engine's ownership model is right here (mux
  never re-examines a hand edit → the footgun persists; watchtower reverts
  hand edits every reinstall). Its load-bearing export is `versionGate()`:
  `keep-coding-instructions` only exists as of Claude Code **v2.0.37**, and
  below that the field is silently ignored, its `false` default applies, and
  CC's software-engineering instructions are STRIPPED with no error — a
  hazard NO file-content test can see, so the guard shells `claude --version`
  (refuse below the floor; fail OPEN when absent/unparseable, since absence is
  not an old version). `resolveRegisterLevel()` is a three-valued
  `{level, ours, source}` ENUMERATOR (blind to force-for-plugin / CLI /
  managed layers, and says so); `REGISTER_STYLE_NAME` must equal the style's
  frontmatter `name` — CC keys custom styles by that field verbatim. Full
  rationale in `templates/CLAUDE.md` § output-register; 40 tests in
  `test/output-register/`.
- `lib/mux-setup.js` — mux runtime installer (copies bin/mux, config
  scripts, and popup to user-level paths; creates data dirs;
  `setupDarwinIntegration()` appends source-file to `~/.tmux.conf`,
  reloads live tmux, and writes/bootstraps the
  `com.mux.screenshot-to-clipboard` launchd plist; migrates legacy
  `com.orenmagid.*` agent). Version gate (act:df1a1dc3): equal
  CC-vs-installed version does NOT early-return — it falls through to
  the SHA256 manifest hash-compare and copies only changed files
  (new `'unchanged'` status when nothing differs), so a
  dogfood-from-source template edit without a version bump still
  propagates on reinstall.
- `lib/watchtower-setup.js` — global watchtower runtime refresher
  (`refreshWatchtowerRuntime`, act:2da80102). REFRESH-ONLY: content-hash
  refreshes the EXISTING `~/.claude-cabinet/watchtower/` runtime
  (scripts/ = all globbed `watchtower-*.mjs` + the shell runners,
  hooks/ = the session hooks, cabinet/ = the two docs) on every
  reinstall by comparing each template against the runtime file's ACTUAL
  on-disk bytes — act:4e05ff53 SEVERED the old manifest-driven skip, so
  the shared `global-manifest.json` SHA256 record is now bookkeeping only,
  not the skip signal; a stale or hand-edited runtime file therefore
  reconverges on the next reinstall (self-heal). Dests keyed by absolute
  path so no collision with mux's entries. No-ops with status `'absent'`
  and zero writes when no runtime exists yet — fresh setup (launchd/cron,
  config.json, migrate-keys, coherence assertion) stays the
  `/watchtower install` SKILL.md step's job. Wired in lib/cli.js after the
  postInstall loop, gated on `selectedModules.includes('watchtower')`. 5
  hermetic tests in `test/watchtower-runtime-refresh/`.

## Memory migration (one-time, off omega)

- `lib/migrate-from-omega.js` — one-time omega → built-in memory migration
  engine (`--migrate-memory`): exports omega memories to Claude Code's
  built-in memory layout (fresh-write or additive `omega-migrated/` merge
  when native memory already exists), backs up first, then tears down
  omega hooks/MCP. Never overwrites native files.
- `lib/migrate-memory-cmd.js` — CLI command wrapper for the migration
  (`--migrate-memory`, `--dry-run`, `--unmigrate-memory` rollback).
- `lib/migration-snapshot.js` — captures pre-migration omega state to a
  JSON snapshot so `--migrate-memory` can do DETERMINISTIC removal of
  hooks / MCP entries / the OMEGA block (match against captured contents,
  not pattern-guess against live state). Searches all three MCP config
  locations and matches omega by command path (contains `omega-venv`)
  rather than key name.
