navori 0.11.0 → 0.11.2

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 (46) 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 +7 -3
  4. package/dist/assets/core/core-assets/agents/reviewer.md +25 -3
  5. package/dist/assets/core/core-assets/agents/scout.md +1 -1
  6. package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +6 -1
  7. package/dist/assets/core/core-assets/hooks/_partials/bash-outcome.sh +332 -0
  8. package/dist/assets/core/core-assets/hooks/_partials/gate-ask.sh +25 -0
  9. package/dist/assets/core/core-assets/hooks/_partials/hook-input.sh +33 -0
  10. package/dist/assets/core/core-assets/hooks/_partials/resolve-worktree.sh +257 -91
  11. package/dist/assets/core/core-assets/hooks/bash-outcome-watch.sh +21 -0
  12. package/dist/assets/core/core-assets/hooks/general-purpose-confirm.sh +70 -0
  13. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +21 -1
  14. package/dist/assets/core/core-assets/hooks/implementer-no-markdown.sh +23 -5
  15. package/dist/assets/core/core-assets/hooks/master-accept-confirm.sh +31 -7
  16. package/dist/assets/core/core-assets/hooks/master-plan-context.sh +7 -1
  17. package/dist/assets/core/core-assets/hooks/model-advisor.sh +23 -16
  18. package/dist/assets/core/core-assets/hooks/plan-gate.sh +45 -5
  19. package/dist/assets/core/core-assets/hooks/pr-publisher-confirm.sh +16 -0
  20. package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +37 -6
  21. package/dist/assets/core/core-assets/hooks/role-guard.sh +263 -0
  22. package/dist/assets/core/core-assets/hooks/routing-watch.sh +31 -0
  23. package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +143 -19
  24. package/dist/assets/core/core-assets/managed/orquestacion.md +26 -19
  25. package/dist/assets/core/core-assets/managed/sdd.md +1 -1
  26. package/dist/assets/core/core-assets/master-plan/en/ux.md +129 -0
  27. package/dist/assets/core/core-assets/master-plan/ux.md +129 -0
  28. package/dist/assets/core/core-assets/presets/fastapi-python/managed/stack.md +7 -0
  29. package/dist/assets/core/core-assets/presets/fastapi-python.json +19 -0
  30. package/dist/assets/core/core-assets/settings/settings-base.json +2 -0
  31. package/dist/assets/core/core-assets/skills/debug-failure.md +1 -1
  32. package/dist/assets/core/core-assets/skills/locate-code.md +1 -1
  33. package/dist/assets/core/core-assets/skills/master-plan.md +21 -15
  34. package/dist/assets/core/core-assets/skills/plan-advanced.md +1 -1
  35. package/dist/assets/core/core-assets/skills/plan-simple.md +2 -2
  36. package/dist/assets/core/core-assets/skills/spec-bootstrap.md +1 -1
  37. package/dist/assets/core/core-assets/skills/verify-before-done.md +3 -3
  38. package/dist/assets/plugins/codegraph/managed/codegraph-search-v2.md +1 -1
  39. package/dist/assets/plugins/codegraph/skills/codegraph-access-v2.md +1 -1
  40. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +77 -16
  41. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +4 -4
  42. package/dist/assets/plugins/tgrep/managed/guard-destructive-search-lane.sh +20 -0
  43. package/dist/assets/plugins/tgrep/plugin.json +10 -0
  44. package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +222 -0
  45. package/dist/index.js +716 -434
  46. 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: 2448
8
9
  ---
9
10
 
10
11
  # Implementer Agent
@@ -40,11 +41,11 @@ You execute **a single** task from start to verification. You don't orchestrate,
40
41
  {{qualityGate.fast}}
