From 96716c0df569c395f5a82c3d910c59f15eae3202 Mon Sep 17 00:00:00 2001 From: Christopher McCrow Date: Sat, 20 Jun 2026 14:17:46 +0100 Subject: [PATCH 1/2] Phase B: mechanical plugin<->repo and plugin<->user sync Leaves behind machinery so the plugin cannot silently drift from its own source again, on two axes. Plugin<->repo (prevent recurrence): - nexus.structure.json: single machine-readable source of structural truth (paths, PARA folders, frontmatter, command set, optional drift vocabulary), narrated by a thin STRUCTURE.md. Nothing imports it; the check makes deviation fail. - scripts/check_plugin_coherence.py: zero-dependency stdlib check asserting every consumer agrees with the structure file (no stale .nexus state paths, one failure-log path everywhere, command set matches the JSON and README, drift vocabulary consistent where it appears, category-free seed log; advisory PARA-folder scan). Wired as a pre-commit hook and a GitHub Action. Plugin<->user (keep installs current): - CHANGELOG.md (Keep-a-Changelog) with the 0.6.0 reconciliation and this 0.7.0 entry; CONTRIBUTING rule that every command/hook/structure change bumps the version and adds an entry. Version 0.6.0 -> 0.7.0. - session_preflight.py prints the installed plugin version, read offline from the manifest (no network). - docs/updating.md: CLI vs desktop update routes; linked from README. - STRUCTURE.md documents the two-layer sync trade-off (discipline in the paste layer, enforcement in the plugin). Category decision (resolved): drift categories are an optional, emergent vocabulary, never imposed on a day-one log. Seed template is now category-free so /nexus-init and the setup prompt agree; the seven codes remain only as an opt-in reference in the skill, CLAUDE-lite, and /failure. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .github/workflows/coherence.yml | 21 ++ .pre-commit-config.yaml | 18 ++ CHANGELOG.md | 70 ++++++ CONTRIBUTING.md | 19 ++ README.md | 3 + ROADMAP.md | 2 +- STRUCTURE.md | 67 ++++++ docs/updating.md | 59 +++++ hooks/session_preflight.py | 23 ++ nexus.structure.json | 35 +++ scripts/check_plugin_coherence.py | 385 ++++++++++++++++++++++++++++++ skills/failure-logging/SKILL.md | 10 +- templates/CLAUDE-lite.md | 18 +- templates/failure-log.md | 63 +++-- 16 files changed, 753 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/coherence.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 STRUCTURE.md create mode 100644 docs/updating.md create mode 100644 nexus.structure.json create mode 100644 scripts/check_plugin_coherence.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5d7f834..c9564c7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "nexus", "description": "The session ritual (/done, /status, /idea), failure logging, session pre-flight, branch verification, and a KB-root foundations bootstrap (/nexus-init). Works in Claude Code and Cowork. Companion to The Coherence Problem field guide.", - "version": "0.6.0", + "version": "0.7.0", "source": "./", "author": { "name": "CrowCreation", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 18d5c35..1452d20 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "nexus", - "version": "0.6.0", + "version": "0.7.0", "description": "Operational discipline for Claude Code and Cowork: the session ritual (/done, /status, /idea), failure logging with the three-occurrence rule, session pre-flight, branch verification, and a KB-root foundations bootstrap (/nexus-init). Companion to The Coherence Problem field guide.", "author": { "name": "CrowCreation", diff --git a/.github/workflows/coherence.yml b/.github/workflows/coherence.yml new file mode 100644 index 0000000..2450779 --- /dev/null +++ b/.github/workflows/coherence.yml @@ -0,0 +1,21 @@ +name: coherence + +# Asserts the plugin agrees with its own single source of structural truth +# (nexus.structure.json). nexus-public is a separate repo, so this CI is a clean +# add. The check is zero-dependency stdlib Python — no install step needed. + +on: + push: + branches: [main] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Run plugin coherence check + run: python scripts/check_plugin_coherence.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fbfb82a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# Pre-commit hooks for the Nexus plugin. +# +# Install once: +# pip install pre-commit +# pre-commit install +# +# The coherence check is zero-dependency stdlib Python, so it runs against the +# system interpreter with no environment to build. +repos: + - repo: local + hooks: + - id: nexus-plugin-coherence + name: Nexus plugin coherence + description: Assert every consumer agrees with nexus.structure.json + entry: python scripts/check_plugin_coherence.py + language: system + pass_filenames: false + always_run: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4f4cbfa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +All notable changes to the Nexus plugin are recorded here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and +the plugin aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The version here matches `.claude-plugin/plugin.json` and +`.claude-plugin/marketplace.json`. Every command, hook, or structure change bumps +the version and adds an entry below. + +## [0.7.0] - 2026-06-20 + +Phase B: mechanical sync so the plugin cannot silently drift from its own source +again — on the plugin-to-repo axis and the plugin-to-user axis. + +### Added + +- `nexus.structure.json` — single machine-readable source of structural truth + (failure-log path, session-state dir, universe path, PARA folders, frontmatter + fields, command set, optional drift vocabulary), narrated by a thin + `STRUCTURE.md`. +- `scripts/check_plugin_coherence.py` — zero-dependency coherence check that + asserts every consumer agrees with `nexus.structure.json`: no stale state paths + under an old `.nexus` directory, one failure-log path everywhere, the command + set matching, and the drift vocabulary consistent wherever it appears. +- `.pre-commit-config.yaml` and `.github/workflows/coherence.yml` — run the + coherence check on commit and in CI. +- `CHANGELOG.md` (this file) and a contributing rule: every command, hook, or + structure change bumps the version and adds a changelog entry. +- `docs/updating.md` — how to keep an installed plugin current (CLI update versus + desktop Customize panel; remove-and-re-add when greyed), linked from the README. +- Offline staleness surface: `session_preflight.py` prints the installed plugin + version (read from the plugin manifest). No network call. + +### Changed + +- Drift categories are now consistently an **optional, emergent vocabulary**, + never imposed on a day-one log. The seed template (`templates/failure-log.md`) + is category-free; `/nexus-init` now seeds the same category-free log the setup + prompt does; the seven codes remain only as an opt-in reference in the + failure-logging skill, CLAUDE-lite, and `/failure`. This resolves the earlier + inconsistency where a `/nexus-init` repo got a categorised log and a + setup-prompt repo got a category-free one. + +## [0.6.0] - 2026-06-20 + +Phase A: reconcile the plugin onto the KB-root state model, so install, scaffold, +and the session ritual all operate on one structure. + +### Changed + +- `/failure`, `/field-report`, and the three hooks now read and write the root + `failure-log.md` and `.claude/session-state/`, replacing the old `.nexus` + directory model. `/failure` is format-tolerant — it matches the log shape + already in use rather than imposing one. +- `/nexus-init` repurposed to scaffold the KB-root foundations (root + `failure-log.md`, the `KB/` PARA skeleton, day-one hygiene files), idempotently. +- README, ROADMAP, manifests, and `templates/universe.md` updated to the + reconciled command set and KB-root framing. + +### Removed + +- `/nexus-onboard` — the one-shot universe-mapping interview. Universe mapping is + now seeded by the setup prompt and kept living by `/done` and the weekly review. +- `templates/config.json` — dead under the KB-root model (nothing read it). + +### Added + +- `docs/cowork-setup.md` — the single desktop story (no-terminal install via the + Customize panel, namespaced `nexus:` commands, refreshing a stale plugin). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9018071..8c8d61f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,25 @@ Concrete incidents are more valuable than abstract opinions. Nexus is intentionally narrow. +## Keeping the plugin coherent + +Two rules keep the plugin honest with itself and current for the people who +install it: + +- **Structure lives in one place.** Paths, the command set, and the drift + vocabulary are declared once in [`nexus.structure.json`](./nexus.structure.json). + Every consumer restates them in its own prose or code, so when you change one, + change it everywhere and run `python scripts/check_plugin_coherence.py`. The + check runs on commit (pre-commit) and in CI; it fails with a precise diff if a + consumer drifts. +- **Every command, hook, or structure change bumps the version and adds a + changelog entry.** Update `version` in both `.claude-plugin/plugin.json` and + `.claude-plugin/marketplace.json`, and add an entry to + [`CHANGELOG.md`](./CHANGELOG.md). This is how an installed plugin can tell it is + behind its source. + +--- + The goal is not to build the biggest system. The goal is to understand how coherence degrades in real workflows, and which disciplines prevent it. diff --git a/README.md b/README.md index 244d903..258252d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Privacy: no telemetry, no network calls, no data leaves your machine. [Full sour **Requirements**: [Claude Code](https://claude.ai/code), Python 3.8+, Git. +**Keeping it current**: the plugin gets updates; an installed copy can fall behind. [Keeping Nexus updated](./docs/updating.md) covers the CLI and desktop routes. (The paste layer above never has this problem — you own your copy.) + ### Using Nexus in Cowork Cowork (the desktop app) loads plugin commands, not the command files sitting in your project folder, and the commands appear namespaced (`nexus:done`, `nexus:status`). The full desktop story — installing with no terminal, the namespaced commands, and refreshing a stale plugin — lives in one place: [Using Nexus in Cowork](./docs/cowork-setup.md). @@ -73,6 +75,7 @@ Cowork (the desktop app) loads plugin commands, not the command files sitting in ## Go deeper - [ROADMAP.md](./ROADMAP.md) - what's built today versus what's planned, honestly +- [STRUCTURE.md](./STRUCTURE.md) - the one structural shape, the coherence check that keeps the plugin honest with itself, and the two-layer sync trade-off - [The Coherence Problem](./docs/the-coherence-problem.md) - the full field guide: why persistent AI systems degrade and the five drift modes - [The Operator Stack](./docs/the-operator-stack.md) - five layers, from substrate to shared intelligence - [Patterns](./patterns/) - five steal-this patterns with implementation details diff --git a/ROADMAP.md b/ROADMAP.md index 2e76c5d..ee3966c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,7 @@ Nexus is an experimental, evolving field manual, not a finished product. This pa - **The failure log** - the core primitive. Append-only record of what broke and why. - **`/done`** - the day-one session-close command: daily note, failure-log entry with three-occurrence check, project state nudge, optional knowledge capture. -- **`/failure`** - plugin command for structured failure-log entries with drift categories. +- **`/failure`** - plugin command for failure-log entries. Format-tolerant: matches the shape your log already uses, with drift categories as an optional vocabulary rather than an imposed one. - **`/field-report`** - formats and redacts a log entry for sharing with other operators. Nothing sent automatically. - **`/nexus-init`** - one idempotent command to bootstrap the KB-root foundations (root `failure-log.md`, the `KB/` PARA skeleton, day-one hygiene files). The one-command alternative to the setup prompt. - **The three hooks** - session pre-flight, branch verification before commit, and session save. diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..452c26f --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,67 @@ +# Structure + +Nexus has one structural shape, and it is written down once, in +[`nexus.structure.json`](./nexus.structure.json). That file is the single source +of truth for where state lives and what the plugin ships: + +- the failure log is the root `failure-log.md` +- session state lives under `.claude/session-state/` +- the living map is the root `universe.md` +- the knowledge base is PARA: `KB/Projects`, `KB/Areas`, `KB/Knowledge`, + `KB/Goals`, `KB/Daily`, `KB/Archive`, `KB/_Admin` +- day-one frontmatter is four fields: `title`, `status`, `tags`, `updated` +- the commands are `done`, `status`, `idea`, `failure`, `field-report`, + `nexus-init` +- drift categories are an optional, emergent vocabulary, never imposed on a + day-one log + +## Why a JSON file nothing imports + +The markdown commands and the Python hooks each restate these paths in their own +prose and code. Editing `nexus.structure.json` does not magically rewrite them — +there is no build step, and a markdown command cannot import a JSON value. + +So the JSON is canonical for a different reason: a check makes deviation fail. +[`scripts/check_plugin_coherence.py`](./scripts/check_plugin_coherence.py) loads +`nexus.structure.json` and asserts that every consumer agrees with it — the same +failure-log path everywhere, the command set matching, no stale state paths under +an old `.nexus` directory left behind, the drift vocabulary consistent wherever +it appears. The check +runs as a pre-commit hook and a GitHub Action. If a command drifts from the +declared structure, the commit fails with a precise diff. + +This is the plugin practising what Nexus preaches. The whole project exists +because a distribution artefact can silently drift from its own source. The +0.5.0 to 0.6.0 reconciliation happened because two state models (an old `.nexus` +directory and KB-root) had been living in one plugin for weeks without anyone +noticing. The +check is the rule written so it cannot happen a third time. + +## The two scaffolders are thin emitters of one structure + +`/nexus-init` and the setup prompt both build the same KB-root foundations. They +are two ways to emit the one structure in `nexus.structure.json`, kept in +agreement by the check. `/field-report` is the human backstop for the drift a +check cannot see — the semantic, "this got sloppy" kind — by formatting failure +entries for sharing with other operators. + +## Two layers, two sync stories + +Nexus ships its discipline on two layers, and they trade off differently: + +- **The paste layer** — [CLAUDE-lite](./templates/CLAUDE-lite.md) and the + [setup prompt](./setup-prompt.md). You copy it into your own repo and own the + copy outright. There is **no user-sync problem**: nothing of ours sits in your + repo waiting to go stale. The cost is the other side of the same coin — you get + no automatic updates. When the patterns improve, your pasted copy does not. + +- **The plugin** — installed from the marketplace. It **does** get updates: a new + version is one `/plugin marketplace update` (or a Customize-panel reinstall) + away. The cost is update friction — the plugin can fall behind its source, and + keeping it current is a real step (see [docs/updating.md](./docs/updating.md)). + +The conclusion we build around, on purpose: the **discipline** lives in the paste +layer, where it can never go stale on the user, and the **enforcement +automation** lives in the plugin, where the update cost buys you hooks and checks +that a paste cannot provide. Discipline first; the plugin is the graduation step, +not the entry point. diff --git a/docs/updating.md b/docs/updating.md new file mode 100644 index 0000000..465851d --- /dev/null +++ b/docs/updating.md @@ -0,0 +1,59 @@ +# Keeping Nexus updated + +The plugin gets better over time. Unlike the paste layer (CLAUDE-lite and the +setup prompt, which you own outright once copied), an installed plugin can fall +behind its source. This is how you pull the latest version. Nothing here phones +home — updating is always something you do, never something that happens to you. + +> First install is a different thing and lives elsewhere. The terminal install is +> in the [README](../README.md#going-further-the-plugin); the desktop, no-terminal +> install is in [Using Nexus in Cowork](./cowork-setup.md). This page is only +> about moving an already-installed plugin to a newer version. + +## Am I behind? + +At session start the pre-flight prints the installed version, for example +`Nexus plugin v0.7.0`. Compare it against the latest entry in +[CHANGELOG.md](../CHANGELOG.md). If yours is older, update with one of the routes +below. The check is deliberately offline — the plugin does not reach out to a +marketplace to compare, so this manual glance is the surface. + +## Terminal (CLI) + +Two steps, because refreshing the marketplace listing and reinstalling the plugin +are separate: + +``` +/plugin marketplace update +/plugin install nexus@nexus +``` + +The first pulls the latest marketplace metadata for `crowcreation/nexus`; the +second reinstalls the plugin at the version the marketplace now points to. Run +both. Updating the marketplace alone does not move an installed plugin. + +## Desktop app (Cowork) + +The desktop app has no `/plugin` command — it is CLI-only and does nothing in the +Chat, Cowork, or Code tabs. Plugins are managed through the **Customize panel**. + +- Open the Customize panel and find the Nexus plugin under its plugins or + marketplace section. +- If an update control is offered, use it. +- **If the plugin looks greyed out or stale, or an update control is missing, + remove the plugin and re-add it.** A clean remove-and-re-add picks up the latest + version. This is the reliable desktop refresh today. + +Remember the desktop commands are namespaced: `nexus:done`, `nexus:status`, +`nexus:idea`, `nexus:failure`. After an update, type `/` and confirm they still +appear before relying on them. + +## A note on the two layers + +If update friction ever gets in your way, remember the discipline does not depend +on the plugin. The five patterns live in [CLAUDE-lite](../templates/CLAUDE-lite.md), +which you paste and own — it never goes stale on you because nothing of ours sits +in your repo. The plugin is the enforcement and graduation layer on top. Keep the +discipline in the paste layer; treat the plugin update as the cost of the +automation it buys you. See [STRUCTURE.md](../STRUCTURE.md#two-layers-two-sync-stories) +for the full trade-off. diff --git a/hooks/session_preflight.py b/hooks/session_preflight.py index 04235ec..40547f9 100644 --- a/hooks/session_preflight.py +++ b/hooks/session_preflight.py @@ -38,6 +38,25 @@ def find_state_dir(): return Path.cwd() / ".claude" / "session-state" +def plugin_version(): + """Installed plugin version, read offline from the plugin manifest. + + Reads ${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json — no network call, so + nothing phones home. Returns "" if the env var is unset (running outside the + plugin) or the manifest is unreadable. A future opt-in could compare this + against the marketplace; that is deliberately deferred. + """ + root = os.environ.get("CLAUDE_PLUGIN_ROOT", "") + if not root: + return "" + try: + manifest = Path(root) / ".claude-plugin" / "plugin.json" + data = json.loads(manifest.read_text(encoding="utf-8")) + return str(data.get("version", "")) + except Exception: + return "" + + def load_last_session(state_dir): state_file = state_dir / "last-session.json" if state_file.exists(): @@ -60,6 +79,10 @@ def main(): print(json.dumps({})) return + version = plugin_version() + if version: + lines.append(f"Nexus plugin v{version}") + lines.append(f"Branch: {branch} ({head})") last = load_last_session(state_dir) diff --git a/nexus.structure.json b/nexus.structure.json new file mode 100644 index 0000000..bb53568 --- /dev/null +++ b/nexus.structure.json @@ -0,0 +1,35 @@ +{ + "$comment": "Single machine-readable source of structural truth for the Nexus plugin. Consumers (markdown commands, hooks, templates, docs) restate these values in prose; editing this file does NOT auto-propagate. scripts/check_plugin_coherence.py is what keeps every consumer honest by asserting they all agree. When you change a path, a command, or the drift vocabulary, change it here AND in every consumer, then run the check.", + "schema_version": 1, + "failure_log_path": "failure-log.md", + "session_state_dir": ".claude/session-state", + "universe_path": "universe.md", + "para_folders": [ + "KB/Projects", + "KB/Areas", + "KB/Knowledge", + "KB/Goals", + "KB/Daily", + "KB/Archive", + "KB/_Admin" + ], + "frontmatter_fields": [ + "title", + "status", + "tags", + "updated" + ], + "commands": [ + "done", + "status", + "idea", + "failure", + "field-report", + "nexus-init" + ], + "drift_categories": { + "$comment": "Optional, emergent vocabulary — never imposed on a day-one log. The check asserts these codes are consistent WHERE they appear (the failure-logging skill, CLAUDE-lite, /failure, the optional pointer in templates/failure-log.md), not that every log is categorised.", + "optional": true, + "codes": ["SS", "CF", "ID", "DF", "CO", "FL", "UN"] + } +} diff --git a/scripts/check_plugin_coherence.py b/scripts/check_plugin_coherence.py new file mode 100644 index 0000000..da85457 --- /dev/null +++ b/scripts/check_plugin_coherence.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +"""Coherence check for the Nexus plugin. + +Loads nexus.structure.json (the single source of structural truth) and asserts +that every consumer in the repo agrees with it. The plugin restates its own +structure in many places: markdown commands describe paths in prose, hooks +hardcode them in Python, docs reference them in links. Nothing imports the JSON, +so the JSON is only canonical because this check makes deviation fail. + +Why it exists: the 0.5.0 to 0.6.0 reconciliation happened because two state +models (a `.nexus/` directory and the KB-root structure) had been living in one +plugin for weeks without anyone noticing. This check is the rule written so that +class of silent drift cannot happen a third time. + +Zero dependencies. Standard library only — no network, no third-party packages, +matching the hooks' design. Run it directly: + + python scripts/check_plugin_coherence.py + +Exits 0 when every consumer agrees with nexus.structure.json. Exits 1 with a +precise diff when something has drifted. Advisory findings (the scaffolder PARA +check) print as warnings and never change the exit code. +""" + +import json +import os +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +STRUCTURE_FILE = REPO_ROOT / "nexus.structure.json" + +# --------------------------------------------------------------------------- +# Consumer allowlists. These are the files Phase A reconciled (the 9 functional +# files) plus the failure-log referencers (~13). They are listed explicitly so a +# reviewer can see exactly what the check covers, and so a NEW consumer that +# forgets to agree with the structure is a visible omission, not a silent gap. +# --------------------------------------------------------------------------- + +# Files whose drift state-paths matter. ROADMAP.md is deliberately excluded from +# the `.nexus/` ban: it carries the single permitted historical mention in its +# "Retired commands" note. The check script itself lives under scripts/ and is +# pruned from the walk (it necessarily contains the string it searches for). +NEXUS_BAN_EXCLUDE_DIRS = {".git", "scripts", "images", ".obsidian", "node_modules"} +NEXUS_BAN_EXCLUDE_FILES = {"ROADMAP.md", "nexus.structure.json"} +NEXUS_BAN_EXTS = {".md", ".py", ".json"} + +# The three hooks must each anchor session state at the declared session_state_dir. +HOOK_FILES = [ + "hooks/session_preflight.py", + "hooks/branch_verify.py", + "hooks/session_save.py", +] + +# The scaffolders that emit the PARA skeleton (advisory check only — the +# setup-prompt is LLM prose, so we can flag a missing folder name but cannot +# prove the scaffold actually emits it). +SCAFFOLDER_FILES = [ + "setup-prompt.md", + "commands/nexus-init.md", +] + +# Files that state the drift-category vocabulary. Per the category decision, +# categories are an OPTIONAL, emergent vocabulary — never imposed on a day-one +# log. Where the vocabulary is stated, it must be the same seven codes. The seed +# template (templates/failure-log.md) lists them only in an explicitly-optional +# pointer; it is checked here for vocabulary agreement AND, separately, asserted +# to keep a category-free entry format. +DRIFT_VOCAB_FILES = [ + "skills/failure-logging/SKILL.md", + "templates/CLAUDE-lite.md", + "commands/failure.md", + "templates/failure-log.md", +] + +# Directory prefixes allowed in front of `failure-log.md`. The empty prefix is +# the canonical root state path. The rest are legitimate documentation links +# pointing at the repo's own template and pattern files. +ALLOWED_LOG_PREFIXES = { + "", + "./", + "templates/", + "../templates/", + "patterns/", + "../patterns/", +} + +_LOG_PATH_RE = re.compile(r"((?:[\w.@~-]+/)*)failure-log\.md") +_NEXUS_RE = re.compile(r"\.nexus/") + +# Drift-code extraction patterns (see extract_drift_codes). +_TABLE_CELL_RE = re.compile(r"(?m)^\|\s*([A-Z]{2})\s*\|") +_PAREN_LIST_RE = re.compile(r"\(([A-Z]{2}(?:,\s*[A-Z]{2}){2,})\)") +_PIPE_LIST_RE = re.compile(r"([A-Z]{2}(?:\|[A-Z]{2}){2,})") + + +class Result: + def __init__(self): + self.errors = [] + self.warnings = [] + self.checks_run = 0 + + def error(self, msg): + self.errors.append(msg) + + def warn(self, msg): + self.warnings.append(msg) + + def ok(self): + self.checks_run += 1 + + +def read_text(relpath): + path = REPO_ROOT / relpath + if not path.exists(): + return None + return path.read_text(encoding="utf-8") + + +def extract_drift_codes(text): + """Collect drift codes stated in a recognised category context. + + Three forms cover every statement in the repo: a markdown table whose first + cell is the code, a parenthesised comma list `(SS, CF, ...)`, and a + pipe-delimited list `SS|CF|...`. Plain `CODE (Name)` prose pairs are not + parsed — they never introduce a code the other forms miss. + """ + codes = set() + for m in _TABLE_CELL_RE.finditer(text): + codes.add(m.group(1)) + for m in _PAREN_LIST_RE.finditer(text): + codes.update(c.strip() for c in m.group(1).split(",")) + for m in _PIPE_LIST_RE.finditer(text): + codes.update(m.group(1).split("|")) + return codes + + +def check_structure_file(result): + """The structure file itself must parse and declare every required key.""" + required = [ + "failure_log_path", + "session_state_dir", + "universe_path", + "para_folders", + "frontmatter_fields", + "commands", + "drift_categories", + ] + missing = [k for k in required if k not in STRUCTURE] + if missing: + result.error( + f"nexus.structure.json is missing required key(s): {', '.join(missing)}" + ) + else: + result.ok() + + +def check_no_nexus_state(result): + """(1) No functional `.nexus/` reference remains anywhere.""" + found = [] + for dirpath, dirnames, filenames in os.walk(REPO_ROOT): + dirnames[:] = [d for d in dirnames if d not in NEXUS_BAN_EXCLUDE_DIRS] + for name in filenames: + if Path(name).suffix not in NEXUS_BAN_EXTS: + continue + rel = Path(dirpath, name).relative_to(REPO_ROOT).as_posix() + if rel in NEXUS_BAN_EXCLUDE_FILES: + continue + text = (REPO_ROOT / rel).read_text(encoding="utf-8", errors="replace") + for i, line in enumerate(text.splitlines(), 1): + if _NEXUS_RE.search(line): + found.append(f" {rel}:{i}: {line.strip()}") + if found: + result.error( + "Stale `.nexus/` state reference(s) found (the KB-root model uses " + "root `failure-log.md` / `universe.md` and `.claude/session-state/`):\n" + + "\n".join(found) + ) + else: + result.ok() + + +def check_failure_log_path(result): + """(2) The failure-log path string is identical across all consumers.""" + declared = STRUCTURE.get("failure_log_path", "failure-log.md") + if declared != "failure-log.md": + result.error( + f"nexus.structure.json failure_log_path is '{declared}', expected the " + "root 'failure-log.md' for the KB-root model." + ) + return + bad = [] + for dirpath, dirnames, filenames in os.walk(REPO_ROOT): + dirnames[:] = [d for d in dirnames if d not in NEXUS_BAN_EXCLUDE_DIRS] + for name in filenames: + if Path(name).suffix not in NEXUS_BAN_EXTS: + continue + rel = Path(dirpath, name).relative_to(REPO_ROOT).as_posix() + text = (REPO_ROOT / rel).read_text(encoding="utf-8", errors="replace") + for i, line in enumerate(text.splitlines(), 1): + for m in _LOG_PATH_RE.finditer(line): + prefix = m.group(1) + if prefix not in ALLOWED_LOG_PREFIXES: + bad.append( + f" {rel}:{i}: path '{prefix}failure-log.md' " + f"(expected root 'failure-log.md')" + ) + if bad: + result.error( + "failure-log.md referenced at a non-root path (the log lives at the " + "repo root under the KB-root model):\n" + "\n".join(bad) + ) + else: + result.ok() + + +def check_commands(result): + """(3) The command set matches the JSON and the README references.""" + declared = sorted(STRUCTURE.get("commands", [])) + cmd_dir = REPO_ROOT / "commands" + on_disk = sorted(p.stem for p in cmd_dir.glob("*.md")) + if on_disk != declared: + missing = sorted(set(declared) - set(on_disk)) + extra = sorted(set(on_disk) - set(declared)) + parts = [] + if missing: + parts.append(f"declared but no commands/{{{','.join(missing)}}}.md") + if extra: + parts.append(f"commands/*.md present but not declared: {', '.join(extra)}") + result.error("Command set mismatch: " + "; ".join(parts)) + else: + result.ok() + + readme = read_text("README.md") or "" + missing_in_readme = [ + c for c in declared + if f"/{c}" not in readme and f"nexus:{c}" not in readme + ] + if missing_in_readme: + result.error( + "Command(s) declared in nexus.structure.json but not referenced in " + f"README.md: {', '.join(missing_in_readme)}" + ) + else: + result.ok() + + +def check_drift_vocabulary(result): + """(4) Drift-category handling is consistent per the category decision.""" + dc = STRUCTURE.get("drift_categories", {}) + canonical = set(dc.get("codes", [])) + if not canonical: + result.error("nexus.structure.json declares no drift_categories.codes") + return + + for rel in DRIFT_VOCAB_FILES: + text = read_text(rel) + if text is None: + result.error(f"Drift-vocabulary consumer missing: {rel}") + continue + found = extract_drift_codes(text) + if not found: + # A consumer that states no codes is fine for the seed template + # (categories are optional); it just is not asserting a vocabulary. + result.ok() + continue + if found != canonical: + missing = sorted(canonical - found) + extra = sorted(found - canonical) + detail = [] + if missing: + detail.append(f"missing {', '.join(missing)}") + if extra: + detail.append(f"unexpected {', '.join(extra)}") + result.error( + f"{rel}: drift codes {sorted(found)} disagree with " + f"nexus.structure.json {sorted(canonical)} ({'; '.join(detail)})" + ) + else: + result.ok() + + # The seed template must keep a category-free ENTRY format: categories are + # optional, never imposed on a day-one log. (The codes may appear only in an + # explicitly-optional pointer, which is checked for vocabulary above.) + seed = read_text("templates/failure-log.md") or "" + if re.search(r"\*\*Category\*\*|^\s*Category:", seed, re.MULTILINE): + result.error( + "templates/failure-log.md imposes a mandatory Category field on the " + "seed log. The day-one log must stay category-free (categories are an " + "opt-in vocabulary)." + ) + else: + result.ok() + + +def check_session_state_dir(result): + """The hooks must anchor session state at the declared directory.""" + declared = STRUCTURE.get("session_state_dir", ".claude/session-state") + # Hooks build the path from components (Path(top) / ".claude" / "session-state"), + # so match each path segment rather than the joined string. + parts = [p for p in declared.split("/") if p] + for rel in HOOK_FILES: + text = read_text(rel) + if text is None: + result.error(f"Hook missing: {rel}") + elif not all(p in text for p in parts): + absent = [p for p in parts if p not in text] + result.error( + f"{rel}: does not reference session_state_dir '{declared}' " + f"(missing segment: {', '.join(absent)})" + ) + else: + result.ok() + + +def check_para_folders_advisory(result): + """(5) Advisory: scaffolders mention the declared PARA folders. + + The setup-prompt is an LLM prose prompt, not executable, so this flags the + absence of a folder name; it cannot prove the scaffold emits it. Advisory + only — never changes the exit code. + """ + folders = STRUCTURE.get("para_folders", []) + for rel in SCAFFOLDER_FILES: + text = read_text(rel) + if text is None: + result.warn(f"Scaffolder missing (advisory): {rel}") + continue + for folder in folders: + name = folder.split("/")[-1] + if folder not in text and name not in text: + result.warn( + f"{rel}: PARA folder '{folder}' not mentioned (advisory)" + ) + + +def main(): + global STRUCTURE + if not STRUCTURE_FILE.exists(): + print(f"FAIL: {STRUCTURE_FILE} not found", file=sys.stderr) + return 1 + try: + STRUCTURE = json.loads(STRUCTURE_FILE.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + print(f"FAIL: nexus.structure.json is not valid JSON: {exc}", file=sys.stderr) + return 1 + + result = Result() + check_structure_file(result) + check_no_nexus_state(result) + check_failure_log_path(result) + check_commands(result) + check_drift_vocabulary(result) + check_session_state_dir(result) + check_para_folders_advisory(result) + + for w in result.warnings: + print(f"ADVISORY: {w}") + + if result.errors: + print("") + print("Nexus plugin coherence check FAILED:") + print("") + for e in result.errors: + print(f"FAIL: {e}") + print("") + print( + f"{len(result.errors)} coherence error(s). Fix the consumer(s) to " + "agree with nexus.structure.json, or update the structure file if the " + "shape genuinely changed." + ) + return 1 + + print( + f"Nexus plugin coherence check passed " + f"({result.checks_run} assertions, {len(result.warnings)} advisory)." + ) + return 0 + + +STRUCTURE = {} + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/failure-logging/SKILL.md b/skills/failure-logging/SKILL.md index 520af2a..b808573 100644 --- a/skills/failure-logging/SKILL.md +++ b/skills/failure-logging/SKILL.md @@ -3,9 +3,15 @@ name: failure-logging description: "Use when recording AI workflow failures, reviewing failure patterns, or applying the three-occurrence rule. Provides drift category codes and structured failure capture guidance." --- -## Drift Categories +## Drift Categories (optional) -When recording a failure, classify it using one of these categories: +Categories are an **opt-in vocabulary, never a requirement**. The day-one +failure log is a plain date-and-root-cause line with no category at all, and most +logs never need more than that. Reach for these codes only once your own entries +ask for a shorthand — when the same kinds of failure keep recurring and grouping +them helps. Do not impose them up front. + +When you do choose to categorise, use one of these seven: | Code | Category | What it means | Diagnostic question | |------|----------|---------------|-------------------| diff --git a/templates/CLAUDE-lite.md b/templates/CLAUDE-lite.md index f1c6b48..612aaf4 100644 --- a/templates/CLAUDE-lite.md +++ b/templates/CLAUDE-lite.md @@ -123,13 +123,20 @@ assumption, a duplicated effort — add an entry: **What happened:** [one sentence] **Root cause:** [one sentence] -**Category:** [SS|CF|ID|DF|CO|FL|UN] +**Category:** [optional — SS|CF|ID|DF|CO|FL|UN] **Severity:** [CONTAINED|EXTERNAL] **Status:** DETECTED **Count:** [how many times this root cause has appeared] ``` -### Drift Categories +The date, what happened, and root cause are the whole of it. The category, +severity, and status fields are optional — useful once patterns recur, skippable +on day one. A plain line is a complete entry. + +### Drift Categories (optional) + +An opt-in vocabulary, never required. Adopt it only when recurring failures want +a shorthand: | Code | Name | Signal | |------|------|--------| @@ -181,8 +188,8 @@ When the session is ending: should have been. 2. **If something went wrong, append to failure-log.md.** One entry - per failure. One sentence for what happened, one for why. Pick the - closest category. + per failure. One sentence for what happened, one for why. A category + is optional — add one only if you are already using them. 3. **If nothing went wrong, don't write anything.** @@ -261,7 +268,8 @@ disagree, the live source wins. ### Failure Log Append to failure-log.md when something goes wrong. Format: what -happened, root cause, category, count. Categories: SS (State +happened, root cause, count. Categories are optional — an opt-in +shorthand once failures recur, not a day-one cost: SS (State Staleness), CF (Context Fragmentation), ID (Instruction Decay), DF (Discovery Failure), CO (Coordination Failure), FL (Feedback Loss), UN (Uncategorised). Severity: CONTAINED or EXTERNAL. Don't diff --git a/templates/failure-log.md b/templates/failure-log.md index d9ff3be..22d2f30 100644 --- a/templates/failure-log.md +++ b/templates/failure-log.md @@ -1,53 +1,48 @@ # Failure Log -Append-only record of operational friction. Review weekly for patterns. - - +Append-only record of operational friction. When something goes wrong in your +AI work — a wrong recommendation, a wasted run, a stale assumption, a duplicated +effort — write one plain-English line. Review weekly for patterns. + +Record what happened and your best guess at the root cause. Keep it plain. The +labels and categories come later, if at all — let the patterns emerge from your +own entries rather than imposing a scheme on day one. + +When the same root cause shows up **three times**, it is no longer a one-off. It +is structural, and the system is missing a rule. Write one preventive rule into +your `CLAUDE.md` and move on. --- ## Entries - - --- ## Weekly Review - + ### Week of YYYY-MM-DD - **Entries this week**: -- **Clusters found**: -- **Rules proposed**: +- **Clusters found** (root causes appearing 2+ times): - **Rules promoted to CLAUDE.md**: - **Open questions**: + +--- + + From c96b76e7e0062a822f0643e713f836f011bb0300 Mon Sep 17 00:00:00 2001 From: Christopher McCrow Date: Sat, 20 Jun 2026 14:39:26 +0100 Subject: [PATCH 2/2] docs: add Getting Started onboarding doc + README pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greenfield terminal path from empty terminal to a running Nexus in ~10 minutes. Folded into this branch because it appeared in the working tree alongside the Phase B README edits; it is additive onboarding, not part of the sync machinery. KB-root consistent (root failure-log.md, declared commands, no .nexus state paths) — coherence check stays green. Co-Authored-By: Claude Opus 4.8 (1M context) --- GETTING-STARTED.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 62 insertions(+) create mode 100644 GETTING-STARTED.md diff --git a/GETTING-STARTED.md b/GETTING-STARTED.md new file mode 100644 index 0000000..141be34 --- /dev/null +++ b/GETTING-STARTED.md @@ -0,0 +1,60 @@ +# Getting started + +A greenfield setup, from an empty terminal to a running Nexus, in about ten minutes. This is the terminal path, which is the reliable one today. Prefer the desktop app? See [Using Nexus in Cowork](./docs/cowork-setup.md). + +## What you need first + +- **Git** - version control ([install](https://git-scm.com/downloads)). +- **Claude Code** - the AI tool that runs in your terminal ([install](https://claude.ai/code)). +- A **GitHub account** - so your work is backed up ([sign up](https://github.com)). + +## Steps + +1. **Open a terminal.** + +2. **Make a folder for your Nexus and go into it.** + ``` + mkdir my-nexus + cd my-nexus + ``` + +3. **Turn it into a Git repository.** + ``` + git init + ``` + +4. **Start Claude Code.** + ``` + claude + ``` + +5. **Install the Nexus plugin** (this is what gives you the commands). Type these in the Claude session: + ``` + /plugin marketplace add crowcreation/nexus + /plugin install nexus@nexus + ``` + +6. **Scaffold your Nexus.** Type: + ``` + /nexus-init + ``` + This creates your `failure-log.md`, the `KB/` folders, and the day-one hygiene files. It is safe to run again later; it only adds what is missing. + + *Prefer a guided setup that also captures your goal and maps your world?* Instead of `/nexus-init`, paste the [setup prompt](./setup-prompt.md) and answer its few questions. + +7. **Back it up to GitHub.** Ask Claude: *"back this up to a new private GitHub repo"*. It will run the steps for you. The first time, it may ask you to sign in once with `gh auth login` (choose GitHub.com and log in through the browser). + +8. **You are running.** From now on a session has a shape: + - `/status` to start - what should I focus on? + - `/idea` as you go - capture a thought. + - `/done` to close - every time. Two minutes: what happened, what broke. + + Once a week, take about an hour for the weekly review: read your failure log, look for anything that has happened three times, and tidy your projects. + +> The commands may show in the menu with a `nexus:` prefix (for example `nexus:done`). That is the same command. + +## Where next + +- [Your First Hour](./learn/00-your-first-hour.md) - what these five things are, and why. +- [The Disciplines](./learn/04-the-disciplines.md) - the habits that make it compound. +- [The course](./learn/) - the full picture. diff --git a/README.md b/README.md index 258252d..9731db7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Operational coherence for persistent AI systems. When AI becomes a long-running **This is for** developers running persistent AI workflows (Claude Code, Cursor, Aider) where operational state survives across sessions, repositories evolve over time, and instructions outlive the conversation that created them. If your AI usage is mostly isolated conversations, many of these pathologies never emerge. +**Just want to set it up?** [Getting Started](./GETTING-STARTED.md) takes you from an empty terminal to a running Nexus in about ten minutes. + --- ## Start here: the discipline