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.
- package/README.md +6 -2
- package/dist/assets/core/core-assets/agents/architect.md +1 -1
- package/dist/assets/core/core-assets/agents/implementer.md +5 -4
- package/dist/assets/core/core-assets/agents/orchestrator.md +3 -3
- package/dist/assets/core/core-assets/agents/publisher.md +17 -16
- package/dist/assets/core/core-assets/agents/reviewer.md +2 -2
- package/dist/assets/core/core-assets/agents/scribe.md +4 -0
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +59 -4
- package/dist/assets/core/core-assets/hooks/managed-drift-watch.sh +18 -2
- package/dist/assets/core/core-assets/hooks/routing-watch.sh +21 -5
- package/dist/assets/core/core-assets/lib-skills/better-auth.md +68 -0
- package/dist/assets/core/core-assets/lib-skills/eas-release.md +55 -0
- package/dist/assets/core/core-assets/lib-skills/expo-router.md +61 -0
- package/dist/assets/core/core-assets/lib-skills/hono.md +58 -0
- package/dist/assets/core/core-assets/lib-skills/react-email.md +55 -0
- package/dist/assets/core/core-assets/lib-skills/react-native-reusables.md +1 -1
- package/dist/assets/core/core-assets/lib-skills/shadcn-base-ui.md +63 -0
- package/dist/assets/core/core-assets/lib-skills/tailwind-v4.md +68 -0
- package/dist/assets/core/core-assets/lib-skills/tanstack-router.md +68 -0
- package/dist/assets/core/core-assets/lib-skills/uniwind.md +58 -0
- package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
- package/dist/assets/core/core-assets/managed/orquestacion.md +2 -1
- package/dist/assets/core/core-assets/presets/monorepo-turbopnpm/skills/turbo-workspaces.md +1 -1
- package/dist/assets/core/core-assets/settings/settings-base.json +1 -0
- package/dist/assets/core/core-assets/skills/review-diff.md +4 -4
- package/dist/assets/core/core-assets/skills/scoped-gate.md +14 -5
- package/dist/assets/core/core-assets/skills/solution-design.md +6 -2
- package/dist/assets/core/core-assets/skills/verify-before-done.md +7 -4
- package/dist/assets/plugins/jscpd/skills/jscpd-review.md +9 -9
- package/dist/assets/plugins/semgrep/skills/semgrep-review.md +11 -8
- package/dist/index.js +344 -339
- package/package.json +2 -2
- package/dist/assets/core/core-assets/lib-skills/nativewind.md +0 -57
- 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/
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}}
|
|
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
|
|
106
|
-
-
|
|
107
|
-
-
|
|
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:** `
|
|
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,
|
|
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
|
|
131
|
-
- **Commit drag** — only when
|
|
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}}
|
|
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
|
-
|
|
142
|
-
- Ticket if applicable: branch name (e.g. `BT-1234-fix-x` → `BT-1234`) or
|
|
143
|
-
- `.claude/progress/impl_<feature>.md`
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
+
-->
|