navori 0.11.0 → 0.11.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 (38) hide show
  1. package/README.md +21 -2
  2. package/dist/assets/core/core-assets/agents/architect.md +6 -4
  3. package/dist/assets/core/core-assets/agents/implementer.md +6 -2
  4. package/dist/assets/core/core-assets/agents/reviewer.md +24 -2
  5. package/dist/assets/core/core-assets/agents/scout.md +1 -1
  6. package/dist/assets/core/core-assets/hooks/_partials/bash-outcome.sh +245 -0
  7. package/dist/assets/core/core-assets/hooks/_partials/gate-ask.sh +25 -0
  8. package/dist/assets/core/core-assets/hooks/_partials/resolve-worktree.sh +257 -91
  9. package/dist/assets/core/core-assets/hooks/bash-outcome-watch.sh +21 -0
  10. package/dist/assets/core/core-assets/hooks/general-purpose-confirm.sh +56 -0
  11. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +21 -1
  12. package/dist/assets/core/core-assets/hooks/implementer-no-markdown.sh +23 -5
  13. package/dist/assets/core/core-assets/hooks/master-accept-confirm.sh +17 -5
  14. package/dist/assets/core/core-assets/hooks/model-advisor.sh +23 -16
  15. package/dist/assets/core/core-assets/hooks/plan-gate.sh +40 -2
  16. package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +27 -4
  17. package/dist/assets/core/core-assets/hooks/routing-watch.sh +14 -0
  18. package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +143 -19
  19. package/dist/assets/core/core-assets/managed/orquestacion.md +26 -19
  20. package/dist/assets/core/core-assets/master-plan/en/ux.md +129 -0
  21. package/dist/assets/core/core-assets/master-plan/ux.md +129 -0
  22. package/dist/assets/core/core-assets/presets/fastapi-python/managed/stack.md +7 -0
  23. package/dist/assets/core/core-assets/presets/fastapi-python.json +19 -0
  24. package/dist/assets/core/core-assets/settings/settings-base.json +2 -0
  25. package/dist/assets/core/core-assets/skills/locate-code.md +1 -1
  26. package/dist/assets/core/core-assets/skills/master-plan.md +16 -10
  27. package/dist/assets/core/core-assets/skills/plan-advanced.md +1 -1
  28. package/dist/assets/core/core-assets/skills/plan-simple.md +2 -2
  29. package/dist/assets/core/core-assets/skills/verify-before-done.md +1 -1
  30. package/dist/assets/plugins/codegraph/managed/codegraph-search-v2.md +1 -1
  31. package/dist/assets/plugins/codegraph/skills/codegraph-access-v2.md +1 -1
  32. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +75 -14
  33. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +2 -2
  34. package/dist/assets/plugins/tgrep/managed/guard-destructive-search-lane.sh +18 -0
  35. package/dist/assets/plugins/tgrep/plugin.json +10 -0
  36. package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +220 -0
  37. package/dist/index.js +708 -432
  38. package/package.json +3 -2
package/README.md CHANGED
@@ -41,7 +41,7 @@ npx navori init
41
41
  cd ~/tu-repo
42
42
  navori init --recommended
43
43
 
44
- # + proveedores externos (tgrep, codegraph, semgrep, jscpd, acli) + pre-commit hook +
44
+ # + proveedores externos (tgrep, semgrep, jscpd, acli) + pre-commit hook +
45
45
  # scan-monorepo + project block estricto — requiere instalar los binarios de esos proveedores.
46
46
  # También avisa si falta algún binario y cómo instalarlo, sin instalarlo nunca.
47
47
  navori init --full
@@ -63,6 +63,24 @@ Y genera:
63
63
  - `CLAUDE.md` con managed blocks que el CLI mantiene sincronizados
64
64
  - `.claude/` con agentes, skills, hooks y settings
65
65
 
66
+ ### Aviso opcional de actualización
67
+
68
+ En comandos interactivos, navori puede avisar si detecta una versión más nueva. La
69
+ consulta al registro de npm se ejecuta en segundo plano y no bloquea el comando;
70
+ por eso, la primera ejecución solo inicia la actualización de la caché y el aviso
71
+ puede aparecer en una ejecución posterior. Se limita a una consulta y un aviso
72
+ cada 24 horas. No se muestra en CI, en modo JSON ni en comandos de ayuda o versión.
73
+
74
+ Para desactivarlo, define `NAVORI_NO_UPDATE_NOTIFIER=1`. El aviso recomienda el
75
+ comando de actualización según el instalador detectado, con `npx` como alternativa.
76
+ No instala ni actualiza automáticamente y tampoco ejecuta `render`: después de
77
+ actualizar navori, corre `navori render --apply` para aplicar los cambios a tu
78
+ harness.
79
+
80
+ Si un cierre abrupto deja bloqueado el aviso, detén primero todos los procesos
81
+ `navori`. Después elimina únicamente `~/.navori/update-notice/lock`; conserva los
82
+ archivos de caché y reserva de ese directorio.
83
+
66
84
  ## Comandos