41
42
  ```
42
43
 
43
- If it fails: fix it and re-run. Don't return with red. You are the single owner of this gate run: never share it with another process, never poll `pgrep`/`ps` for it, and a timeout is never a success signal. If the gate 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 (no shell `&`, no `run_in_background`, no `Monitor`) — you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait. When you can't explain WHY it failed, apply `.claude/skills/debug-failure/SKILL.md` before touching anything — the size of the output is not the trigger, the missing root cause is, and a failure whose error stream you truncated away reads the same as one you understand. If your second fix attempt fails the same way, that same skill's hypothesis re-check governs instead of throwing a third patch.
44
+ If it fails: fix it and re-run. Don't return with red. You are the single owner of this gate run: never share it with another process, never poll `pgrep`/`ps` for it, and a timeout is never a success signal. If the gate 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 (<!-- navori:if-not onCodex -->no shell `&`, no `run_in_background`, no `Monitor`<!-- /navori:if-not --><!-- navori:if onCodex -->no shell `&`, no background execution<!-- /navori:if -->) — you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait. When you can't explain WHY it failed, apply `.claude/skills/debug-failure/SKILL.md` before touching anything — the size of the output is not the trigger, the missing root cause is, and a failure whose error stream you truncated away reads the same as one you understand. If your second fix attempt fails the same way, that same skill's hypothesis re-check governs instead of throwing a third patch.
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: 2387
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
 
@@ -79,7 +80,7 @@ Apply `.claude/skills/review-diff/SKILL.md` — the full checklist by dimensions
79
80
  {{qualityGate.full}}
80
81
  ```
81
82
 
82
- Read it in full to verify (exit code + failure count), but leave only `exit 0` + the summary line in the report (e.g. `N passed`); when red, only the failing tail. Don't drag the full verbose log turn to turn. This evidence —green gate over the final diff, this cycle— is what the `publisher` reuses so it does **not** re-run the gate, so it must be fresh and over the diff that's going to be committed. You are the single owner of this gate run: the only handle that exists is this Bash call itself, correlated to the diff you're reviewing this turn — never share it with another process, and never poll `pgrep`/`ps` for it (it also matches other sessions' commands and never exits). A timeout is never a success signal. Run it in the foreground with the Bash tool's max `timeout`; if `{{qualityGate.full}}` can exceed it, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its `&&`-chained steps one by one in the foreground, each under the timeout — never background it (no shell `&`, no `run_in_background`, no `Monitor`), you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait.
83
+ Read it in full to verify (exit code + failure count), but leave only `exit 0` + the summary line in the report (e.g. `N passed`); when red, only the failing tail. Don't drag the full verbose log turn to turn. This evidence —green gate over the final diff, this cycle— is what the `publisher` reuses so it does **not** re-run the gate, so it must be fresh and over the diff that's going to be committed. You are the single owner of this gate run: the only handle that exists is this Bash call itself, correlated to the diff you're reviewing this turn — never share it with another process, and never poll `pgrep`/`ps` for it (it also matches other sessions' commands and never exits). A timeout is never a success signal. Run it in the foreground with the Bash tool's max `timeout`; if `{{qualityGate.full}}` can exceed it, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its `&&`-chained steps one by one in the foreground, each under the timeout — never background it (<!-- navori:if-not onCodex -->no shell `&`, no `run_in_background`, no `Monitor`<!-- /navori:if-not --><!-- navori:if onCodex -->no shell `&`, no background execution<!-- /navori:if -->), you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait.
83
84
 
84
85
  Don't gate a screen change on browser validation by default. Only if the user explicitly requested a visual/browser check and it wasn't done do you mark it incomplete — otherwise the diff + the repo's tests are the gate.
85
86
 
