navori 0.10.0 → 0.10.1

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.
Files changed (34) hide show
  1. package/README.md +6 -2
  2. package/dist/assets/core/core-assets/agents/architect.md +1 -1
  3. package/dist/assets/core/core-assets/agents/implementer.md +5 -4
  4. package/dist/assets/core/core-assets/agents/orchestrator.md +3 -3
  5. package/dist/assets/core/core-assets/agents/publisher.md +17 -16
  6. package/dist/assets/core/core-assets/agents/reviewer.md +2 -2
  7. package/dist/assets/core/core-assets/agents/scribe.md +4 -0
  8. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +59 -4
  9. package/dist/assets/core/core-assets/hooks/managed-drift-watch.sh +18 -2
  10. package/dist/assets/core/core-assets/hooks/routing-watch.sh +21 -5
  11. package/dist/assets/core/core-assets/lib-skills/better-auth.md +68 -0
  12. package/dist/assets/core/core-assets/lib-skills/eas-release.md +55 -0
  13. package/dist/assets/core/core-assets/lib-skills/expo-router.md +61 -0
  14. package/dist/assets/core/core-assets/lib-skills/hono.md +58 -0
  15. package/dist/assets/core/core-assets/lib-skills/react-email.md +55 -0
  16. package/dist/assets/core/core-assets/lib-skills/react-native-reusables.md +1 -1
  17. package/dist/assets/core/core-assets/lib-skills/shadcn-base-ui.md +63 -0
  18. package/dist/assets/core/core-assets/lib-skills/tailwind-v4.md +68 -0
  19. package/dist/assets/core/core-assets/lib-skills/tanstack-router.md +68 -0
  20. package/dist/assets/core/core-assets/lib-skills/uniwind.md +58 -0
  21. package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
  22. package/dist/assets/core/core-assets/managed/orquestacion.md +2 -1
  23. package/dist/assets/core/core-assets/presets/monorepo-turbopnpm/skills/turbo-workspaces.md +1 -1
  24. package/dist/assets/core/core-assets/settings/settings-base.json +1 -0
  25. package/dist/assets/core/core-assets/skills/review-diff.md +4 -4
  26. package/dist/assets/core/core-assets/skills/scoped-gate.md +14 -5
  27. package/dist/assets/core/core-assets/skills/solution-design.md +6 -2
  28. package/dist/assets/core/core-assets/skills/verify-before-done.md +7 -4
  29. package/dist/assets/plugins/jscpd/skills/jscpd-review.md +9 -9
  30. package/dist/assets/plugins/semgrep/skills/semgrep-review.md +11 -8
  31. package/dist/index.js +344 -339
  32. package/package.json +2 -2
  33. package/dist/assets/core/core-assets/lib-skills/nativewind.md +0 -57
  34. package/dist/assets/plugins/semgrep/managed/semgrep-protocol.md +0 -15
package/README.md CHANGED
@@ -36,12 +36,14 @@ npx navori init
36
36
 
37
37
  ```bash
38
38
  # Modo opinado: cero preguntas, harness completo sin instalar software externo
39
- # (engram siempre activo, +gh si el repo tiene remote de GitHub)
39
+ # (engram siempre activo, +gh si el repo tiene remote de GitHub).
40
+ # Avisa si falta el binario de algún plugin habilitado y cómo instalarlo.
40
41
  cd ~/tu-repo
41
42
  navori init --recommended
42
43
 
43
44
  # + proveedores externos (tgrep, codegraph, semgrep, jscpd, acli) + pre-commit hook +
44
- # scan-monorepo + project block estricto — requiere instalar los binarios de esos proveedores
45
+ # scan-monorepo + project block estricto — requiere instalar los binarios de esos proveedores.
46
+ # También avisa si falta algún binario y cómo instalarlo, sin instalarlo nunca.
45
47
  navori init --full
46
48
 
47
49
  # O wizard interactivo con detección de stack
@@ -175,6 +177,8 @@ El harness que genera `navori` trae permisos seguros desde el arranque, para que
175
177
  - **Lo catastrófico se rechaza** (`deny`): `rm -rf /`, `sudo rm`, `mkfs`, …
176
178
  - Un hook `guard-destructive` actúa como backstop adicional.
177
179
 
180
+ **Estado efímero fuera del árbol**: los dos hooks del harness (`managed-drift-watch.sh` y `routing-watch.sh`) escriben su estado en `<git-common-dir>/navori/` — fuera del árbol de trabajo, invisible a `git status`. Además, `render` y `sync` escriben un `.claude/.gitignore` versionado que ignora `progress/`, `worktrees/` y `settings.local.json`, impidiendo que esos paths aparezcan como untracked en `git status`. Si `codex` está habilitado, también genera `.codex/.gitignore` con solo entradas efímeras de ese directorio. Para repos actualizados, también ignora los archivos legacy `.claude/.managed-drift-stamp` y `.claude/.routing-watch/`. Nada que los engines necesiten se ignora.
181
+
178
182
  ## Workspace + tickets cross-repo
179
183
 
180
184
  Si un ticket toca varios repos (frontend + backend + microservicio), el workspace te da un punto único:
@@ -21,7 +21,7 @@ The orchestrator hands you a task that fired a `solution-design` signal (new sha
21
21
  - "Derive the decision drivers from the project's own rules (DIRECTION, CLAUDE.md, EXTENDING, `quality-attributes`) before you list any option."
22
22
  - "Explore at least three rungs — the existing pattern, an extension, a new abstraction. A discarded rung gets one line with its evidence; a surviving one is developed in full."
23
23
  - "Recommend the option that best fits the drivers, not the cheapest by default."
24
- - "Verify every 'already exists' claim against `origin/main`."
24
+ - "Verify every 'already exists' claim against `origin/{{branchBase}}` after `git fetch origin {{branchBase}}`; if the fetch fails or the ref doesn't exist, name the ref you actually used — or mark the claim *unverified* with the cause."
25
25
 
26
26
  ## Protocol
27
27
 
@@ -56,8 +56,8 @@ When the encargo opens with `workplan: <feature>`, read `.claude/progress/workpl
56
56
  - **Strong typing, `any` forbidden in new code.** Define correct types before moving on. Use `unknown` + narrowing, generics, or domain types. Cover parameters, returns, callbacks, events, props, hooks, and service responses. If typing it well is genuinely impossible (third-party lib without types), a `// any justified: <reason>` comment — last resort, not a shortcut.
57
57
  - **No hardcode**: secrets / URLs / endpoints via env vars (`process.env.*`, `import.meta.env.*`, depending on the stack).