67
85
 
68
86
  | Comando | Qué hace |
@@ -110,6 +128,7 @@ Un preset aporta skills y reglas específicas del stack además del core. El `in
110
128
  | `nestjs` | NestJS (backend) |
111
129
  | `express` | Express (backend, agnóstico de DB) |
112
130
  | `express-mongoose` | Express + Mongoose (backend) |
131
+ | `fastapi-python` | FastAPI (backend, Python) |
113
132
  | `bun-keystone` | Keystone 6 + Prisma (backend, Bun) |
114
133
  | `background-worker` | Worker de fondo (jobs + colas: agenda / bullmq / amqplib) |
115
134
  | `medusa` | Medusa.js v2 (backend) |
@@ -135,7 +154,7 @@ La resolución es **local → bundled**: si tienes un preset local con el mismo
135
154
  | Plugin | Para qué | External tool |
136
155
  |---|---|---|
137
156
  | `engram` | Memoria persistente entre sesiones | `engram` binary |
138
- | `codegraph` | Descubrimiento estructural de código vía MCP | `codegraph` binary |
157
+ | `codegraph` | Descubrimiento estructural de código vía MCP; opt-in explícito (no incluido en `--full`) | `codegraph` binary |
139
158
  | `tgrep` | Descubrimiento textual de código vía CLI indexado | `tgrep` binary |
140
159
  | `acli` | Leer tickets de Jira desde la terminal | `acli` |
141
160
  | `gh` | GitHub Issues, PRs y workflow runs | `gh` |
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: architect
3
3
  description: Proposes what to build and why for a task with an architectural signal (shared abstraction, ownership, contract, migration, hard-to-reverse decision), a level-2 workplan, or a spec's design.md. Not for verdicts, decomposition, or user questions. Use when the architectural row fires, `classify` returns level 2, or a spec is scaffolded.
4
- tools: Read, Glob, Grep, Bash, Write, WebFetch, WebSearch
4
+ tools: Read, Glob, Grep, Bash, Write, WebFetch, WebSearch, Agent(scout, scribe)
5
5
  model: {{models.architect}}
6
6
  effort: {{effort.architect}}
7
7
  # spec 0032 R26/R28: the method and the level-3 output live here
@@ -23,17 +23,19 @@ For a **master-plan** task, write the requested `<etapa>/plans/plan<n>.md` and p
23
23
  - "Derive the decision drivers from the project's own rules (DIRECTION, CLAUDE.md, EXTENDING, `quality-attributes`) before you list any option."
24
24
  - "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."
25
25
  - "Recommend the option that best fits the drivers, not the cheapest by default."
26
- - "Only verify empirically what would change the recommendation if false — documentation before a probe, never a full stack install; a probe like `npx <tool>@<version> --help` for a deciding flag still qualifies. 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."
26
+ - "Only verify empirically what would change the recommendation if false — documentation before a probe, never a full stack install. 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."
27
27
 
28
28
  ### Sources
29
29
 
30
30
  Use URLs de documentación oficial for expiring facts and include the fecha de consulta. If a source is unavailable, label the claim `[SIN VERIFICAR]` and list the sources consulted or unavailable.
31
31
 
32
+ Via `Agent` you may dispatch `scout`/`scribe`; plan-gate and Agent hooks fire inside you with `agent_id`, expected.
33
+
32
34
  ## Protocol
33
35
 
34
36
  1. `CLAUDE.md` is already in your context when your host injects it — read it from disk only if it wasn't.
35
37
  2. Apply `.claude/skills/solution-design/SKILL.md` and the Method above: what already exists (evidence), the real problem, genuine approaches only, the chosen solution and why not the others, only the dimensions the signal raises.
36
- 3. Follow Code discovery routing (project instructions): the structural provider first for relationships or impact, `Grep`/`Glob` for literals — find what already solves this before proposing anything new.
38
+ 3. Follow Code discovery routing (project instructions): the structural provider first for relationships or impact, `Grep`/`Glob` for literals.
37
39
  4. Write the first full draft of `.navori/state/handoffs/solution_<scope>.md` — the skill's template, plus `Decision drivers`, `Options` (survivors developed in full, discarded ones in one line each), `Recommendation`, and `Durable knowledge` naming the proposed destination (Dominio / CLAUDE.md / user-section / skill); mark whatever only a probe would confirm `UNVERIFIED — <why it would change the recommendation>`. "You propose the destination; you never write it." A human decision goes under "Open questions" for the orchestrator to raise — never guessed, never asked directly. Then verify each `UNVERIFIED` item per the Method, updating the artifact after each.
38
40
  5. **Level 3 only**: instead of step 4, write `specs/<feature>/design.md` using `spec-bootstrap`'s template.
39
41
  6. You do NOT run the challenge — the orchestrator hands the artifact to a fresh-context `auditor` (or the skill's fallback). You do NOT issue READY/CONCERNS/BLOCKED — the orchestrator's, post-challenge.
@@ -45,7 +47,7 @@ Use URLs de documentación oficial for expiring facts and include the fecha de c
45
47
  - ❌ Never decompose into implementer tasks — the orchestrator's, after the verdict.
46
48
  - ❌ Never ask the user — record it as an open question for the orchestrator.
47
49
  - ✅ Every "already exists" claim carries `file:line`. No cite, no claim.
48
- - ✅ ≥2 approaches only when genuinely viable, never a straw alternative.
50
+ - ✅ ≥2 approaches only when genuinely viable.
49
51
 
50
52
  ## Communication with the orchestrator
51
53
 
@@ -4,7 +4,8 @@ description: Implements ONE scoped task with its tests, respects CLAUDE.md conve
4
4
  tools: Read, Write, Edit, Glob, Grep, Bash
5
5
  model: {{models.implementer}}
6
6
  effort: {{effort.implementer}}
7
- maxWords: 2350
7
+ maxTurns: 160
8
+ maxWords: 2433
8
9
  ---
9
10
 
10
11
  # Implementer Agent
@@ -44,7 +45,7 @@ You execute **a single** task from start to verification. You don't orchestrate,
44
45
  5. **UI**: for screen changes, the default evidence is the repo's tests plus a correct diff — **do NOT spin up a browser or dev server automatically**. Visual/browser validation is **optional and strictly on-request**: run it only when the user explicitly asks to check the UI in this prompt, and then drive the repo's browser-automation tool if one is set up (e.g. `playwright-cli`, whose installer ships its own skill).
45
46
  6. **No commits** without the `reviewer`'s approval. When you finish, <!-- navori:if-not scribeOwnsMarkdown -->write the report<!-- /navori:if-not --><!-- navori:if scribeOwnsMarkdown -->write your JSON evidence<!-- /navori:if --> and return the reference.
46
47
  <!-- navori:if planTiers -->
47
- When the encargo opens with `workplan: <feature>`, read `.navori/state/handoffs/workplan_<feature>.json`, run each assigned `A<n>` command and report it in `impl_<feature>.json` under `acceptance` (`id`, `command`, `exitCode`, `excerpt`). A file outside the workplan's files is a blocker to report, not a change to make.
48
+ When the encargo opens with `workplan: <feature>`, read `.navori/state/handoffs/workplan_<feature>.json`, run each assigned `A<n>` `command` exactly as written (same string, foreground, no rewrites), then mark progress and report it in `impl_<feature>.json` under `acceptance` (`id`, `command`, `exitCode`, `excerpt`). The routing-watch hook records acceptance evidence only when the host itself ran that exact command; `navori plan update <feature> --progress A<n>=cumplido` is rejected without it and never runs the command for you. A file outside the workplan's files is a blocker to report, not a change to make.
48
49
  <!-- /navori:if -->
49
50
 
50
51
  ## Hard rules (generic, always apply)
@@ -155,10 +156,13 @@ Write `.navori/state/handoffs/impl_<feature>.json` — the only artifact you pro
155
156
  "markdownRequests": [
156
157
  { "path": "<repo-relative .md/.mdx path>", "intent": "<what to change and why>", "evidence": "<file:line or commit that backs it>" }
157
158
  ],
159
+ "doubts": [],
158
160
  "blockers": []
159
161
  }
160
162
  ```
