@nurix/etna 0.20.23 → 0.20.25

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.
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "rules": [
3
3
  "changelog",
4
+ "cli",
4
5
  "cloudflare",
5
6
  "code-eval-batcher",
6
7
  "database",
@@ -0,0 +1,68 @@
1
+ ---
2
+ paths:
3
+ - "**/packages/cli/**"
4
+ - "**/packages/*-cli/**"
5
+ - "**/bin/*.js"
6
+ - "**/bin/*.mjs"
7
+ - "**/src/cli/**"
8
+ - "**/src/commands/**"
9
+ ---
10
+ # CLI — credentials, login doors, and the machine contract
11
+
12
+ What a `@nurix/*` command-line tool holds true about **how it authenticates, where it keeps what it is given, and how a script or an agent drives it**. Packaging, `npx` distribution, self-update, managed file blocks and publishing are the `nustack` skill's `cli-design.md` reference; the product API's own gate is the product's feature doc. A CLI that talks to no authenticated service reads only §5.
13
+
14
+ ## 1. Two credentials, and the order they are read in
15
+
16
+ | Credential | Held by | Arrives by | Lives | Dies by | Verified by |
17
+ | --- | --- | --- | --- | --- | --- |
18
+ | **Grant** — an OAuth refresh token, rotated on every use | a person's machine | a login door (§2) | until revoked | revocation at the auth service | the auth service; the CLI never sees a secret it can check |
19
+ | **Token** — an opaque, prefixed secret (`nst_pat_…`) | a pipeline, an agent, a headless host | the `<TOOL>_TOKEN` environment variable | until revoked or expired | a column on its row | the product API, locally, against a stored hash |
20
+
21
+ - **Resolution order is fixed and never prompts: env token → OS store → file store → signed out.** A `<TOOL>_TOKEN` in the environment wins outright and touches no store; `login`, `logout` and `doctor` say so instead of acting on the store behind it.
22
+ - **The grant refreshes; the token never does.** A grant spends the refresh token for a fifteen-minute access JWT per resource and writes the rotated pair back. A token is presented as-is on every call, and a `401` against it is final — no refresh, no retry, no browser.
23
+ - **Only the refresh token and the cached access tokens are stored — never a password, never an ID token, never the token a pipeline injected.**
24
+ - **Never a `--token` flag.** `argv` is readable in `ps` and shell history on every host the command runs on; the environment is the only way a secret enters a process. A flag that exists for a legacy caller is documented as deprecated, never added new.
25
+ - **A token cannot mint a token.** Minting, listing and revoking are person-only routes; a credential that can create credentials turns one leak into an unbounded family.
26
+
27
+ ## 2. Three login doors, one per situation
28
+
29
+ | Door | When | Shape |
30
+ | --- | --- | --- |
31
+ | **Loopback** | a browser runs on the same machine | authorization code + PKCE; `redirect_uri` is `http://127.0.0.1:<ephemeral>/callback`; the listener answers one matching `state` and closes |
32
+ | **Pasted code** | the browser cannot reach the terminal — SSH, a container | the same authorize URL with `redirect_uri` set to the auth service's own code page; the person pastes the code the page shows |
33
+ | **Device grant** (RFC 8628) | a GUI app with no listener of its own | `/device/code`, a `user_code`, polling `/oauth2/token` |
34
+
35
+ - **The CLI hardcodes one hostname — the product's.** The auth service's base URL, the client id and the resource audiences come from the product's discovery route (`GET /api/v1/auth/config`); a literal auth URL in a client pins every installed copy to one deployment.
36
+ - **Send `provider=<id>` on the authorize URL when the client already knows the provider** — the auth service's sign-in page starts that provider server-side with no paint when there is no session. Omitting it is the "click Google, then click Google again" defect.
37
+ - **Never open a browser, and never prompt, when stdin or stdout is not a TTY or when the env token is set.** The refusal is the CLI's own `SIGNED_OUT` code naming the login command, exit 1 — never a bare HTTP status, never a hang waiting on a page nobody can see.
38
+ - **Refusals are coded, and only two spend the credential.** `invalid_grant` is a dead grant: clear the item, run the login door, resume the command. `invalid_target` is a live grant asked for a resource it never covered: re-authorize wider, keep the item. A network failure, a `5xx` or a `429` keeps the item and raises — the item is shared with every client on the machine, and clearing it on a blip signs all of them out.
39
+
40
+ ## 3. Where a credential lives
41
+
42
+ - **OS store where one is probed present, a `0600` file where none is, and the absence of an OS store is never a reason to refuse login.** macOS `security`, Linux `secret-tool` (libsecret); otherwise `~/.<tool>/credentials.json`, mode `0600`, in a `0700` directory, written atomically (temp file + rename). `<TOOL>_CREDENTIAL_STORE=auto|keychain|file` overrides the probe.
43
+ - **Never `localStorage`, never a world-readable file, never the project directory.** The link file a project carries (`.<tool>/project.json`) holds identifiers only — a project id, a host, a stack — and is committed; a secret beside it ships with the repo.
44
+ - **One item, several clients — read-modify-write with the schema versioned.** A CLI and a desktop app sharing an item merge into it, preserve keys they do not know, and stamp `schema_version`; a newer schema is refused whole and an older one reads as *sign in again*. Concurrent refreshes race by design — the auth service's reuse window absorbs the honest double refresh, so the second loser is a rotation, never a wipe.
45
+ - **Logout is revoke-then-clear, best effort on the revoke.** An offline machine still clears its local material.
46
+
47
+ ## 4. The server's doors
48
+
49
+ - **Every machine credential carries a prefix the gate routes by, and the prefixed branches are ordered after the JWT branch.** Two credentials both presented as `Authorization: Bearer <opaque>` with no prefix cannot be told apart; a prefixed branch ahead of the JWT branch would have to decline every JWT correctly on every path, while one placed last cannot shadow a credential that already resolved.
50
+ - **One credential, one door.** A token minted for one surface is refused at every other, whatever it is worth — the audience rule is what bounds a leak.
51
+ - **A token row is `id`, owner, `label`, `token_hash`, `token_prefix`, `expires_at`, `revoked_at`, `last_used_at`.** The raw value appears in the `201` that minted it and nowhere else; the prefix is what a list shows; revocation sets a column and keeps the row, so the audit trail stays readable and the hash can never be re-issued; an expired, a revoked and an unknown token all answer the same `401`, so a caller never learns which.
52
+
53
+ ## 5. The machine contract
54
+
55
+ A script or an agent drives the CLI as often as a person does, and it reads three channels: **stdout carries the one datum** — the URL, the id, or the `--json` envelope — **stderr carries everything else, and the exit code is the verdict.** `cmd > out.txt` with a `0` exit is the whole contract for the caller; a progress line on stdout breaks it. TTY detection, `--yes`, and failing loudly outside a TTY are `cli-design.md` §5; on top of it:
56
+
57
+ - **`--json` implies non-interactive**, and every error is a coded envelope, never prose.
58
+ - **A command that waits on a remote job offers `--no-wait`** (return the id at once) **and, where the job streams a log, `--ci`** (stream the build log, exit with the job's verdict, fall back to polling when the stream dies). The default waits and streams.
59
+ - **Upload and go-live are separate verbs when the platform has a pointer.** `deploy --no-promote` stages, `promote` and `rollback` move the pointer, and a `deploy` without the flag does both. A rollback that rebuilds is a redeploy, not a rollback.
60
+ - **Pack what git would ship.** `.gitignore` plus a `.<tool>ignore` decide what leaves the machine; `--no-gitignore` is the escape hatch, named, never the default.
61
+
62
+ ## 6. Pitfalls — symptom → cause → fix
63
+
64
+ - **The sign-in page painted although the client sent `provider=`** → the hint fell off a hop, or the URL carries `error=` from an earlier attempt (the page always paints on `error=`) → follow the chain hop by hop with `curl -s -o /dev/null -D - <url>` and read each `location:`; the hint must survive every hop.
65
+ - **Works on macOS, asks to log in on every run elsewhere** → OS-store-only storage with no file fallback → §3.
66
+ - **CI hangs at the first authenticated command** → a prompt or a browser opened outside a TTY → §2; the fix is the `SIGNED_OUT` refusal, not a longer timeout.
67
+ - **Two commands at once, and the machine is "signed out"** → a concurrent refresh treated `invalid_grant` on the loser as a dead grant → check the service's reuse window before clearing anything (§3).
68
+ - **A `401` on a `<TOOL>_TOKEN` call loops or opens a browser** → the token took the grant's refresh path → §1: a token's `401` is final.
@@ -9,7 +9,7 @@ paths:
9
9
  ---
10
10
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
11
11
 
12
- How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
12
+ How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and a CLI's packaging live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
13
13
 
14
14
  ---
15
15
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "722680de8132",
2
+ "fingerprint": "c24bc6f1af10",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -157,7 +157,7 @@ Defaults — ❌ prohibitions hold unless the user explicitly asks otherwise;
157
157
  - ✅ **Verification** — batched at the gate, never continuous, and the gate is the build's last step, never a separate ask; the regimes and the gate are `.claude/rules/testing.md`, the procedure the `testing` skill (gate mode).
158
158
  - ❌ **Dependencies** – Never upgrade pinned packages without asking; security patches and test-tooling installs at `latest` (`testing.md`) are exempt.
159
159
  - ❌ **Compatibility** – Never maintain backward compatibility unless specified.
160
- - ❌ **Branches & Worktrees [override]** – Never self-create git branches or worktrees; commit straight to the checked-out branch. This replaces any built-in "branch first on the default branch" default. Exception: when the user asks for a pull request, create the branch the PR requires before committing its changes.
160
+ - ❌ **Branches & Worktrees [override]** – `dev` and `production` are merge-only. Work on a short-lived branch cut from `dev` (`feat/<slug>`, `fix/<slug>`, `docs/<slug>`; `hotfix/<slug>` off `production`), commit there, and open a pull request into `dev` (or `production` for a hotfix) — never commit to either long-lived branch, and never create a worktree. The branch is created when the first commit is due, not at session start. This replaces any built-in "branch first on the default branch" default: the branch is required, and it is never `main`.
161
161
  - ❌ **Protected Files** – Once per session, if `docs/operations/protected-files.md` exists, read it and never modify any path it lists; never modify `AGENTS.md` (harness-regenerated) or `protected-files.md` itself.
162
162
  - ✅ **Browser Automation** – If the runtime provides browser automation, use it freely; no need to ask first. If it doesn't — or the executing agent can't hold the browser connection (see Runtime Notes) — return a browser-verification checklist (URLs, actions, expected outcomes) for the orchestrating session or the user to run.
163
163
  - ✅ **Bookkeeping [override]** — dispatching outranks a runtime default that reserves sub-agents for explicit requests (§ Delegation), and `hooks/compliance.mjs` enforces it: Stop is blocked on unresolved rows unless the transcript carries a real `subagent_type: bookkeeper` record. Hooks append doc-debt rows to `docs/operations/bookkeeping.md`; three walls read it at three widths (Stop = your session's rows · commit = rows implicating staged files · CI = all), each re-verifying conditions at gate time. Dispatch the `bookkeeper` for your rows — **never drain `auto:`/`note:` rows in your own loop**, whichever agent you are: the drain is dispatched work at every nesting level (a sub-agent dispatches its own `bookkeeper` — § Delegation), and only `needs-session:` rows are yours to resolve directly. A drained row is **deleted**. The full grammar arrives in the deny message when a wall fires.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.19.1",
4
+ "version": "3.19.3",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -25,5 +25,5 @@
25
25
  }
26
26
  ],
27
27
  "docsRoot": "docs",
28
- "digest": "bc085dd8505c"
28
+ "digest": "5181bf532ddd"
29
29
  }