@@ -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
@@ -249,6 +249,9 @@ navori_audit_log() {
249
249
  # must treat "names nobody" as invalid data rather than as a different agent
250
250
  # (`ownerOf` in `lib/audit/parse.ts` is where that rule lives).
251
251
 
252
+ # Optional third argument `kind` (hard | ask | advisory): how binding the
253
+ # outcome was. Additive: callers that pass nothing keep today's records, and
254
+ # readers treat a missing `kind` as unclassified.
252
255
  printf '%s\n' "$(jq -cn \
253
256
  --arg name "${navori_audit_name:-unknown}" \
254
257
  --arg phase "${navori_audit_phase:-unknown}" \
@@ -258,13 +261,15 @@ navori_audit_log() {
258
261
  --arg src "${navori_audit_source:-core}" \
259
262
  --arg agent "${navori_audit_agent:-}" \
260
263
  --arg toolUseId "${navori_audit_tool_use_id:-}" \
264
+ --arg kind "${3:-}" \
261
265
  --argjson ms "$navori_audit_ms" \
262
266
  --argjson tsMs "$navori_audit_end" \
263
267
  '{tsMs:$tsMs,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
264
268
  + (if $tool == "" then {} else {tool:$tool} end)
265
269
  + (if $reason == "" then {} else {reason:$reason} end)
266
270
  + (if $agent == "" then {} else {agentId:$agent} end)
267
- + (if $toolUseId == "" then {} else {toolUseId:$toolUseId} end)' 2>/dev/null)" \
271
+ + (if $toolUseId == "" then {} else {toolUseId:$toolUseId} end)
272
+ + (if $kind == "" then {} else {kind:$kind} end)' 2>/dev/null)" \
268
273
  >> "$navori_audit_file" 2>/dev/null
269
274
 
270
275
  return 0
@@ -0,0 +1,332 @@
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
+ #
92
+ # `action` is `reset`/`failure` (Claude: the event says which) or `codex`
93
+ # (spec 0041 T12: Codex fires PostToolUse on failure too, so the OUTCOME is read
94
+ # from the rollout, `$2` = its path, `$3` = the call's `tool_use_id`).
95
+ navori_bash_failure_state() {
96
+ local action=$1 sid=${CLAUDE_CODE_SESSION_ID:-} dir file
97
+ if [ "$action" = codex ]; then
98
+ navori_plain_field session_id
99
+ sid=$navori_field
100
+ fi
101
+ case "$sid" in "" | *[!A-Za-z0-9._-]*) return 0 ;; esac
102
+ [ -n "${nv_project_dir:-}" ] || return 0
103
+ dir=$nv_project_dir/.navori/state/hooks/bash-outcome-watch
104
+ file=$dir/$sid
105
+ if [ "$action" = reset ]; then
106
+ [ -f "$file" ] && [ ! -L "$file" ] || return 0
107
+ fi
108
+ command -v node >/dev/null 2>&1 || return 0
109
+ NV_BASH_OUTCOME_ACTION=$action NV_BASH_OUTCOME_DIR=$dir NV_BASH_OUTCOME_FILE=$file \
110
+ NV_BASH_OUTCOME_SID=$sid NV_BASH_OUTCOME_TRANSCRIPT=${2:-} NV_BASH_OUTCOME_TOOL_USE=${3:-} \
111
+ NV_BASH_OUTCOME_ROOT=$nv_project_dir node -e '
112
+ const fs = require("node:fs");
113
+ const path = require("node:path");
114
+ const crypto = require("node:crypto");
115
+ const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
116
+ const env = process.env;
117
+ // Codex outcome (spec 0041 T12): the exit code of THIS call lives in the rollout
118
+ // record `event_msg`/`item_completed` whose `item.id` is the `tool_use_id` of the call
119
+ // (probe V4). Bounded to the tail of the file; only `exit_code` is read, never
120
+ // the output fields. Anything missing or unreadable -> null -> silence.
121
+ const codexExit = () => {
122
+ const max = 1048576;
123
+ const fd = fs.openSync(env.NV_BASH_OUTCOME_TRANSCRIPT, "r");
124
+ try {
125
+ const size = fs.fstatSync(fd).size;
126
+ const len = Math.min(size, max);
127
+ const buf = Buffer.alloc(len);
128
+ fs.readSync(fd, buf, 0, len, size - len);
129
+ const lines = buf.toString("utf8").split("\n");
130
+ for (let i = lines.length - 1; i >= 0; i--) {
131
+ const line = lines[i];
132
+ if (!line.includes(env.NV_BASH_OUTCOME_TOOL_USE) || !line.includes("item_completed")) continue;
133
+ let record;
134
+ try { record = JSON.parse(line); } catch { continue; }
135
+ const item = record?.payload?.item;
136
+ if (record?.type !== "event_msg" || record?.payload?.type !== "item_completed") continue;
137
+ if (item?.type !== "CommandExecution" || item?.id !== env.NV_BASH_OUTCOME_TOOL_USE) continue;
138
+ return Number.isInteger(item.exit_code) ? item.exit_code : null;
139
+ }
140
+ return null;
141
+ } finally { fs.closeSync(fd); }
142
+ };
143
+ let input = "";
144
+ process.stdin.setEncoding("utf8");
145
+ process.stdin.on("data", (chunk) => { input += chunk; });
146
+ process.stdin.on("end", () => {
147
+ try {
148
+ const payload = JSON.parse(input);
149
+ if (payload.tool_name !== "Bash" || payload.session_id !== env.NV_BASH_OUTCOME_SID) return;
150
+ if (payload.is_interrupt === true || payload.tool_response?.is_interrupt === true) return;
151
+ const command = payload.tool_input?.command;
152
+ const cwd = payload.cwd;
153
+ if (typeof command !== "string" || !command.trim() || typeof cwd !== "string" || !cwd) return;
154
+ const agent = typeof payload.agent_id === "string" ? payload.agent_id : "";
155
+ let action = env.NV_BASH_OUTCOME_ACTION;
156
+ let exitCode = null;
157
+ if (action === "codex") {
158
+ exitCode = codexExit();
159
+ if (exitCode === null) return;
160
+ action = exitCode === 0 ? "reset" : "failure";
161
+ }
162
+ const key = digest(JSON.stringify([command.trim().replace(/\s+/g, " "), cwd, agent]));
163
+ const root = path.resolve(env.NV_BASH_OUTCOME_ROOT);
164
+ const dir = env.NV_BASH_OUTCOME_DIR;
165
+ const file = env.NV_BASH_OUTCOME_FILE;
166
+ if (dir !== path.join(root, ".navori/state/hooks/bash-outcome-watch")) return;
167
+ for (const component of [root, path.join(root, ".navori"), path.join(root, ".navori/state"), path.join(root, ".navori/state/hooks"), dir]) {
168
+ if (fs.existsSync(component)) {
169
+ if (!fs.lstatSync(component).isDirectory() || fs.lstatSync(component).isSymbolicLink()) return;
170
+ } else if (action === "failure") {
171
+ fs.mkdirSync(component);
172
+ } else return;
173
+ }
174
+ if (fs.existsSync(file) && (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())) return;
175
+ let rows = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)) : [];
176
+ if (!Array.isArray(rows) || rows.some((row) => typeof row.key !== "string" || typeof row.sig !== "string" || typeof row.count !== "number" || typeof row.epoch !== "number")) return;
177
+ if (action === "reset") {
178
+ const next = rows.filter((row) => row.key !== key);
179
+ if (next.length === rows.length) return;
180
+ rows = next;
181
+ } else {
182
+ const error = exitCode === null ? payload.error : "";
183
+ if (typeof error !== "string") return;
184
+ const match = exitCode === null ? /exit code\s+(\d+)/i.exec(error) : [null, String(exitCode)];
185
+ if (!match) return;
186
+ const code = match[1];
187
+ const escapedRoot = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
188
+ const home = env.HOME ? env.HOME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
189
+ const body = error.replace(/^.*exit code\s+\d+.*\r?\n?/im, "")
190
+ .replace(/\x1b\[[0-9;]*m/g, "")
191
+ .replace(new RegExp(escapedRoot, "g"), "<root>")
192
+ .replace(home ? new RegExp(home, "g") : /\b(?!x)x\b/g, "~")
193
+ .replace(/(?:\/tmp|\/private\/tmp|\/var\/folders)\/[^\s:]+/g, "<tmp>")
194
+ .replace(/\b\d{4}-\d\d-\d\d[T ]\d\d:\d\d:\d\d(?:\.\d+)?Z?\b/g, "<t>")
195
+ .replace(/\b\d\d:\d\d:\d\d(?:\.\d+)?\b/g, "<t>")
196
+ .replace(/\b\d+(?:\.\d+)?\s?(?:ms|seconds?|minutes?)\b/gi, "<d>")
197
+ .replace(/\b[0-9a-f]{8,}\b/gi, "<h>")
198
+ .split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 20).join("\n");
199
+ if (!body && exitCode === null) return;
200
+ const sig = digest(code + "\n" + body);
201
+ const previous = rows.find((row) => row.key === key);
202
+ const count = previous?.sig === sig ? Math.min(previous.count + 1, 3) : 1;
203
+ const notified = previous?.sig === sig && previous.notified === true;
204
+ rows = rows.filter((row) => row.key !== key);
205
+ rows.push({ key, sig, count, notified: notified || count === 3, epoch: Date.now() });
206
+ if (count === 3 && !notified) {
207
+ const first = body.split("\n")[0].slice(0, 100);
208
+ const note = `navori: el comando ${command.trim().slice(0, 100)} falló 3 veces con la misma firma (exit ${code}${first ? ": " + first : ""}). Cambia de enfoque con debug-failure o escala al usuario. Si el rojo es intencional, ignora esta nota.`;
209
+ var output = note.slice(0, 400);
210
+ }
211
+ }
212
+ rows.sort((a, b) => b.epoch - a.epoch);
213
+ rows = rows.slice(0, 50);
214
+ const temp = file + "." + process.pid + ".tmp";
215
+ fs.writeFileSync(temp, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length ? "\n" : ""), { flag: "wx", mode: 0o600 });
216
+ fs.renameSync(temp, file);
217
+ if (output) process.stdout.write(output + "\n");
218
+ } catch { /* advisory: never change the tool result */ }
219
+ });
220
+ ' <<< "$payload" 2>/dev/null || true
221
+ return 0
222
+ }
223
+
224
+ # A plain top-level string field of the raw payload, with builtins only (no
225
+ # fork), into `navori_field`. Matches `"key":"value"` with an unescaped quote
226
+ # before the colon, which a `tool_response` cannot forge (inside it every quote is
227
+ # `\"`). Empty when absent, spaced or escaped: the caller treats that as silence.
228
+ navori_plain_field() {
229
+ local needle="\"$1\":\"" rest
230
+ navori_field=
231
+ case "$payload" in *"$needle"*) ;; *) return 0 ;; esac
232
+ rest=${payload#*"$needle"}
233
+ navori_field=${rest%%\"*}
234
+ case "$navori_field" in *\\*) navori_field= ;; esac
235
+ return 0
236
+ }
237
+
238
+ # Spec 0041 T12 (R11): the Codex Bash outcome lane, run by `routing-watch.sh`
239
+ # on `PostToolUse(Bash)` under Codex. Codex fires PostToolUse on failure too, so
240
+ # the exit code is read from the rollout (`transcript_path`): a fixed-string grep
241
+ # over the BOUNDED tail finds the `item_completed` record of this `tool_use_id`.
242
+ # A success with no failure state costs no Node; everything else (a failure, or a
243
+ # success that must reset a counter) goes through `navori_bash_failure_state`,
244
+ # which parses only `exit_code`. FAIL-OPEN and silent: no rollout, no matching
245
+ # record or an unreadable one means no advice. Prints the advice, if any.
246
+ navori_bash_codex_outcome() {
247
+ local tid tp sid line
248
+ navori_plain_field tool_use_id
249
+ tid=$navori_field
250
+ navori_plain_field transcript_path
251
+ tp=$navori_field
252
+ navori_plain_field session_id
253
+ sid=$navori_field
254
+ case "$tid" in "" | *[!A-Za-z0-9._:-]*) return 0 ;; esac
255
+ case "$sid" in "" | *[!A-Za-z0-9._-]*) return 0 ;; esac
256
+ case "$tp" in *.jsonl) ;; *) return 0 ;; esac
257
+ [ -f "$tp" ] && [ -r "$tp" ] || return 0
258
+ line=$(tail -c 1048576 "$tp" 2>/dev/null | grep -F -- "$tid" 2>/dev/null | grep -F '"item_completed"' 2>/dev/null | tail -n 1)
259
+ [ -n "$line" ] || return 0
260
+ case "$line" in
261
+ *'"exit_code":0,'* | *'"exit_code":0}'*)
262
+ [ -f "${nv_project_dir:-}/.navori/state/hooks/bash-outcome-watch/$sid" ] || return 0
263
+ ;;
264
+ esac
265
+ navori_bash_failure_state codex "$tp" "$tid"
266
+ return 0
267
+ }
268
+
269
+ # Called by `routing-watch.sh` once it knows the tool is `Bash`, and only under
270
+ # `claude-post-tool-use` (Codex fires PostToolUse on failure too, so no signal).
271
+ navori_bash_success_lane() {
272
+ local idx=$nv_project_dir/$nv_progress_dir/acceptance-index
273
+ [ -s "$idx" ] || return 0
274
+ # A backgrounded call "succeeds" when it LAUNCHES, not when it finishes; an
275
+ # interrupted one did not complete.
276
+ case "$payload" in
277
+ *'"run_in_background":true'* | *'"interrupted":true'*) return 0 ;;
278
+ esac
279
+ local c f i d hits=
280
+ while IFS=$'\t' read -r c f i d; do
281
+ [ -n "$c" ] || continue
282
+ case "$payload" in
283
+ *'"command":"'"$c"'"'[,}]*) hits="$hits$c"$'\t'"$f"$'\t'"$i"$'\t'"$d"$'\n' ;;
284
+ esac
285
+ done < "$idx"
286
+ [ -n "$hits" ] || return 0
287
+
288
+ # Slow path: exactly one criterion command ran. Everything is computed first,
289
+ # the write is last.
290
+ local cwd tree head wt htree dirty sid agent ts root
291
+ cwd=$(payload_field cwd)
292
+ [ -n "$cwd" ] || return 0
293
+ tree=$(navori_git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || return 0
294
+ case "$cwd$tree" in *[[:cntrl:]]*) return 0 ;; esac
295
+ head=$(navori_git -C "$tree" rev-parse HEAD 2>/dev/null) || head=
296
+ wt=$(navori_tree_fingerprint "$tree") || return 0
297
+ [ -n "$wt" ] || return 0
298
+ htree=$(navori_git -C "$tree" rev-parse 'HEAD^{tree}' 2>/dev/null) || htree=
299
+ dirty=true
300
+ [ "$wt" = "$htree" ] && dirty=false
301
+ sid=$(payload_field session_id | tr -cd 'A-Za-z0-9._-')
302
+ agent=$(payload_field agent_id | tr -cd 'A-Za-z0-9._-')
303
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
304
+ root=$(pwd -P 2>/dev/null) || root=$nv_project_dir
305
+
306
+ local line extra file
307
+ while IFS=$'\t' read -r c f i d; do
308
+ [ -n "$f" ] || continue
309
+ # The index is CLI-written state, but the hook is the one that opens a path
310
+ # taken from it: slug and id shapes, an absolute directory inside this
311
+ # project, no symlinks.
312
+ case "$f" in *[!a-z0-9._-]* | [!a-z0-9]*) continue ;; esac
313
+ case "$i" in
314
+ A[0-9]*) case "${i#A}" in *[!0-9]*) continue ;; esac ;;
315
+ *) continue ;;
316
+ esac
317
+ case "$d" in "$root"/* | "$nv_project_dir"/*) ;; *) continue ;; esac
318
+ case "$d" in *[[:cntrl:]]* | */../* | */..) continue ;; esac
319
+ [ -d "$d" ] && [ ! -L "$d" ] || continue
320
+ file=$d/workplan_$f.evidence.jsonl
321
+ [ ! -L "$file" ] || continue
322
+ # `$c` is already JSON-escaped: it is the index's own field.
323
+ extra=
324
+ [ -z "$sid" ] || extra=$extra',"sessionId":"'$sid'"'
325
+ [ -z "$agent" ] || extra=$extra',"agentId":"'$agent'"'
326
+ 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'}'
327
+ printf '%s\n' "$line" >> "$file" 2>/dev/null || true
328
+ done <<NAVORI_HITS
329
+ $hits
330
+ NAVORI_HITS
331
+ return 0
332
+ }
@@ -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
+ }
@@ -90,6 +90,39 @@ nv_subagent_type() {
90
90
  fi
91
91
  }