161
163
 
164
+ `doubts` is optional: declare one `{ "file": "<path>", "reason": "<why you are unsure>" }` entry per spot where you are unsure the change is right, so the reviewer answers it.
165
+
162
166
  `markdownRequests` carries every piece of prose your task needs — your own non-obvious decisions, a CONTRIBUTING/README update, a spec task checkbox, a skill or agent tweak. State the `intent` and the `evidence`; never the finished sentence — drafting the prose from that intent is the `scribe`'s job, not yours. Empty array when the task touches no Markdown at all.
163
167
 
164
168
  ## Communication with the orchestrator
@@ -4,7 +4,7 @@ description: Strict reviewer — approves or rejects a diff against CLAUDE.md an
4
4
  tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.reviewer}}
6
6
  effort: {{effort.reviewer}}
7
- maxWords: 2200
7
+ maxWords: 2372
8
8
  ---
9
9
 
10
10
  # Reviewer Agent
@@ -15,7 +15,7 @@ You are a strict reviewer. Your only function is to **approve or reject**. You d
15
15
 
16
16
  ### Setup (common to both passes)
17
17
 
18
- 1. Ground yourself in `CLAUDE.md` — already in your context when your host injects it; read it from disk ONLY if your host did not inject it. Then read `.navori/state/handoffs/impl_<feature>.md`, `.navori/state/handoffs/audit_ticket_<ID>.md` and `.navori/state/handoffs/solution_<scope>.md` (whichever exist). When there IS a solution artifact, the diff is judged against the approach it records — an implementation that quietly took a different path is a `SPEC_MISS`, even if the code is good. You do NOT re-open the design itself: whether that approach was the right one was settled in its own phase; your question is whether the code did what was agreed.
18
+ 1. Ground yourself in `CLAUDE.md` — already in your context when your host injects it; read it from disk ONLY if your host did not inject it. Then read `.navori/state/handoffs/impl_<feature>.md` (when `impl_<feature>` carries `doubts`, read them too: each is a `{file, reason}`), `.navori/state/handoffs/audit_ticket_<ID>.md` and `.navori/state/handoffs/solution_<scope>.md` (whichever exist). When there IS a solution artifact, the diff is judged against the approach it records — an implementation that quietly took a different path is a `SPEC_MISS`, even if the code is good. You do NOT re-open the design itself: whether that approach was the right one was settled in its own phase; your question is whether the code did what was agreed.
19
19
  2. Identify modified files. Diff against `{{prTarget}}` (the PR's target
20
20
  branch), **not** against the fork point: it's the EXACT diff GitHub will show and
