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.
- package/README.md +21 -2
- package/dist/assets/core/core-assets/agents/architect.md +6 -4
- package/dist/assets/core/core-assets/agents/implementer.md +6 -2
- package/dist/assets/core/core-assets/agents/reviewer.md +24 -2
- package/dist/assets/core/core-assets/agents/scout.md +1 -1
- package/dist/assets/core/core-assets/hooks/_partials/bash-outcome.sh +245 -0
- package/dist/assets/core/core-assets/hooks/_partials/gate-ask.sh +25 -0
- package/dist/assets/core/core-assets/hooks/_partials/resolve-worktree.sh +257 -91
- package/dist/assets/core/core-assets/hooks/bash-outcome-watch.sh +21 -0
- package/dist/assets/core/core-assets/hooks/general-purpose-confirm.sh +56 -0
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +21 -1
- package/dist/assets/core/core-assets/hooks/implementer-no-markdown.sh +23 -5
- package/dist/assets/core/core-assets/hooks/master-accept-confirm.sh +17 -5
- package/dist/assets/core/core-assets/hooks/model-advisor.sh +23 -16
- package/dist/assets/core/core-assets/hooks/plan-gate.sh +40 -2
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +27 -4
- package/dist/assets/core/core-assets/hooks/routing-watch.sh +14 -0
- package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +143 -19
- package/dist/assets/core/core-assets/managed/orquestacion.md +26 -19
- package/dist/assets/core/core-assets/master-plan/en/ux.md +129 -0
- package/dist/assets/core/core-assets/master-plan/ux.md +129 -0
- package/dist/assets/core/core-assets/presets/fastapi-python/managed/stack.md +7 -0
- package/dist/assets/core/core-assets/presets/fastapi-python.json +19 -0
- package/dist/assets/core/core-assets/settings/settings-base.json +2 -0
- package/dist/assets/core/core-assets/skills/locate-code.md +1 -1
- package/dist/assets/core/core-assets/skills/master-plan.md +16 -10
- package/dist/assets/core/core-assets/skills/plan-advanced.md +1 -1
- package/dist/assets/core/core-assets/skills/plan-simple.md +2 -2
- package/dist/assets/core/core-assets/skills/verify-before-done.md +1 -1
- package/dist/assets/plugins/codegraph/managed/codegraph-search-v2.md +1 -1
- package/dist/assets/plugins/codegraph/skills/codegraph-access-v2.md +1 -1
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +75 -14
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +2 -2
- package/dist/assets/plugins/tgrep/managed/guard-destructive-search-lane.sh +18 -0
- package/dist/assets/plugins/tgrep/plugin.json +10 -0
- package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +220 -0
- package/dist/index.js +708 -432
- 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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|