92
92
 
93
+ # The agent a spawn call TARGETS, never the one that makes the call (spec 0041
94
+ # H16). Claude: `tool_input.subagent_type`. Codex PreToolUse/PostToolUse of a
95
+ # spawn: `tool_input.agent_type` only — the top-level `agent_type` there is the
96
+ # CALLER's. On `SubagentStop` the top-level `agent_type` IS the finishing
97
+ # subagent, so that event reads it instead.
98
+ nv_spawn_target_type() {
99
+ if [ "$nv_engine" = codex ]; then
100
+ case "$(payload_field hook_event_name)" in
101
+ SubagentStop) payload_field agent_type ;;
102
+ *) payload_field tool_input.agent_type ;;
103
+ esac
104
+ else
105
+ payload_field tool_input.subagent_type
106
+ fi
107
+ }
108
+
109
+ # The agent RUNNING the current event: the top-level `agent_type`, which the
110
+ # host adds only inside a subagent (empty on the main thread). Never confuse it
111
+ # with `nv_spawn_target_type` (H16). `nv_subagent_type` above keeps its legacy
112
+ # mixed behaviour for existing hooks; new code uses these two helpers.
113
+ nv_event_agent_type() {
114
+ payload_field agent_type
115
+ }
116
+
117
+ # True for the spawn tool: Codex V1 `spawn_agent`, or any V2 name that ends in
118
+ # `spawn_agent` (V2 flattens the namespace into the tool name).
119
+ nv_is_spawn_tool() {
120
+ case "$(payload_field tool_name)" in
121
+ *spawn_agent) return 0 ;;
122
+ *) return 1 ;;
123
+ esac
124
+ }
125
+
93
126
  # Deliberately NO `nv_emit_context` helper here. `hook-output-contract.test.ts`
94
127
  # ("los partials no hablan con el host, solo escriben al log") holds every
95
128
  # `_partials/*.sh` file to zero host-output vocabulary — a partial is inlined