21
21
  the one publisher reviews. In most repos the branch you forked from and
@@ -59,6 +59,7 @@ Does the diff do EXACTLY what was asked? You don't review style yet.
59
59
  - **SDD traceability** (only if `{{sdd.specsDir}}/<feature>/tasks.md` exists): each `R<n>` in the batch is covered by ≥1 test that references it with `// Covers: R<n>`. An `R<n>` in the batch without a traceable test → `SPEC_MISS`.
60
60
  <!-- navori:if planTiers -->
61
61
  - With a workplan: an assigned `A<n>` without evidence in `acceptance`, a file outside the workplan's files without a covering decision, or `navori plan classify <feature> --diff` returning a higher level than declared → `CHANGES_REQUESTED`.
62
+ - With a workplan: run `navori plan check <feature> --json`; every `A<n>` marked `cumplido` without recorded evidence (the routing-watch hook records it only when the host ran the exact `command`) is a finding → `CHANGES_REQUESTED`.
62
63
  <!-- /navori:if -->
63
64
  - Screen changes are reviewed on the **diff + the repo's tests** — browser/visual validation is **not a default gate**. Only when the user explicitly requested a visual check in this task do you confirm it happened; if it was requested and skipped, flag it. Never escalate a screen change to a human just because no browser check ran.
64
65
 
@@ -142,6 +143,12 @@ Write `.navori/state/handoffs/review_<feature>.md`:
142
143
  **Spec gaps (if SPEC_MISS):**
143
144
  1. <file>:<line> — <what's missing vs what was asked>
144
145
 
146
+ ### Implementer doubts
147
+ (only when `impl_<feature>` declares `doubts`; reviewed in addition to the diff, not instead of it)
148
+ - <file>:<line> — resolved | issue — <conclusion, with file:line evidence>
149
+
150
+ An unanswered doubt, or one concluded `issue`, follows the normal severity and score rules.
151
+
145
152
  ## Pass 2 — Code quality (only if SPEC_OK)
146
153
  **Partial verdict:** QUALITY_OK | QUALITY_MISS
147
154
 
@@ -160,6 +167,21 @@ Write `.navori/state/handoffs/review_<feature>.md`:
160
167
 
161
168
  ### Informational observations (50-79, don't block)
162
169
  1. [score:65] <file>:<line> — <nitpick or suggestion>
