@nurix/etna 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,36 @@
1
1
  # @nurix/etna
2
2
 
3
- CLI to bootstrap a project with a Nurix harness. Installs a Claude Code plugin from the `nurixlabs/skills` marketplace and drops the harness's `CLAUDE.md` at the project root.
3
+ CLI to bootstrap a project with a Nurix harness. Produces the same on-disk state as running
4
4
 
5
- See [`../../harness/README.md`](../../harness/README.md) for what a harness is.
5
+ ```
6
+ /plugin marketplace add nurixlabs/skills
7
+ /plugin install nurix-harness@nurixlabs
8
+ ```
9
+
10
+ inside Claude Code, plus drops the harness's `CLAUDE.md` at the project root. See [`../../harness/README.md`](../../harness/README.md) for what a harness is and why the install model is designed this way.
6
11
 
7
12
  ## Usage
8
13
 
9
14
  From the root of the project you want to bootstrap:
10
15
 
11
16
  ```sh
12
- GITHUB_TOKEN=ghp_... npx @nurix/etna nurix-harness
17
+ npx @nurix/etna nurix-harness
13
18
  ```
14
19
 
15
- After this completes, the directory contains:
20
+ After this completes, three things have changed on disk:
16
21
 
17
22
  ```
18
23
  your-project/
19
- ├── CLAUDE.md ← from harness/<folder>/CLAUDE.md (bundled in this package)
24
+ ├── CLAUDE.md ← bundled in this package; the harness's policy
20
25
  └── .claude/
21
- ├── settings.json ← enabledPlugins updated
22
- └── plugins/
23
- └── nurix-harness/ ← fetched from nurixlabs/skills/plugins/nurix-harness/
24
- ├── .claude-plugin/plugin.json
25
- ├── skills/{documentation, workflow-mapper, introspect}/
26
- ├── agents/{change-review, contract-monitor, dep-health, git-history, maintenance, review, spec-mode}.md
27
- ├── scripts/{build-ownership-index, check-doc-drift}.sh
28
- ├── rules/{general, nomenclature}.md
29
- └── README.md
26
+ └── settings.json ← enabledPlugins["nurix-harness@nurixlabs"] = true
27
+
28
+ ~/.claude/settings.json ← extraKnownMarketplaces["nurixlabs"] registered
30
29
  ```
31
30
 