@@ -8,16 +8,16 @@
8
8
  },
9
9
  "hooks": {
10
10
  "SessionStart": [
11
- { "matcher": "", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/awareness.mjs\"", "timeout": 10 } ] }
11
+ { "matcher": "", "hooks": [ { "type": "command", "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/awareness.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"", "timeout": 10 } ] }
12
12
  ],
13
13
  "PostToolUse": [
14
- { "matcher": "Edit|Write|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/awareness.mjs\"", "timeout": 10 }, { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/build-graph.mjs\"", "timeout": 10 } ] }
14
+ { "matcher": "Edit|Write|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/awareness.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"", "timeout": 10 }, { "type": "command", "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/build-graph.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"", "timeout": 10 } ] }
15
15
  ],
16
16
  "PreToolUse": [
17
- { "matcher": "Edit|Write|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/compliance.mjs\"", "timeout": 10 } ] }
17
+ { "matcher": "Edit|Write|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/compliance.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"", "timeout": 10 } ] }
18
18
  ],
19
19
  "Stop": [
20
- { "matcher": "", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/compliance.mjs\"", "timeout": 15 } ] }
20
+ { "matcher": "", "hooks": [ { "type": "command", "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/compliance.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"", "timeout": 15 } ] }
21
21
  ]
22
22
  }
23
23
  }