58
58
  - **No `console.log`** in code that will be merged (guard with `import.meta.env.DEV` or the runtime's equivalent).
59
- - **Zero new errors** introduced by your code in the quality gate tools (vs. baseline) — see the evidence table below for the predates-you check. Returning with any tool red (because of your change) is automatic grounds for `CHANGES_REQUESTED`.
60
- - **Never mutate or discard the shared working tree**: no stashing, no checkout/reset that discards local changes, no working-tree clean — these hit the `ask` permission rule and can stall a background agent indefinitely, and in the repo root they'd destroy other parallel agents' work. Same reasoning for scratch files: leave them, don't clean them with a recursive delete.
59
+ - **Zero new errors** introduced by your code in the quality gate tools (vs. baseline) — classify per `verify-before-done`'s Failure attribution, never by diff location alone; see the evidence table below. Returning with any tool red (because of your change) is automatic grounds for `CHANGES_REQUESTED`.
60
+ - **Never mutate or discard the shared working tree**: no stashing, no checkout/reset that discards local changes, no working-tree clean — these hit the `ask` permission rule and can stall a background agent indefinitely. Same reasoning for scratch files: leave them, don't clean them with a recursive delete.
61
61
  - **JSDoc** mandatory on public exports and functions >15 lines or with dense conditional logic.
62
62
  - **SDD traceability** (only if the feature has `{{sdd.specsDir}}/<feature>/tasks.md`, see the SDD block in `CLAUDE.md`): each `R<n>` in your batch is covered by ≥1 test, and each test references its requirements with a `// Covers: R<n>` comment above the case. Without full traceability the `reviewer` rejects.
63
63
  - **Guard/policy coverage** (only if your task introduces or modifies a guard, policy or permission check): your report carries the enumeration, not just the diff — every entry point that mutates the same resource (routes, bulk/admin variants, jobs, scripts) with its `file:line` evidence, each marked covered or excluded with the reason. Locate them with `locate-code`; an entry point you didn't list is one the `reviewer` has to rediscover.
@@ -91,9 +91,9 @@ Before returning `done -> .claude/progress/impl_<feature>.<!-- navori:if-not scr
91
91
  | `{{qualityGate.fast}}` green | Full command run **this turn** with exit 0 | "ran it before", "should be green" |
92
92
  | UI validated in the browser (only when the user asked for a visual check) | Repro step + observed state via the repo's browser tool (e.g. `playwright-cli`) this turn | "looks fine in the code" |
93
93
  | Bug fixed (if applicable) | Reproduce the original symptom and see it NOT happen | "code changed, assumed fixed" |
94
- | Zero new errors in typecheck/lint | `git diff --name-only {{branchBase}}` — a failure outside that file list predates you | "lint said OK" with no baseline |
94
+ | Zero new errors in typecheck/lint | Classify per `verify-before-done`'s Failure attribution: state per failure, demonstrated over `{{branchBase}}` | "lint said OK" with no baseline |
95
95
 
96
- If any claim can't be backed with fresh evidence this turn, declare it EXPLICITLY in the report. Never infer success.
96
+ If any claim can't be backed with evidence you ran this turn, declare it EXPLICITLY in the report. Never infer success.
97
97
 
98
98
  <!-- navori:if-not scribeOwnsMarkdown -->
99
99
  ## Closing report
@@ -148,6 +148,7 @@ Write `.claude/progress/impl_<feature>.json` — the only artifact you produce,
148
148
  "worktree": "<absolute worktree path>",
149
149
  "branch": "<branch>",
150
150
  "commits": ["<sha>"],
151
+ "head": "<40-hex sha: git rev-parse HEAD at the end>",
151
152
  "filesTouched": ["<path>"],
152
153
  "rootCause": "<file:line + why, bugfix only>",
153
154
  "verification": { "command": "{{qualityGate.fast}}", "exitCode": 0, "summary": "<n files / n tests>" },
@@ -9,7 +9,7 @@ maxWords: 3050
9
9
 
10
10
  # Orchestrator Playbook (embodied by the main agent)
11
11
 
12
- > This file is a **depth reference** — the orchestrator role **is embodied by the main agent**, not a subagent. The essential mechanics (escalation table, parallelism, synthesis) live in the "## Role: orchestrator" block, which the `SessionStart` hook delivers to the session — not to a subagent, which is the point: only the main agent can act on it. Here is the extended detail and, below, the **Project rules**. Do NOT invoke `Agent(subagent_type: orchestrator)`.
12
+ > This file is a **depth reference** — the orchestrator role **is embodied by the main agent**, not a subagent. The essential mechanics (escalation table, parallelism, synthesis) live in the "## Role: orchestrator" block, which the `SessionStart` hook delivers to the session, not to a subagent: only the main agent can act on it. Below: extended detail and the **Project rules**. Do NOT invoke `Agent(subagent_type: orchestrator)`.
13
13
 
14
14
  Your only job as orchestrator is to **decompose and coordinate**, never to implement. Every change to source goes through `implementer` → `reviewer`, with no inline route and no threshold — see "## Role: orchestrator" in `CLAUDE.md`.
15
15
 
@@ -36,7 +36,7 @@ One route removes the decision entirely. It is more expensive per change and tha
36
36
 
37
37
  When you start a complex task with a prior audit, **hand the implementer the path to `.claude/progress/audit_ticket_<ID>.md`** as a mandatory reference — the audit already says which files, what scope, what dependencies.
38
38
  <!-- navori:if scribeOwnsMarkdown -->
39
- **The `scribe` leg.** The `implementer` writes no Markdown (R1); when its `impl_<feature>.json` carries a non-empty `markdownRequests`, chain `implementer` → `scribe` → `reviewer` — the `scribe` applies the requested prose in the producer's own worktree and branch, in a commit of its own, before the `reviewer` sees the diff. A prose-only change (no code) skips the `implementer` entirely: `scribe` → `reviewer`. Model is chosen PER DISPATCH, not by config default: pass `{{models.scribe}}` when the scribe is only rendering the handoff, and `model: sonnet` on that `Agent` call when any `markdownRequests` path belongs to the diff that ships (R8) — `models.scribe` stays on its cheap default for the common case.
39
+ **The `scribe` leg.** The `implementer` writes no Markdown (R1); when its `impl_<feature>.json` carries a non-empty `markdownRequests`, chain `implementer` → `scribe` → `reviewer` — the `scribe` applies the requested prose in the producer's own worktree and branch, in a commit of its own, before the `reviewer` sees the diff. A prose-only change (no code) skips the `implementer` entirely: `scribe` → `reviewer`. Model is chosen PER DISPATCH, not by config default: pass `{{models.scribe}}` when the scribe is only rendering the handoff, and `model: sonnet` on that `Agent` call when any `markdownRequests` path belongs to the diff that ships (R8) — `models.scribe` stays on its cheap default for the common case. Before dispatching the `scribe` or the `reviewer`, run the handoff check the orchestration block names; dispatch only on `"status":"ok"`.
40
40
  <!-- /navori:if -->
41
41
  For a scoped question or a broad exploratory map (where does X live in the repo?), use `scout`. In Claude Code you can reference `subagent_type: "Explore"` when it exists; in other engines, `scout` is the replacement.
42
42
 
@@ -104,7 +104,7 @@ When you launch subagents, the **literal path** of the file each one must write
104
104
  done -> .claude/progress/<file>.md
105
105
  ```
106
106
 
107
- Those files are **input to the next step of the pipeline**, not chat summaries for a reader: the `reviewer` opens the `implementer`'s, the `publisher` opens the `reviewer`'s and its `receipt.txt`, and the `subagent-stop-handoff` hook flags one that lands empty or without its `Status:`/verdict line (that hook never sees one that didn't land at all — that check is yours). A host rule against writing report files does not reach them — it exempts files written as input to another tool, and these are exactly that. Say so in the encargo if a subagent hesitates.
107
+ Those files are **input to the next step of the pipeline**, not chat summaries for a reader: the `reviewer` opens the `implementer`'s, the `publisher` opens the `reviewer`'s and its `receipt.txt`, and the `subagent-stop-handoff` hook flags one that lands empty or without its `Status:`/verdict line (that hook never sees one that didn't land at all — that check is yours). A host rule against writing report files does not reach them — it exempts files written as input to another tool, and these qualify. Say so if a subagent hesitates.
108
108
 
109
109
  **Re-verify only the load-bearing claims.** AFTER its `done -> file` lands — not while it runs, which duplicates work in flight — check the claims your decision actually rests on: each cited `file:line` exists and says what the report says, plus the diff it touched. Don't re-run its investigation; take the rest from the report.
110
110
 
@@ -24,7 +24,7 @@ You own the **end of the cycle**: well-structured commits in the configured styl
24
24
  - Harness active and THIS feature's review — `.claude/progress/review_<feature>.md`, the single file the pre-flight below identifies by name — contains `CHANGES_REQUESTED` → no PR is created. Never scan the directory for it: a `CHANGES_REQUESTED` belonging to someone else's closed cycle must not abort your PR, exactly as another feature's `APPROVED` never unblocks it.
25
25
  - Quality gate red this turn.
26
26
 
27
- > **Two branches, one that decides:** `{{prTarget}}` is the PR's target branch — the one `gh pr create --base` receives and the one every diff below is computed against. The fork point (the branch this one was branched from) is a separate setting the repo declares on its own; in most repos the two name the same branch and the distinction costs you nothing. Where they differ, the fork-point diff is NOT the PR's, so the target always wins and you never have to work out which of the two a given name refers to.
27
+ > **Two branches, one that decides:** `{{prTarget}}` is the target for `gh pr create --base` and every diff below. The fork point is a separate setting; in most repos both name the same branch. Where they differ, the target branch wins — the fork-point diff is never the PR's.
28
28
 
29
29
  ## Mandatory pre-flight
30
30
 
@@ -100,11 +100,11 @@ For every live-file `DRIFT`, the JSON provides the approved blob and the exact i
100
100
 
101
101
  ### Gate: `{{qualityGate.full}}` green before the PR
102
102
 
103
- The PR gate is the FULL one, `{{qualityGate.full}}` — **not** the fast one, `{{qualityGate.fast}}`. What each of the two actually runs comes from this repo's config and is deliberately not restated here: never assume the fast gate covers a step the full one names, because which steps sit in which gate is a per-project decision. `full` must be green over the diff that ships. Two paths:
103
+ The PR gate is the FULL one, `{{qualityGate.full}}`, not `{{qualityGate.fast}}`. Which steps sit where is a per-project decision; don't assume the fast gate covers all full steps. Three paths:
104
104
 
105
- - **Reviewed (the normal path):** the `reviewer` already ran `{{qualityGate.full}}` green over this same diff in Pass 2 (evidence in `review_<feature>.md`, this cycle) and you **don't edit code** — trust it, don't re-run. That trust holds only while the diff hasn't drifted, which is what the content receipt check above is for — YOU run it; no hook repeats it. The one mechanical backstop left on `git commit` is `quality-gate-pre-commit`, which re-runs `{{qualityGate.fast}}` and blocks if it fails. Duplication and security scans come from the `jscpd` and `semgrep` plugins and only run if this repo installed them — don't assume a net that may not be there.
106
- - **Declared inline (no reviewer):** there's no review evidence to trust — YOU run `{{qualityGate.full}}` green in pre-flight before `gh pr create`. If it can outlive the Bash timeout, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its chained steps one by one in the foreground, never background them — you won't be re-woken to read the result.
107
- - ▶️ **Re-run `{{qualityGate.full}}` by hand** whenever the diff changed since the review (rebase/merge/follow-up edit) or there's no fresh evidence over the diff being committed — stale evidence doesn't count.
105
+ - **Reviewed:** the reviewer ran `{{qualityGate.full}}` green in Pass 2 (see `review_<feature>.md`). Skip re-running **only** when `navori receipt check` reports `"fresh":true`. The `quality-gate-pre-commit` hook re-runs `fast` on `git commit` and blocks if it fails. Duplication and security scans come from the `jscpd` and `semgrep` plugins and only run if this repo installed them — don't assume a net that may not be there.
106
+ - **`"fresh":false`:** no trustworthy evidence — YOU run `{{qualityGate.full}}` green in pre-flight before `gh pr create`. Follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row if it outlives the timeout.
107
+ - **Declared inline (no reviewer):** no review evidence either — run `{{qualityGate.full}}` yourself.
108
108
 
109
109
  Never open the PR with the gate red.
110
110
 
@@ -119,35 +119,35 @@ Never open the PR with the gate red.
119
119
  4. If you touch potentially sensitive files (`.env*`, credentials, odd lockfiles), **flag the user before staging**.
120
120
  5. `git add <files>` (prefer explicit over `git add -A`).
121
121
  6. `git commit -m "..."` with a HEREDOC for the body if applicable.
122
- 7. Validate with `git status` that the commit landed.
123
- 8. **Consume the receipt:** `rm -f .claude/progress/receipt.txt`. The approval is now frozen into the commit; leaving it armed could false-block a later feature that touches the same file.
122
+ 7. Validate with `git status` that the commit landed. Foreign modified files are reported as an observation and never discarded, restored or reverted.
123
+ 8. **Consume the receipt:** `mv -f .claude/progress/receipt.txt .claude/progress/receipt.consumed.txt`. The approval is now frozen into the commit; renaming it (instead of deleting it) keeps the evidence on disk without it being rearmed — a plain `check` never reads a consumed receipt again, only the opt-in flag documented in `cierre-sesion.md` does.
124
124
 
125
125
  ## PR flow
126
126
 
127
- 1. **Gather context** (curated, don't dump the whole repo). The PR diff is against `{{prTarget}}` (what GitHub will show):
127
+ 1. **Gather context** (curated, not the whole repo). The PR diff is against `{{prTarget}}`:
128
128
  - `git log origin/{{prTarget}}..HEAD --oneline` — commits included.
129
129
  - `git diff origin/{{prTarget}}...HEAD --stat` — always.
130
- - `git diff origin/{{prTarget}}...HEAD` — only if the diff < 500 lines. If larger, use only the stat + file list + the hunks of the 2–3 most relevant files.
131
- - **Commit drag** — only when the fork point and the target are different branches. Don't assert that they differ: let the shell settle it, so the ordinary case (both names resolve to the same branch, nothing can drag) simply doesn't run instead of producing a comparison of a branch with itself.
130
+ - `git diff origin/{{prTarget}}...HEAD` — only if diff < 500 lines. If larger, stat + file list + hunks of 2–3 most relevant files only.
131
+ - **Commit drag** — only when fork point and target differ. Let the shell settle it:
132
132
 
133
133
  ```bash
134
- base={{branchBase}} # the fork point, as the repo declares it
134
+ base={{branchBase}}
135
135
  if [ "$base" != "{{prTarget}}" ]; then
136
136
  git fetch origin "$base" --quiet
137
137
  git rev-list --count "origin/{{prTarget}}..origin/$base"
138
138
  fi
139
139
  ```
140
140
 
141
- A count > 0 means the fork point is ahead of `{{prTarget}}` and your PR drags those foreign commits: warn the user and suggest rebasing onto `{{prTarget}}` before opening.
142
- - Ticket if applicable: branch name (e.g. `BT-1234-fix-x` → `BT-1234`) or a reference in the first commit.
143
- - `.claude/progress/impl_<feature>.md` if it exists — non-obvious decisions.
141
+ Count > 0 means your PR drags foreign commits: warn the user and suggest rebase.
142
+ - Ticket if applicable: branch name (e.g. `BT-1234-fix-x` → `BT-1234`) or first commit.
143
+ - `.claude/progress/impl_<feature>.md` — non-obvious decisions.
144
144
 
145
145
  2. **Draft title and body**:
146
146
  - **Title**: follows the configured commit style (`{{commits}}`), ≤70 chars, imperative and without a trailing period.
147
147
  - **Body**: the repo's exact template (below). No empty sections.
148
148
 
149
149
  3. **Validate** before firing `gh`:
150
- - Every body bullet backed by the diff or the implementer's report. **No handoff on disk** (`impl_<feature>.*`, `review_<feature>.md`) → draft from the diff and the issue only; drop any claim neither backs (#1001).
150
+ - Every claim in the title and body — path, command, count or decision — traces to the cycle's handoffs (`impl_<feature>.*`, `review_<feature>.md`), `git log`/`git diff` against the base, or the spec; nothing else backs a claim, so no inferred path, command, count or decision goes in. **No handoff on disk** → draft from the diff and the issue only. A fact you can't trace is omitted, or reported to the orchestrator — never filled in (#1001, #1028).
151
151
  - If you mention a file that is NOT in `--stat`, remove it.
152
152
  - No emojis. No AI attribution: no `Co-Authored-By` trailer for an AI, no "Generated with…" footer, no mention of Claude or any other AI tool in the title or body.
153
153
 
@@ -189,7 +189,7 @@ Never open the PR with the gate red.
189
189
 
190
190
  Every comment, review or ticket update you publish — on a PR, an issue or a Jira ticket — follows one rule: **the body lives in a file, never inline.** Bodies inline in a command truncate or mis-render under the shell's own quoting, and an inline `--body` gives the pre-flight nothing to inspect before it fires.
191
191
 
192
- 1. Write the text into a file inside the progress directory (e.g. `.claude/progress/comment_<feature>.md`). The content comes ONLY from a handoff artifact already on disk (`impl_<feature>.md`, `review_<feature>.md`, the PR/issue itself) — never invent technical claims that aren't already written down somewhere upstream.
192
+ 1. Write the text into a file in `.claude/progress/` (e.g. `comment_<feature>.md`). Content comes ONLY from handoff artifacts on disk (`impl_<feature>.md`, `review_<feature>.md`, the PR/issue) — never invent claims not already documented.
193
193
  2. Publish it with the flag that reads the file, per channel:
194
194
 
195
195
  | Channel | Command | File flag |
@@ -253,6 +253,7 @@ wc -c CLAUDE.md # after
253
253
 
254
254
  ## Hard rules
255
255
 
256
+ - ❌ A stop report (gate red, missing review, protected branch, etc.) is the last action of this cycle. Do not continue investigating, re-running the gate, or calling `git`/`gh` after emitting it. If flaky, the next invocation decides.
256
257
  - ❌ Never push with `--force` to `{{branchBase}}` or another protected branch.
257
258
  - ❌ Never skip hooks (`--no-verify`) unless the user explicitly asks.
258
259
  - ❌ Never ask for a merge / approve the PR yourself. Your job ends with the URL.
@@ -149,7 +149,7 @@ Write `.claude/progress/review_<feature>.md`:
149
149
  | Check | Status | Evidence |
150
150
  |---|---|---|
151
151
  | `{{qualityGate.full}}` | [x] / [ ] | <output or exit code from this turn> |
152
- | Zero new errors vs baseline | [x] / [ ] | <failing paths cross-checked against `git diff --name-only origin/{{prTarget}}`, this turn> |
152
+ | Failure attribution | [x] / [ ] | <state per failure (per `verify-before-done`) + the run over `origin/{{prTarget}}` that demonstrates it, this turn> |
153
153
 
154
154
  ### Conventions (CLAUDE.md + orchestrator's Project rules)
155
155
  - <repo-specific check>: [x] / [ ]
@@ -184,7 +184,7 @@ CHANGES_REQUESTED -> .claude/progress/review_<feature>.md
184
184
  - ❌ Never include as a blocker (in "Issues ≥80") a finding with confidence <80.
185
185
  - ✅ Apply `.claude/skills/verify-before-done/SKILL.md` before marking APPROVED: each `[x]` must be backed by evidence run this turn (not from the implementer's cached report).
186
186
  - ❌ Never approve with `{{qualityGate.full}}` red.
187
- - ❌ Never approve if the new code **adds new errors or warnings** vs baseline.
187
+ - ❌ Never approve a failure legitimately classified *introduced (demonstrated)* per `verify-before-done`'s Failure attribution, and never classify one *pre-existing* by diff location alone.
188
188
  - ❌ Never approve new code with explicit or implicit `any` without a valid `// any justified: <reason>`.
189
189
  - ❌ Don't block or escalate a screen change to a human for lack of browser validation — the default gate is the diff + tests; require a visual check only when the user explicitly asked for one.
190
190
  - ✅ On APPROVED, write the content receipt (`.claude/progress/receipt.txt`) so the commit is bound to the reviewed bytes.
@@ -26,6 +26,10 @@ This role is registered ahead of the typed-handoff migration. Until a producer/c
26
26
  <!-- /navori:if-not --><!-- navori:if scribeOwnsMarkdown -->
27
27
  You are the sole author of the Markdown that lands in the diff. Two jobs, in this order when both apply: **render** a producer's JSON evidence into its prescribed Markdown handoff (R5), and **draft** the prose any `markdownRequests` entry asks for (R7) — reading the repo as needed for accuracy, never taking a design decision the request doesn't state. Preserve the producer's feature identity, status, evidence, files and verification exactly; add no claim the evidence doesn't carry.
28
28
 
29
+ ## Preflight the handoff
30
+
31
+ Before you read, edit or commit anything, run `navori handoff check <feature> --for scribe --cwd <checkout you will edit> --json`. Any result whose `status` is not `"ok"` is `BLOCKED`: report it in chat and write nothing. Edit and commit only inside the `worktree` the JSON returns, not necessarily the cwd you started from.
32
+
29
33
  ## Render the handoff (R5, R6)
30
34
 
31
35
  The `implementer` (or another JSON-handoff producer) leaves `.claude/progress/impl_<feature>.json`. Read it:
@@ -815,6 +815,7 @@ fi
815
815
  # this rule still surfaces. This rule is the seatbelt; that one is the net.
816
816
  managed_dir='\.claude/(agents|skills|hooks)|\.agents/skills|\.codex/(agents|hooks)|\.cursor/rules'
817
817
  managed_path="(CLAUDE\.md|AGENTS\.md|\.claude/settings\.json|\.codex/config\.toml|(${managed_dir})/[^[:space:];&|]+)"
818
+ managed_rewrite_msg="shell rewrite of a navori-managed file — edit the source asset and run 'navori render --apply' (or 'navori sync'); a direct write invalidates the block hash and freezes it. If the target is outside this project (e.g. a scratchpad), write it with '>' or 'tee' and an absolute path instead"
818
819
  # The redirect check reads `scan`, NOT `segments`: the split rewrites every `|`
819
820
  # into a newline, so `>| CLAUDE.md` (forced clobber) would be torn in half and
820
821
  # the target would land in a segment of its own. Reading the unsplit copy is
@@ -824,10 +825,64 @@ managed_path="(CLAUDE\.md|AGENTS\.md|\.claude/settings\.json|\.codex/config\.tom
824
825
  # `-[a-zA-Z]*i[a-zA-Z]*[^[:space:]]*` accepts the backup-suffix spellings that
825
826
  # are the everyday form on both platforms: GNU `sed -i.bak`, BSD `sed -i ''`.
826
827
  # Missing them would have left the rule covering the tutorial spelling only.
827
- if printf '%s' "$scan" | grep -qE "(^|[^>])>\|?[[:space:]]*(\./)?${managed_path}([[:space:]]|\$)" \
828
- || printf '%s' "$segments" | grep -qE "(^|[[:space:]])sed[[:space:]]+(-[a-zA-Z]*i[a-zA-Z]*[^[:space:]]*|--in-place)([[:space:]]|=).*${managed_path}" \
829
- || printf '%s' "$segments" | grep -qE "(^|[[:space:]])tee[[:space:]]+([^-][^[:space:]]*[[:space:]]+)*(\./)?${managed_path}([[:space:]]|\$)"; then
830
- block "shell rewrite of a navori-managed file — edit the source asset and run 'navori render --apply' (or 'navori sync'); a direct write invalidates the block hash and freezes it"
828
+ # Case-INSENSITIVE (`-i`), same reasoning as the heredoc script-extension check
829
+ # above (~line 319): APFS is case-insensitive by default, so `> .CLAUDE/AGENTS/a.md`
830
+ # writes the same file as `.claude/agents/a.md` and must not slip past a
831
+ # case-sensitive match.
832
+ if printf '%s' "$scan" | grep -qiE "(^|[^>])>\|?[[:space:]]*(\./)?${managed_path}([[:space:]]|\$)" \
833
+ || printf '%s' "$segments" | grep -qiE "(^|[[:space:]])sed[[:space:]]+(-[a-zA-Z]*i[a-zA-Z]*[^[:space:]]*|--in-place)([[:space:]]|=).*${managed_path}" \
834
+ || printf '%s' "$segments" | grep -qiE "(^|[[:space:]])tee[[:space:]]+([^-][^[:space:]]*[[:space:]]+)*(\./)?${managed_path}([[:space:]]|\$)"; then
835
+ block "$managed_rewrite_msg"
836
+ fi
837
+
838
+ # 6b (#1034). The `>`/`>|`/`tee` arms above only ever match a BARE or
839
+ # `./`-relative managed path, by design — `sed -i`'s own arm is unanchored
840
+ # (`.*managed_path`) so it already catches an absolute target as a side effect,
841
+ # but `>`/`tee` never reach `managed_path` at all for an absolute one, in or out
842
+ # of the project (confirmed empirically, challenge_1027.md CONCERN 1). #1027
843
+ # proposed closing that by RESOLVING the target against the filesystem at
844
+ # check-time (`cd -P`, symlink checks) to decide "inside vs outside" — rejected
845
+ # (challenge_1027.md BLOCKER 1): an ordinary `rm <target> && ln -s <managed
846
+ # file> <target> && tee <target>` in the SAME command swaps what the literal
847
+ # path resolves to between the check and the write, so a filesystem check is
848
+ # TOCTOU-defeated by construction, not by an adversarial trick.
849
+ #
850
+ # What stays textual and TOCTOU-proof: matching an absolute target whose
851
+ # CHARACTERS start with this project's own path — no `cd`, no `pwd -P`, no
852
+ # `[ -L ]`, so there is nothing for a same-command `rm`/`ln -s` to invalidate.
853
+ # Two forms reach that:
854
+ # - the RESOLVED value of $CLAUDE_PROJECT_DIR, regex-escaped so a literal
855
+ # char in the path (this repo's own path has a space and a hyphen, "Dev -
856
+ # Docs") is never read as a metacharacter;
857
+ # - the UNRESOLVED variable text itself, `$CLAUDE_PROJECT_DIR` /
858
+ # `${CLAUDE_PROJECT_DIR}`, which an agent can compose into a redirect
859
+ # without ever expanding it (so this arm doesn't depend on the value).
860
+ # Quoted and unquoted forms both matter, and NOT just as "the whole target is
861
+ # quoted or it isn't": `"$CLAUDE_PROJECT_DIR"/CLAUDE.md` — quoting only the
862
+ # variable expansion, then continuing unquoted — is ordinary POSIX style a
863
+ # legitimate command uses unprompted, so the quote has to be optional AROUND
864
+ # THE PREFIX TOKEN ITSELF (both the literal value and the `$VAR`/`${VAR}` text),
865
+ # not only at the two ends of the whole match (#1034 round 2 review). A path
866
+ # with a space in it (like this repo's) also HAS to be quoted somewhere to be
867
+ # valid shell at all, so this isn't a cosmetic tolerance.
868
+ # Absolute paths OUTSIDE the project (any other prefix) stay allowed on
869
+ # purpose — the block message above offers exactly that escape (#1036), and a
870
+ # sibling directory that merely shares the prefix (`<proj>-otro/CLAUDE.md`)
871
+ # does not match either: the pattern requires a literal `/` (optionally
872
+ # quoted) right after the project path, and a sibling has `-otro/…` there
873
+ # instead.
874
+ # Without `$CLAUDE_PROJECT_DIR` set, this arm is skipped entirely — same
875
+ # behavior as before #1034 — rather than matching against an empty prefix.
876
+ if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
877
+ # Strip a trailing slash before escaping: `CLAUDE_PROJECT_DIR=/proj/` would
878
+ # otherwise need a doubled `//` to line up with `/${managed_path}` below and
879
+ # silently stop matching (round 2 review, informational note 1).
880
+ cpd_literal=$(printf '%s' "${CLAUDE_PROJECT_DIR%/}" | sed -E 's#[^a-zA-Z0-9_/ -]#\\&#g')
881
+ abs_managed_path="[\"']?(${cpd_literal}|\\\$\\{?CLAUDE_PROJECT_DIR\\}?)[\"']?/${managed_path}[\"']?"
882
+ if printf '%s' "$scan" | grep -qiE "(^|[^>])>\|?[[:space:]]*${abs_managed_path}([[:space:]]|\$)" \
883
+ || printf '%s' "$segments" | grep -qiE "(^|[[:space:]])tee[[:space:]]+([^-][^[:space:]]*[[:space:]]+)*${abs_managed_path}([[:space:]]|\$)"; then
884
+ block "$managed_rewrite_msg"
885
+ fi
831
886
  fi
832
887
 
833
888
  # navori:user-section
@@ -171,7 +171,23 @@ trap navori_audit_on_exit EXIT
171
171
 
172
172
  cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0
173
173
 
174
- stamp=".claude/.managed-drift-stamp"
174
+ # #1024: the stamp used to live at `.claude/.managed-drift-stamp`, written
175
+ # unconditionally with no `gitignoreHarness` check — so under the default
176
+ # `"off"` config the FIRST tool call in every session dirtied the tree, with no
177
+ # `.gitignore` involved to catch it. `--git-common-dir` resolves to the SHARED
178
+ # `.git` even when this hook runs inside an agent worktree, where `.git` is a
179
+ # file pointing at the main checkout (#454's same scope note applies here).
180
+ # Outside a git repo, or a corrupt/missing `.git`, there is nowhere safe to
181
+ # persist this stamp: skip silently rather than fall back to `.claude/` — this
182
+ # is a detector, not a gate, so losing one session's drift check costs less
183
+ # than reintroducing the untracked-file problem it exists to prevent.
184
+ common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
185
+ case "$common_dir" in
186
+ /*) ;;
187
+ *) common_dir="$PWD/$common_dir" ;;
188
+ esac
189
+ state_dir="$common_dir/navori"
190
+ stamp="$state_dir/managed-drift-stamp"
175
191
 
176
192
  # sha1 tool, resolved once. `shasum` on macOS, `sha1sum` on most Linuxes; both
177
193
  # print `<hash> <path>` for a file list, which is the format the stamp stores.
@@ -205,7 +221,7 @@ current=$(find $roots -type f -exec $sha {} + 2>/dev/null | sort || true)
205
221
  # current state as the baseline and say nothing. Reporting every block on the
206
222
  # first command would train the reader to ignore this hook by lunchtime.
207
223
  if [ ! -f "$stamp" ]; then
208
- mkdir -p .claude 2>/dev/null || exit 0
224
+ mkdir -p "$state_dir" 2>/dev/null || exit 0
209
225
  printf '%s\n' "$current" > "$stamp" 2>/dev/null || true
210
226
  exit 0
211
227
  fi
@@ -72,11 +72,12 @@ set -uo pipefail
72
72
  # right after that `case` ends the run before anything locates a
73
73
  # stamp. Measured over the park, 53.8% of Bash calls — and Bash is
74
74
  # 80.4% of every tool call — so this is the dominant path.
75
- # 2 spawns — a session already `#delegated` or `#notified`: + `session_id`,
76
- # which is what names the stamp, and the stamp check exits.
77
- # 4 spawns — an edit that actually counts: + `agent_id` (the subagent guard
75
+ # 3 spawns — a session already `#delegated` or `#notified`: + `session_id`
76
+ # and `git rev-parse --git-common-dir` (#1024 — resolves where the
77
+ # stamp now lives, off `.claude/`), and the stamp check exits.
78
+ # 5 spawns — an edit that actually counts: + `agent_id` (the subagent guard
78
79
  # below) + the file path.
79
- # 5 spawns — a `Bash` whose PAYLOAD looked like a write but whose COMMAND is
80
+ # 6 spawns — a `Bash` whose PAYLOAD looked like a write but whose COMMAND is
80
81
  # not one: + `extract_cmd`, and the same probe re-applied to the
81
82
  # command ends it one fork before the extraction pipeline.
82
83
  # The discard `case` therefore comes BEFORE `session_id`: a tool this hook does
@@ -210,7 +211,22 @@ fi
210
211
  sid=$(payload_field session_id | tr -cd 'A-Za-z0-9._-')
211
212
  [ -n "$sid" ] || sid="unknown-session"
212
213
 
213
- stamp_dir=".claude/.routing-watch"
214
+ # #1024: the stamp dir used to live at `.claude/.routing-watch/`, written
215
+ # unconditionally with no `gitignoreHarness` check — so under the default
216
+ # `"off"` config an ordinary 4-file session dirtied the tree, with no
217
+ # `.gitignore` involved to catch it. `--git-common-dir` resolves to the SHARED
218
+ # `.git` even inside an agent worktree, where `.git` is a file pointing at the
219
+ # main checkout — mirrors `managed-drift-watch.sh`'s identical fix. Outside a
220
+ # git repo, or a corrupt/missing `.git`, there is nowhere safe to persist this
221
+ # stamp: skip silently (fail-open, same discipline as every other exit here)
222
+ # rather than fall back to `.claude/`.
223
+ common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
224
+ case "$common_dir" in
225
+ /*) ;;
226
+ *) common_dir="$PWD/$common_dir" ;;
227
+ esac
228
+
229
+ stamp_dir="$common_dir/navori/routing-watch"
214
230
  stamp="$stamp_dir/$sid"
215
231
 
216
232
  # Append a line unless it is already there. Silent on every failure: a stamp
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: better-auth
3
+ description: Use when touching auth server config, session middleware, the auth client, or a Better Auth plugin (email OTP, bearer, rate limiting) — server/client wiring, session retrieval, schema. Not for a hand-rolled JWT/session implementation.
4
+ metadata:
5
+ type: reference
6
+ ---
7
+
8
+ # Better Auth — conventions
9
+
10
+ Target version: Better Auth 1.7. On a different major, recheck the CLI command names and plugin pairing below — auth is a surface where a stale assumption becomes a security bug, not just a broken build.
11
+
12
+ ## The pattern
13
+
14
+ One server config, mirrored by a matching client config — the plugin arrays on both sides must line up:
15
+
16
+ ```ts
17
+ // server
18
+ export const auth = betterAuth({
19
+ database: drizzleAdapter(db, { provider: 'pg' }),
20
+ secret, // >= 32 chars — see gotchas
21
+ trustedOrigins, // keep in sync with CORS
22
+ emailAndPassword: { enabled: true },
23
+ rateLimit: { enabled: true, storage: 'database' },
24
+ plugins: [emailOTP({ sendVerificationOTP }), bearer()],
25
+ });
26
+
27
+ app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw)); // before session routes
28
+
29
+ // client
30
+ export const client = createAuthClient({
31
+ baseURL,
32
+ plugins: [emailOTPClient()],
33
+ fetchOptions: { auth: { type: 'Bearer', token: () => storedToken } },
34
+ });
35
+ ```
36
+
37
+ ## Gotchas that bite
38
+
39
+ - **`secret` must be at least 32 characters**; gate it with a schema check (e.g. `z.string().min(32)`) at startup — it signs sessions/tokens.
40
+ - **`trustedOrigins` is the same allowlist CORS needs — keep them in sync manually**; a request can pass CORS and still get rejected by auth.
41
+ - **Mount `/api/auth/*` before any session-dependent route**; mounted later, that route sees no session even on a valid request.
42
+ - **`auth.api.getSession({ headers: c.req.raw.headers })` in middleware, storing only `{ id, email }` in context**, not the raw session object.
43
+ - **Client calls return `{ data, error }` (or throw on network failure) — unify that in one helper**; a missed ad hoc `if (error)` check treats a failed call as success.
44
+ - **Server and client plugin arrays must pair up.** Server `emailOTP()` + `bearer()` needs client `emailOTPClient()` plus `fetchOptions.auth`, or the feature fails silently.
45
+ - **Bearer transport means capturing `set-auth-token` and resending it as `Authorization: Bearer <token>`** — no cookie jar does it for you; no CSRF surface since it never rides on cookies.
46
+ - **`sendVerificationOTP({ email, otp, type })` fires for every OTP type** — guard on `type` (e.g. `'forget-password'`) or you'll send the wrong email for other flows.
47
+ - **Not every sensitive endpoint is rate-limited by default** — verify coverage and add a `customRules` entry if missing; on Workers the in-memory store never throttles, use `storage: 'database'`.
48
+ - **Schema: `npx auth@latest generate`** (Prisma/Drizzle/Kysely) produces the schema/migration without applying it; **`npx auth@latest migrate`** applies it directly but only with the Kysely adapter — with Drizzle/Prisma, apply what `generate` produced via that ORM's own tool. A hand-maintained Drizzle schema mirroring Better Auth's shape also works — never rename a column there without a matching adapter change.
49
+
50
+ ## Before declaring done
51
+
52
+ - `secret` length is enforced by a schema check, not just documentation.
53
+ - `trustedOrigins` matches the CORS config for this same change.
54
+ - Context carries only the minimal session shape; the raw session object doesn't leak past its middleware.
55
+ - New/edited plugins are paired on both server and client.
56
+ - `{{qualityGate.fast}}` green.
57
+
58
+ If any item fails, fix it and re-run the whole list.
59
+
60
+ <!-- navori:user-section -->
61
+ ## This repo's auth setup (your domain)
62
+
63
+ <!-- user: add here what only applies to THIS repo. Suggestions:
64
+ - Which adapter/schema path is in use (CLI-generated vs. hand-maintained Drizzle schema) and why.
65
+ - The transport in use (cookies vs. bearer) per client (web vs. mobile).
66
+ - Which plugins are enabled and what each one is for.
67
+ - The rate-limit storage backend and any custom rules beyond the defaults.
68
+ -->
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: eas-release
3
+ description: Use when touching eas.json build/submit profiles, deciding between an OTA update and a new native build, wiring per-environment env vars/secrets for a build, or preparing a store submission. Only applies to a repo that actually has eas.json — an Expo app without it doesn't use EAS and this skill doesn't apply. Not for expo-router (routing) or Uniwind (styling).
4
+ metadata:
5
+ type: reference
6
+ ---
7
+
8
+ # EAS Build/Submit/Update
9
+
10
+ EAS CLI flags move between minors, so confirm any command against `eas --help` / the installed CLI version before relying on it. Not every Expo project uses EAS — check `eas.json` exists before applying anything here.
11
+
12
+ ## `eas.json` shape
13
+
14
+ - `cli.version` pins the minimum EAS CLI version; `cli.appVersionSource: "remote"` means the store version is tracked by EAS, not read from the local Expo config.
15
+ - `build.<profile>` — one block per build profile (e.g. `development`, `preview`, `production`). Fields seen in practice: `developmentClient`, `distribution` (`internal` vs store), `channel` (the EAS Update channel this build's runtime binds to), `autoIncrement`.
16
+ - `submit.<profile>` — store submission config, keyed by the same profile names.
17
+
18
+ ## Channels, branches, and runtime compatibility
19
+
20
+ - A **channel** is what a built binary listens on for OTA updates; an **update** is published to a **branch**, and a channel maps to a branch (by default, same name). A build's `channel` decides which branch its installs pull from.
21
+ - **runtimeVersion** is the real compatibility gate, not the channel name: an OTA update only reaches a binary whose `runtimeVersion` matches. Three policies — `appVersion` (from the app's version string), `nativeVersion` (from native fields like `CFBundleVersion`/`versionCode`), `fingerprint` (a hash of the native project — a native-dependency change bumps it automatically, so a JS-only change stays compatible). No default; pick one explicitly.
22
+
23
+ ## OTA update vs new native build
24
+
25
+ - **Safe as an OTA update**: JS/TS changes, assets, most React Native code — anything that doesn't touch native modules, permissions, `app.json`/`app.config.ts` native fields, or the Expo SDK version.
26
+ - **Needs a new native build**: a new/updated native module, a config plugin change, a permission, an SDK upgrade. Under `fingerprint` this is automatic; under `appVersion`/`nativeVersion` it's a manual call — get it wrong and the update either misses everyone or crashes a binary that can't support it.
27
+
28
+ ## Env vars, secrets, and source maps
29
+
30
+ - Per-environment values inject at build time via `eas.json`'s per-profile `env` block or EAS's environment-variable store, managed with `eas env:set`/`get`/`list`/`pull`/`push`/`delete`/`exec` — creating a variable goes through `env:set`, there is no `env:create`. Default environments: `development`, `preview`, `production`. Visibility: plain text, sensitive, or secret (secret never readable outside EAS's servers).
31
+ - Source maps (e.g. Sentry) upload as a build/update step so stack traces resolve to real source — wire into CI, not a manual local step.
32
+
33
+ ## Store submission basics
34
+
35
+ `submit.<profile>` in `eas.json` holds store-specific fields (e.g. an Apple/Google service account reference) — never a raw credential committed to the repo. A submission consumes a build already produced by `build.<profile>`; it doesn't rebuild.
36
+
37
+ ## Before declaring done
38
+
39
+ Copy and check off:
40
+
41
+ - [ ] `eas.json` actually exists in this repo before applying any of the above.
42
+ - [ ] The change is classified correctly as OTA-safe vs native-build-required.
43
+ - [ ] No secret/API key is hardcoded per environment — it goes through EAS env or the profile's `env` block.
44
+ - [ ] `{{qualityGate.fast}}` green.
45
+
46
+ If any item fails, fix it and re-run the whole list.
47
+
48
+ <!-- navori:user-section -->
49
+ ## This repo's release process (your domain)
50
+
51
+ <!-- user: add here what only applies to THIS repo. Suggestions:
52
+ - The branch-to-channel-to-environment mapping in use.
53
+ - The runtimeVersion policy chosen and why.
54
+ - Where secrets are managed (EAS env, a secrets manager) and who can rotate them.
55
+ -->
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: expo-router
3
+ description: Use when adding, moving, or protecting a route on Expo — file-based routing under app/, layouts and route groups, typed routes, redirects/auth guards, or reading URL/search params. Applies to repos already on expo-router. Not for a web SPA's router (TanStack Router/React Router) and not for the app's business logic, which belongs in src/features, not in app/.
4
+ metadata:
5
+ type: reference
6
+ ---
7
+
8
+ # expo-router — file-based routing (Expo SDK 57)
9
+
10
+ Written for expo-router ~57 on Expo SDK 57. Check the version in `package.json` first; layout/typed-route APIs have moved between majors before — verify against the changelog on a major bump.
11
+
12
+ ## Wiring
13
+
14
+ - The `expo-router` Expo plugin goes in `app.json`/`app.config.ts`'s `plugins`, and `"main": "expo-router/entry"` in `package.json`.
15
+ - `experiments.typedRoutes: true` in the Expo config generates typed `href`s from the `app/` tree — a route that doesn't exist becomes a type error, not a runtime 404.
16
+ - `scheme` in the Expo config is the deep-link scheme; every route under `app/` is reachable by it once the app is installed.
17
+
18
+ ## File-based routes
19
+
20
+ - Every file under `app/` is a route; its path mirrors the file path (`app/settings/index.tsx` → `/settings`).
21
+ - `_layout.tsx` wraps its directory (and nested layouts nest). Use it for a `Stack`/`Tabs` navigator, not for business logic.
22
+ - A parenthesized segment (`app/(auth)/`) is a **group**: it organizes/nests layouts without adding a path segment.
23
+ - Keep route files thin — read params, call a hook from `src/features/<feature>`, render. Logic, data fetching, validation live in `src/features`, not `app/`.
24
+
25
+ ## Auth guards and redirects
26
+
27
+ - `<Redirect href="..." />` renders nothing and navigates immediately — use it for an unconditional bounce (e.g. an unauthenticated root redirecting to `/(auth)/sign-in`).
28
+ - `Stack.Protected` (also on other layout navigators) conditionally mounts/unmounts a screen or group based on a `guard` boolean, without a remount flash — prefer it over a `Redirect` inside every protected screen when the whole group shares one guard.
29
+ - Auth state itself is read from the app's own session hook (e.g. a Zustand store's `useSession()`), never re-derived inside a route file.
30
+
31
+ ## Search params: local vs global
32
+
33
+ - `useLocalSearchParams()` only updates while its route is focused — the default for reading a route's own params.
34
+ - `useGlobalSearchParams()` updates on every navigation event, even for routes that aren't focused — use it sparingly (a screen reacting to a param change in a sibling route), since it re-renders more often.
35
+
36
+ ## Gotchas that bite
37
+
38
+ - **A non-route file in `app/`** (a shared component, a hook) still becomes routable unless it starts with `_` or lives outside `app/` — keep non-route files in `src/`.
39
+ - **Typed routes require the generated types.** After adding/renaming a route, restart the dev server (`--clear` if it doesn't pick it up) so typed `href`s regenerate.
40
+ - **Deep links need the scheme AND the route to exist** — a scheme without a matching `app/` path 404s at the router level even if the OS opens the app.
41
+
42
+ ## Before declaring done
43
+
44
+ Copy and check off:
45
+
46
+ - [ ] New route files are thin — logic lives in `src/features`, not `app/`.
47
+ - [ ] Auth-protected routes use `Stack.Protected` or a `Redirect`, never a bare unauthenticated render.
48
+ - [ ] `useGlobalSearchParams` is used only where cross-route reactivity is actually needed.
49
+ - [ ] `expo export --clear` succeeds after any routing/layout change.
50
+ - [ ] `{{qualityGate.fast}}` green.
51
+
52
+ If any item fails, fix it and re-run the whole list.
53
+
54
+ <!-- navori:user-section -->
55
+ ## This repo's routes (your domain)
56
+
57
+ <!-- user: add here what only applies to THIS repo. Suggestions:
58
+ - The route groups in use and what each protects.
59
+ - Where the session/auth hook lives and its shape.
60
+ - Deep link scheme and any universal-link domains configured.
61
+ -->