32
- Open Claude Code in the project; it will pick up the plugin on next start. If Claude Code is already running, run `/reload-plugins`.
31
+ That's the entire footprint. Open Claude Code in the project; on next session start it resolves `nurix-harness@nurixlabs`, clones `nurixlabs/skills` into `~/.claude/plugins/marketplaces/`, fills the plugin cache at `~/.claude/plugins/cache/nurixlabs/nurix-harness/<version>/`, and loads the plugin. The CLI does not touch any of those Claude Code internals itself — that's deliberate (see [Installation goals](../../harness/README.md#installation-goals)).
32
+
33
+ If Claude Code is already running, run `/reload-plugins`.
33
34
 
34
35
  ## Available harnesses
35
36
 
@@ -37,28 +38,21 @@ Listed by `npx @nurix/etna` (no args) using the bundled `data/harness-index.json
37
38
 
38
39
  | Plugin name | Harness folder in etna | What it ships |
39
40
  |---|---|---|
40
- | `nurix-harness` | [`harness/code/`](../../harness/code/) | The canonical v7 harness — 7 audit agents, 3 workflow skills, 2 rules, 2 scripts |
41
-
42
- ## Environment
43
-
44
- | Variable | Required | Purpose |
45
- |---|---|---|
46
- | `GITHUB_TOKEN` | Yes (while `nurixlabs/skills` is private) | Personal access token with `repo:read` scope. Access to `nurixlabs/skills` is the access control for the harness. |
47
- | `NURIX_SKILLS_REF` | No | Git ref to fetch from in `nurixlabs/skills`. Default `dev`. |
41
+ | `nurix-harness` | [`harness/code/`](../../harness/code/) | The canonical v7 harness — governance agents, baseline workflow skills, ownership/drift scripts |
48
42
 
49
43
  ## How it works
50
44
 
51
- 1. Looks up `<plugin-name>` in the bundled `data/harness-index.json` to find the matching harness folder.
52
- 2. Walks `nurixlabs/skills/plugins/<plugin-name>/` via the GitHub Contents API and downloads each file into `./.claude/plugins/<plugin-name>/`.
53
- 3. Copies the bundled `data/harnesses/<harness-folder>/CLAUDE.md` to `./CLAUDE.md`.
54
- 4. Adds `<plugin-name>@nurixlabs: true` to `./.claude/settings.json` under `enabledPlugins`.
45
+ 1. Looks up `<plugin-name>` in the bundled `data/harness-index.json` to find the matching harness folder and the marketplace metadata.
46
+ 2. Copies the bundled `data/harnesses/<harness-folder>/CLAUDE.md` to `./CLAUDE.md` (backing up any existing one to `CLAUDE.md.bak.<ISO-timestamp>`).
47
+ 3. Adds the marketplace entry to `~/.claude/settings.json` under `extraKnownMarketplaces`.
48
+ 4. Adds `<plugin-name>@<marketplace>: true` to `./.claude/settings.json` under `enabledPlugins`.
55
49
 
56
- Existing `CLAUDE.md` or plugin folder at the destination is renamed to `<name>.bak.<ISO-timestamp>` before install. Nothing is overwritten.
50
+ No GitHub fetch. No `.claude/plugins/<name>/` folder. Claude Code's plugin resolver handles all of that on next launch.
57
51
 
58
52
  ## Why two sources
59
53
 
60
- - **Plugin content** comes from the marketplace (`nurixlabs/skills`) so it's always the latest CI-published version.
61
- - **`CLAUDE.md`** is bundled in this npm package, versioned with the package. Harness policy changes warrant a package version bump.
54
+ - **Plugin content** lives in the marketplace (`nurixlabs/skills`) and is fetched by Claude Code at session start — always the latest CI-published version.
55
+ - **`CLAUDE.md`** is bundled in this npm package, versioned with the package. A plugin cannot ship a project-root `CLAUDE.md` (Claude Code's plugin spec doesn't support it), so the CLI ships that half itself. Harness policy changes warrant a package version bump.
62
56
 
63
57
  ## Building / contributing
64
58
 
@@ -73,10 +67,4 @@ npm run build:data
73
67
 
74
68
  ## Publishing
75
69
 
76
- From `packages/cli/`:
77
-
78
- ```sh
79
- npm publish --access restricted
80
- ```
81
-
82
- The `publishConfig.access` is `restricted` so the scoped package is private by default.
70
+ CI auto-publishes on push to `dev` when `packages/cli/**` or `harness/**` change — see [`.github/workflows/publish-etna-to-npm.yml`](../../.github/workflows/publish-etna-to-npm.yml). The `publishConfig.access` is `restricted` so the scoped package is private by default.
package/bin/etna.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // packages/cli/bin/etna.js
3
- // `npx @nurix/etna <plugin-name>` — install a Nurix harness into the current directory.
3
+ // `npx @nurix/etna <plugin-name>` — bootstrap a Nurix harness in the current directory.
4
+ // Produces the same on-disk state as running `/plugin marketplace add <repo>` followed by
5
+ // `/plugin install <plugin>@<marketplace>` inside Claude Code, plus drops the harness's
6
+ // CLAUDE.md at the project root. Claude Code's plugin resolver fills the cache on next launch.
4
7
 
5
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, lstatSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
6
10
  import { join, dirname, resolve } from 'node:path';
7
11
  import { fileURLToPath } from 'node:url';
8
12
 
@@ -10,20 +14,17 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
10
14
  const pkgRoot = resolve(__dirname, '..');
11
15
  const dataDir = join(pkgRoot, 'data');
12
16
 
13
- const SKILLS_REPO = 'nurixlabs/skills';
14
- const SKILLS_REF = process.env.NURIX_SKILLS_REF || 'dev';
15
-
16
17
  function usage() {
17
- console.log(`\nUsage: npx @nurix/etna <plugin-name>\n`);
18
18
  const idx = loadHarnessIndex();
19
+ console.log(`\nUsage: npx @nurix/etna <plugin-name>\n`);
19
20
  const names = Object.keys(idx.plugins);
20
21
  if (names.length) {
21
- console.log(`Available harnesses (plugin → harness folder):`);
22
- for (const name of names) console.log(` ${name.padEnd(20)} → harness/${idx.plugins[name]}`);
22
+ console.log(`Available harnesses (plugin → marketplace):`);
23
+ for (const name of names) {
24
+ console.log(` ${name.padEnd(20)} → ${idx.marketplace.name}`);
25
+ }
23
26
  }
24
- console.log(`\nEnvironment:`);
25
- console.log(` GITHUB_TOKEN Required if ${SKILLS_REPO} is private (token with repo:read scope)`);
26
- console.log(` NURIX_SKILLS_REF Git ref to fetch from (default: dev)`);
27
+ console.log(`\nMarketplace: ${idx.marketplace.source.repo} (github)`);
27
28
  console.log();
28
29
  }
29
30
 
@@ -40,54 +41,6 @@ function die(msg, code = 1) {
40
41
  process.exit(code);
41
42
  }
42
43
 
43
- function ghHeaders() {
44
- const h = {
45
- Accept: 'application/vnd.github+json',
46
- 'User-Agent': '@nurix/etna',
47
- 'X-GitHub-Api-Version': '2022-11-28',
48
- };
49
- if (process.env.GITHUB_TOKEN) h.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
50
- return h;
51
- }
52
-
53
- // Recursively download a GitHub repo subdirectory into a local destination.
54
- async function fetchPluginFolder(pluginName, destDir) {
55
- const apiUrl = `https://api.github.com/repos/${SKILLS_REPO}/contents/plugins/${pluginName}?ref=${SKILLS_REF}`;
56
- await fetchDir(apiUrl, destDir);
57
- }
58
-
59
- async function fetchDir(apiUrl, destDir) {
60
- const res = await fetch(apiUrl, { headers: ghHeaders() });
61
- if (res.status === 401 || res.status === 403) {
62
- die(
63
- `GitHub returned ${res.status} for ${apiUrl}.\n` +
64
- `${SKILLS_REPO} is private — set GITHUB_TOKEN (a personal access token with repo:read scope).\n` +
65
- ` export GITHUB_TOKEN=ghp_...`,
66
- );
67
- }
68
- if (res.status === 404) {
69
- die(`Plugin not found in ${SKILLS_REPO}@${SKILLS_REF}. Has the marketplace publish pipeline run yet?`);
70
- }
71
- if (!res.ok) die(`GitHub API ${res.status}: ${await res.text()}`);
72
-
73
- const items = await res.json();
74
- mkdirSync(destDir, { recursive: true });
75
-
76
- for (const item of items) {
77
- const localPath = join(destDir, item.name);
78
- if (item.type === 'dir') {
79
- await fetchDir(item.url, localPath);
80
- } else if (item.type === 'file') {
81
- const dl = item.download_url;
82
- if (!dl) die(`File ${item.path} has no download_url`);
83
- const fileRes = await fetch(dl, { headers: ghHeaders() });
84
- if (!fileRes.ok) die(`Failed to download ${item.path}: ${fileRes.status}`);
85
- const buf = Buffer.from(await fileRes.arrayBuffer());
86
- writeFileSync(localPath, buf);
87
- }
88
- }
89
- }
90
-
91
44
  // Move <path> to <path>.bak.<timestamp> if it exists. Returns the backup path, or null.
92
45
  function backupIfExists(path) {
93
46
  if (!existsSync(path) && !isSymlink(path)) return null;
@@ -105,28 +58,46 @@ function isSymlink(path) {
105
58
  }
106
59
  }
107
60
 
108
- // Add pluginName to enabledPlugins in .claude/settings.json (creating the file if needed).
109
- // Uses the conventional shape `"plugin-name@marketplace": true`.
110
- function enablePlugin(targetDir, pluginName) {
111
- const claudeDir = join(targetDir, '.claude');
112
- mkdirSync(claudeDir, { recursive: true });
113
- const settingsPath = join(claudeDir, 'settings.json');
114
-
115
- let settings = {};
116
- if (existsSync(settingsPath)) {
117
- try {
118
- settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
119
- } catch (e) {
120
- die(`Failed to parse existing ${settingsPath}: ${e.message}`);
121
- }
61
+ function readJson(path) {
62
+ if (!existsSync(path)) return {};
63
+ try {
64
+ return JSON.parse(readFileSync(path, 'utf8'));
65
+ } catch (e) {
66
+ die(`failed to parse ${path}: ${e.message}`);
122
67
  }
68
+ }
69
+
70
+ // 2-space indent matches Claude Code's own writes to settings.json.
71
+ function writeJson(path, value) {
72
+ mkdirSync(dirname(path), { recursive: true });
73
+ writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
74
+ }
75
+
76
+ // Register the marketplace in ~/.claude/settings.json (extraKnownMarketplaces).
77
+ // Mirrors what `/plugin marketplace add <owner>/<repo>` writes, plus opts the
78
+ // marketplace into auto-updates (off by default for non-Anthropic marketplaces).
79
+ function registerMarketplace(marketplace) {
80
+ const path = join(homedir(), '.claude', 'settings.json');
81
+ const settings = readJson(path);
82
+ settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
83
+ settings.extraKnownMarketplaces[marketplace.name] = {
84
+ source: marketplace.source,
85
+ autoUpdate: true,
86
+ };
87
+ writeJson(path, settings);
88
+ return path;
89
+ }
123
90
 
91
+ // Add `<plugin>@<marketplace>: true` to <target>/.claude/settings.json under enabledPlugins.
92
+ // Mirrors what `/plugin install <plugin>@<marketplace>` writes at project scope.
93
+ function enablePlugin(targetDir, pluginName, marketplaceName) {
94
+ const path = join(targetDir, '.claude', 'settings.json');
95
+ const settings = readJson(path);
124
96
  settings.enabledPlugins = settings.enabledPlugins || {};
125
- const key = `${pluginName}@nurixlabs`;
97
+ const key = `${pluginName}@${marketplaceName}`;
126
98
  settings.enabledPlugins[key] = true;
127
-
128
- writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
129
- return { settingsPath, key };
99
+ writeJson(path, settings);
100
+ return { path, key };
130
101
  }
131
102
 
132
103
  async function main(args) {
@@ -150,32 +121,28 @@ async function main(args) {
150
121
  }
151
122
 
152
123
  const targetDir = process.cwd();
153
- const pluginDir = join(targetDir, '.claude', 'plugins', pluginName);
154
124
  const claudeMdDest = join(targetDir, 'CLAUDE.md');
125
+ const marketplace = idx.marketplace;
126
+ const fqKey = `${pluginName}@${marketplace.name}`;
155
127
 
156
- console.log(`\nInstalling ${pluginName} (from harness/${harnessFolder}) into ${targetDir}`);
128
+ console.log(`\nInstalling ${fqKey} into ${targetDir}`);
157
129
 
158
- // 1. Backup conflicts
159
- const pluginBackup = backupIfExists(pluginDir);
160
- if (pluginBackup) console.log(` backed up existing plugin folder → ${pluginBackup}`);
161
130
  const claudeBackup = backupIfExists(claudeMdDest);
162
131
  if (claudeBackup) console.log(` backed up existing CLAUDE.md → ${claudeBackup}`);
163
132
 
164
- // 2. Fetch plugin from nurixlabs/skills marketplace
165
- console.log(` fetching plugin from ${SKILLS_REPO}@${SKILLS_REF}…`);
166
- await fetchPluginFolder(pluginName, pluginDir);
167
- console.log(` installed plugin → ${pluginDir}`);
168
-
169
- // 3. Copy bundled CLAUDE.md to project root
170
133
  copyFileSync(bundledClaude, claudeMdDest);
171
134
  console.log(` installed CLAUDE.md → ${claudeMdDest}`);
172
135
 
173
- // 4. Enable plugin in .claude/settings.json
174
- const { settingsPath, key } = enablePlugin(targetDir, pluginName);
175
- console.log(` enabled '${key}' in ${settingsPath}`);
136
+ const marketplacePath = registerMarketplace(marketplace);
137
+ console.log(` registered marketplace '${marketplace.name}' → ${marketplacePath}`);
138
+
139
+ const { path: enablePath } = enablePlugin(targetDir, pluginName, marketplace.name);
140
+ console.log(` enabled '${fqKey}' → ${enablePath}`);
176
141
 
177
- console.log(`\nDone. Open Claude Code in this directory; it will load the plugin on next start.`);
178
- console.log(`Run /plugin to verify, or /reload-plugins if it's already running.\n`);
142
+ console.log(`\nDone. Open Claude Code in ${targetDir}.`);
143
+ console.log(
144
+ `It will fetch ${marketplace.source.repo}, install ${pluginName}, and load it on next session start.\n`,
145
+ );
179
146
  }
180
147
 
181
148
  main(process.argv.slice(2)).catch((e) => {
@@ -1,5 +1,13 @@
1
1
  {
2
+ "marketplace": {
3
+ "name": "nurixlabs",
4
+ "source": {
5
+ "source": "github",
6
+ "repo": "nurixlabs/skills"
7
+ }
8
+ },
2
9
  "plugins": {
3
- "nurix-harness": "code"
10
+ "nurix-harness": "code",
11
+ "kb-agi": "kb-agi"
4
12
  }
5
13
  }
@@ -73,6 +73,13 @@ Feature docs in `docs/features/` must include YAML frontmatter with `owns`, `tou
73
73
 
74
74
  The spec-mode agent checks for ownership conflicts during research. If conflicts are found, address them in the spec before proceeding.
75
75
 
76
+ ### Architecture Decisions (ADRs)
77
+
78
+ Record an ADR when a decision (a) changes how state, dependencies, or boundaries are organized, (b) has a rejected alternative worth remembering, and (c) could plausibly be re-litigated. Default: don't write one. Style preferences, local refactors, and obvious-from-code choices don't qualify.
79
+
80
+ - File: `docs/architecture/adr/NNNN-kebab-case.md` (4-digit sequential number). Frozen once written — supersede via a new ADR, never edit.
81
+ - Invoke the `documentation` skill with "record an ADR" or "architecture decision".
82
+
76
83
  ---
77
84
 
78
85
  ## Agents
@@ -88,7 +95,7 @@ Seven sub-agents and three skills are defined under `.claude/`. Reach for them b
88
95
  | `dep-health` | Agent | Dependency supply-chain audit | "audit dependencies", "check CVEs", "dep health" |
89
96
  | `git-history` | Agent | Classify recent commits into a velocity/severity CSV | "classify commits", "commit history report" |
90
97
  | `change-review` | Agent | Velocity heatmap, risk zones (high churn + low coverage) | "where is risk accumulating", "velocity report" |
91
- | `documentation` | Skill | Baseline audit, scaffolding, templates | "baseline docs", "create docs", "audit docs" |
98
+ | `documentation` | Skill | Baseline audit, scaffolding, templates, ADRs, guardrails | "baseline docs", "create docs", "audit docs", "record an ADR", "architecture decision", "create guardrails" |
92
99
  | `workflow-mapper` | Skill | Discover and diagram codebase workflows | "map workflows", "trace flows" |
93
100
  | `introspect` | Skill | Pre-implementation impact analysis against guardrails | "introspect this plan", "impact analysis" |
94
101
 
@@ -140,20 +147,22 @@ Unless explicitly requested:
140
147
 
141
148
  The six procedural folders below are fixed. Domain folders live as subfolders inside these — `docs/architecture/core/` for core module internals, `docs/features/{feature}/` for deep features. Discovery flows through **[`docs/routing.md`](docs/routing.md)** when present.
142
149
 
143
- | Folder | Purpose | Mutability |
144
- | -------------------- | ------------------------------------------------------ | --------------------- |
145
- | `docs/architecture/` | System-wide patterns, technical decisions, API specs | Living |
146
- | `docs/features/` | How implemented features work today | Living |
147
- | `docs/operations/` | Setup, development, deployment | Living |
148
- | `docs/history/` | Plans, changelogs, backlog | Frozen or append-only |
149
- | `docs/memory/` | Project knowledge + agent state | Living |
150
- | `docs/tests/` | Test plans and automation | Living |
150
+ | Folder | Purpose | Mutability |
151
+ | ------------------------ | -------------------------------------------------------- | --------------------- |
152
+ | `docs/architecture/` | System-wide patterns, technical decisions, API specs | Living |
153
+ | `docs/architecture/adr/` | Architecture Decision Records — numbered, one per file | Frozen (supersede) |
154
+ | `docs/features/` | How implemented features work today | Living |
155
+ | `docs/operations/` | Setup, development, deployment | Living |
156
+ | `docs/history/` | Plans, changelogs, backlog | Frozen or append-only |
157
+ | `docs/memory/` | Project knowledge + agent state | Living |
158
+ | `docs/tests/` | Test plans and automation | Living |
151
159
 
152
160
  ### Naming & Lifecycle
153
161
 
154
162
  - Features: `kebab-case.md` — living reference, status `Active`
155
163
  - Plans: `YYYY-MM-DD-kebab-case.md` — frozen after `Complete` or `Abandoned`
156
164
  - Changelogs: `YYYY-MM-DD-kebab-case.md` — append-only
165
+ - ADRs: `NNNN-kebab-case.md` (4-digit zero-padded number, sequential) — frozen; supersede via a new ADR
157
166
  - Memory: `{agent-name}.md` — agent-maintained
158
167
 
159
168
  ### Documentation Maintenance
@@ -0,0 +1,57 @@
1
+ # nupilot Knowledge Hub — router
2
+
3
+ This repo is a flat Obsidian vault that you read on the user's behalf. It's the bridge from the front (positioning, claims) to the back (product and service specs); detailed execution lives downstream in the service repos.
4
+
5
+ You are the **router**. Every session starts with a short interview, then follows one of two paths: **ask** (read the vault and answer) or **contribute** (help the user add to the vault). The contract that governs both — the note model, the frontmatter hints, who may write — is [`meta/guidance.md`](meta/guidance.md). Read it before you write anything.
6
+
7
+ ---
8
+
9
+ ## Step 0 — first run
10
+
11
+ If `meta/guidance.md` is missing, this vault isn't initialized — invoke the **kb-agi-init** agent to scaffold the `meta/` control plane, then continue.
12
+
13
+ ---
14
+
15
+ ## Step 1 — the opening interview
16
+
17
+ Before answering anything, run a short interview with the `AskUserQuestion` tool to establish two things:
18
+
19
+ 1. **What brings you?** → *ask* (you have a question) or *contribute* (you have something to add).
20
+ 2. **Your user-type (genre):** → *Sales*, *PM*, or *Contributor*. This sets the default depth and framing of answers, nothing else. (If none fits, let them say so — a missing genre just means use plain working depth.)
21
+
22
+ Then load the matching template from [`meta/genre/`](meta/genre/) — `sales.md`, `pm.md`, or `contributor.md` — and follow the path below.
23
+
24
+ Keep the interview to those two questions. Don't re-interview on every follow-up in the same session; carry the answers forward.
25
+
26
+ ---
27
+
28
+ ## The ask path — read-only
29
+
30
+ You are **read-only** on this path. You never edit a note. Your only write is appending a query log to `meta/sessions/` (see below).
31
+
32
+ 1. **Find the entry note(s)** for the question. Start from [`meta/intent-index.md`](meta/intent-index.md) (keyed by *what the user wants to do*) and the entry notes named in their genre template.
33
+ 2. **Traverse the note graph both ways** — *up* toward summaries and entry notes, *down* into detail and evidence. The `[[wikilinks]]` are the structure. Lead with `canonical` notes; treat `canonical: false` ones as background.
34
+ 3. **Synthesize at the genre's default depth**, then adjust to how the question was actually asked — go deeper when they reach for it, compress when they want the headline. Depth shapes the answer; it never hides notes.
35
+ 4. **Cite the notes you used** so the user can stand behind the answer and follow the links.
36
+ 5. **Log the session** to `meta/sessions/` in the format in that folder's README — including, honestly, whether you could fully answer. A question you *couldn't* answer is a gap; logging it feeds the gap-finder and lets the user resume later.
37
+
38
+ If the vault can't answer, say so plainly and log it as a gap. Don't invent an answer the notes don't support.
39
+
40
+ ## The contribute path — the human steers
41
+
42
+ Here the user is *adding* knowledge, not reading. This path is deliberately thin and grows with use. There are **no sub-agents** — it's you and the user in a normal session.
43
+
44
+ 1. **Orient them** with [`meta/guidance.md`](meta/guidance.md) (the note model and where things belong) and the target folder's `metadata.md` (what belongs there, what's already nearby).
45
+ 2. **What does the hub want next?** Run the `kb-find-gaps` agent (it reads `meta/sessions/` gaps + folder `metadata.md`) — that's the live content roadmap.
46
+ 3. **Draft and place notes** as the user directs: one idea per note, light frontmatter per the contract, linked to its neighbours. Links form naturally as you write.
47
+ 4. Writing here is the human-in-the-loop exception to read-only — a person is steering every write, and every write is a git commit.
48
+
49
+ ---
50
+
51
+ ## What you do *not* do
52
+
53
+ - **You don't run upkeep.** The kb-agi upkeep agents (`kb-find-gaps`, `kb-sync-metadata`, `kb-catch-stale`, `kb-fix-links`, `kb-refresh-frontdoor`, `kb-refresh-summaries`) are the only writers of canon, invoked by name and only when *deliberately* run — not as a side effect of a question. Don't trigger them from the ask path.
54
+ - **You don't hand-edit `meta/_kg/`** — it's a derived graph, regenerated from the notes.
55
+ - **You don't mutate notes on the ask path.** Read-only means read-only.
56
+
57
+ Everything is git-checked-in, so any write is reviewable and reversible (doctrine P3).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Bootstrap a project with a Nurix harness — installs a Claude Code plugin from the nurixlabs/skills marketplace and drops the harness's CLAUDE.md at the project root.",
5
5
  "type": "module",
6
6
  "bin": {