@@ -0,0 +1,68 @@
1
+ ---
2
+ paths:
3
+ - "**/packages/cli/**"
4
+ - "**/packages/*-cli/**"
5
+ - "**/bin/*.js"
6
+ - "**/bin/*.mjs"
7
+ - "**/src/cli/**"
8
+ - "**/src/commands/**"
9
+ ---
10
+ # CLI — credentials, login doors, and the machine contract
11
+
12
+ What a `@nurix/*` command-line tool holds true about **how it authenticates, where it keeps what it is given, and how a script or an agent drives it**. Packaging, `npx` distribution, self-update, managed file blocks and publishing are the `nustack` skill's `cli-design.md` reference; the product API's own gate is the product's feature doc. A CLI that talks to no authenticated service reads only §5.
13
+
14
+ ## 1. Two credentials, and the order they are read in
15
+
16
+ | Credential | Held by | Arrives by | Lives | Dies by | Verified by |
17
+ | --- | --- | --- | --- | --- | --- |
18
+ | **Grant** — an OAuth refresh token, rotated on every use | a person's machine | a login door (§2) | until revoked | revocation at the auth service | the auth service; the CLI never sees a secret it can check |
19
+ | **Token** — an opaque, prefixed secret (`nst_pat_…`) | a pipeline, an agent, a headless host | the `<TOOL>_TOKEN` environment variable | until revoked or expired | a column on its row | the product API, locally, against a stored hash |
20
+
21
+ - **Resolution order is fixed and never prompts: env token → OS store → file store → signed out.** A `<TOOL>_TOKEN` in the environment wins outright and touches no store; `login`, `logout` and `doctor` say so instead of acting on the store behind it.
22
+ - **The grant refreshes; the token never does.** A grant spends the refresh token for a fifteen-minute access JWT per resource and writes the rotated pair back. A token is presented as-is on every call, and a `401` against it is final — no refresh, no retry, no browser.
23
+ - **Only the refresh token and the cached access tokens are stored — never a password, never an ID token, never the token a pipeline injected.**
24
+ - **Never a `--token` flag.** `argv` is readable in `ps` and shell history on every host the command runs on; the environment is the only way a secret enters a process. A flag that exists for a legacy caller is documented as deprecated, never added new.
25
+ - **A token cannot mint a token.** Minting, listing and revoking are person-only routes; a credential that can create credentials turns one leak into an unbounded family.
26
+
27
+ ## 2. Three login doors, one per situation
28
+
29
+ | Door | When | Shape |
30
+ | --- | --- | --- |
31
+ | **Loopback** | a browser runs on the same machine | authorization code + PKCE; `redirect_uri` is `http://127.0.0.1:<ephemeral>/callback`; the listener answers one matching `state` and closes |
32
+ | **Pasted code** | the browser cannot reach the terminal — SSH, a container | the same authorize URL with `redirect_uri` set to the auth service's own code page; the person pastes the code the page shows |
33
+ | **Device grant** (RFC 8628) | a GUI app with no listener of its own | `/device/code`, a `user_code`, polling `/oauth2/token` |
34
+
35
+ - **The CLI hardcodes one hostname — the product's.** The auth service's base URL, the client id and the resource audiences come from the product's discovery route (`GET /api/v1/auth/config`); a literal auth URL in a client pins every installed copy to one deployment.
36
+ - **Send `provider=<id>` on the authorize URL when the client already knows the provider** — the auth service's sign-in page starts that provider server-side with no paint when there is no session. Omitting it is the "click Google, then click Google again" defect.
37
+ - **Never open a browser, and never prompt, when stdin or stdout is not a TTY or when the env token is set.** The refusal is the CLI's own `SIGNED_OUT` code naming the login command, exit 1 — never a bare HTTP status, never a hang waiting on a page nobody can see.
38
+ - **Refusals are coded, and only two spend the credential.** `invalid_grant` is a dead grant: clear the item, run the login door, resume the command. `invalid_target` is a live grant asked for a resource it never covered: re-authorize wider, keep the item. A network failure, a `5xx` or a `429` keeps the item and raises — the item is shared with every client on the machine, and clearing it on a blip signs all of them out.
39
+
40
+ ## 3. Where a credential lives
41
+
42
+ - **OS store where one is probed present, a `0600` file where none is, and the absence of an OS store is never a reason to refuse login.** macOS `security`, Linux `secret-tool` (libsecret); otherwise `~/.<tool>/credentials.json`, mode `0600`, in a `0700` directory, written atomically (temp file + rename). `<TOOL>_CREDENTIAL_STORE=auto|keychain|file` overrides the probe.
43
+ - **Never `localStorage`, never a world-readable file, never the project directory.** The link file a project carries (`.<tool>/project.json`) holds identifiers only — a project id, a host, a stack — and is committed; a secret beside it ships with the repo.
44
+ - **One item, several clients — read-modify-write with the schema versioned.** A CLI and a desktop app sharing an item merge into it, preserve keys they do not know, and stamp `schema_version`; a newer schema is refused whole and an older one reads as *sign in again*. Concurrent refreshes race by design — the auth service's reuse window absorbs the honest double refresh, so the second loser is a rotation, never a wipe.
45
+ - **Logout is revoke-then-clear, best effort on the revoke.** An offline machine still clears its local material.
46
+
47
+ ## 4. The server's doors
48
+
49
+ - **Every machine credential carries a prefix the gate routes by, and the prefixed branches are ordered after the JWT branch.** Two credentials both presented as `Authorization: Bearer <opaque>` with no prefix cannot be told apart; a prefixed branch ahead of the JWT branch would have to decline every JWT correctly on every path, while one placed last cannot shadow a credential that already resolved.
50
+ - **One credential, one door.** A token minted for one surface is refused at every other, whatever it is worth — the audience rule is what bounds a leak.
51
+ - **A token row is `id`, owner, `label`, `token_hash`, `token_prefix`, `expires_at`, `revoked_at`, `last_used_at`.** The raw value appears in the `201` that minted it and nowhere else; the prefix is what a list shows; revocation sets a column and keeps the row, so the audit trail stays readable and the hash can never be re-issued; an expired, a revoked and an unknown token all answer the same `401`, so a caller never learns which.
52
+
53
+ ## 5. The machine contract
54
+
55
+ A script or an agent drives the CLI as often as a person does, and it reads three channels: **stdout carries the one datum** — the URL, the id, or the `--json` envelope — **stderr carries everything else, and the exit code is the verdict.** `cmd > out.txt` with a `0` exit is the whole contract for the caller; a progress line on stdout breaks it. TTY detection, `--yes`, and failing loudly outside a TTY are `cli-design.md` §5; on top of it:
56
+
57
+ - **`--json` implies non-interactive**, and every error is a coded envelope, never prose.
58
+ - **A command that waits on a remote job offers `--no-wait`** (return the id at once) **and, where the job streams a log, `--ci`** (stream the build log, exit with the job's verdict, fall back to polling when the stream dies). The default waits and streams.
59
+ - **Upload and go-live are separate verbs when the platform has a pointer.** `deploy --no-promote` stages, `promote` and `rollback` move the pointer, and a `deploy` without the flag does both. A rollback that rebuilds is a redeploy, not a rollback.
60
+ - **Pack what git would ship.** `.gitignore` plus a `.<tool>ignore` decide what leaves the machine; `--no-gitignore` is the escape hatch, named, never the default.
61
+
62
+ ## 6. Pitfalls — symptom → cause → fix
63
+
64
+ - **The sign-in page painted although the client sent `provider=`** → the hint fell off a hop, or the URL carries `error=` from an earlier attempt (the page always paints on `error=`) → follow the chain hop by hop with `curl -s -o /dev/null -D - <url>` and read each `location:`; the hint must survive every hop.
65
+ - **Works on macOS, asks to log in on every run elsewhere** → OS-store-only storage with no file fallback → §3.
66
+ - **CI hangs at the first authenticated command** → a prompt or a browser opened outside a TTY → §2; the fix is the `SIGNED_OUT` refusal, not a longer timeout.
67
+ - **Two commands at once, and the machine is "signed out"** → a concurrent refresh treated `invalid_grant` on the loser as a dead grant → check the service's reuse window before clearing anything (§3).
68
+ - **A `401` on a `<TOOL>_TOKEN` call loops or opens a browser** → the token took the grant's refresh path → §1: a token's `401` is final.
@@ -9,7 +9,7 @@ paths:
9
9
  ---
10
10
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
11
11
 
12
- How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
12
+ How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and a CLI's packaging live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
13
13
 
14
14
  ---
15
15
 
@@ -1,6 +1,6 @@
1
1
  # Component catalog — the fused index
2
2
 
3
- > **Generated folder — never hand-edit.** Everything under `skills/design/components/` is emitted by the components-repo regen instruction set (`docs/playbooks/skills/case-studies/components-regen-spec.md`, run against the `@nurix/components` library checkout). Regeneration may reshape any file beneath this one; the design skill's only seam with this folder is this index.
3
+ > **Generated folder — never hand-edit.** Everything under `skills/design/components/` is emitted by the components-repo regen instruction set (`docs/skills/design/components-regen-spec.md`, run against the `@nurix/components` library checkout). Regeneration may reshape any file beneath this one; the design skill's only seam with this folder is this index.
4
4
 
5
5
  **Load contract.** This index loads in **both** positions of the project's `primitives` switch (`design.md`, `primitives: nurix | native`) — it is how the skill knows what chrome *exists* to compose with. The per-category deep references beside it ([inputs](inputs.md), [overlays](overlays.md), [collections](collections.md), [navigation](navigation.md), [layout](layout.md), [feedback](feedback.md), [charts](charts.md), [typography](typography.md), [media](media.md), [motion](motion.md), [hooks](hooks.md), [utils](utils.md), plus [setup](setup.md) for install/Tailwind wiring) load **only under `primitives: nurix`**, and only when that category is touched.
6
6
 
@@ -14,20 +14,21 @@ button,inputs,Button (element/button),Button,"native <button>, variants via clas
14
14
  text-input,inputs,"Input (element/input) — onSave commit, regex validator",Input,"<input type=text>, commit on blur/Enter"
15
15
  number-input,inputs,"NumberInput (element/number-input) — min/max/step",—,"<input inputmode=numeric>, clamp on commit, Escape reverts"
16
16
  textarea,inputs,"Textarea (element/textarea) — Cmd/Ctrl+Enter commits",Textarea,"<textarea>, blur commits, Escape resets draft"
17
+ inline-edit,inputs,"InlineEdit (element/inline-edit) — click-to-edit contentEditable span",—,"span[contenteditable] reading as a label until focused, Enter/blur commits, Escape reverts"
17
18
  checkbox,inputs,Checkbox (element/checkbox),Checkbox,"<input type=checkbox> + label, indeterminate via JS"
18
19
  switch,inputs,Switch (element/switch),Switch,"role=switch button, aria-checked, Space toggles"
19
20
  radio-group,inputs,"RadioGroup + card/segmented variants (element/radio-group)",RadioGroup,"fieldset of <input type=radio>, arrow-key roving focus"
20
- select,inputs,"Select — searchable, multi, async (element/select)","Select; Combobox (Popover + Command)","<select> or listbox popover, typeahead, option objects not raw values"
21
- native-select,inputs,"Select* Radix family (element/native-select)",Select,"plain <select> behind a styled trigger"
21
+ combobox,inputs,"Combobox — searchable, multi, async (compound/combobox)","Combobox (Popover + Command)","input + filtered listbox popup, typeahead, option objects not raw values"
22
+ calendar,inputs,"Calendar (compound/calendar) — react-day-picker",Calendar,"month grid <table>, arrow-key day navigation"
23
+ select,inputs,"Select — closed listbox, one string value, no query field (element/select)",Select,"<select> or listbox popup, typeahead, one string value"
22
24
  date-picker,inputs,DatePicker (compound/date-picker),Date Picker (Calendar + Popover),"<input type=date> or calendar popover, ISO value"
23
25
  date-range-picker,inputs,"DateRangePicker — presets sidebar (compound/date-picker)",—,"two bound date inputs, partial selection reverts on close"
24
- calendar,inputs,"Calendar (element/calendar) — react-day-picker",Calendar,"month grid <table>, arrow-key day navigation"
25
26
  slider,inputs,Slider (element/slider),Slider,"<input type=range> with visible value readout"
26
27
  rating,inputs,Rating (element/rating),—,"radio group of icon buttons, half-step optional"
27
- toggle,inputs,"Toggle · ToggleGroup (element/toggle, element/toggle-group)","Toggle, Toggle Group","aria-pressed buttons, single or multiple selection"
28
+ toggle,inputs,"Toggle · ToggleGroup (element/toggle, compound/toggle-group)","Toggle, Toggle Group","aria-pressed buttons, single or multiple selection"
28
29
  otp-input,inputs,InputOTP (compound/input-otp),Input OTP,"segmented one-char inputs, paste distributes across slots"
29
30
  icon-picker,inputs,"IconPicker · Icon (compound/icon-picker)",—,"searchable icon grid in a popover, categorized"
30
- file-upload,inputs,MediaFilePicker (compound/media-file-picker),—,"<input type=file> + drop zone, previews, size and count caps"
31
+ file-upload,inputs,"FileUpload family — dropzone, list, item progress (compound/file-picker) · FilePreview (compound/file-preview)",—,"<input type=file> + drop zone, previews, size and count caps"
31
32
  form-field,inputs,"Cell — label/content/helper/error slots (compound/cell)",Form + FormField (react-hook-form),"label + control + helper in one block, error slot below control"
32
33
  form-validation,inputs,,Form (react-hook-form),"form element with submit-time validation and inline field errors"
33
34
  rich-text-editor,inputs,,—,"@tiptap/react editor with toolbar and markdown shortcuts"
@@ -39,19 +40,20 @@ bottom-drawer,overlays,Sheet side=bottom (block/sheet),Drawer (vaul),"fixed bott
39
40
  command-palette,overlays,Command (compound/command),Command (cmdk),"filtered list in a dialog, ⌘K opens, arrow-key + Enter"
40
41
  dropdown-menu,overlays,DropdownMenu (compound/dropdown-menu),Dropdown Menu,"role=menu under a trigger, arrow-key nav, outside click closes"
41
42
  context-menu,overlays,ContextMenu (compound/context-menu),Context Menu,"right-click menu at pointer position"
43
+ speed-dial,overlays,SpeedDial (compound/speed-dial),—,"floating trigger button that fans a bounded set of icon actions out along one axis; the trigger doubles as the close affordance"
42
44
  popover,overlays,Popover (element/popover),Popover,"anchored floating panel, dismiss on outside/ESC"
43
45
  hover-card,overlays,HoverCard (element/popover),Hover Card,"preview panel on hover intent, delayed open/close"
44
46
  tooltip,overlays,Tooltip (element/tooltip),Tooltip,"short hint on hover/focus, never load-bearing content"
45
47
  data-table,collections,"Table + defineColumn (block/table)","<Table> + TanStack Table","semantic <table>, sticky header, tabular-nums, real pagination, status chips"
46
48
  table-primitives,collections,UiTable* family (block/ui-table),Table,"bare styled <table>/<thead>/<tbody> parts"
47
49
  list,collections,"List + ListToolbar + ListPagination (block/list)",—,"<ul> of rows with toolbar and pager"
48
- record-header,collections,"Header — config-driven (block/app-header)",—,"page-top block: breadcrumbs, title, status bar, actions"
49
- page-header-stats,collections,"PageHeader — Stat/Crumb/BackLink (block/page-header)",—,"title row + KPI strip + back link"
50
- gantt-timeline,collections,GanttChart (activity-timeline),—,"time-scaled task rows, date axis, horizontal scroll"
50
+ record-header,collections,,—,"page-top block: breadcrumbs, title, status bar, actions"
51
+ page-header-stats,collections,"Heading (element/heading) · Stat (element/stat) — composed at the call site",—,"title row + KPI strip + back link"
52
+ gantt-timeline,collections,,—,"time-scaled task rows, date axis, horizontal scroll"
51
53
  changes-feed,collections,ChangesList (block/changes-list),—,"grouped change-history feed, entity + operation labels"
52
- message-thread,collections,"Message family + MessageScroller (compound/message, block/message-scroller)",—,"scrolling feed pinned to bottom, jump-to-new button"
53
- chat-bubble,collections,BubbleGroup + Bubble (element/bubble),—,"aligned bubble rows with reactions row"
54
- attachment,collections,Attachment family (compound/attachment),—,"file card: media, title, actions, error state, dashed idle border"
54
+ message-thread,collections,"MessageScroller + useMessageScroller (block/message-scroller)",—,"scrolling feed pinned to bottom, jump-to-new button"
55
+ chat-bubble,collections,,—,"aligned bubble rows with reactions row"
56
+ attachment,collections,"FilePreview (compound/file-preview)",—,"file card: media, title, actions, error state, dashed idle border"
55
57
  thread-marker,collections,"Marker — separator/border variants (element/marker)",—,"inline separator row with icon and label"
56
58
  json-viewer,collections,,—,"react18-json-view collapsible tree, mono"
57
59
  virtualized-list,collections,,—,"react-window fixed-row virtualization for 1k+ rows"
@@ -65,79 +67,79 @@ file-tree,navigation,FileTree (block/sidebar),—,"nested tree with disclosure c
65
67
  breadcrumb,navigation,Breadcrumb (compound/breadcrumb),Breadcrumb,"<nav aria-label=breadcrumb>, current page unlinked"
66
68
  tabs,navigation,Tabs (compound/tabs),Tabs,"role=tablist, arrow-key switching, one panel per tab"
67
69
  pagination,navigation,Pagination (compound/pagination),Pagination,"page buttons + prev/next, aria-current on active page"
68
- navbar,navigation,Navbar (block/app-header),—,"horizontal top <nav> with links and actions"
70
+ navbar,navigation,,—,"horizontal top <nav> with links and actions"
69
71
  navigation-menu,navigation,NavigationMenu (compound/navigation-menu),Navigation Menu,"top-level nav with flyout panels"
70
72
  menubar,navigation,Menubar (compound/menubar),Menubar,"desktop-app menu strip with full keyboard model"
71
- slide-out-menu,navigation,SlideOutMenu (block/slide-out-menu),—,"off-canvas nav panel for narrow viewports"
72
73
  icon-rail,navigation,VerticalIconBar (block/vertical-icon-bar),—,"narrow vertical icon strip with tooltips"
73
74
  stepper,navigation,,—,"ordered steps with done/current/todo states, aria-current=step"
74
- alert,feedback,Alert (element/alert),Alert,"role=alert block, tone via border + icon"
75
+ alert,feedback,Alert (compound/alert),Alert,"role=alert block, tone via border + icon"
75
76
  empty-state,feedback,EmptyState (compound/empty-state),Empty,"icon + heading + motivation line + primary action"
76
77
  error-boundary,feedback,ErrorBoundary (lib/error-boundary),—,"React boundary rendering a retry block"
77
78
  not-found,feedback,NotFound (compound/empty-state),—,"404 block with a way back"
78
79
  skeleton,feedback,Skeleton (element/skeleton),Skeleton,"pulsing placeholder matching the final layout"
79
80
  loader,feedback,Loader (element/loader),Spinner,"centered spinner with optional label"
80
- text-spinner,feedback,"Braille (braille) · spinner frames (element/loader)",—,"animated unicode frames inline with text"
81
+ text-spinner,feedback,,—,"animated unicode frames inline with text"
81
82
  progress,feedback,Progress (element/progress),Progress,"role=progressbar with aria-valuenow"
82
83
  toast,feedback,Toaster (compound/sonner),Sonner,"stacked corner notifications, auto-dismiss, action slot"
83
- typing-indicator,feedback,TypingIndicator (element/typing-indicator),—,"three-dot pulse row"
84
- sparkline,charts,"Spark — line|bar|pie (element/spark) · Sparkline (sparkline)",—,"inline no-axis SVG mark, token or self-contained color"
85
- inline-bar,charts,BarGraph (element/bar-graph),—,"compact bar SVG with skeleton, token colors"
86
- inline-line,charts,LineGraph (line-graph),—,"compact line SVG with skeleton"
87
- pie-donut,charts,"PieGraph — pie|half-pie|doughnut|half-doughnut (pie-graph)",—,"SVG arcs, max ~6 slices, center slot in doughnut"
88
- segmented-progress,charts,ProgressBar (progress-bar),—,"stacked horizontal segments with legend"
89
- area-chart,charts,"AreaChart + Area/RangeArea marks (charts)",Chart (recharts),"axes + gridlines + filled series + tooltip"
90
- bar-chart,charts,"BarChart + ParetoLine, BarSort, stack modes (charts)",Chart (recharts),"grouped/stacked bars, value labels opt-in"
91
- line-chart,charts,"LineChart + projection/reference overlays (charts)",Chart (recharts),"multi-series lines, reference lines, brush"
92
- scatter-chart,charts,ScatterChart + Scatter (charts),—,"x/y point field, hover ring, box select"
93
- composed-chart,charts,ComposedChart (charts),—,"mixed marks over one shared scale pair"
94
- histogram,charts,Histogram + computeHistogramBins (charts),—,"binned bars over a continuous axis"
84
+ typing-indicator,feedback,"Loader — pulsing dots (element/loader)",—,"three-dot pulse row"
85
+ sparkline,charts,"Spark — line|bar|pie (element/spark)",—,"inline no-axis SVG mark, token or self-contained color"
86
+ status-timeline,charts,"StatusTimeline + .Skeleton (compound/status-timeline)",—,"one stacked bar per day of uptime status, hover card per day — no axis, not a bar chart"
87
+ inline-line,charts,"Spark — line (element/spark)",—,"compact line SVG with skeleton"
88
+ pie-donut,charts,"Chart — pie spec (block/chart)",—,"SVG arcs, max ~6 slices, center slot in doughnut"
89
+ segmented-progress,charts,"Chart — progress spec (block/chart)",—,"stacked horizontal segments with legend"
90
+ area-chart,charts,"Chart — area spec (block/chart)",Chart (recharts),"axes + gridlines + filled series + tooltip"
91
+ bar-chart,charts,"Chart — bar spec (block/chart)",Chart (recharts),"grouped/stacked bars, value labels opt-in"
92
+ line-chart,charts,"Chart — line spec (block/chart)",Chart (recharts),"multi-series lines, reference lines, brush"
93
+ scatter-chart,charts,"Chart — scatter spec (block/chart)",—,"x/y point field, hover ring, box select"
94
+ composed-chart,charts,"Chart — composed spec (block/chart)",—,"mixed marks over one shared scale pair"
95
+ histogram,charts,"Chart — bar spec with mode: histogram (block/chart)",—,"binned bars over a continuous axis"
95
96
  bullet-chart,charts,"Bullet (gauge; also charts)",—,"measure bar + target tick + zone bands"
96
- funnel,charts,"Funnel + FunnelTooltip/FunnelLegend (funnel)",—,"ordered stage bands, percent-of-previous labels"
97
- gauge,charts,"Gauge — arc or notch track, zones, target (gauge)",—,"value arc with threshold zones and center readout"
98
- contribution-heatmap,charts,Heatmap + chrome (heatmap),—,"calendar cell grid with level legend"
99
- matrix-heatmap,charts,MatrixHeatmap + legend/tooltip (heatmap),—,"x-by-y cell matrix on the tokenized ramp"
100
- radar,charts,Radar (radar),—,"spider axes with series polygons"
101
- ring,charts,Ring (ring),—,"segmented circular progress with center info"
102
- chart-chrome,charts,"ChartEmpty · ChartHoverCard · ChartLegend (chart-primitives)",—,"shared empty/tooltip/legend chrome for inline charts"
97
+ funnel,charts,"Chart — funnel spec (block/chart)",—,"ordered stage bands, percent-of-previous labels"
98
+ gauge,charts,"Chart — gauge spec (block/chart)",—,"value arc with threshold zones and center readout"
99
+ contribution-heatmap,charts,"Chart — heatmap-calendar spec (block/chart)",—,"calendar cell grid with level legend"
100
+ matrix-heatmap,charts,"Chart — heatmap-matrix spec (block/chart)",—,"x-by-y cell matrix on the tokenized ramp"
101
+ radar,charts,"Chart — radar spec (block/chart)",—,"spider axes with series polygons"
102
+ ring,charts,"Chart — ring spec (block/chart)",—,"segmented circular progress with center info"
103
+ chart-chrome,charts,"Chart chrome — legend, tooltip and empty state set by the spec theme · ChartErrorCard (block/chart)",—,"shared empty/tooltip/legend chrome for inline charts"
103
104
  card,layout,Card family (block/card),Card,"bordered section: header, content, footer"
104
- stat-card,layout,Statistics family (statistics),—,"number + comparison + trend + state + door, composable regions"
105
+ stat-card,layout,"Stat (element/stat) · Chart — stat spec (block/chart)",—,"number + comparison + trend + state + door, composable regions"
105
106
  accordion,layout,Accordion (compound/accordion),Accordion,"stacked disclosure sections, single or multiple open"
107
+ collapsible-panel,layout,SidePanel (block/side-panel),—,"in-flow side region docked left or right that collapses to a rail instead of disappearing; siblings reflow, nothing overlays"
106
108
  collapsible,layout,"Collapsible (element/collapsible) · CollapsiblePanel (compound/accordion)",Collapsible,"button-controlled region with hidden state"
107
109
  split-panes,layout,ResizablePanelGroup (block/resizable),Resizable,"draggable divider panes, keyboard resize"
108
110
  scroll-area,layout,ScrollArea (element/scroll-area),Scroll Area,"styled overflow container, visible-on-hover scrollbar"
109
111
  separator,layout,"Separator (element/separator) · SeparatorWithText (element/separator)",Separator,"<hr>/role=separator, optional centered label"
110
112
  aspect-ratio,layout,AspectRatio (element/aspect-ratio),Aspect Ratio,"CSS aspect-ratio box"
111
113
  carousel,layout,Carousel (compound/carousel),Carousel,"scroll-snap track with prev/next controls"
112
- bento-grid,layout,BentoGrid + BentoCard (bento-grid),—,"asymmetric feature grid with spanning cells"
113
- editable-card,layout,EditableCard (compound/editable-card),—,"card with edit-in-place body and commit footer"
114
+ bento-grid,layout,,—,"asymmetric feature grid with spanning cells"
115
+ editable-card,layout,,—,"card with edit-in-place body and commit footer"
114
116
  drag-reorder,layout,Sortable family (compound/sortable),—,"dnd-kit list reorder, keyboard sortable, drag overlay ghost"
115
- text,typography,Text (element/text),Typography,"semantic p/span with size and weight scale"
116
- badge,typography,Badge (element/badge),Badge,"small status pill, tokenized tone"
117
+ text,typography,,Typography,"semantic p/span with size and weight scale"
118
+ badge,typography,"Chip (element/chip)",Badge,"small status pill, tokenized tone"
117
119
  chip,typography,"Chip (element/chip) · Option display (element/select)",—,"removable tag, optional color dot"
118
120
  code,typography,Code (element/code),—,"inline <code>, mono"
119
121
  kbd,typography,Kbd (element/kbd),Kbd,"<kbd> keycap"
120
122
  label,typography,Label (element/label),Label,"<label for> bound to a control"
121
123
  link,typography,Link (element/link),—,"<a> with underline discipline"
122
- secure-text,typography,SecureText (secure-text),—,"masked value with reveal toggle, mono"
124
+ secure-text,typography,"SecureText · maskText (element/secure-text)",—,"masked value with reveal toggle, mono"
123
125
  markdown,typography,Markdown (element/markdown),—,"react-markdown rendering with typographic defaults"
124
126
  avatar,media,Avatar (element/avatar),Avatar,"image with initials fallback, round"
125
127
  copy-button,media,CopyButton (compound/copy-button),—,"clipboard write with copied-state swap"
126
- icon,media,"IconRenderer (element/icon-renderer) + icons-data (lib/icons-data)",—,"lucide icon by name, consistent stroke and size grid"
127
- voice-waveform,media,"LiveWaveform (element/live-waveform) · MicIcon (element/mic-icon)",—,"animated level bars from mic input"
128
+ icon,media,"Icon · IconPicker (compound/icon-picker) + iconsData (lib/icons-data)",—,"lucide icon by name, consistent stroke and size grid"
129
+ voice-waveform,chat,"MicInput (compound/mic-input)",—,"latched dictation button plus mic level bars; button={false} for the meter alone, recorder={…} for record-then-review"
128
130
  brand-logo,media,"Logo — blue-black|white-white|lime-white|blue-white (element/logo)",—,"inline SVG logo sized by height"
129
131
  shine-border,media,ShineBorder (effect/shine-border),—,"animated gradient border wrapper, decorative"
130
- theme-toggle,media,ThemeToggler (element/theme-toggler),—,"light/dark switch writing data-theme"
132
+ theme-toggle,media,"ThemeToggle (effect/theme-toggle)",—,"light/dark switch toggling the .dark class on the root element"
131
133
  view-switcher,media,ViewSwitcher (compound/tabs),—,"segmented control switching view-type projections"
132
134
  animated-number,motion,"AnimatedNumber (element/animated-number) · SlidingNumber (element/animated-number)",—,"tick-up numerals, renders plain text until JS ready"
133
- stagger-entrance,motion,AnimatedGroup (effect/animated-group),—,"staggered child arrival, identity moments only, never on 100×/day actions"
135
+ stagger-entrance,motion,"AnimatedList (effect/animated-list)",—,"staggered child arrival, identity moments only, never on 100×/day actions"
134
136
  text-effects,motion,"TextEffect · TextMorph · TextScramble · TextShimmer (effect/*)",—,"per-character text animation, low-frequency surfaces only"
135
137
  morphing-overlay,motion,"MorphingDialog · MorphingPopover (effect/*)",—,"trigger morphs into overlay, interruptible"
136
- panel-transitions,motion,"TransitionPanel · Disclosure · motion Accordion (effect/*) · animated Tabs (animate-ui)",—,"crossfade/slide between sibling panels, height-animated reveal, interruptible"
138
+ panel-transitions,motion,"TransitionPanel (effect/transition-panel) · Accordion (compound/accordion) · Tabs (compound/tabs)",—,"crossfade/slide between sibling panels, height-animated reveal, interruptible"
137
139
  decorative-effects,motion,"Spotlight · GlowEffect · AnimatedBackground · InfiniteSlider · Highlight (effect/*, effect/*)",—,"pointer-tracked washes and loops, decorative only, never on data"
138
- dynamic-toolbar,motion,ToolbarDynamic (effect/toolbar-dynamic),—,"toolbar expanding to contextual actions"
140
+ dynamic-toolbar,motion,,—,"toolbar expanding to contextual actions"
139
141
  utility-hooks,hooks,"useClickOutside · useFileUpload · useMediaQuery · useIsMobile (lib/*)",—,"outside-click, file-upload state, matchMedia boolean, <768px boolean"
140
142
  class-merge,utils,cn (lib/utils),cn,"clsx + tailwind-merge"
141
- formatting-utils,utils,"formatDate (lib/format) · generateId (lib/id) · getErrorMessage (lib/handle-error)",—,"Intl date formatting, nanoid ids, error-to-message normalization"
143
+ formatting-utils,utils,"formatDate (lib/format) · generateId (lib/id) · formatRelativeTime (lib/time)",—,"Intl date formatting, nanoid ids, relative-time strings"
142
144
  composition-utils,utils,"getStrictContext (lib/get-strict-context) · composeRefs (lib/compose-refs)",—,"throwing Provider/hook pair, ref composition"
143
145
  ```
@@ -113,7 +113,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
113
113
  | Derive/update an owning doc's `owns:` (feature, package, system) | [`references/frontmatter.md`](references/frontmatter.md) |
114
114
  | Update existing doc | § Editing an existing doc (below) |
115
115
  | Instruction file (`CLAUDE.md`, a rule, an agent, skill prose) | [`references/instruction-style.md`](references/instruction-style.md) — not these templates; every-load prose stays lean, and the present-tense rule still applies |
116
- | Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`10`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
116
+ | Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`13`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
117
117
  | **No template matches** | [`references/untemplated.md`](references/untemplated.md) — shape decisions + the co-authoring workflow |
118
118
 
119
119
  ---
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.35",
4
+ "version": "0.2.36",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -10,5 +10,5 @@
10
10
  "seeds": [],
11
11
  "tools": [],
12
12
  "docsRoot": "docs",
13
- "digest": "f4807d25316f"
13
+ "digest": "c2d4fb33502b"
14
14
  }
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/generate-ownership-index.mjs\"",
9
+ "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/generate-ownership-index.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"",
10
10
  "timeout": 10
11
11
  }
12
12
  ]
@@ -113,7 +113,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
113
113
  | Derive/update an owning doc's `owns:` (feature, package, system) | [`references/frontmatter.md`](references/frontmatter.md) |
114
114
  | Update existing doc | § Editing an existing doc (below) |
115
115
  | Instruction file (`CLAUDE.md`, a rule, an agent, skill prose) | [`references/instruction-style.md`](references/instruction-style.md) — not these templates; every-load prose stays lean, and the present-tense rule still applies |
116
- | Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`10`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
116
+ | Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`13`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
117
117
  | **No template matches** | [`references/untemplated.md`](references/untemplated.md) — shape decisions + the co-authoring workflow |
118
118
 
119
119
  ---
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "discussion",
3
3
  "name": "discussion",
4
- "version": "0.6.1",
4
+ "version": "0.6.2",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -10,5 +10,5 @@
10
10
  "seeds": [],
11
11
  "tools": [],
12
12
  "docsRoot": "docs",
13
- "digest": "37b98928394e"
13
+ "digest": "ed9548ee592a"
14
14
  }
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/update-topics.sh",
8
+ "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/update-topics.sh\"; [ -f \"$f\" ] || exit 0; bash \"$f\"",
9
9
  "timeout": 15,
10
10
  "statusMessage": "Syncing workspace metadata"
11
11
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.25",
4
+ "version": "1.5.26",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -10,5 +10,5 @@
10
10
  "seeds": [],
11
11
  "tools": [],
12
12
  "docsRoot": "docs",
13
- "digest": "d84075a6eda5"
13
+ "digest": "6448aac5edb5"
14
14
  }
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/generate-ownership-index.mjs\"",
9
+ "command": "f=\"$CLAUDE_PROJECT_DIR/.claude/hooks/generate-ownership-index.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\"",
10
10
  "timeout": 10
11
11
  }
12
12
  ]
@@ -113,7 +113,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
113
113
  | Derive/update an owning doc's `owns:` (feature, package, system) | [`references/frontmatter.md`](references/frontmatter.md) |
114
114
  | Update existing doc | § Editing an existing doc (below) |
115
115
  | Instruction file (`CLAUDE.md`, a rule, an agent, skill prose) | [`references/instruction-style.md`](references/instruction-style.md) — not these templates; every-load prose stays lean, and the present-tense rule still applies |
116
- | Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`10`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
116
+ | Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`13`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
117
117
  | **No template matches** | [`references/untemplated.md`](references/untemplated.md) — shape decisions + the co-authoring workflow |
118
118
 
119
119
  ---
@@ -227,10 +227,19 @@ function groupHasCommand(group, command) {
227
227
  return (group.hooks || []).some((h) => h.command === command);
228
228
  }
229
229
 
230
+ // The harness script a hook command runs, by its `.claude/hooks/<name>` path — the identity
231
+ // that survives a change to the command's shape. Null for a command that runs no such script.
232
+ function hookScript(command) {
233
+ const found = /\.claude\/hooks\/([\w.-]+)/.exec(command || '');
234
+ return found ? found[1] : null;
235
+ }
236
+
230
237
  // Deep-merge the bundle's hooks.json object into <project>/.claude/settings.json under
231
238
  // "hooks", preserving every existing key. Idempotent: for each event, an incoming entry
232
239
  // merges into the matcher-group of the same matcher, skipping any hook whose command
233
- // already exists there; a new matcher creates a new group. Returns count of hooks added.
240
+ // already exists there; a hook already registered for the same script under an older command
241
+ // shape is replaced in place, never doubled; a new matcher creates a new group. Returns the
242
+ // count of hooks added or replaced.
234
243
  function mergeHooks(targetDir, bundledHooksJson) {
235
244
  if (!existsSync(bundledHooksJson)) return 0;
236
245
  const bundled = readJson(bundledHooksJson);
@@ -261,7 +270,10 @@ function mergeHooks(targetDir, bundledHooksJson) {
261
270
  match.hooks = match.hooks || [];
262
271
  for (const hook of group.hooks || []) {
263
272
  if (groupHasCommand(match, hook.command)) continue;
264
- match.hooks.push(hook);
273
+ const script = hookScript(hook.command);
274
+ const staleIndex = script ? match.hooks.findIndex((h) => hookScript(h.command) === script) : -1;
275
+ if (staleIndex === -1) match.hooks.push(hook);
276
+ else match.hooks[staleIndex] = hook;
265
277
  added += 1;
266
278
  }
267
279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.23",
3
+ "version": "0.20.25",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "722680de8132"
45
+ "etnaFingerprint": "c24bc6f1af10"
46
46
  }