Architecture
Auto-generated from docs/ARCHITECTURE.md — always current.
Cheap high-level map of how nervepack fits together. Read before any code change so you know what your change touches and can stay consistent with patterns that work.
What nervepack is
A versioned hub of skills, rules, memory, and dev-env setup that follows Pat
across machines, delivered into each AI session as user skills (via
.claude-plugin/) and wired into the session lifecycle by hooks + crons. It
is mostly Bash/Python glue around the claude CLI plus Markdown knowledge. No
service, no daemon. Everything is a hook, a cron, or a committed file. (One
deliberate exception: the dashboard’s localhost-only server,
evaluator.dashboard_serve, default on (opt-out). See the Dashboard row.)
Skill namespaces (tiers)
| Prefix | Tier | Holds | Ships in |
|---|---|---|---|
np-core- | cognition machinery | capture, recall, sync, toggle, contribute | engine |
np-flow- | workflows | recurring agent prompts (agents/*.md) | engine |
np-kb- | knowledge base | domain how-to (branding, chrome-ext, coding rules…) | content overlay |
np-env- | environment | machine setup (ubuntu, plugins, vscode, secrets) | content overlay |
The prefixes name the namespace convention; they do not all ship from the same repo.
The engine repo is machinery-only — it ships np-core-* + np-flow-merge-gate. The
domain np-kb-*/np-env-* skills are content (personal/instance identity), delivered by
your content overlay (or a starter), not the public engine (PR #89). cli.py setup link-skills
(np_link_skills.py) merges engine + overlay(s) at link time so every tier reaches the session.
Feature catalog (every feature ↔ its toggle ↔ its code ↔ its design doc)
Toggles are declared in engine/setup/toggles.conf; every runtime check goes through
np_toggle.enabled in engine/nervepack_engine/np_toggle.py (the sole resolver since the sourced
bash toggle lib was retired in phase 18). Flip with np-core-toggle. This table
is the code/toggle/doc locator; each feature’s purpose, enforcing workflow, and a
worked example live in FEATURES.md.
| Feature | Toggle | Core code | Design doc |
|---|---|---|---|
| Session directive (“consult nervepack first”) | directive | engine/nervepack_engine/hooks/session_directive.py (Python port; dispatched as cli.py hook session-directive), engine/setup/nervepack-session-directive.md | CLAUDE.md §“Why every session…” |
| Episodic memory (auto working-memory) | memory | engine/nervepack_engine/hooks/episodic_capture.py (Python port; dispatched as cli.py hook episodic-capture <mode>, backed by np_capture.py), engine/nervepack_engine/hooks/episodic_recall.py (Python port via cli.py dispatcher), episodic-match.sh, np_scrub.py, np-transcript-extract.py, agents/np-flow-episodic-maintain.md | specs/2026-06-02-episodic-memory-layer-design.md |
| Back-capture sweep (reliable capture path) | memory (.backcapture; backcapture_days = max discovery window default 7, backcapture_max = per-sweep cap default 5) | engine/nervepack_engine/cli.py (dispatcher) + engine/nervepack_engine/hooks/backcapture_sweep.py (Python port, first script migrated per the bash-to-python CLI consolidation — content overlay docs/superpowers/specs/2026-07-15-nervepack-python-cli-consolidation-design.md); registered via cli.py setup install-hooks (from engine/setup/hooks.manifest; SessionStart, backgrounded); persistent queue ~/.cache/nervepack/backcapture-queue/<sid> tracks pending work independent of the current mtime window — enqueued once, processed oldest-first, survives aging past backcapture_days | CLAUDE.md §“Back-capture sweep”; see invariant 12 |
| Local memory promotion | memory.promote (sub of memory) | engine/setup/np_agentic_cron.py (Python port; memory_promote(), dispatched via cli.py cron memory-promote) | CLAUDE.md §“Memory-store promotion” |
| Resume pointer (deterministic where-we-left-off) | resume (.interval, .max_age, .cron, .cron_min, .active_window) | engine/nervepack_engine/hooks/resume_write.py (writer, no LLM calls; dispatched as cli.py resume-write), engine/nervepack_engine/hooks/resume_sessionstart.py (SessionStart, backgrounded — reliable trigger; dispatched as cli.py hook resume-sessionstart), engine/nervepack_engine/hooks/resume_recall.py (UserPromptSubmit — surface + throttled write; dispatched as cli.py hook resume-recall), np-transcript-extract.py --last-user, registration via cli.py setup install-hooks (engine/setup/hooks.manifest) | plans/2026-07-09-resume-pointer.md |
| Lessons (auto-distilled, provenance-tagged, optionally enforced) | lessons (.enforce, default on) | engine/nervepack_engine/hooks/lesson_recall.py, engine/nervepack_engine/hooks/lesson_guard.py (both Python ports via cli.py dispatcher), memory/lessons/, agents/np-flow-episodic-maintain.md (distills capture struggles[]→provenance: failure and strategies[]→provenance: success) | specs/2026-07-02-lessons-layer-merge-design.md (was: specs/2026-06-03-playbook-layer-design.md + specs/2026-06-05-nervepack-vs-sota-evaluation.md) |
| Performance evaluator | evaluator | engine/nervepack_engine/hooks/evaluator.py (Python port; dispatched as cli.py hook evaluator, backed by np_evaluator.py), np-eval-signals.py | specs/2026-06-03-performance-evaluator-design.md |
| Metrics aggregation | evaluator (.aggregate; retain_days=90 TTL prune, 0 = unlimited) | engine/setup/np_aggregate.py (Python port; dispatched via cli.py cron aggregate-metrics) | ↑ same |
| Dashboard | evaluator (.dashboard_open, default on) | dashboard/build.py, dashboard/index.html, engine/nervepack_engine/hooks/open_dashboard.py (SessionStart, once/boot; dispatched via cli.py hook open-dashboard, backed by engine/nervepack_engine/np_dashboard.py), np_dashboard.open_manual() (the manual on-demand open, cli.py open-dashboard — no boot guard; the retired open-dashboard.sh/np-dashboard-launch.sh bash scripts folded into np_dashboard.py in phase 16), np-core-dashboard; resolved-suggestions ledger: np_suggestion_resolve.py (cli.py suggestion-resolve) + build.py load_resolved() (routed via np_content_dir; NP_RESOLVED_SUGGESTIONS overrides); graduation panel: build.py load_graduation() ← committed graduation-candidates.json (written by np_skill_maintain.py, content-routed; NP_GRADUATION_CANDIDATES overrides) → window.GRADUATION. Back-capture backlog panel: build.py backlog_metrics() ← local-cache BACKCAPTURE_QUEUE_DIR/BACKCAPTURE_SEEN_DIR (same dirs/env-var names as np-backcapture-sweep.sh, not committed content) + memory.backcapture_days toggle (via np_toggle.param, imported straight from engine/nervepack_engine/np_toggle.py) → window.BACKLOG = {pending, oldest_pending_days, ceiling_days, resolved_last_24h}, rendered by index.html renderBacklog(). Data bridge: index.html loads data/metrics.js as a relative sibling; in a split layout <engine>/dashboard/data is a symlink into <content>/dashboard/data, created idempotently by np_link_dashboard_data.py (cli.py setup link-dashboard-data) (run via cli.py setup link-dashboard-data, wired into np_onboard.py’s orchestrator, verified by np_doctor.py’s dashboard-data capability). Team-layer-aware (Phase 3): learned_counts() merges memory/lessons/ (split by provenance) across team+personal overlays per team.merge; metrics stay personal-only. | specs/2026-06-03-p2-dashboard-design.md |
| Wiki navigation (dashboard left-nav + search) | evaluator (.wiki_nav, default on) | build.py wiki_index() → window.WIKI = {topics[], concepts[]} in metrics.js (indexes overlay wiki/topics/ + wiki/concepts/, resolved via np_content_dir; WIKI_NAV env gate); index.html grouped/collapsible sidebar (Topics — each topic nests its synthesis page + sources — and Concepts) + client-side search + build-rendered HTML pages opened in a new tab (build.py md_to_html/render_pages → data/wiki/{topics,concepts}/*.html; content-resident & gitignored). Data resides in the content overlay; engine carries code only. Mermaid diagrams in wiki pages render client-side via vendored dashboard/vendor/mermaid.min.js (no external fetch), gated by evaluator.wiki_mermaid (default on). Phase 2b: wiki_index() scans the team+personal overlays (np_merge_roots/np_merge_mode via shell-out) and merges per team.merge. Layer badge: every entry carries layer (build.py _layer_label() — the personal _content_dir() is the literal personal, any other merge root is its dir basename; compared against the content dir rather than derived from merge-root position, since team-only drops personal from that list), rendered by index.html as the .wl pill beside the kind pill, folded into the search haystack, and stamped into each rendered page header via render_pages() → md_to_html(meta["layer"]). Per-layer split (#142): _wiki_roots() = merge roots + the engine root appended last (_engine_dir(), NP_ENGINE_DIR overrides — the engine ships its own wiki/topics/, which merge_roots() never returns, so those pages were invisible; skipped when the engine dir IS a content layer, and under team-only). Every layer is indexed and lower-precedence copies are marked shadowed, not dropped — so wiki_index() emits one entry per (layer, topic) plus an ordered layers[] (only layers that contributed). Page paths are layer-qualified (data/wiki/<slug>/<topics|concepts>/…, _layer_slug() sanitizes a team dir’s basename to one safe segment) since two layers may hold the same topic+page name; render_pages() therefore records each page’s source .md at index time (src) instead of recovering it from the html path, and prunes <out>/wiki before rendering so the old scheme’s files don’t orphan. index.html renders one <details class="wikilayer"> per layer (highest-precedence open) only when >1 layer resolves — a single layer renders exactly as before, and the now-redundant .wl badge is hidden inside a split (.wikilist.split). | specs/2026-06-03-p2-dashboard-design.md |
| Feature-toggle panel (dashboard) | evaluator (.toggle_ui, default on) | engine/setup/toggle-schema.json (param type/validation data) + np_toggle_schema.py (loader/validator) + np_toggle.py all_params(); GET /api/toggles/POST /api/toggle in np-dashboard-server.py (calls np_toggle.flip() in-process for bare-feature flips — shared scope commits+pushes, local/managed stays local — and writes dotted params directly via np_toggle.set_local(); self-lockout guard refuses to flip evaluator or its own dashboard-gating params dashboard_open/dashboard_serve/toggle_ui); index.html’s Feature Toggles panel (switches for bare features, schema-typed inputs for params, confirm dialog before a shared-scope flip). | specs/2026-07-09-dashboard-toggle-controls-design.md |
| Suggestions review | evaluator (.dashboard_serve, .dashboard_port, .suggestions_top params) | np-suggestions-review.py (list/clear), np-dashboard-server.py (opt-in localhost backend), np-core-suggestions-review | (this work) |
| Suggestion implement/reject (dashboard P3) | evaluator (.implement, .implement_mode pr|direct) | np_implement_suggestion.py (dispatched as cli.py implement-suggestion, phase 10 — async agentic job; worktree-isolated; tries the engine repo first, then falls back to the content overlay via np_content_dir when the engine attempt is NOT_IMPLEMENTABLE/no-commit and a distinct git-tracked overlay is configured — a content-overlay success always lands with a direct push, independent of implement_mode), agents/np-flow-implement-suggestion.md, np-dashboard-server.py /api/implement, index.html buttons (Implement · Modify · Reject — Modify edits the wording inline and sends edited alongside text; the agent gets the edit, the ORIGINAL keys the status file and is what gets resolved, with the edit recorded as a 3rd tab field on the ledger line). A commit is VERIFIED, never inferred (_agent_commit: rev-parse succeeded, 40-hex, ≠ base, descends from base) and _land refuses any sha that isn’t verified — inferring success from end_sha != base_sha meant a failed rev-parse read as a commit, resolving suggestions that were never implemented and pushing ":refs/heads/<base>", which git executes as a branch DELETE. Cross-machine guard: the job re-checks the synced ledger before spending an agent pass (np_suggestion_resolve.is_resolved) and reports already_resolved; cli.py suggestion-unresolve is the idempotent recovery path back onto the dashboard | specs/2026-06-08-suggestion-implement-reject-design.md |
| Struggle escalation (mid-session) | evaluator (.escalation; escalation_min_struggles=2, escalation_min_prompts=3) | engine/nervepack_engine/hooks/struggle_escalation.py (Python port via cli.py dispatcher; UserPromptSubmit, once/session), registered via cli.py setup install-hooks (engine/setup/hooks.manifest) | — |
| Skill trigger recall (prompt-pattern skill routing) | skills (trigger_recall=on param, not a sub-toggle — a sub-toggle inherits skills and never lands in toggles.conf, so the flag was invisible; the hook also checks the family so skills off stays decisive) | engine/nervepack_engine/hooks/skill_trigger_recall.py (Python port via cli.py dispatcher; UserPromptSubmit, once/session; once-per-session state in ~/.cache/nervepack/skill-trigger-state, NP_SKILL_TRIGGER_STATE overrides), registered via cli.py setup install-hooks (engine/setup/hooks.manifest) | — |
| Security recall (security-review skill routing) | skills (security_recall=on param; same param+family gate as trigger recall) | engine/nervepack_engine/hooks/security_recall.py (UserPromptSubmit, once/session; injects a reminder to invoke np-kb-security-review when a prompt hits security/vulnerability keywords; state in ~/.cache/nervepack/security-recall-state, NP_SECURITY_RECALL_STATE overrides), registered via cli.py setup install-hooks (engine/setup/hooks.manifest) | — |
| Skill maintenance (daily auto-split + advisory checks) | skills (graduate_seen/graduate_kb params) | engine/setup/np_skill_maintain.py (Python port; dispatched via cli.py cron skill-maintain), np_skill_budget.py, np_skill_validate.py, np_graduation_detect.py (flags proven/over-budget lessons to graduate→skill; surfaces to log + graduation-candidates marker, never auto-promotes), np_architecture_freshness.py (advisory map-drift, cli.py-importable, called in-process from np_skill_maintain.py), agents/np-flow-skill-maintain.md | specs/2026-06-05-skill-maintenance-routine-design.md |
| LLM agent seam | — | engine/setup/np_llm_agent.py (run_agent — the shared seam for multi-turn agent loops; dispatched from orchestrators; consumed by np_skill_maintain.py’s split loop since Phase 10) calls np_model.agent() in-process (phase 9 of the bash→Python migration ported it; phase 19 retired the bash wrapper np-llm.sh, so np_model.py is the sole seam); the claude backend is itself bash-free (no bash -c wrapper), the local backend’s NP_LLM_AGENT_CMD path still shells via bash -c | — |
| Engine maintenance — refine (weekly lint + cross-ref audit) | maintain.refine (sub of maintain, default on) | cli.py cron refine (backed by np_agentic_cron.py), agents/np-flow-scheduled-refine.md | specs/2026-06-19-provider-agnostic-scheduled-agents-phase1-design.md |
| Engine maintenance — compact (weekly skill dedup + split proposals) | maintain.compact (sub of maintain, default on) | cli.py cron compact (backed by np_agentic_cron.py), agents/np-flow-weekly-compact.md | specs/2026-06-19-provider-agnostic-scheduled-agents-phase1-design.md |
| Cross-machine sync | sync (.content, default on — extends the ff-only treatment to the personal content overlay, no-op on a single-repo legacy layout) | engine/nervepack_engine/np_sync.py (cli.py sync — phase 17: full defensive sync incl. team-ff + content-overlay-ff (shared _ff_only_layer_sync) + on-ff relink/hook-reinstall/5x-installer sweep; 40-sync-nervepack.sh retired). Neither the engine nor any layer sync ever pushes — “local ahead” is report-only in this module; the standing behavior for what to do about it lives in np-core-sync’s own written protocol for a human-attended invocation. | CLAUDE.md §“sync nervepack” |
| Feature toggles | (self) | np_toggle.py (sole resolver + full write surface: state/param edits, path-limited commit+push, managed allowlist install/remove — the sourced bash toggle lib was retired in phase 18), np_toggle_audit.py, cli.py toggle (status/flip/param/audit/menu), toggles.conf | specs/2026-06-03-feature-toggles-design.md |
| Permission allowlist | allowlist | engine/nervepack_engine/np_toggle.py (install_permissions/remove_permissions, via cli.py toggle — ports of the retired 90/91-…-permissions.sh) | — |
| Secrets refresh | — | np-env-secrets-refresh skill | specs/2026-05-26-secrets-refresh-design.md |
| Wiki/sources (curated reference) | — | Whatever tree the layer’s own layout declares (see the Layer-layout row); the personal overlay uses wiki/topics/<topic>/ + wiki/concepts/<concept>/ folders (one synthesis page + co-located sources; no separate top-level sources/ dir), log.md | AGENTS.md §“Wiki layer” |
| Layer layout (where each content layer keeps its content) | — | engine/nervepack_engine/np_layout.py (manifest <layer>/.nervepack/layout.json, disk inference, open-question detection, contained route()) + np_layout_cli.py (cli.py layout show/infer/questions/record/route); consumed by np-core-contribute, np-mcp-server.py _tool_contribute, np_generate_index.py’s Knowledge section, and the doctor’s layer-layout SHOULD check; discovery interview lives in the np-core-layout skill. The engine owns the KIND vocabulary (skill/knowledge/reference/roadmap/prompt); each layer owns the PATHS, so topics-vs-concepts is one layer’s variant split rather than engine machinery (#234). | specs/2026-08-13-dynamic-layer-layout-design.md |
| LLM-agnostic onboarding | — | engine/nervepack_engine/np_model.py (the model seam — complete and agent modes both run in-process as np_model.complete()/np_model.agent(); the local backend’s NP_LLM_AGENT_CMD path shells bash -c from within agent() when configured; the bash wrapper np-llm.sh was retired in phase 19, np-implement-suggestion.sh in phase 10), engine/onboard/{ONBOARD.md,capabilities.json,adapters/}, engine/nervepack_engine/np_doctor.py (the sole doctor, run via cli.py doctor; np-doctor.sh retired phase 15), np-core-onboard skill | specs/2026-06-05-agnostic-onboarding-design.md |
| Sourced-lib resolvers (bash originals retired, phase 18) | — | engine/nervepack_engine/np_toggle.py (enabled/param) and engine/nervepack_engine/np_content.py (content_dir/content_layers/merge_mode/merge_roots/layer_roots) are now the sole resolvers — the three sourced bash libs (np-toggle-lib.sh, np-content-lib.sh, np-layer-lib.sh) were deleted in phase 18 along with their A/B parity tests, once every caller had moved off them (the last production sourcer, 58-install-mcp.sh, now gates via python3 np_toggle.py enabled mcp). The resolver edge-case coverage the parity + bash-lib tests held was ported to in-process Python tests (tests/toggles/test_np_toggle_resolver.py, tests/content/test_layer_resolver.py) before deletion. Python callers import np_toggle/np_content; shell steps call the np_*.py <verb> CLI. | — |
| Onboard orchestrator + toolchain bootstrap (phase 7 of the bash→Python migration) | — | engine/setup/np_onboard.py (cli.py onboard — the full-onboard sequence: link-skills, dashboard-data bridge, every 5x/6x hook installer, the OS-scheduler step, the doctor; same entry point the MCP nervepack_onboard tool calls), engine/setup/np_bootstrap.py (cli.py setup install-apt-baseline / install-brew-baseline / install-rustup / install-claude-plugins / prewarm-serena / install-pii-deps / install-vscode-extensions — one-time toolchain-baseline steps, no toggle family since they’re not runtime hooks) | (no dedicated design doc; see the migration spec’s phase-7 entry) |
| Pipeline-health checks (doctor, advisory) | memory (.drain_grace_days, default 2), maintain (.freshness_grace_days) | engine/nervepack_engine/np_maintenance_freshness.py (capability maintenance-freshness: did each cron fire within its cadence?) + engine/nervepack_engine/np_episodic_freshness.py (capability episodic-freshness: did anything come OUT? — WARNs when notes sit in the episodic inbox past drain_grace_days while memory/episodic/INDEX.md stays unmoved; NP_EPISODIC_INBOX overrides). These are deliberately different questions — the #113 outage passed the first and fails the second: episodic-maintain fired daily, wrote its run header and exited 0 for over a week while the headless call underneath it had no valid credential, so nothing drained. Both are advisory, never gate, and degrade to PASS on any internal error. | GH #113 |
| Scheduled-auth token (long-lived claude OAuth token for launchd/cron) | — (doctor capability scheduled-auth-token, SHOULD) | engine/setup/np-token-lib.sh (np_claude_token_env_prefix/_store/_status; the Python scheduler installers use only the small engine/nervepack_engine/np_token_lib.py port of claude_token_env_prefix, not this bash file — _store/_status stay bash-only until phase 8), engine/nervepack_engine/np_token_status.py (stdlib date math), engine/setup/62-install-scheduled-auth-token.sh (the one-time interactive walkthrough — claude setup-token needs a real terminal + browser approval, so it can’t be scripted end-to-end), wired into np_scheduler_install.py’s install_launchd/install_cron (each scheduled job’s command is prefixed with a snippet that re-reads the token file at RUN TIME, so rotating the token later is just overwriting the file — no plist/crontab reinstall needed), np_doctor.py scheduled-auth-token core check (WARNs ~30 days before the ~1-year token expires). install_schtasks (Windows) is NOT wired — the token snippet’s embedded "..." would collide with the nested double-quoting schtasks //TR "..." already requires, and this needs verifying on a real Windows/Git-bash host before shipping (tracked as a roadmap issue; unchanged by the phase-6 Python port — deliberately preserved, not an oversight). | (no content-overlay spec; this is engine-only glue) |
| MCP layer (model-agnostic surface) | mcp (writes/contribute params) | engine/setup/np-mcp-server.py (stdlib stdio JSON-RPC dispatcher), engine/bin/nervepack-mcp (launcher), engine/setup/58-install-mcp.sh (Claude Code registration), engine/bin/nervepack-install → cli.py setup mcp-install (engine/setup/np_mcp_install.py — the guided one-line install: prompt content/team overlay → register → run the doctor; non-interactive falls back to defaults; test tests/onboard/test_mcp_install.sh), engine/setup/tests/mcp/test_mcp_server.py. Team-layer-aware (Phase 3): _tool_recall merges episodic/lesson recall across team+personal overlays per team.merge. | specs/2026-06-08-nervepack-mcp-layer-design.md |
| Content seam (engine/overlay split) | — (config: NP_CONTENT_DIR) | engine/nervepack_engine/np_content.py (content_dir resolver + content_origin/is_explicit — the single explicit-vs-implicit detector, issue #12; the sole resolver since the sourced bash content/layer libs were retired in phase 18; consumed across recall/guard hooks, skill link+index, metrics, MCP, doctor). Personal-content writers (memory-promote, episodic-maintain, skill-maintain, np_aggregate.py) skip their commit when the dir is the implicit engine-root fallback (NP_CONTENT_DIR unset AND no ~/.config/nervepack/content-dir), so they never pollute the PII-clean engine; the doctor content check warns. A deliberate single-repo user opts in via the config file (origin config). Example overlay: the public nervepack-content-example repo. Optional team overlay (NP_TEAM_DIR / ~/.config/nervepack/team-dir, toggle team) overrides personal skills + merged INDEX (Phase 1); resolved by np_team_dir/np_team_dir_origin. The team value may be a comma-separated list of up to 4 dirs (first = highest precedence), stacking team[0] > … > team[n] > personal > engine; np_team_dirs is the single parse/validate/cap point and np_team_dir is its highest-precedence first entry. Phase 2a: recall hooks (lesson/episodic) read across layers via np_content.py (content_layers/merge_mode/merge_roots/layer_roots); mode = team.merge param (override|concatenate|team-only). Phase 2b: wiki_index() merges team+personal wiki overlays. Phase 3: dashboard/build.py learned_counts() and engine/setup/np-mcp-server.py _tool_recall merged — team content layer complete through Phase 3 (metrics remain personal-only by design). | specs/2026-06-09-nervepack-engine-content-architecture-design.md |
| CI PII guard (secret/PII gate) | — (always-on CI job) | publish/np-publish-scan.py (secret/PII scanner; LAN-IP/RFC1918 rule, never loopback) + scan-allowlist.txt; CI job pii-guard in .github/workflows/ci.yml; pre-publish gate publish/np-publish-snapshot.sh (+ publish/PUBLISH.md); tests engine/setup/tests/publish/ | specs/2026-06-09-nervepack-engine-content-architecture-design.md |
| PII filter (context-window and storage-time scrub) | pii_filter (default off) | np-pii-filter.py, np_scrub.py (extended, NP_PII_FILTER=1), episodic_recall.py (extended — shells to np-pii-filter.py via a sys.executable subprocess, pii_filter_fn injectable for tests), lesson_recall.py (extended, same pattern), np_scrub.py (extended, NP_PII_FILTER=1), cli.py setup install-pii-deps | specs/2026-07-06-pii-filter-design.md |
| Open artifact on write (auto-open a spec/plan doc so a human reads it) | focus | engine/nervepack_engine/hooks/open_artifact.py (PostToolUse, matcher Write; dispatched as cli.py hook open-artifact; reuses np_dashboard.resolve_opener()), registered via cli.py setup install-hooks (engine/setup/hooks.manifest) | specs/2026-07-21-open-artifact-on-write-design.md |
| Risk tier registry (what tier is this diff, really) | — (policy data, not a runtime toggle) | engine/setup/risk-tiers.json (ordered glob→tier rules, last match wins like CODEOWNERS), engine/setup/np_risk_tiers.py (resolver), consumed by engine/setup/np-spec-guard.py for both exemption (a diff is exempt when every path is standard tier) and escalation (a spec declaring lower than its paths require fails). The registry classifies ITSELF high, so the policy cannot be rewritten in a spec-free diff | docs/RISK-TIERS.md; change-specs/feat-f7-risk-tiers.md |
| Differential gating by tier (what must be true before a human merges this) | gates (.tier_guard.enforce, declared by F12/#259, still inert — the CI job is advisory and reads no toggle; the key waits for a local pre-check) | engine/setup/np_tier_policy.py (the requirement table as data: which gate verdicts must be PASSED per tier, whether a rollback section is required, whether the adversarial lens must have RUN, and whether the tier may auto-merge at all), engine/setup/np-tier-gate.py (the tier-gate CI job — resolves the diff’s tier through np_risk_tiers, reads every gate-verdict-*.json the other jobs uploaded, writes tier-policy.json). Every PR runs the SAME jobs; the tier decides which verdicts are load-bearing — conditionally skipping a required check strands the PR forever, because GitHub cannot tell skipped from pending. .github/CODEOWNERS is GENERATED from the registry by engine/setup/np_codeowners.py and declares the high-risk paths; it gates nothing (0 required approvals, one owner) | docs/BRANCH-PROTECTION.md; change-specs/feat-f8-tier-gate.md |
| Confidence-gated auto-merge (a standard-tier change merges with no human) | — (policy data: engine/setup/automerge.json, enabled SHIPS FALSE) | engine/setup/np_automerge.py (the decision: four eligibility conditions, plus a kill switch kept SEPARATE from eligibility so the record still says whether a change WOULD have merged), engine/setup/np-automerge-gate.py (the auto-merge CI job). It never merges — it asks GitHub to enable NATIVE auto-merge, a waiting mechanism on the same side of the gate as a human clicking merge. Author comes from github.event.pull_request.user.login, NEVER github.actor (the last identity to ACT on the PR — the documented Dependabot pwn-request). diff-review must have RUN, not passed: its findings become unresolved threads the ruleset already blocks on, so a finding demotes a change from merges-itself to a-human-looks, never to blocked | docs/BRANCH-PROTECTION.md § Auto-merge; change-specs/feat-f9-automerge.md |
| Documentation coupling (did the docs move with the change) | — (policy data: engine/setup/doc-coupling.json, enabled true) | engine/setup/np_doc_coupling.py (two rules: an enumerated TRIGGER list, and DANGLING REFERENCES — a path this diff removed or renamed that a doc it did not touch still names), engine/setup/np-doc-coupling-gate.py (the doc-coupling PR job, advisory, plus doc-coupling-issue which runs only on push-to-main). Advisory permanently, not pending promotion: GitLab’s hard doc gate needed a three-day escape hatch and Danger ships #trivial, so the consequence is an ISSUE opened at merge — closable but not dismissible — rather than a red X. Rule 2 is the load-bearing one: ICPC 2019 (1.3B AST changes, 1,500 systems) found doc drift arrives mostly as a side effect of REFACTORING, which a feature-path rule misses | docs/DOC-COUPLING.md; change-specs/feat-f10-doc-coupling.md |
| Spec-drift gate (block an edit that leaves the change spec’s declared paths) | gates (.drift_guard.enforce, default on — the ONLY one of the three gates.* keys with a consumer today; .spec_guard.enforce and .tier_guard.enforce are declared by F12/#259 and settable, but inert until the local spec-guard pre-check and tier-guard (#254) exist, so flipping them changes nothing) | engine/nervepack_engine/hooks/drift_guard.py (PreToolUse, matchers Write/Edit; dispatched as cli.py hook drift-guard), engine/setup/np_change_spec.py (the blast-radius matcher, shared with the spec-guard CI job so the local and CI gates cannot disagree about the same policy); reads change-specs/<branch-slug>.md in whichever repo the edited file lives in, and is silent in any repo that has no such file | change-specs/feat-f3-drift-guard.md; content overlay skills/np-flow-develop/references/hooks.md §2 |
| Turn-completion gate (block a turn that changed UI without showing it) | turn_gate (.ui block, .diff warn, .form warn, .form_threshold 12, .timeout_s 5) | engine/nervepack_engine/hooks/turn_gate.py (Stop; dispatched as cli.py hook turn-gate), engine/nervepack_engine/np_turn_parse.py (the pure transcript-turn extractor — the ONLY file coupled to Claude Code’s transcript shape); the form check shells to the overlay’s np-ste-lint.py via np_content.content_dir() and is silently skipped when no overlay is configured | content overlay specs/2026-08-12-turn-completion-gate-design.md |
Runtime wiring — what fires what
Lifecycle hooks (registered in ~/.claude/settings.json by cli.py setup install-hooks, from engine/setup/hooks.manifest):
| Event | Scripts (in order) |
|---|---|
SessionStart | cli.py sync & · cli.py hook session-directive · cli.py hook open-dashboard & · cli.py hook backcapture-sweep & · cli.py hook resume-sessionstart & |
UserPromptSubmit | cli.py hook episodic-recall · cli.py hook lesson-recall · cli.py hook struggle-escalation · engine/nervepack_engine/cli.py hook skill-trigger-recall · cli.py hook resume-recall |
PreToolUse | cli.py hook lesson-guard (matchers: Bash, Read, Edit, Write, Skill, mcp__.*) |
PreToolUse | cli.py hook drift-guard (matchers: Write, Edit) — not backgrounded, for the same reason as Stop: a backgrounded hook cannot return a decision |
PostToolUse | cli.py hook open-artifact (matcher: Write) |
Stop | cli.py hook turn-gate (not backgrounded — a backgrounded hook cannot return a decision) |
PreCompact | cli.py hook episodic-capture checkpoint |
SessionEnd | cli.py sync exit & · cli.py hook episodic-capture session-end & · cli.py hook evaluator & · cli.py hook session-flush (promotes both inboxes on exit; crons = backup) — the three & entries are backgrounded so Claude Code’s hook-runner returns before it would otherwise report them “Hook cancelled” (invariant 12); session-flush backgrounds itself internally (subprocess.Popen(start_new_session=True)), not via a settings.json & |
Crons (installed by cli.py setup install-memory-cron / install-memory-launchd / install-memory-schtasks, per-OS):
| When | Job |
|---|---|
| Daily 08:00 | cli.py cron memory-promote (backed by np_agentic_cron.py) |
| Daily 08:30 | cli.py cron episodic-maintain (backed by np_agentic_cron.py) |
| Daily 09:00 | cli.py cron aggregate-metrics (backed by np_aggregate.py) |
| Daily 09:15 | cli.py cron skill-maintain (backed by np_skill_maintain.py) |
| Weekly Sun 09:30 | cli.py cron refine (backed by np_agentic_cron.py; maintain.refine toggle, default on) |
| Weekly Wed 10:00 | cli.py cron compact (backed by np_agentic_cron.py; maintain.compact toggle, default on) |
Every resume.cron_min min (opt-in, resume.cron=off by default) | cli.py resume-write --active --throttle |
Setup numbering: 30 link-skills (+60 index) · 35 link-dashboard-data (content bridge) · 40
sync · lifecycle hooks are no longer numbered .sh files — the 11 50–63 -install-*.sh installers + np-hook-lib.sh were consolidated (phase 13) into engine/setup/hooks.manifest, applied by cli.py setup install-hooks (np_hook.py) · 58 install-mcp · 62 install-scheduled-auth-token · 71–77 cron bodies (the OS scheduler that installs them is cli.py setup install-memory-{cron,launchd,schtasks} — Python, no numbered 70-*.sh) · 90–91
permissions. Scripts are idempotent and run in order on a fresh box. The toolchain
baselines and VSCode extensions (formerly 00/00-brew/10-rustup/20-claude-plugins/
21-prewarm-serena/25-install-pii-deps/80-install-vscode-extensions) and the whole
np-onboard.sh orchestrator are Python now (phase 7): cli.py setup <step> (e.g.
install-apt-baseline, install-claude-plugins, install-vscode-extensions) and
cli.py onboard — no numbered files, so they’re outside this numbering scheme entirely.
The two data pipelines (the heart of the system)
Both follow the same shape: a cheap hook captures → a local
~/.cache/nervepack/ inbox → the on-exit flush (cli.py hook session-flush,
backed by engine/nervepack_engine/hooks/session_flush.py) promotes
it into a committed layer immediately → a reader surfaces it. The daily/weekly
crons are an idempotent backup (empty inbox = no-op).
The reliable capture trigger is SessionStart, not SessionEnd (invariant 12).
Claude Code kills slow SessionEnd claude -p hooks before they finish and /exit
doesn’t fire SessionEnd at all, so the SessionEnd capture/evaluator are
best-effort; the engine/nervepack_engine/hooks/backcapture_sweep.py SessionStart
hook (dispatched via cli.py hook backcapture-sweep) is what actually
back-captures the previous session (from its now-complete on-disk transcript) by
re-running the same capture + evaluator. Same inboxes, same readers. Only the
trigger differs. Both SessionEnd entries are registered with a trailing &
(their engine/setup/hooks.manifest rows, applied by cli.py setup install-hooks) so the hook
process itself returns immediately instead of being killed mid-flight and
surfaced to the user as “Hook cancelled” — this is cosmetic (it doesn’t make the
backgrounded claude -p call any more likely to finish), the sweep is still what
makes capture actually reliable.
EPISODIC MEMORY
SessionEnd ─ cli.py hook episodic-capture ─ claude -p (haiku summary)
└─▶ ~/.cache/nervepack/episodic-inbox/*.jsonl
└─ cli.py hook session-flush (on exit) → 72-maintain ─▶ memory/episodic/<topic>.md (committed)
│ [backup cron: daily 08:30]
└─ read by: cli.py hook episodic-recall (UserPromptSubmit) + np-core-recall (/recall)
PERFORMANCE
SessionEnd ─ cli.py hook evaluator (np-eval-signals.py extracts signals+tokens) ─ claude -p (haiku verdict)
└─▶ ~/.cache/nervepack/evaluator-inbox/*.jsonl
└─ cli.py hook session-flush (on exit) → cli.py cron aggregate-metrics ─▶ dashboard/data/metrics.jsonl (committed)
│ [backup cron: daily 09:00]
└─ build.py ─▶ metrics.js ─▶ dashboard/index.html (windowed to last N)
Record shapes (keep these stable; readers depend on them):
- episodic inbox:
{session_id, ts, project, cwd, mode} + {headline, body, candidate_topics[], keywords[], struggles[]}(session_idlets the evaluator count this session’sstruggles[]cross-pipeline) - metrics:
{session_id, ts, project, signals{skills_invoked[], playbook_fires, playbook_heeded, recall_injections, directive_present, directive_tokens, struggles, tool_calls, tokens{…}}, contribution_score, helped[], shortfalls[], suggestions[], assets_used[]}(field-by-field source + zero-bias notes:docs/FEATURES.md“Performance evaluator + signals”)
Design invariants — the proven choices. Don’t relitigate these silently.
-
Hooks fail open. Every lifecycle hook ends
exit 0and routes each early return through abail()that logs one dated line to a*.login~/.cache/nervepack/. A hook must never break a session, and must never block one by accident. (→ coding-rules §8)Two deliberate exceptions may block, on purpose. Both are bounded the same four ways: toggle-gated, cheap-check-first before any parse or file access, at most one decision per event, and every one of their own error paths still returns "" and allows. Blocking is the feature in both, not a failure mode. Do not read this as license for blocking hooks generally — a third blocking hook needs its own amendment here.
turn_gate(Stop): toggle-gated onturn_gate.ui; checksstop_hook_activebefore any parse, toggle read, or file access; one block per turn.drift_guard(PreToolUse on Write/Edit, F3/#249): toggle-gated ongatesandgates.drift_guard.enforce(off downgrades every deny to a warn); silent wherever it has no jurisdiction — outside a git repo, on a detached HEAD, or in any repo with nochange-specs/<branch-slug>.md, which is every repo but this one; one decision per tool call. It fails closed on a policy violation and open on its own error, and it never widens a blast radius itself — silent widening is the exact failure it exists to prevent. A spec that declares noblast_radiuswarns rather than denying:spec-guardalready fails that branch in CI, and denying every edit in the repo over an authoring error would brick the session.
-
Headless
claude -prules (→np-kb-claude-headless-scripting): prompt via stdin not a trailing positional (variadic--allowedToolseats it);--append-system-promptto stop it continuing the transcript; cap input and extract text first (np-transcript-extract.py); and any hook that callsclaude -pMUST setNERVEPACK_AGENT=1on the call and bail when that marker is set. Headless-pre-fires the lifecycle hooks, so without the guard a SessionEnd hook recurses forever (§7). The runtime no longer callsclaude -pdirectly: it goes throughnp_model.py(the backend-neutral model seam, in-process — the bash wrappernp-llm.shwas retired in phase 19), which setsNERVEPACK_AGENTcentrally and lets a non-Claude host swap the backend. -
Everything is toggle-gated. New runtime behavior checks
np_enabled <feature>and adds a row totoggles.conf(→specs/…feature-toggles). Fail-open: unknown = on. -
Cheap model by default. Summarizers/judges →
claude-haiku-4-5-20251001; agentic crons →claude-sonnet-4-6. Never Opus in automation. (→ CLAUDE.md §“Model selection”) -
Superseded: “Bash for glue, Python for parsing/logic.” The bash→Python CLI cutover is COMPLETE (phases 1–20, 2026-07). The original split (→ CLAUDE.md §“Harness language policy”) kept hot-path hooks and OS-glue scripts bash; the full-cutover roadmap (content overlay specs
2026-07-15-nervepack-python-cli-consolidation-design.md+2026-07-23-...full-cutover-roadmap-design.md) overturned it: the user accepted the ~20–70ms/call latency cost of a full Python port in exchange for one language everywhere, planning to reclaim performance in a later compiled-language phase (Phase B, tracked inROADMAP.md, not designed yet). Every lifecycle hook, cron, resolver, installer, and the model/doctor/sync/dashboard/toggle seams are now Python, dispatched throughengine/nervepack_engine/cli.py. All ofnp-hook-lib.sh,np-toggle-lib.sh,np-content-lib.sh,np-layer-lib.sh,np-doctor.sh,40-sync-nervepack.sh,30-link-skills.sh,60-generate-index.sh,np-mcp-install.sh,open-dashboard.sh,np-dashboard-launch.sh, the 11NN-install-*.shhook installers,episodic-scrub.sh, andnp-llm.shhave been deleted. Seedocs/PYTHON-CUTOVER.mdfor the full cutover guide + verification checklist. The bash that REMAINS is deliberate and enumerated (see PYTHON-CUTOVER.md §“What bash legitimately remains”): (a)np_bashlib.argv()-wrapped shell-outs to git/native tools; (b) the two model-seam paths that run a user-configured command —np_model.agent()’s localNP_LLM_AGENT_CMDandnp_implement_suggestion’sIMPLEMENT_LLMoverride (neither re-implementable); (c) the OS-scheduler interop (np_scheduler_install.pyshells cron/launchctl/schtasks — invariant 16); (d) the served-mode HTTP server + a handful of thin.shtest/CI entrypoints (run-all.sh); (e) thenp_hook.pyWindowsbash -lcwrap. Treat “which language is this in” as a per-script fact you check in the feature catalog above, not an inferable rule — but the default is now Python. -
Every script has a regression test in
engine/setup/tests/(stdlibunittest/ plain bash; stubclaudeviaCLAUDE_BIN). The whole suite runs viaengine/setup/tests/run-all.sh(hermetic, zero third-party deps outsidee2e/); CI runs it as the blockingregressionjob and gatesmainon it. (→ coding-rules §5) Per-script tests are necessary but NOT sufficient — a feature also needs a test of its COMPOSITION. The dashboard was completely dead for seven weeks while every unit test passed: the server test spawned the server directly and proved it served, the hook test proved the hook’s branching, and nothing asserted that the hook actually produces a reachable dashboard. When a feature spans a hook + a spawn + a renderer + a toggle, add one test that exercises the whole chain and asserts the user-visible end state (tests/evaluator/test_dashboard_lifecycle.pyis the worked example), plus a wiring test that every knob is fed/declared/typed end-to-end (test_dashboard_param_wiring.py). Validate any such guard by running it against the pre-fix commit — a regression test that passes before AND after the fix is worthless. -
Skills stay lean (~6 KB soft / 8 KB hard; overflow →
references/), enforced daily by skill-maintain. -
GUI side-effects guard once-per-boot (
engine/nervepack_engine/hooks/open_dashboard.py, dispatched ascli.py hook open-dashboard). SessionStart fires repeatedly and a raw GUI open self-sustains a reconnect loop (§4). -
Commits: conventional prefix (
skill()/setup()/feat()/fix()/docs()/manual()/evaluator()/agent()), authored as the repo’s configured git identity, no LLM-attribution trailer (→ coding-rules §6). Ask before pushing. -
Concurrency-safe sync: fast-forward only, re-check the tip before any destructive git op; never force-push a concurrent agent’s branch.
-
Cache-stable context injection (Manus): the SessionStart directive is a byte-stable prefix (no timestamps/volatile fields, regression-tested). All variable, session-specific context (episodic/lesson recall) is injected later via
UserPromptSubmit, never interleaved into the stable block, so the KV-cache survives. nervepack’s own injection cost is attributed viadirective_tokensin the evaluator signals. -
SessionEnd is unreliable for slow work; SessionStart is the reliable trigger. Claude Code exits without awaiting slow SessionEnd hooks and
/exitdoesn’t fire SessionEnd at all (GH #35892/#41577), so any SessionEnd step that callsclaude -p(capture, evaluator) is best-effort. The guaranteed path is the SessionStartengine/nervepack_engine/hooks/backcapture_sweep.py(dispatched viacli.py hook backcapture-sweep), which back-captures the previous session from its complete on-disk transcript. New per-session capture/scoring work must ride the sweep (or another awaited trigger), never depend on SessionEnd completing. -
Pre-flight gates check the backend, not the
claudebinary. A hook/cron that needs a model must gate on${NP_LLM_BACKEND:-claude}, never on-x "$CLAUDE"alone: require theclaudebinary only on theclaudebackend; thelocalbackend has its own prerequisites (complete→NP_LLM_BASE_URL/MODEL_CHEAPvianp-llm-local.py;agent→NP_LLM_AGENT_CMD). A bare claude-binary check silently disables the whole pipeline on a non-Claude host even though the backend works. This is the #4b finding behind the five backend-aware gates (capture, evaluator,71/72/75). (→ invariant 2; the seam already owns backend dispatch, don’t re-scatterclaude -p) -
Markdown is the internal representation; HTML is only for human render. Everything the model ingests (skills, wiki, sources, injected context) stays Markdown. Measured on this corpus (2026-06-17), the same content as HTML costs ~26% more tokens on a clean render (real-world HTML 2–10×; published HTML→MD conversions report ~67–87% token savings), while Markdown is only ~5% over bare plain text (structure for nearly free, and the format LLMs are trained most on). HTML earns its place only for human-facing rendered pages (e.g. the dashboard’s “open the source” tab, where the model never reads it) or merged-cell tables (
colspan/rowspan, which nervepack content doesn’t use). So: author/store Markdown, render → HTML at build time for humans, never feed HTML to the model. (Resolves the highest-priority “HTML vs Markdown efficiency” roadmap item.) -
mainbranch protection keepsenforce_admins: false. The auto-commit crons (np-flow-episodic-maintain,weekly-compact,scheduled-refine, the metrics aggregator, andnp_implement_suggestion.pydirect mode) push directly tomainas the repo owner. Classic branch protection requires a PR + the three blocking CI checks + linear history for contributors, but admins — and therefore those crons — bypass it viaenforce_admins: false. Enabling admin enforcement, or requiring ≥1 approval, silently breaks every auto-push cron. The required status-check contexts are the CI jobname:s verbatim (Syntax sweep (stdlib-only)/Regression suite (zero-dep)/Secret/PII guard (terminal gate)/Windows suite (Git-bash)— green end-to-end under Git-bash onwindows-latest— andBash-free MCP suite (no Git-bash), the git-for-windows-free MCP gate:windows-latestwith the Git-bash dirs stripped fromPATH, running the ported MCP surface);dashboard-e2estays informational and must never be a required check. (→ invariant 10; CLAUDE.md/AGENTS.md §concurrency) -
OS/host backends are portable-shell-shelling-to-the-native-tool, never a native-shell script. The three scheduler backends now all shell to the OS scheduler from Python (
np_scheduler_install.py, phase 6 of the bash->Python migration): cron (Linux),launchctl(macOS,install_launchd),schtasks.exe(native Windows,install_schtasks, run under Git-bash). A Windows-native.ps1was rejected: invariant 6 requires every script to have a regression test in the zero-dep Ubuntu CI suite, and PowerShell isn’t on those runners — a.ps1ships untested. The Python installer is stub-testable on Linux (NP_*_FORCE+ an injectableuname_fn/schtasks_fn/launchctl_fn— the same seam pattern every other ported hook/cron uses — and Layer-1 Windows already requires Git-bash to run the scheduled task’s target command, so bash availability is a given. Same reasoning for the hook shim: rather than a.cmd/PowerShell entrypoint,np_hook.py(install-hooks) wraps the stored commandbash -lc '<cmd>'on a MINGW/MSYS kernel (NP_HOOK_WRAP) so PowerShell-dispatched hooks resolve to Git-bash, leaving Linux/macOS byte-for-byte unchanged. (→ invariant 6; the bash-vs-Python language policy in AGENTS.md — native-shell scripts are the one form that can’t be CI-tested, so they’re out for cross-platform glue.)
Change-impact map — touch X, then check Y
| If you change… | Also check / update |
|---|---|
content layer dir names (memory/{episodic,lessons}, wiki/{topics,concepts}/<x>/) | np_layer_dir/np_layer_roots (the single bash resolver) and its Python mirror np_content.merge_roots() (np_content.py, the same /memory/<layer> subpath — change it in both places, not at each consumer); the recall + guard hooks (episodic_recall.py, lesson_recall.py, lesson_guard.py — all Python ports via cli.py dispatcher), np_aggregate.py (dispatched via cli.py cron aggregate-metrics), dashboard/build.py (wiki_index, learned_counts), np-mcp-server.py _tool_recall; the maintain-agent write path (agents/np-flow-episodic-maintain.md); the example-layout fixture + tests/content/test_example_layout.sh (the anti-drift contract) |
engine/setup/risk-tiers.json (the tier policy) | it is a high-tier file by its own rules, so a change needs a spec with tier: high and a rollback. Order matters: last match wins, so a standard glob appended below a high one silently downgrades everything it matches — test_risk_tiers.py asserts no standard rule follows a high one, and that the file still classifies itself, np_risk_tiers.py, and np-spec-guard.py as high. Keep the “synthesis, not a citation” sentence: a test asserts it, because no published standard enumerates high-risk paths and the list reads as authoritative once it is in a file. Consumers to check: np-spec-guard.py today, #254 (differential gating) and #255 (auto-merge) next |
engine/setup/np_tier_policy.py (what each tier requires) | it is high by its own registry rule, so lowering a threshold needs a spec with tier: high, a rollback plan, and the adversarial lens. test_tier_policy.py asserts the table, that the tiers stay monotonic (no tier requires LESS than the one below it), and that an unrecognized tier gets the strictest policy rather than reading as permission. It states the tier names as a literal instead of importing them, so a tier added to risk-tiers.json and not here would silently take the unknown-tier branch — a test asserts the two lists agree. Consumer to check: #255 reads auto_merge_eligible and acts on it |
engine/setup/doc-coupling.json (the doc triggers) | it is high by its own registry rule. Adding a trigger is cheap; the risk runs the other way — a list that fires on everything teaches people to ignore the check, and the mechanism works only while its output is believed. Add one when a real drift gets through, not in anticipation. change-specs/** counts as documentation, so a change spec satisfies a trigger on its own |
engine/setup/automerge.json (who and what may merge unattended) | it is high by its own registry rule. Widening allowed_tiers or trusted_authors needs a spec with tier: high, a rollback plan and the adversarial lens — that cost is the point, since this file is the cheapest imaginable path around every other gate. enabled is the kill switch: flipping it false stops auto-merge with no other change, and gh api -X PATCH repos/<repo> -f allow_auto_merge=false stops it without merging anything at all |
| the ledger’s contents or the merge flow that feeds it | np-ledger-append.py --backfill, which is now how an AUTO-merged change gets its line: CI cannot write dashboard/data/ at all (private overlay, no cross-repo credential, deliberately), so the durable record catches up locally. Idempotence rests entirely on existing_change_ids keying off change_id, so anything that changes how a change_id is derived from a branch name will re-append every historical entry |
a required CI check’s name: (the display string, not the job id) | the required_status_checks list in .github/branch-protection/ruleset-main.json, and then the LIVE ruleset — GitHub matches a required context by display name, so a rename makes the old context stop reporting and the PR waits for it forever. Nothing in CI applies that file: a workflow able to rewrite its own required checks would be a gate that can disable itself. docs/BRANCH-PROTECTION.md carries the apply and rollback commands |
engine/setup/np_change_spec.py (the blast-radius matcher) | BOTH gates that read it — engine/setup/np-spec-guard.py (the spec-guard CI job) and engine/nervepack_engine/hooks/drift_guard.py (the PreToolUse gate). Sharing one matcher is the whole point: a second copy lets a branch pass locally and fail CI on the radius alone. fnmatch’s * already crosses /, so every committed blast_radius assumes wildcards reach arbitrary depth — changing that reinterprets specs already merged. Its tests: engine/setup/tests/nervepack_engine/test_np_change_spec.py (incl. the identity assertion that spec-guard has not re-inlined a copy) and test_drift_guard.py |
engine/setup/hooks.manifest’s {NP_DIR} token | np_hook.substitute_root and main_worktree_root. Rows carry {NP_DIR}, never a literal install path: before #257 all 26 carried ~/Code/nervepack, so a clone anywhere else registered hooks pointing at a directory that does not exist — and hooks FAIL OPEN, so the whole lifecycle would silently do nothing. Substitution happens at manifest-READ time so every consumer sees the string that lands in settings.json. Registering from a linked worktree resolves to the MAIN checkout, or a git worktree remove would point every hook on the machine at a deleted directory |
| any lifecycle hook | its row in engine/setup/hooks.manifest (event|matcher|command, in registration order — the driver cli.py setup install-hooks / np_hook.py applies them; keep session-flush LAST in SessionEnd), fail-open + bail(), the NERVEPACK_AGENT guard (all globally-registered hooks must carry it — not only those that call claude -p), and engine/setup/tests/ (tests/setup/test_install_hooks.py for the driver, tests/nervepack_engine/test_np_hook.py for register/purge). On native Windows np_hook.py wraps the stored command as bash -lc '<cmd>' (NP_HOOK_WRAP, auto on a MINGW/MSYS kernel) so PowerShell-dispatched hooks resolve to Git-bash — keep hook commands single-quote-free. register’s dedup keys on (matcher, base) — where base is the cli.py <group> <name> tail for a CLI hook, else the first *.sh/*.py filename. That per-matcher key is what lets lesson-guard’s six matchers (Bash/Read/Edit/Write/Skill/mcp__.*) coexist; the empty-matcher bucket reproduces the old np_register_hook. It still can’t recognize a full command-shape migration (a bash script retired for cli.py hook <name>) as “the same hook”, so a bash→cli.py change adds the new entry beside the stale one. That is handled by hardcoded one-off purge() calls in np_hook.install_hooks (the 53 legacy list: playbook-guard.sh/lesson-guard.sh on PreToolUse, playbook-recall.sh/strategy-recall.sh/lesson-recall.sh on UserPromptSubmit); a future such migration needs a new purge entry, not a manual settings.json pass. ~/.config/nervepack/adapter.json’s host-recorded doctor verify greps go stale the same way and independently — its strings are substring matches against the registered command’s basename, so they stop matching post-migration and need a manual one-line fix |
episodic_capture.py (or its prompt/schema, or the np_capture.capture() it dispatches to) | episodic-recall, episodic-match, np_scrub, dedup fingerprint, np-flow-episodic-maintain (consumes the inbox shape, incl. struggles[]→lessons provenance: failure and strategies[]→lessons provenance: success), np-transcript-extract.py, np-mcp-server.py _tool_capture (calls np_capture.capture() unconditionally, in-process) |
| the lessons layer (recall, enforcement, or distillation) | the capture schema (struggles[]/strategies[]), np-flow-episodic-maintain §5b/5c (writes memory/lessons/, tags provenance, adds enforce only when warranted), lesson_recall.py/lesson_guard.py (Python ports via cli.py dispatcher) + their registration (the PreToolUse Bash/Read/Edit/Write/Skill/mcp__.* + UserPromptSubmit rows in engine/setup/hooks.manifest, applied by cli.py setup install-hooks — plus the 53 legacy purge() list in np_hook.install_hooks), lesson_guard.py’s tool_name_match re.fullmatch (an alternation targets a family of MCP tool names in one lesson — see its module docstring), the lessons/lessons.enforce toggle, the dashboard “learned” counts (build.py, split by provenance), and the graduation detector (np_graduation_detect.py reads a lesson’s seen/status + byte size — keep those stable; skills.graduate_seen/graduate_kb params; np_skill_maintain.py wiring + tests/skills/test_graduation_detect.py) |
evaluator.py (or the np_evaluator.evaluate() it dispatches to) / the metrics record shape | np-eval-signals.py, np_aggregate.py (dispatched via cli.py cron aggregate-metrics), dashboard/build.py, the panels in dashboard/index.html, sample-metrics.jsonl, the dashboard test, np-mcp-server.py _tool_evaluate (calls np_evaluator.evaluate() unconditionally, in-process) |
| add a model call anywhere | use the model seam np_model.py (np_model.complete()/agent() in-process, or python3 np_model.py; don’t hardcode claude -p); it sets NERVEPACK_AGENT=1 — still guard the calling hook (invariant 2). The seam raises np_model.AuthError when the backend reports an auth failure (the CLI prints it to stdout and exits 0, so it is invisible to a returncode check): a caller that fails open on broad Exception MUST re-raise it, or it reads as a benign empty result (#201, #211) |
shell out to bash from Python (any .py that runs a .sh or bash -c) | route the argv through engine/setup/np_bashlib.py argv() (runtime) or engine/setup/tests/_lib/nptest.py sh/u/bash_eval (tests). On Windows a bare bash resolves to C:\Windows\System32\bash.exe (WSL, no distro) not Git-bash, and backslash/.sh paths can’t be opened — both helpers fix this and are no-ops off Windows. NP_BASH (exported by run-all.sh and engine/bin/nervepack-mcp) pins the interpreter; the suite runs green on windows-latest (required CI gate). Current callers: np-mcp-server.py, np-dashboard-server.py, dashboard/build.py (np-eval-signals.py no longer shells to bash — phase 12 routed its toggle read through np_toggle in-process). (→ invariant 16; np-kb-testing-ci §8) |
spawn a subprocess with timeout=, or check whether a PID is alive, from Python (Windows) | Four distinct, hard-won Windows-only gotchas, all found chasing one real multi-cycle Windows CI failure in np_implement_suggestion.py (2026-07-22): (1) subprocess.run(..., timeout=N) is NOT process-tree-safe on Windows — CPython’s own subprocess.run source shows that on a timeout it calls process.kill() (kills the DIRECT child only) then, on the mswindows branch, process.communicate() again with no timeout to drain captured pipes; a surviving grandchild (anything the killed process spawned) that still holds the stdout/stderr pipe’s write end open makes that second call hang forever. Fix: engine/setup/np_bashlib.py’s run_killtree() — spawns the child in its own process group (CREATE_NEW_PROCESS_GROUP on Windows, a new session on POSIX) and kills the WHOLE tree via taskkill /F /T (Windows) / killpg (POSIX) before any further blocking read; same call contract as subprocess.run (raises TimeoutExpired, returns a CompletedProcess), so callers don’t change. Route every capture_output=True/stdout=PIPE + timeout= subprocess call through it — a call that routes output to DEVNULL instead of a pipe was never at risk (no pipe to drain), which is why patching only one hang site previously just relocated the hang rather than fixing it. (2) Text-mode Popen/subprocess.run defaults to the host locale’s ANSI codepage on Windows (e.g. cp1252), not UTF-8 — any non-Latin-1 character written to a child’s input= stdin (or read from its stdout/stderr) raises UnicodeEncodeError/DecodeError; run_killtree() forces encoding="utf-8" whenever text=True. (3) os.kill(pid, 0) is NOT a safe, side-effect-free existence probe on Windows the way POSIX signal 0 is — per the os.kill docs, Windows calls TerminateProcess for any signal value other than CTRL_C_EVENT/CTRL_BREAK_EVENT, so checking liveness this way can actually kill the process being checked. Fix: np_implement_suggestion.py’s _pid_alive_windows() — a read-only check via ctypes (OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION, no TERMINATE right, plus GetExitCodeProcess). (4) Any ctypes.windll call returning a pointer-sized value (a HANDLE, e.g. OpenProcess’s return) MUST set explicit argtypes/restype (via ctypes.wintypes) — ctypes’ default assumed return type is a 32-bit signed c_int, and a genuinely valid 64-bit handle can be silently truncated/misinterpreted through that default, making a live process look dead with no exception raised (this exact bug shipped once before the fix landed). Separately, when a PID crosses from a Git-bash/MSYS process into native Windows Python (e.g. bash’s own $$, passed to a Python child as a lock-file owner), it is NOT guaranteed to be a real, OpenProcess-resolvable native Windows PID — MSYS/Cygwin maintain their own PID numbering, and /proc/<pid>/winpid is their documented translation to the real native Windows PID when the two differ. engine/setup/tests/evaluator/test_pid_alive_diag.sh is the isolated diagnostic (self-pid check, live-parent-pid check via both $$ and /proc/$$/winpid, raw OpenProcess/GetLastError evidence, dead-pid check) written specifically because three blind production-code fix attempts produced zero change in outcome before this diagnostic revealed the real evidence (GetLastError=87/ERROR_INVALID_PARAMETER) in one CI cycle — write one of these BEFORE a second fix attempt on any Windows-only failure with no local repro. Final resolution (same chase, one cycle later): _pid_alive_windows() itself was never broken — confirmed by extending the same diagnostic to check a real backgrounded python3 process’s own os.getpid() (the actual production shape _acquire_lock() writes), which resolved correctly via OpenProcess on real Windows CI. The bug was in test_implement.sh scenario 5/5b themselves: they simulated “a live/dead external pid” using bash’s own $! (a backgrounded sleep), which is not reliably the native Windows pid OpenProcess needs — bash/MSYS and Windows draw pids from different pools, so a bash-tracked pid can coincidentally resolve correctly in one CI run (as it did in the isolated diagnostic) and fail to resolve in the next (as it did in the exact same scenario, same mechanism, same run cycle) with zero code change in between. Rule: never use a bash-tracked pid ($$/$!) to simulate “a live/dead external process” in a Windows-touching test — spawn a real python3 helper and use its own self-reported os.getpid() instead, since that is the only pid shape production itself ever hands to _pid_alive(). |
np_toggle.py / np_content.py / episodic-match.sh / np_doctor.py / np_sync.py (resolvers + recall + doctor + sync; the three sourced bash resolver libs were retired in phase 18, so the Python modules are the sole implementations) | engine/nervepack_engine/np_toggle.py (enabled/param + the write/status surface scope/features/set_local/is_local_set/status_lines), np_content.py (content_dir/origin/is_explicit/team_dir/content_layers/merge_mode/merge_roots), np_episodic_match.py (match), and np_doctor.py (report — all 15 capabilities, core + adapter; phase 15 made it the sole doctor and retired np-doctor.sh), np_sync.py (sync — the defensive engine fast-forward), np_model.py (complete/agent — the sole model seam for the ported capture/evaluate + the agentic crons; the bash wrapper np-llm.sh was retired in phase 19, its argv/env contract now held host-agnostically by tests/llm/test_np_model_contract.py), np_scrub.py (scrub — the byte-exact secret-redaction port of episodic-scrub.sh, used by the ported capture before the inbox write), np_capture.py (capture — the episodic-capture pipeline: gate → transcript-extract → np_model → json-extract → np_scrub → inbox note, building the record to match jq -nc), and np_evaluator.py (evaluate — the evaluator pipeline: signals → transcript-extract → np_model → json-extract → cost-aware suggestion → np_scrub → inbox record) must stay equivalent — the long-running MCP server resolves toggles+content, matches recall, reads the toggle status table, writes all toggle changes (local, shared-toggles.conf+commit, and managed allowlist — phase 14 ported the write surface into np_toggle.py), and runs the engine sync in-process via these (no bash subprocess per request; phase 17 folded the sync’s team-layer ff + skill relink + hook-reinstall + 5x-installer sweep into np_sync.py, so there is no remaining sync hybrid. Phase 12 removed the bash fallback for the fully-ported resolvers — toggle read (np_enabled/np_param), content/merge (content_dir/merge_roots/merge_mode), and recall matching (episodic-match) now run in-process unconditionally, on every host, with no NP_MCP_PURE_PYTHON branch; the two standalone free-win callers np-eval-signals.py (directive) and np-dashboard-server.py (implement_mode) were repointed at np_toggle the same way). Sync is no longer a hybrid (phase 17): _tool_sync calls np_sync.sync() unconditionally, in-process — the full defensive sync (5 engine cases + team-layer ff + on-ff skill relink / hook-reinstall / 5x-installer sweep) runs natively, no bash preference. 40-sync-nervepack.sh was retired; it was the LAST MCP hybrid, so no MCP tool is a bash hybrid any more. The doctor is no longer a hybrid — phase 15 made np_doctor.py the sole implementation, running all 15 capabilities (core + adapter, incl. llm-cli) in-process, so _tool_doctor calls np_doctor.report() unconditionally and np-doctor.sh was retired. Capture and evaluate are no longer hybrids — episodic-capture.sh/np-evaluator.sh (the bash originals) are retired, so _tool_capture/_tool_evaluate call np_capture.capture()/np_evaluator.evaluate() unconditionally, in-process, with no bash preference at all. A/B parity is enforced by engine/setup/tests/mcp/parity/test_episodic_match_parity.sh (byte-identical stdout/files across footgun tables; matcher: header/separator skip, scoring, sort -rn tie-break, hyphenated keywords; the model parity tests — test_{model,agent}_parity.sh — were retired in phase 19 with np-llm.sh, the model seam’s argv/env contract now held in-process by tests/llm/test_np_model_contract.py + tests/llm/test_np_llm_local.py; the doctor parity test was retired in phase 15 with np-doctor.sh, covered by tests/onboard/test_np_doctor.py; the toggle-write parity test in phase 14 with nervepack-toggle.sh, covered by tests/toggles/test_{cli,menu,audit,allowlist}.py; the toggle- and content-resolver parity tests in phase 18 when the three sourced bash libs were deleted — their resolver/merge edge cases are now held by the in-process tests/toggles/test_np_toggle_resolver.py + tests/content/test_layer_resolver.py; the sync and scrub parity tests in phase 17 with their bash originals — np_sync.py is now covered hermetically by tests/sync/test_np_sync.py (5 engine cases + gate/throttle/dry-run/not-a-git) + tests/sync/test_team_sync*.sh + tests/sync/test_sync_reinstalls_hooks.sh, and np_scrub.py by tests/episodic/test_scrub{,_failure}.sh) — the capture/evaluator parity tests were likewise deleted along with the bash originals they compared against, since there’s no longer a bash side to diff. Change one side → the parity test goes red until the other matches. The git-for-windows-free MCP milestone is complete (overlay specs/2026-06-30-git-for-windows-free-mcp-design.md): every MCP tool runs bash-free except flush/maintain (agent-mode crons, deferred — they refuse cleanly on a bash-free host), and engine/bin/nervepack-mcp.cmd spawns the server without bash. The NP_MCP_PURE_PYTHON=0 escape hatch back to the bash originals no longer applies to any MCP tool — every tool (resolvers, toggle writes, doctor, capture/evaluate, and sync as of phase 17) runs its Python implementation unconditionally; the flag is now effectively inert for the tool handlers. Bash stays the source of truth for the hot-path hooks/crons. Bash-free proof: engine/setup/tests/mcp/test_bashfree.py + the windows-bashfree CI lane (keeps native git, strips only the Git-bash dirs). |
np_model.py (the model seam; bash wrapper np-llm.sh retired phase 19) | every runtime caller (capture, evaluator, memory-promote/episodic-maintain/skill-maintain); BOTH backend branches (claude, and local → engine/setup/np-llm-local.py for any OpenAI-compatible endpoint via NP_LLM_BASE_URL/_API_KEY/_MODEL_CHEAP); the complete/agent contract (agent on local needs NP_LLM_AGENT_CMD) held by tests/llm/test_np_model_contract.py + tests/llm/test_np_llm_local.py; engine/setup/tests/llm/ |
engine/onboard/capabilities.json (the contract) | np_doctor.py / cli.py doctor (reads it), ONBOARD.md, the np-core-onboard skill, and host adapter.json manifests |
engine/nervepack_engine/hooks/session_flush.py (on-exit promotion; dispatched as cli.py hook session-flush) | the NERVEPACK_AGENT re-entry guard (enforced generically at cli.py’s dispatch layer — the maintain substep calls claude -p; without it SessionEnd recurses), that it stays LAST in SessionEnd (after capture+evaluator write the inboxes), that the crons remain idempotent backups, and its single cross-platform detach path (subprocess.Popen(start_new_session=True), replacing the bash original’s Linux-setsid-vs-macOS-nohup+disown branch — proven equivalent on both), and the memory.flush_interval throttle + single-flush lock (SessionEnd fires far more often than there is work to drain; unthrottled and unlocked it ran ~640x/day with up to 3 concurrent writers on one shared git tree). The throttle is checked in the PARENT only — the detached child re-enters run() and would otherwise be blocked by the stamp its own parent just wrote |
engine/nervepack_engine/hooks/backcapture_sweep.py (the reliable capture trigger) | it reuses the ported episodic-capture + np-evaluator paths (keep their stdin payload contract {session_id,transcript_path,cwd} stable — Phase B reconstructs it from the queue file, not from a fresh find); its hooks.manifest registration (SessionStart, backgrounded with the >/dev/null 2>&1 & redirect); the memory.backcapture toggle + backcapture_days (max discovery window)/backcapture_max (per-sweep processing cap) params; the two-phase design — Phase A discovery/enqueue into BACKCAPTURE_QUEUE_DIR (one-way ratchet, survives the item’s mtime aging past backcapture_days) vs Phase B processing oldest-enqueued-first out of the queue; dedup vs metrics.jsonl sids + the per-sid claim marker (BACKCAPTURE_SEEN_DIR); invariant 12; its test (engine/setup/tests/nervepack_engine/test_backcapture_sweep.py, incl. the oldest-first-ordering and tracked-past-window cases) |
np-mcp-server.py (the dispatcher) | the in-process module contracts + stdin/arg contracts of every wrapped tool (np_doctor.py, episodic-match.sh, np_toggle.py — toggle status/flip/param/managed, dashboard/build.py, and the Phase-6 scripts); the mcp/mcp.writes/mcp.contribute toggles; the protocol method allowlist; engine/setup/tests/mcp/ |
| a wrapped script’s CLI/stdin contract | np-mcp-server.py is now a second caller alongside the hooks — update its call site |
np_mcp_install.py (cli.py setup mcp-install, the guided installer; phase 17 port of np-mcp-install.sh) | it writes ~/.config/nervepack/{content-dir,team-dir} (must match np_content.py’s resolver paths — $HOME/.config, not XDG), calls 58-install-mcp.sh for registration and the in-process np_doctor.report() for verification, and must never flip the shared team toggle (that commits to the engine repo — the overlay is on by default); keep the non-interactive default-on-empty-stdin behavior; engine/bin/nervepack-install is the one-line wrapper; engine/setup/tests/onboard/test_mcp_install.sh; engine/onboard/MCP.md documents it |
np_content.py / content-dir resolution | every content-dir consumer (recall/guard hooks, np_link_skills.py, np_generate_index.py, np_aggregate.py, np-backcapture-sweep, np-mcp-server.py, np_suggestion_resolve.py (resolved-suggestions ledger default), build.py (_content_dir() mirrors the resolver — used for memory/lessons/ AND the resolved-suggestions default via default_resolved(); NP_LESSONS_DIR/NP_RESOLVED_SUGGESTIONS still override), np-doctor content check); the backward-compat default (unset → $NP); np_content_dir’s stdout MUST stay byte-identical — the explicit-vs-implicit signal lives in the sibling np_content_dir_origin/np_content_is_explicit (issue #12), which the personal-content writers (memory-promote/episodic-maintain/skill-maintain, np_aggregate.py) gate their commit on (skip on implicit fallback, fail-open) and the doctor warns on; engine/setup/tests/content/ (incl. test_writer_implicit_fallback.sh). np_team_dir/np_team_dir_origin consumers: np_link_skills.py, np_generate_index.py, np-doctor, np_sync.py. np_team_dirs (the comma-list resolver; ≤4 cap, first=highest) is the new single parse point — np_team_dir returns its first line; consumers np_link_skills.py / np_generate_index.py / np_sync.py / doctor now iterate np_team_dirs. |
np_content.py layer stack / team.merge param | lesson_recall.py and episodic_recall.py (both Python ports via cli.py dispatcher — call the in-process np_content.merge_roots()/np_content.merge_mode(), the sole layer-stack resolver since the sourced bash layer lib was retired in phase 18); np_doctor.py (reports the resolved mode); dashboard/build.py wiki_index() (live consumer — calls np_content.merge_roots()/merge_mode(); fail-open to personal-only); dashboard/build.py learned_counts() (Phase 3 — unions memory/lessons/ across team+personal overlays per team.merge, split by provenance; metrics stay personal-only); engine/setup/np-mcp-server.py _tool_recall (Phase 3 — merges episodic/lesson recall across layers per team.merge); engine/nervepack_engine/hooks/session_directive.py (feeds each merge root’s directive-routing.md fragment, team>personal, so a team overlay’s domain-skill routing reaches sessions — fail-open, byte-stable; tests tests/nervepack_engine/test_session_directive.py::test_5_team_on_both_fragments_team_before_personal/test_6_team_off_personal_only). np_content_layers/np_merge_roots now span all configured team roots, not just the first (team-only mode keeps every team root). |
publish/np-publish-scan.py (the PII guard) | scan-allowlist.txt (vetted fake-token FPs only — never real PII) and the scanner’s own SKIP_FILES (its source + its tests, incl. test_snapshot.sh, + the allowlist hold detection patterns by design — a new test that plants a fake secret MUST be added to SKIP_FILES); publish/np-publish-snapshot.sh is now a second consumer (it runs the scanner over a history-free export); the pii-guard CI job in .github/workflows/ci.yml; engine/setup/tests/publish/{test_scan.py,test_no_engine_pii.py,test_snapshot.sh} (the second asserts the engine tree scans clean) |
publish/np-publish-snapshot.sh (the pre-publish gate) | the scanner it calls (np-publish-scan.py), publish/PUBLISH.md (the runbook documenting it), and engine/setup/tests/publish/test_snapshot.sh. It NEVER pushes — the public gh repo create --public stays a manual, human-gated step (ARCHITECTURE has no auto-publish path) |
maintenance-agent commit identity (agents/np-flow-*.md) | uses the runner’s git config, else NP_GIT_AUTHOR_*, else a neutral bot (never hardcode a person); engine/setup/tests/publish/test_no_engine_pii.py; the onboard env doc. pat-browne/the canonical repo URL are KEPT project identity (not PII). |
engine/setup/tests/run-all.sh / the test harness | _lib/harness.sh + _lib/report.sh (hermetic-env + report helpers); the meta/test_run_all.sh meta-test (tests the runner itself); the regression CI job in .github/workflows/ci.yml (blocking, gates main); and engine/setup/tests/README.md. Note: e2e/ stays excluded from the default run; use --with-e2e explicitly. |
engine/setup/tests/e2e/ (Playwright dashboard suite) | requirements.txt (pinned deps — update when Playwright version changes); harness.py’s server env contract (NP_IMPLEMENT, NP_METRICS, NP_RESOLVED_SUGGESTIONS, NP_IMPLEMENT_STATUS_DIR, NP_DASH_PORT); and the dashboard-e2e CI job (informational, continue-on-error: true — never a merge gate). This is the ONLY suite with a third-party dependency; keep it isolated in e2e/ so the rest of the suite stays zero-dep. |
main branch protection (rules / required checks) | keep enforce_admins: false so the auto-commit crons (7x, np-implement-suggestion direct mode) keep pushing directly; the required status-check contexts must match the CI job name:s exactly (Syntax sweep (stdlib-only) / Regression suite (zero-dep) / Secret/PII guard (terminal gate) / Windows suite (Git-bash) / Bash-free MCP suite (no Git-bash)); dashboard-e2e stays informational and MUST NOT be required; invariant 15 |
toggles.conf (add/rename a feature or param) | every np_enabled/np_param caller, cli.py toggle (np_toggle.py/np_toggle_audit.py), and the feature catalog above; note: adding a param with a default that prunes historic data (e.g. evaluator.retain_days) can cause existing test records to be pruned — tests with old timestamps must set NP_TOGGLES_CONF to control retain_days; a param intended to be dashboard-editable also needs an entry in engine/setup/toggle-schema.json (absent → the dashboard panel renders it read-only, never guesses) |
nervepack-session-directive.md | this injects into every session globally — high blast radius; keep it lean |
a cron body (7x) | its schedule entry in ALL THREE scheduler backends — np_scheduler_install.py’s install_cron (Linux crontab), install_launchd (macOS LaunchAgents), install_schtasks (native-Windows Task Scheduler, runs under Git-bash); remember its claude -p fires SessionEnd hooks (set the guard); 76/77 also need their maintain.refine/maintain.compact toggle rows in toggles.conf |
np_token_lib.py/np-token-lib.sh (the scheduled-auth token seam) | np_scheduler_install.py’s install_cron + install_launchd (each job’s generated command must stay prefixed with np_token_lib.claude_token_env_prefix); np_model.py’s _base_env() (the non-cron path: it falls back to the same token file for the claude backend when CLAUDE_CODE_OAUTH_TOKEN isn’t already exported, so an expired CLI login doesn’t break doctor/capture/evaluator/recall while scheduled-auth-token reports PASS); on launchd, the prefix contains && so the plist-writer’s _xml_escape() (in np_scheduler_install.py) must still run on it or the generated plist is malformed XML; np_doctor.py’s scheduled-auth-token core check + its engine/onboard/capabilities.json entry; 62-install-scheduled-auth-token.sh (the interactive walkthrough, still bash — _store/_status aren’t ported); its tests (engine/setup/tests/token/, engine/setup/tests/nervepack_engine/test_np_scheduler_install.py). install_schtasks is intentionally NOT wired (unverified Windows quote-nesting risk — see the feature catalog row) — don’t wire it without testing on a real Git-bash host, and remove this caveat once it is |
dashboard/ data shape or build | build.py, index.html, the build test, and the committed metrics.js (rebuild from real metrics.jsonl). The build emits window.METRICS/LEARNED/TOKENS_SAVED/WIKI/GRADUATION/BACKLOG into one metrics.js; BACKLOG (backlog_metrics()) is the one field NOT derived from metrics.jsonl or committed content — it reads np-backcapture-sweep.sh’s local-cache queue/seen dirs live at build time, so it’s only meaningful on the machine that actually runs the sweep (a fresh checkout/CI renders its fail-open zeros, same as a missing graduation-candidates.json); window.WIKI = {topics[], concepts[]} — the wiki index (wiki_index(), evaluator.wiki_nav / WIKI_NAV env, sourced from overlay wiki/topics/ + wiki/concepts/) is content data — keep it out of the engine repo, and pass WIKI_NAV from np_aggregate.py + np_dashboard.open_manual(); new render step (md_to_html/render_pages) writes data/wiki/{topics,concepts}/*.html — keep escaping + href-sanitization (see render tests) |
np_link_dashboard_data.py (cli.py setup link-dashboard-data) (dashboard data bridge) | the dashboard-data capability in engine/onboard/capabilities.json; the np_doctor.py dashboard-data core check; engine/setup/tests/setup/test_np_link_dashboard_data.py; and the Dashboard row of this feature catalog. The symlink <engine>/dashboard/data -> <content>/dashboard/data is the bridge index.html relies on; don’t remove it. |
the suggestions-review engine (np-suggestions-review.py) | it imports dashboard/build.py (_norm/load_resolved/load_records) — keep those stable; the server (np-dashboard-server.py) and the np-core-suggestions-review skill both shell out to it; its test |
np-dashboard-server.py (the opt-in daemon) | keep it 127.0.0.1-only + path-sanitized + fixed route allowlist; np_dashboard.py (dashboard_url()) starts it; the evaluator.dashboard_serve/_port/suggestions_top params; index.html’s http-only buttons; its test |
suggestion implement (np_implement_suggestion.py / /api/implement) | the detached-spawn route stays under the CSRF guard; keep the job’s lock + worktree isolation (the agent runs in a throwaway git worktree off the committed base — a dirty main tree no longer blocks implement and the agent’s commit can’t sweep the user’s WIP; direct mode advances local base only when clean) + NOT_IMPLEMENTABLE/no-commit handling; the job MUST commit its resolution artifacts (resolved-suggestions.txt+metrics.js) so it leaves the tree clean (else the next implement refuses “dirty”); it writes a per-suggestion status file (implement-status/<hash>.json) that the dashboard polls via /api/implement-status; untrusted text is nonce-delimited (prompt-injection); agents/np-flow-implement-suggestion.md (must NOT push/PR — the wrapper owns the remote); evaluator.implement/implement_mode params; the Implement/Reject + Mode buttons + status polling in index.html; both tests (test_implement.sh, server test) |
the evaluator toggle params (dashboard_serve/dashboard_port/suggestions_top) | engine/nervepack_engine/np_dashboard.py (dashboard_url() reads all three; consumed by engine/nervepack_engine/hooks/open_dashboard.py and np_dashboard.open_manual()), np-dashboard-server.py, and the feature catalog above |
np_instruction_block.py (cli.py instruction-block) (managed instruction-file block) | its test (tests/onboard/); the knowledge capability hint in capabilities.json; ONBOARD.md recipe; never run by default on a host that already injects the directive via a session-start hook (double-injection) |
toggle-schema.json (dashboard param types + help text) | np_toggle_schema.py (the loader/validator — keep load()/validate() stable), np-dashboard-server.py’s /api/toggles//api/toggle (consumers — bare-feature entries are description-only, keyed by the plain feature name, and never reach validate()), index.html’s per-type render branch (bool/number/enum/string) and its ? help-icon tooltip (family- and param-level), and its test (engine/setup/tests/mcp/test_toggle_schema.py) |
| add/remove a skill | cli.py setup link-skills (np_link_skills.py — relinks + regenerates INDEX.md via np_generate_index.py), .claude-plugin/plugin.json |
| the setup ordering | idempotency + the 00→91 numbering contract (fresh-box bootstrap runs them in order) |
Before you commit — the cheap checklist
- Read this map; confirmed the change-impact rows for what you touched.
- New runtime behavior is toggle-gated and fails open.
- Any new
claude -pcall setsNERVEPACK_AGENT=1and the hook is guarded. - A regression test in
engine/setup/tests/covers the change (red→green). - Conventional commit prefix, authored as the repo’s configured git identity, no AI trailer.
- If you made a new durable decision, fold it into the right skill/spec (→
np-core-contribute).
Where to read more (the “child docs” — depth lives here, not duplicated above)
- Protocols & conventions:
AGENTS.md(tool-neutral manual) +CLAUDE.md(Claude Code wiring;@imports AGENTS.md). - Per-feature design: the design specs + plans live in the content overlay, not the engine (
$NP_CONTENT_DIR/docs/superpowers/{specs,plans}/; filenames like*-design.mdare referenced by name in the feature catalog above for provenance). Brainstorm/plan output is content, so a public engine-only clone won’t carry them. Historical/one-time:2026-06-03-nervepack-rebrand-design.md(the brain→nervepack rename, not a live subsystem). In progress (Phases 1–5 built:np-llm.sh, the onboard contract, the doctor, the Claude adapter, thenp-core-onboardskill; Goose validation pending):2026-06-05-agnostic-onboarding-design.md(LLM-agnostic onboarding). - Behavioral rules / gotchas: the overlay’s
np-kb-coding-rules,np-kb-claude-headless-scripting,np-kb-brandingskills (content-resident — thenp-kb-*tier ships in the overlay, not this repo). - Human overview & bringup:
README.md. Deferred work:ROADMAP.md. Audit trail:log.md. - Skill catalog:
INDEX.md(auto-generated; scan before adding a skill).