170
+
171
+ ### Coverage
172
+ Required when `APPROVED` with no issue ≥80 (a bare approval with no Coverage is not allowed): one row per `review-diff` section 1-9.
173
+
174
+ | Section | Reviewed (what) / n/a + reason |
175
+ |---|---|
176
+ | Types and contracts | <what you checked> / n/a + <reason> |
177
+ | Data layer | |
178
+ | Error handling | |
179
+ | Security | |
180
+ | No hardcode | |
181
+ | Naming and structure | |
182
+ | Over-engineering | |
183
+ | Dead code | |
184
+ | Quality gate | |
163
185
  ```
164
186
 
165
187
  ## Chat reply
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: scout
3
3
  description: Read-only reconnaissance — maps a broad area or answers one scoped question, with cited evidence. Does not modify code. Use when a sub-question is worth running in parallel, or a lookup is worth isolating from the coordinator's own context — not a proxy for a Code discovery routing call the coordinator can make itself this turn.
4
- tools: Read, Glob, Grep, Bash, Write, mcp__engram__mem_search, mcp__engram__mem_get_observation, mcp__codegraph__codegraph_explore
4
+ tools: Read, Glob, Grep, Bash, Write, WebFetch, WebSearch, mcp__engram__mem_search, mcp__engram__mem_get_observation, mcp__codegraph__codegraph_explore
5
5
  model: {{models.scout}}
6
6
  effort: {{effort.scout}}
7
7
  maxWords: 1050
@@ -0,0 +1,245 @@
1
+ # Shared Bash-outcome helpers (spec 0039 R6/R7, carry-over 0038 D1) — inlined at
2
+ # render time. Defines functions only: nothing runs on include, so a hook that
3
+ # carries this partial pays parse time and no process.
4
+ #
5
+ # Depends on `payload`/`payload_field` (`extract-cmd`) and `nv_project_dir` /
6
+ # `nv_progress_dir` (`hook-input`); include both in the same script.
7
+ #
8
+ # THE SUCCESS LANE. The host runs a criterion's `command` in its own Bash tool;
9
+ # `PostToolUse` fires only on success, so this lane's very firing is the success
10
+ # signal. It appends ONE line per matching pending criterion to
11
+ # `<state dir>/workplan_<feature>.evidence.jsonl`, which `plan update` compares
12
+ # against the criterion and the current tree (`lib/plan/evidence.ts`). It never
13
+ # runs the criterion's command and never writes anything but that line.
14
+ #
15
+ # FAIL-OPEN, NO PARTIAL LINE. Every path returns 0. The line is built entirely in
16
+ # memory and written by a single `printf >>` as the LAST step, so a kill at any
17
+ # earlier point (the tree fingerprint is the slow part) leaves no line at all and
18
+ # `plan update` rejects with "no run recorded".
19
+ #
20
+ # FAST PATH, BUILTINS ONLY. `acceptance-index` (rewritten by the CLI) holds
21
+ # `<command JSON-escaped>\t<feature>\t<A<n>>\t<state dir>` per pending
22
+ # criterion. The command is matched as the EXACT JSON string value of
23
+ # `"command"` in the raw payload — no decoding, no normalization, no process.
24
+ # A `tool_response` cannot forge a match: inside it every quote is `\"`.
25
+
26
+ # Git with the repo-controlled code paths neutralized (invariant 9): fsmonitor is
27
+ # a command git runs on index refresh, hooksPath redirects hooks. Same flags as
28
+ # `GIT_HARDENING` in `lib/plan/evidence.ts`.
29
+ navori_git() {
30
+ git -c core.fsmonitor=false -c core.hooksPath=/dev/null "$@"
31
+ }
32
+
33
+ # Content hash of the whole working tree — the SAME procedure as
34
+ # `fingerprintTree` in `lib/plan/evidence.ts`, which `plan update` re-runs to
35
+ # compare (change one, change both): no `git add` (filters/fsmonitor), blobs by
36
+ # `hash-object --no-filters`, a scratch index, `write-tree`. Prints the tree hash
37
+ # or returns 1. Exec bit is `[ -x ]` where the TypeScript side tests
38
+ # `mode & 0o111`; they differ only for a file executable by group/other but not
39
+ # by its owner, which then fails safe (plan update asks for a rerun).
40
+ navori_tree_fingerprint() {
41
+ (
42
+ cd "$1" 2>/dev/null || exit 1
43
+ scratch=$(navori_git rev-parse --path-format=absolute --git-path navori-fp-index 2>/dev/null) || exit 1
44
+ [ -n "$scratch" ] || exit 1
45
+ [ ! -e "$scratch.lock" ] || exit 1
46
+ work=$(mktemp -d "${TMPDIR:-/tmp}/navori-fp.XXXXXX" 2>/dev/null) || exit 1
47
+ trap 'rm -f "$work"/list "$work"/files "$work"/links "$work"/paths "$work"/shas; rmdir "$work" 2>/dev/null' EXIT
48
+ navori_git ls-files -z --cached --others --exclude-standard --deduplicate -- . \
49
+ ':(exclude).navori/state' ':(exclude).claude/progress' ':(exclude).codex/progress' \
50
+ ':(exclude).claude/worktrees' > "$work/list" 2>/dev/null || exit 1
51
+ : > "$work/links"
52
+ while IFS= read -r -d '' p; do
53
+ case "$p" in *$'\n'*) exit 1 ;; esac
54
+ if [ -L "$p" ]; then
55
+ target=$(readlink "./$p") || exit 1
56
+ sha=$(printf '%s' "$target" | navori_git hash-object -w --no-filters --stdin 2>/dev/null) || exit 1
57
+ [ -n "$sha" ] || exit 1
58
+ printf '120000 %s\t%s\n' "$sha" "$p" >&3
59
+ elif [ -f "$p" ]; then
60
+ if [ -x "$p" ]; then mode=100755; else mode=100644; fi
61
+ printf '%s\t%s\n' "$mode" "$p"
62
+ fi
63
+ done < "$work/list" > "$work/files" 3>> "$work/links"
64
+ if [ -s "$work/files" ]; then
65
+ cut -f2- "$work/files" > "$work/paths"
66
+ navori_git hash-object -w --no-filters --stdin-paths < "$work/paths" > "$work/shas" 2>/dev/null || exit 1
67
+ [ "$(wc -l < "$work/paths" | tr -d ' ')" = "$(wc -l < "$work/shas" | tr -d ' ')" ] || exit 1
68
+ awk -F'\t' 'NR==FNR { m[FNR]=$1; p[FNR]=substr($0, index($0, "\t")+1); next } { printf "%s %s\t%s\n", m[FNR], $0, p[FNR] }' \
69
+ "$work/files" "$work/shas" >> "$work/links" || exit 1
70
+ fi
71
+ export GIT_INDEX_FILE=$scratch
72
+ navori_git read-tree --empty >/dev/null 2>&1 || exit 1
73
+ if [ -s "$work/links" ]; then
74
+ tr '\n' '\0' < "$work/links" | navori_git update-index --add -z --index-info >/dev/null 2>&1 || exit 1
75
+ fi
76
+ navori_git write-tree 2>/dev/null
77
+ )
78
+ }
79
+
80
+ # Escapes backslash and double quote for a JSON string value; prints it.
81
+ navori_json_str() {
82
+ local s=$1
83
+ s=${s//\\/\\\\}
84
+ s=${s//\"/\\\"}
85
+ printf '%s' "$s"
86
+ }
87
+
88
+ # Repeat-failure state is shared by the PostToolUseFailure watcher and the
89
+ # existing success lane. A successful Bash call only pays for Node when its
90
+ # session already has a failure-state file; ordinary calls use builtins only.
91
+ navori_bash_failure_state() {
92
+ local action=$1 sid=${CLAUDE_CODE_SESSION_ID:-} dir file
93
+ case "$sid" in "" | *[!A-Za-z0-9._-]*) return 0 ;; esac
94
+ [ -n "${nv_project_dir:-}" ] || return 0
95
+ dir=$nv_project_dir/.navori/state/hooks/bash-outcome-watch
96
+ file=$dir/$sid
97
+ if [ "$action" = reset ]; then
98
+ [ -f "$file" ] && [ ! -L "$file" ] || return 0
99
+ fi
100
+ command -v node >/dev/null 2>&1 || return 0
101
+ NV_BASH_OUTCOME_ACTION=$action NV_BASH_OUTCOME_DIR=$dir NV_BASH_OUTCOME_FILE=$file \
102
+ NV_BASH_OUTCOME_ROOT=$nv_project_dir node -e '
103
+ const fs = require("node:fs");
104
+ const path = require("node:path");
105
+ const crypto = require("node:crypto");
106
+ const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
107
+ const env = process.env;
108
+ let input = "";
109
+ process.stdin.setEncoding("utf8");
110
+ process.stdin.on("data", (chunk) => { input += chunk; });
111
+ process.stdin.on("end", () => {
112
+ try {
113
+ const payload = JSON.parse(input);
114
+ if (payload.tool_name !== "Bash" || payload.session_id !== env.CLAUDE_CODE_SESSION_ID) return;
115
+ if (payload.is_interrupt === true || payload.tool_response?.is_interrupt === true) return;
116
+ const command = payload.tool_input?.command;
117
+ const cwd = payload.cwd;
118
+ if (typeof command !== "string" || !command.trim() || typeof cwd !== "string" || !cwd) return;
119
+ const agent = typeof payload.agent_id === "string" ? payload.agent_id : "";
120
+ const key = digest(JSON.stringify([command.trim().replace(/\s+/g, " "), cwd, agent]));
121
+ const root = path.resolve(env.NV_BASH_OUTCOME_ROOT);
122
+ const dir = env.NV_BASH_OUTCOME_DIR;
123
+ const file = env.NV_BASH_OUTCOME_FILE;
124
+ if (dir !== path.join(root, ".navori/state/hooks/bash-outcome-watch")) return;
125
+ for (const component of [root, path.join(root, ".navori"), path.join(root, ".navori/state"), path.join(root, ".navori/state/hooks"), dir]) {
126
+ if (fs.existsSync(component)) {
127
+ if (!fs.lstatSync(component).isDirectory() || fs.lstatSync(component).isSymbolicLink()) return;
128
+ } else if (env.NV_BASH_OUTCOME_ACTION === "failure") {
129
+ fs.mkdirSync(component);
130
+ } else return;
131
+ }
132
+ if (fs.existsSync(file) && (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())) return;
133
+ let rows = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)) : [];
134
+ if (!Array.isArray(rows) || rows.some((row) => typeof row.key !== "string" || typeof row.sig !== "string" || typeof row.count !== "number" || typeof row.epoch !== "number")) return;
135
+ if (env.NV_BASH_OUTCOME_ACTION === "reset") {
136
+ const next = rows.filter((row) => row.key !== key);
137
+ if (next.length === rows.length) return;
138
+ rows = next;
139
+ } else {
140
+ const error = payload.error;
141
+ if (typeof error !== "string") return;
142
+ const match = /exit code\s+(\d+)/i.exec(error);
143
+ if (!match) return;
144
+ const code = match[1];
145
+ const escapedRoot = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
146
+ const home = env.HOME ? env.HOME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
147
+ const body = error.replace(/^.*exit code\s+\d+.*\r?\n?/im, "")
148
+ .replace(/\x1b\[[0-9;]*m/g, "")
149
+ .replace(new RegExp(escapedRoot, "g"), "<root>")
150
+ .replace(home ? new RegExp(home, "g") : /\b(?!x)x\b/g, "~")
151
+ .replace(/(?:\/tmp|\/private\/tmp|\/var\/folders)\/[^\s:]+/g, "<tmp>")
152
+ .replace(/\b\d{4}-\d\d-\d\d[T ]\d\d:\d\d:\d\d(?:\.\d+)?Z?\b/g, "<t>")
153
+ .replace(/\b\d\d:\d\d:\d\d(?:\.\d+)?\b/g, "<t>")
154
+ .replace(/\b\d+(?:\.\d+)?\s?(?:ms|seconds?|minutes?)\b/gi, "<d>")
155
+ .replace(/\b[0-9a-f]{8,}\b/gi, "<h>")
156
+ .split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 20).join("\n");
157
+ if (!body) return;
158
+ const sig = digest(code + "\n" + body);
159
+ const previous = rows.find((row) => row.key === key);
160
+ const count = previous?.sig === sig ? Math.min(previous.count + 1, 3) : 1;
161
+ const notified = previous?.sig === sig && previous.notified === true;
162
+ rows = rows.filter((row) => row.key !== key);
163
+ rows.push({ key, sig, count, notified: notified || count === 3, epoch: Date.now() });
164
+ if (count === 3 && !notified) {
165
+ const first = body.split("\n")[0].slice(0, 100);
166
+ const note = `navori: el comando ${command.trim().slice(0, 100)} falló 3 veces con la misma firma (exit ${code}: ${first}). Cambia de enfoque con debug-failure o escala al usuario. Si el rojo es intencional, ignora esta nota.`;
167
+ var output = note.slice(0, 400);
168
+ }
169
+ }
170
+ rows.sort((a, b) => b.epoch - a.epoch);
171
+ rows = rows.slice(0, 50);
172
+ const temp = file + "." + process.pid + ".tmp";
173
+ fs.writeFileSync(temp, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length ? "\n" : ""), { flag: "wx", mode: 0o600 });
174
+ fs.renameSync(temp, file);
175
+ if (output) process.stdout.write(output + "\n");
176
+ } catch { /* advisory: never change the tool result */ }
177
+ });
178
+ ' <<< "$payload" 2>/dev/null || true
179
+ return 0
180
+ }
181
+
182
+ # Called by `routing-watch.sh` once it knows the tool is `Bash`, and only under
183
+ # `claude-post-tool-use` (Codex fires PostToolUse on failure too, so no signal).
184
+ navori_bash_success_lane() {
185
+ local idx=$nv_project_dir/$nv_progress_dir/acceptance-index
186
+ [ -s "$idx" ] || return 0
187
+ # A backgrounded call "succeeds" when it LAUNCHES, not when it finishes; an
188
+ # interrupted one did not complete.
189
+ case "$payload" in
190
+ *'"run_in_background":true'* | *'"interrupted":true'*) return 0 ;;
191
+ esac
192
+ local c f i d hits=
193
+ while IFS=$'\t' read -r c f i d; do
194
+ [ -n "$c" ] || continue
195
+ case "$payload" in
196
+ *'"command":"'"$c"'"'[,}]*) hits="$hits$c"$'\t'"$f"$'\t'"$i"$'\t'"$d"$'\n' ;;
197
+ esac
198
+ done < "$idx"
199
+ [ -n "$hits" ] || return 0
200
+
201
+ # Slow path: exactly one criterion command ran. Everything is computed first,
202
+ # the write is last.
203
+ local cwd tree head wt htree dirty sid agent ts root
204
+ cwd=$(payload_field cwd)
205
+ [ -n "$cwd" ] || return 0
206
+ tree=$(navori_git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || return 0
207
+ case "$cwd$tree" in *[[:cntrl:]]*) return 0 ;; esac
208
+ head=$(navori_git -C "$tree" rev-parse HEAD 2>/dev/null) || head=
209
+ wt=$(navori_tree_fingerprint "$tree") || return 0
210
+ [ -n "$wt" ] || return 0
211
+ htree=$(navori_git -C "$tree" rev-parse 'HEAD^{tree}' 2>/dev/null) || htree=
212
+ dirty=true
213
+ [ "$wt" = "$htree" ] && dirty=false
214
+ sid=$(payload_field session_id | tr -cd 'A-Za-z0-9._-')
215
+ agent=$(payload_field agent_id | tr -cd 'A-Za-z0-9._-')
216
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
217
+ root=$(pwd -P 2>/dev/null) || root=$nv_project_dir
218
+
219
+ local line extra file
220
+ while IFS=$'\t' read -r c f i d; do
221
+ [ -n "$f" ] || continue
222
+ # The index is CLI-written state, but the hook is the one that opens a path
223
+ # taken from it: slug and id shapes, an absolute directory inside this
224
+ # project, no symlinks.
225
+ case "$f" in *[!a-z0-9._-]* | [!a-z0-9]*) continue ;; esac
226
+ case "$i" in
227
+ A[0-9]*) case "${i#A}" in *[!0-9]*) continue ;; esac ;;
228
+ *) continue ;;
229
+ esac
230
+ case "$d" in "$root"/* | "$nv_project_dir"/*) ;; *) continue ;; esac
231
+ case "$d" in *[[:cntrl:]]* | */../* | */..) continue ;; esac
232
+ [ -d "$d" ] && [ ! -L "$d" ] || continue
233
+ file=$d/workplan_$f.evidence.jsonl
234
+ [ ! -L "$file" ] || continue
235
+ # `$c` is already JSON-escaped: it is the index's own field.
236
+ extra=
237
+ [ -z "$sid" ] || extra=$extra',"sessionId":"'$sid'"'
238
+ [ -z "$agent" ] || extra=$extra',"agentId":"'$agent'"'
239
+ line='{"ts":"'$ts'","feature":"'$f'","id":"'$i'","command":"'$c'","tree":"'$(navori_json_str "$tree")'","cwd":"'$(navori_json_str "$cwd")'","head":"'$head'","worktreeTree":"'$wt'","dirty":'$dirty$extra'}'
240
+ printf '%s\n' "$line" >> "$file" 2>/dev/null || true
241
+ done <<NAVORI_HITS
242
+ $hits
243
+ NAVORI_HITS
244
+ return 0
245
+ }
@@ -0,0 +1,25 @@
1
+ # Can this gate hand a no-verdict outcome to the user instead of blocking?
2
+ # Inlined at render time (see lib/render/hook-includes.ts). Decides only; the
3
+ # JSON that speaks to the host stays in each script (partials never carry host
4
+ # output vocabulary — hook-output-contract.test.ts).
5
+ #
6
+ # WHY (#1117): when the TOOL failed (scanner missing a flag, declared runner not
7
+ # on PATH) the gate has no verdict, and "run it yourself outside the agent" is a
8
+ # dead end. Claude Code's PreToolUse can ask the human; that is the right exit.
9
+ #
10
+ # Returns 0 only when ALL hold; any doubt is "no", and "no" is today's block:
11
+ # - the script is not a Codex copy (Codex drops `permissionDecision`, so an
12
+ # ask there would let the call PROCEED unasked). Decided by WHERE THE SCRIPT
13
+ # LIVES, the same rule as the hook-input partial; hooks live in `.codex/hooks/`,
14
+ # plugin scripts in `.codex/scripts/`.
15
+ # - the payload is a real PreToolUse hook call. A git hook or the CLI
16
+ # (`</dev/null`) has no payload, so there is nobody to ask.
17
+ # - jq exists to build the reason JSON safely; without it, block.
18
+ # Needs `payload`/`payload_field` from the extract-cmd partial.
19
+ navori_can_ask() {
20
+ case "$0" in
21
+ *".codex/hooks/"* | *".codex/scripts/"*) return 1 ;;
22
+ esac
23
+ command -v jq >/dev/null 2>&1 || return 1
24
+ [ "$(payload_field hook_event_name)" = "PreToolUse" ]
25
+ }