navori 0.8.4 → 0.8.6

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 (31) hide show
  1. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +14 -18
  2. package/dist/assets/core/core-assets/agents/implementer.md +1 -1
  3. package/dist/assets/core/core-assets/agents/leader.md +10 -7
  4. package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +56 -5
  5. package/dist/assets/core/core-assets/hooks/_partials/classify-source.sh +52 -0
  6. package/dist/assets/core/core-assets/hooks/_partials/gate-trigger.sh +31 -12
  7. package/dist/assets/core/core-assets/hooks/audit-mode-trigger.sh +24 -0
  8. package/dist/assets/core/core-assets/hooks/pr-pilot-confirm.sh +137 -0
  9. package/dist/assets/core/core-assets/hooks/routing-watch.sh +90 -15
  10. package/dist/assets/core/core-assets/lib-skills/zod-validation.md +5 -4
  11. package/dist/assets/core/core-assets/managed/cierre-sesion.md +2 -2
  12. package/dist/assets/core/core-assets/managed/operaciones-seguras.md +1 -1
  13. package/dist/assets/core/core-assets/managed/orquestacion.md +33 -23
  14. package/dist/assets/core/core-assets/skills/debug-error.md +15 -10
  15. package/dist/assets/core/core-assets/skills/review-diff.md +4 -0
  16. package/dist/assets/core/core-assets/skills/solution-design.md +2 -2
  17. package/dist/assets/core/core-assets/skills/structural-search.md +7 -1
  18. package/dist/assets/core/core-assets/skills/ticket-intake.md +6 -2
  19. package/dist/assets/core/core-assets/skills/verify-before-done.md +1 -1
  20. package/dist/assets/plugins/codegraph/managed/codegraph-protocol.md +1 -1
  21. package/dist/assets/plugins/engram/managed/engram-protocol.md +1 -1
  22. package/dist/assets/plugins/engram/skills/engram-leader.md +2 -2
  23. package/dist/assets/plugins/tgrep/managed/tgrep-protocol.md +1 -1
  24. package/dist/assets/plugins/tgrep/plugin.json +15 -1
  25. package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +361 -0
  26. package/dist/assets/plugins/tgrep/scripts/tgrep-search.sh +93 -0
  27. package/dist/assets/plugins/tgrep/scripts/tgrep-session.sh +7 -1
  28. package/dist/assets/plugins/tgrep/skills/tgrep-rung.md +2 -2
  29. package/dist/index.js +390 -362
  30. package/package.json +2 -2
  31. package/dist/assets/core/core-assets/skills/pr-create.md +0 -19
@@ -40,7 +40,7 @@ gh auth status # gh authenticated
40
40
 
41
41
  ### The shipping diff — the one set every count in this pre-flight comes from
42
42
 
43
- Coverage of the review, the receipt's fingerprints, and the R1 waiver's file count are three questions about the SAME set of files. Write it once, read it everywhere:
43
+ Coverage of the review and the receipt's fingerprints are two questions about the SAME set of files. Write it once, read it everywhere:
44
44
 
45
45
  ```bash
46
46
  shipping=$({ git -c core.quotepath=false diff --name-only "origin/{{prTarget}}"; \
@@ -58,18 +58,18 @@ If the harness is active, identify THIS feature's review: `.claude/progress/revi
58
58
 
59
59
  Open that specific file and confirm its verdict is `APPROVED` and that its scope/feature section names the same feature you're about to commit. The verdict only counts if the review **covers the whole shipping diff**: the reviewer's content receipt (below) is the authoritative list of the files it actually reviewed, so every file in the shipping diff above must appear there. A touched file the review never saw → the `APPROVED` doesn't cover the full change → it does NOT count as approved. Abort, don't create the PR, and send it back to the reviewer to cover the missing files. It's not enough to mention the difference and carry on. The coverage check is mechanical — see the receipt block.
60
60
 
61
- <!-- This file-coverage rule lives here only; `.claude/skills/pr-create/SKILL.md` is a pointer to this agent (single owner of the PR flow). -->
61
+ <!-- This file-coverage rule lives here only; the commit+PR flow has no second home to this agent (single owner of the PR flow). -->
62
62
 
63
63
 
64
64
  An absent file, ambiguous (more than one candidate), or with a verdict/scope that doesn't match the current feature → does NOT count as approved: abort, tell the user the review is missing, and never assume a generic `APPROVED`.
65
65
 
66
- **Content receipt (R2+): the diff must still match what was approved.** The APPROVED verdict is bound to the reviewed bytes via `.claude/progress/receipt.txt` (written by the `reviewer`, one `<blob-sha> <path>` line per reviewed file, or `deleted <path>` for a removed one). Before committing, the approval has to cover the diff in **both** directions — coverage (every shipping file was reviewed) and no drift (no reviewed file changed its bytes):
66
+ **Content receipt: the diff must still match what was approved.** The APPROVED verdict is bound to the reviewed bytes via `.claude/progress/receipt.txt` (written by the `reviewer`, one `<blob-sha> <path>` line per reviewed file, or `deleted <path>` for a removed one). Before committing, the approval has to cover the diff in **both** directions — coverage (every shipping file was reviewed) and no drift (no reviewed file changed its bytes):
67
67
 
68
68
  ```bash
69
69
  # 1) COVERAGE: `$shipping` is THE SHIPPING DIFF above — assign it in this same
70
70
  # call. Whatever this prints is a shipping file the receipt never listed → a
71
71
  # file the reviewer never saw. Reading the set from one place is the point:
72
- # this check and the R1 waiver's count each spelled it out, and drifted.
72
+ # this check and the withdrawn waiver's count each spelled it out, and drifted.
73
73
  # `grep .` drops the blank line an empty $shipping would otherwise feed comm.
74
74
  comm -23 <(printf '%s\n' "$shipping" | grep .) \
75
75
  <(grep -v '^#' .claude/progress/receipt.txt | sed 's/^[^ ]* //' | sort -u)
@@ -98,7 +98,7 @@ while IFS= read -r line; do
98
98
  done < .claude/progress/receipt.txt
99
99
  ```
100
100
 
101
- Any file printed by (1) is uncovered; any `DRIFT` line from (2) is stale — either one, or a missing `receipt.txt` for a reviewed (R2+) change, means the approval no longer covers the current diff. Abort and don't commit. It's not enough to mention the gap and carry on.
101
+ Any file printed by (1) is uncovered; any `DRIFT` line from (2) is stale — either one, or a missing `receipt.txt` for a reviewed change, means the approval no longer covers the current diff. Abort and don't commit. It's not enough to mention the gap and carry on.
102
102
 
103
103
  **Report the drift with its diff, not just its name.** The reviewer signs with `git hash-object -w`, so the approved bytes are in the object store: for each drifted file, run `git diff <blob-sha> <file>` (the sha is the receipt's own line; `git cat-file -p <blob-sha>` prints the approved content in full) and hand that over. A `DRIFT` reported as a bare filename forces whoever picks it up to reconstruct the change from prose.
104
104
 
@@ -109,28 +109,24 @@ Then route by cause, in the same message:
109
109
 
110
110
  An `ERROR:` line is NOT drift: verification itself failed (git unavailable, wrong cwd, unreadable file) — fix the environment and re-run the check; sending it to the `reviewer` can never resolve it. **This check is the only one that runs** — no hook re-verifies the receipt behind you (#365), so skipping it skips it for everyone.
111
111
 
112
- <!-- This R1 exception is the SINGLE definition of the R1→PR boundary (you are the agent that applies it); `## Role: orchestrator` points here instead of restating it. -->
112
+ <!-- The orchestrator block states the rule (every change goes through implementer -> reviewer); this is where the PR side of it is enforced. -->
113
113
 
114
- **R1 exception (no reviewer):** a change done inline, without a reviewer, per `## Role: orchestrator` has no `review_<feature>.md` and none is required. In that case you do NOT abort for a missing review instead you MUST run `{{qualityGate.full}}` green yourself before the PR (see Gate below).
114
+ **A review is required.** `## Role: orchestrator` routes every change to source through `implementer` `reviewer`, with no inline route and no file-count threshold. So a diff that reaches you with no `review_<feature>.md`, or with one that is not `APPROVED` over this same content, is a deviation **abort and send it to the `reviewer`**.
115
115
 
116
- **What makes that waiver genuine one criterion, and it is countable.** A file in the shipping diff is **non-trivial** when all three of these hold:
116
+ **The one exception: delegation was genuinely impossible, and it was DECLARED.** The operator forbade subagents for the session, or the `Agent` tool was unavailable. The orchestrator must have said so explicitly, naming the reason. Then, and only then:
117
117
 
118
- - **(a) it carries behavior** executable source, or the harness prose an agent obeys — as opposed to config, fixtures, data, lockfiles, copy, docs or generated output;
119
- - **(b) this diff changes that behavior**, rather than propagating an edit the diff settles on its own, with no reasoning about what the program then does: a rename applied across its call sites, an import path updated because a file moved, a pure move, a formatting pass. The line is the VALUE, not the syntax — an edit that changes *where a value comes from* (a literal replaced by an import, a hardcoded constant swapped for a lookup) changes behavior and counts, however mechanical it looks;
120
- - **(c) it is not a test riding along with a source file this same diff already counted.** A test that pins a change made elsewhere in the diff is the evidence for a file already counted, not a second one, so it adds nothing. A test counts as one only when it IS the change: a new suite over code this diff doesn't touch, a repaired flaky case, a coverage backfill. Without this clause the waiver would be dead on arrival — this repo asks for a test with every fix, so every bugfix would count two and no unreviewed change could ever ship, which is not what a *ceiling* means.
118
+ - you do NOT abort for the missing review;
119
+ - you MUST run `{{qualityGate.full}}` green yourself in pre-flight (see Gate below) there is no review evidence to trust;
120
+ - the **PR body must state it**, in one line: what was done inline and why delegation was not possible. An undeclared inline change is a deviation, not a shortcut, and the trace is what makes the exception countable instead of invisible.
121
121
 
122
- A file you cannot classify counts as non-trivial: the fallback is the review, never the waiver.
123
-
124
- **Worked example — the shape that decides.** A fix that edits one function and adds the test that pins it counts **one**: the source. The test rides along under (c), so the waiver applies. Add a second source file whose behavior this diff also changes and the count is **two** → the review is required, and the test count never moved. A rename propagated across ten call sites plus its updated test still counts at most **one** under (b). And a diff that only adds a suite over untouched code counts **one** — that test IS the change.
125
-
126
- Count the non-trivial files in **the shipping diff** — the set defined once at the top of this pre-flight, and for the reason stated there: `...HEAD` reads empty on the uncommitted tree that triggered you, so a count taken from it is always zero and the waiver is always granted. **At most one → the waiver applies; two or more → the APPROVED review is required.** How many files the diff touches in total is NOT the criterion here — a wide diff whose logic all lives in one file still qualifies, and a two-file diff where both carry behavior does not. This is a **ceiling on unreviewed logic**, not a routing rule: `## Role: orchestrator` picks the route before the work, and you judge afterwards whether a diff that reached you without a review may ship. When the two disagree, the ceiling wins — abort and send it to the `reviewer`.
122
+ **No count, no judgement about the diff's content.** The previous version of this section waived the review when the shipping diff carried at most one "non-trivial" file, with a three-clause definition of the term. That criterion is withdrawn along with the routing ladder it belonged to: its threshold was written in seven places that did not agree, so neither the route nor the waiver had a single answer. It returns when the ruling does, stated once and in one place. Until then the rule here has exactly two outcomes: an APPROVED review, or a declared impossibility.
127
123
 
128
124
  ### Gate: `{{qualityGate.full}}` green before the PR
129
125
 
130
126
  The PR gate is the FULL one, `{{qualityGate.full}}` — **not** the fast one, `{{qualityGate.fast}}`. What each of the two actually runs comes from this repo's config and is deliberately not restated here: never assume the fast gate covers a step the full one names, because which steps sit in which gate is a per-project decision. `full` must be green over the diff that ships. Two paths:
131
127
 
132
- - **R2+ (reviewed):** the `reviewer` already ran `{{qualityGate.full}}` green over this same diff in Pass 2 (evidence in `review_<feature>.md`, this cycle) and you **don't edit code** — trust it, don't re-run. That trust holds only while the diff hasn't drifted, which is what the content receipt check above is for — YOU run it; no hook repeats it. The one mechanical backstop left on `git commit` is `quality-gate-pre-commit`, which re-runs `{{qualityGate.fast}}` and blocks if it fails. Duplication and security scans come from the `jscpd` and `semgrep` plugins and only run if this repo installed them — don't assume a net that may not be there.
133
- - **R1 (no reviewer):** there's no review evidence to trust — YOU run `{{qualityGate.full}}` green in pre-flight before `gh pr create`.
128
+ - **Reviewed (the normal path):** the `reviewer` already ran `{{qualityGate.full}}` green over this same diff in Pass 2 (evidence in `review_<feature>.md`, this cycle) and you **don't edit code** — trust it, don't re-run. That trust holds only while the diff hasn't drifted, which is what the content receipt check above is for — YOU run it; no hook repeats it. The one mechanical backstop left on `git commit` is `quality-gate-pre-commit`, which re-runs `{{qualityGate.fast}}` and blocks if it fails. Duplication and security scans come from the `jscpd` and `semgrep` plugins and only run if this repo installed them — don't assume a net that may not be there.
129
+ - **Declared inline (no reviewer):** there's no review evidence to trust — YOU run `{{qualityGate.full}}` green in pre-flight before `gh pr create`.
134
130
  - ▶️ **Re-run `{{qualityGate.full}}` by hand** whenever the diff changed since the review (rebase/merge/follow-up edit) or there's no fresh evidence over the diff being committed — stale evidence doesn't count.
135
131
 
136
132
  Never open the PR with the gate red.
@@ -33,7 +33,7 @@ You execute **a single** task from start to verification. You don't orchestrate,
33
33
  {{qualityGate.fast}}
34
34
  ```
35
35
 
36
- If it fails: fix it and re-run. Don't return with red. When it fails with a WALL of errors, apply `.claude/skills/debug-error/SKILL.md` before touching anything — filter the noise and fix the root cause, not the cascade. If your second fix attempt fails the same way, apply `.claude/skills/loop-back-debug/SKILL.md` instead of throwing a third patch.
36
+ If it fails: fix it and re-run. Don't return with red. When you can't explain WHY it failed, apply `.claude/skills/debug-error/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, apply `.claude/skills/loop-back-debug/SKILL.md` instead of throwing a third patch.
37
37
  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). Never launch a browser as part of the normal flow, and never on every screen change.
38
38
  6. **No commits** without the `reviewer`'s approval. When you finish, write the report and return the reference.
39
39
 
@@ -10,7 +10,11 @@ effort: {{effort.leader}}
10
10
 
11
11
  > This file is a **depth reference** — the orchestrator role **is embodied by the main agent**, not a subagent. The essential mechanics (escalation table, parallelism, synthesis) live in the "## Role: orchestrator" block, which the `SessionStart` hook delivers to the session — not to a subagent, which is the point: only the main agent can act on it. Here is the extended detail and, below, the **Project rules**. Do NOT invoke `Agent(subagent_type: leader)`.
12
12
 
13
- Your only job as orchestrator is to **decompose and coordinate**, never to implement. Note: this applies **when you orchestrate** (R2+ routes of the organic routing). At **R1** (1–3 files, mechanical change or bugfix with a clear cause) you implement **inline yourself**, without opening subagents — see "## Role: orchestrator (organic routing)" in `CLAUDE.md`.
13
+ Your only job as orchestrator is to **decompose and coordinate**, never to implement. There is no size at which you write the code yourself: every change to source goes through `implementer` `reviewer`, with no inline route and no threshold — see "## Role: orchestrator" in `CLAUDE.md`.
14
+
15
+ **Why there is no ladder right now, and what has to be true to bring it back.** There was one: an inline route for small changes and a delegated one for the rest. Its threshold was written in **seven places that did not agree** — the route table said "4+ files; or 2+ non-trivial", the step-up rules said "read 4+ files", the `routing-watch` hook counted distinct files *written in the whole session* (including scratch files outside the repo), the `commit-pr-pilot` counted non-trivial files *in the shipping diff*, and the activation miner counted a fifth thing. So "is this inline?" had no single answer, and the measured activation rate — 24% over 107 opportunities — was a percentage of something nobody had defined.
16
+
17
+ One route removes the decision entirely. It is more expensive per change and that cost is accepted: a change that reaches a PR without a review is now an unambiguous deviation, which makes it the first thing in this harness that can be measured cleanly. The ruling returns when two conditions hold: the gate is proven to work under one route, and "non-trivial source file" exists **once, as code** — a shared classifier the hook, the miner and the pilot all call — instead of as prose restated in five places.
14
18
 
15
19
  ## Startup protocol
16
20
 
@@ -30,8 +34,7 @@ Your only job as orchestrator is to **decompose and coordinate**, never to imple
30
34
 
31
35
  | Complexity | Parallel subagents |
32
36
  |---|---|
33
- | R1 · 1–3 files, mechanical | **inline you do it**, no subagent (see organic routing) |
34
- | Medium / R2 (4+ files or 2+ non-trivial) | 1 `implementer` → 1 `reviewer` |
37
+ | Any change to source one line or forty files | 1 `implementer` 1 `reviewer` |
35
38
  | Multi-bug independent (N bugs with no shared state) | N `implementer` in parallel (1 per bug, isolated scopes) → 1 `reviewer` that validates the N diffs together |
36
39
  | Complex (structural migration, multi-layer refactor) | `ticket-audit` → 2–3 `researcher` or `explorer` in parallel → 1 `implementer` → 1 `reviewer` → `commit-pr-pilot` |
37
40
  | Very complex | Split into sub-tasks and re-apply the table |
@@ -63,11 +66,11 @@ When the `done -> file` come back, **gather and analyze deeply YOURSELF**: read
63
66
 
64
67
  Researchers are leaves (they don't have `Agent`): you open the fan-out. Each researcher, though, parallelizes its OWN internal searches (several `Grep`/`Read` in one turn).
65
68
 
66
- ## Frugal delegation (shape a lean R2 encargo)
69
+ ## Frugal delegation (shape a lean encargo)
67
70
 
68
71
  Fan-out is a lever, not a toll — so when you do delegate, hand the smallest encargo that covers the work:
69
72
 
70
- - **Peel off the mechanical first.** Copies, renames, scaffolding, JSON/string edits → do them yourself in R1 or send them to a low-tier agent; never bundle them into an `implementer`'s encargo, where they inflate its context and its run without raising quality.
73
+ - **Peel off the mechanical first.** Copies, renames, scaffolding, JSON/string edits → send them to a low-tier agent in their own encargo; never bundle them into the `implementer`'s, where they inflate its context and its run without raising quality.
71
74
  - **One encargo = one unit.** A pre-existing bug the `implementer` hits outside its scope → it reports and stops there (a trivial one-liner is the exception); **you** decide whether to open a separate unit. Scope doesn't self-expand mid-run.
72
75
  - **Tier by sub-task, not by round.** A single fix round can mix tiers. Map: **low** → mechanical work (copies, renames, scaffolding, string/JSON edits, a one-line fix); **mid** → a scoped bugfix with a clear cause or a bounded feature; **high** → judgment work (design, security regex, ambiguous root-cause, removal semantics, critical areas).
73
76
  - **One-pass review on small/medium diffs.** Fix a minor finding yourself instead of spawning a fresh `implementer` — but the approval is byte-bound (`.claude/progress/receipt.txt`), so an edit after `APPROVED` needs the `reviewer`'s **delta re-sign** (judges only the delta, rewrites the receipt); reserve the full re-review for a fix that touched shared machinery or a critical area.
@@ -123,7 +126,7 @@ Expected files:
123
126
  When `.claude/progress/review_<feature>.md` contains `APPROVED`:
124
127
 
125
128
  1. Invoke `commit-pr-pilot` to draft the title + body following the repo's format and open the PR.
126
- 2. Pre-flight on you before invoking — the list in `## Role: orchestrator` and nothing more: not on `{{branchBase}}`, `gh auth status` ok. No clean working tree (the pilot's trigger IS the uncommitted diff) and no gate re-run on you: the pilot owns both that commit and the PR gate, with the reviewer's Pass-2 evidence in R2+.
129
+ 2. Pre-flight on you before invoking — the list in `## Role: orchestrator` and nothing more: not on `{{branchBase}}`, `gh auth status` ok. No clean working tree (the pilot's trigger IS the uncommitted diff) and no gate re-run on you: the pilot owns both that commit and the PR gate, with the reviewer's Pass-2 evidence behind it.
127
130
  3. Return to the user only the PR URL + title.
128
131
 
129
132
  If the review returned `CHANGES_REQUESTED`, do NOT invoke `commit-pr-pilot`: launch a **fresh** `implementer` scoped to just the findings — not a resume of the hot one (dragging a large transcript re-feeds its whole history every turn and rarely pays for a bounded fix round), and not the pilot.
@@ -152,7 +155,7 @@ If the repo has no test suite, the `implementer` still can't claim "done" withou
152
155
 
153
156
  ## What you do NOT do
154
157
 
155
- - ❌ Edit project code **when you orchestrate (R2+)** — that's the `implementer`'s. (At **R1**, 1–3 mechanical files, you do edit inline yourself; see organic routing.)
158
+ - ❌ Edit project code — that's the `implementer`'s, always. The only exception is a delegation the orchestrator declared impossible (operator forbade subagents, or the tool is unavailable), and it is declared out loud, not assumed.
156
159
  - ❌ Make commits (that's `commit-pr-pilot` after the `reviewer`'s approval).
157
160
  - ❌ Accept subagent results in chat without a file reference.
158
161
  - ❌ Launch an `implementer` without having clarified the scope against the "Project rules" below.
@@ -95,15 +95,44 @@ navori_audit_log() {
95
95
  # ONE jq for every field, not one per field: this runs on each hook of each
96
96
  # Bash call, and a fork is the most expensive thing in it. Newline-separated,
97
97
  # read back positionally.
98
- navori_audit_fields=$(printf '%s' "${payload:-}" | jq -r '[.session_id // "", .cwd // "", .agent_id // .subagent_id // ""] | .[]' 2>/dev/null) || return 0
98
+ # The third field is the OWNER, and it is never empty: inside a subagent the
99
+ # host sends a real `agent_id`; on the main thread it sends none and the
100
+ # literal below names the orchestrator explicitly.
101
+ #
102
+ # It used to be `// ""`, and the empty case did not stay empty — it came out
103
+ # as the `cwd`. Command substitution strips trailing newlines, so an empty
104
+ # third field left `$navori_audit_fields` with only two lines; then
105
+ # `${rest#*<NL>}` found no newline in `"cwd"` and POSIX says a `#` pattern
106
+ # that does not match returns the string UNCHANGED. 41,581 of the park's
107
+ # 52,460 recorded owners (79%) were a filesystem path for that reason.
108
+ #
109
+ # The consumer's behaviour does not change — `ownerOf` looks the value up
110
+ # among the session's agents and sends anything that names nobody to the
111
+ # orchestrator, which is where a path already went — but the record stops
112
+ # claiming the repo directory is an agent. Old logs keep working through the
113
+ # same "names nobody" branch.
114
+ #
115
+ # The trailing "." is a sentinel, not data: with it the third field is always
116
+ # followed by a newline, so the `%%` below cannot fall into the same trap if
117
+ # a future field is ever empty.
118
+ #
119
+ # The owner is picked with an explicit "first non-empty string", NOT with
120
+ # `.agent_id // .subagent_id // "orchestrator"`. In jq only `null` and `false`
121
+ # are falsy, so a host that sends the key with an EMPTY string satisfies `//`
122
+ # and the chain yields `""` — the field then disappears from the record and
123
+ # `ownerOf` falls back to the time window, which is the guess this field
124
+ # exists to avoid. Caught by a test, not by review.
125
+ navori_audit_fields=$(printf '%s' "${payload:-}" | jq -r '[.session_id // "", .cwd // "", ([.agent_id, .subagent_id] | map(select(type == "string" and . != "")) | first) // "orchestrator", "."] | .[]' 2>/dev/null) || return 0
99
126
  navori_audit_session=${navori_audit_fields%%
100
127
  *}
101
128
  navori_audit_rest=${navori_audit_fields#*
102
129
  }
103
130
  navori_audit_cwd=${navori_audit_rest%%
104
131
  *}
105
- navori_audit_agent=${navori_audit_rest#*
132
+ navori_audit_rest=${navori_audit_rest#*
106
133
  }
134
+ navori_audit_agent=${navori_audit_rest%%
135
+ *}
107
136
  [ -n "$navori_audit_session" ] || return 0
108
137
  # Same character class the CLI enforces (#503): the id composes a path, so
109
138
  # anything path-shaped means the payload is not what we think it is.
@@ -144,7 +173,29 @@ navori_audit_log() {
144
173
  navori_audit_ms=$(( navori_audit_end - ${navori_audit_t0:-$navori_audit_end} ))
145
174
  [ "$navori_audit_ms" -ge 0 ] 2>/dev/null || navori_audit_ms=0
146
175
 
147
- navori_audit_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) || navori_audit_ts=""
176
+ # `tsMs` is the instant at the resolution the log actually needs (#685): the
177
+ # `ts` below truncates to the second, and 84% of a measured session's events
178
+ # share their second with another one. This value is already computed — the
179
+ # duration above is derived from it — so recording it costs nothing.
180
+ #
181
+ # Validated with the same `-ge 0` idiom as `ms`, and for the same reason: it
182
+ # is passed as `--argjson`, so a non-numeric value would make the whole `jq`
183
+ # fail and the event would vanish instead of merely losing a field.
184
+ [ "$navori_audit_end" -ge 0 ] 2>/dev/null || navori_audit_end=0
185
+
186
+ # `ts` is NOT stamped here, and that is the whole point of #696: it was a
187
+ # `date` fork per event — 46,850 of them across this store — for a string
188
+ # fully derivable from the `tsMs` above, which `$EPOCHREALTIME` already
189
+ # produced without spawning anything. The file's own cost doctrine (see
190
+ # `navori_audit_now`) says spend a process only when there is no other way;
191
+ # keeping this one made the comment lie four lines under the value that
192
+ # refutes it.
193
+ #
194
+ # Nothing downstream lost a field: `parse.ts` derives the ISO from `tsMs`
195
+ # when the record carries none, and logs written before this still have
196
+ # theirs. The lifecycle records — `start`, `stop`, `session-end` — keep a real
197
+ # `ts`, so a human reading the raw `.log` still has dated anchors; those are
198
+ # ~3 per session, not one per hook.
148
199
  # `navori_audit_agent` came out of the same single jq above. It is what lets
149
200
  # the report attribute a hook to a subagent WITHOUT guessing: with agents
150
201
  # running in parallel their time windows overlap, so attribution by timestamp
@@ -160,7 +211,6 @@ navori_audit_log() {
160
211
  # (`ownerOf` in `lib/audit/parse.ts` is where that rule lives).
161
212
 
162
213
  printf '%s\n' "$(jq -cn \
163
- --arg ts "$navori_audit_ts" \
164
214
  --arg name "${navori_audit_name:-unknown}" \
165
215
  --arg phase "${navori_audit_phase:-unknown}" \
166
216
  --arg verdict "${1:-unknown}" \
@@ -169,7 +219,8 @@ navori_audit_log() {
169
219
  --arg src "${navori_audit_source:-core}" \
170
220
  --arg agent "${navori_audit_agent:-}" \
171
221
  --argjson ms "$navori_audit_ms" \
172
- '{ts:$ts,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
222
+ --argjson tsMs "$navori_audit_end" \
223
+ '{tsMs:$tsMs,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
173
224
  + (if $tool == "" then {} else {tool:$tool} end)
174
225
  + (if $reason == "" then {} else {reason:$reason} end)
175
226
  + (if $agent == "" then {} else {agentId:$agent} end)' 2>/dev/null)" \
@@ -0,0 +1,52 @@
1
+ # GENERATED by `pnpm gen:schemas` from packages/cli/src/lib/source-classify.ts.
2
+ # Do not edit: edit the module. A drift test fails if this falls behind.
3
+ #
4
+ # navori_is_source <repo-relative-path> — exit 0 when the path carries behavior
5
+ # (clause (a) of "non-trivial source file"), 1 otherwise.
6
+ #
7
+ # Clause (c) — a test riding along does not count twice — is NOT decided here:
8
+ # it needs the whole set of paths in one change, and a caller that has only one
9
+ # path at a time cannot apply it. Callers that count per file are computing a
10
+ # ceiling, exactly like the TypeScript side says.
11
+ navori_is_source() {
12
+ [ -n "${1:-}" ] || return 1
13
+ local _p
14
+ # ephemeral: agent handoffs and reclaimed worktrees: never versioned, never in a diff
15
+ _p='(^|/)\.claude/(progress|worktrees)/'; [[ $1 =~ $_p ]] && return 1
16
+ # ephemeral: throwaway scaffolding; 13.4% of the miner's old 'source' writes were this
17
+ _p='(^|/)(scratchpad|\.scratch)/'; [[ $1 =~ $_p ]] && return 1
18
+ # dependency: a vendored dependency is not ours to change, and a write there is an accident
19
+ _p='(^|/)node_modules/'; [[ $1 =~ $_p ]] && return 1
20
+ # dependency: build output: regenerated from the source this same change may edit
21
+ _p='(^|/)(dist|build|out|coverage|\.next|\.astro)/'; [[ $1 =~ $_p ]] && return 1
22
+ # generated: the rendered harness mirror — the change lives in the source asset, not here
23
+ _p='(^|/)\.claude/'; [[ $1 =~ $_p ]] && return 1
24
+ # generated: rendered from managed blocks; editing it directly is what the drift guard blocks
25
+ _p='(^|/)(CLAUDE|AGENTS)\.md$'; [[ $1 =~ $_p ]] && return 1
26
+ # generated: rendered from the plugin manifests; editing it here is overwritten next render
27
+ _p='(^|/)\.mcp\.json$'; [[ $1 =~ $_p ]] && return 1
28
+ # generated: regenerated from the tree it pins; never hand-authored
29
+ _p='(^|/)__golden__/|\.snap$'; [[ $1 =~ $_p ]] && return 1
30
+ # lockfile: resolved by a tool, reviewed as a diff of intent elsewhere
31
+ _p='(^|/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|Cargo\.lock|poetry\.lock|Gemfile\.lock|go\.sum)$'; [[ $1 =~ $_p ]] && return 1
32
+ # fixture: data the tests read, not behavior the program runs
33
+ _p='(^|/)(__fixtures__|__mocks__|fixtures)/'; [[ $1 =~ $_p ]] && return 1
34
+ # test: clause (c): a test pinning a change counted elsewhere is evidence, not a second file
35
+ _p='\.(test|spec)\.[mc]?[jt]sx?$'; [[ $1 =~ $_p ]] && return 1
36
+ # test: same clause, for repos that group tests by directory instead of by suffix
37
+ _p='(^|/)(__tests__|tests?)/'; [[ $1 =~ $_p ]] && return 1
38
+ # test: same clause, Python and Go naming
39
+ _p='(^|/)(test|tests)_[^/]+\.py$|(^|/)[^/]+_test\.(py|go)$'; [[ $1 =~ $_p ]] && return 1
40
+ # config: clause (a) excludes config explicitly
41
+ _p='(^|/)(package\.json|tsconfig[^/]*\.json|jsconfig\.json|biome\.jsonc?|\.eslintrc[^/]*|\.prettierrc[^/]*|\.editorconfig|\.gitignore|\.npmrc|\.nvmrc)$'; [[ $1 =~ $_p ]] && return 1
42
+ # config: declares what the harness renders; the behavior it changes is navori's, not the repo's
43
+ _p='(^|/)navori\.config\.json$'; [[ $1 =~ $_p ]] && return 1
44
+ # docs: read by humans; no agent obeys it as instruction
45
+ _p='(^|/)(docs|specs)/|(^|/)(README|CONTRIBUTING|CHANGELOG|LICENSE)[^/]*$'; [[ $1 =~ $_p ]] && return 1
46
+ # docs: the session log, versioned but not behavior
47
+ _p='(^|/)progress/'; [[ $1 =~ $_p ]] && return 1
48
+ # Harness prose an agent obeys is behavior even though it is .md.
49
+ _p='(^|/)(core-assets|plugins)/(.*/)?(agents|skills|managed)/[^/]+\.md$'; [[ $1 =~ $_p ]] && return 0
50
+ _p='\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift|sh|bash|zsh|sql|vue|svelte|astro|ipynb)$'; [[ $1 =~ $_p ]] && return 0
51
+ return 1
52
+ }
@@ -14,6 +14,36 @@
14
14
  # the gate silently). Matching a segment START means a quoted `echo "git commit"`
15
15
  # does NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`,
16
16
  # or obfuscation — a seatbelt, not a sandbox.
17
+ # The fast path on its own, so a caller can apply it EARLIER than the segment
18
+ # scan — before it has even paid to extract the command from the payload.
19
+ #
20
+ # Returns 0 when $1 may contain a gated operation, 1 when it provably cannot.
21
+ # The argument is the one the block below spells out: no $TRIGGER_RE can match
22
+ # without one of the caller's literal TOKENS appearing in the segment it
23
+ # matches, and every segment is a substring of the input. So the absence of
24
+ # every token is proof that no segment can match — and the same proof holds one
25
+ # level up, over the raw PAYLOAD the command was extracted from: JSON escaping
26
+ # touches `"`, `\` and control characters, never the letters of a token.
27
+ #
28
+ # Disarms when $TRIGGER_TOKENS is unset: with no tokens declared there is
29
+ # nothing to prove absent, so it answers "maybe" and the caller does the work.
30
+ # Fail-open to the SLOW path, never to a skip.
31
+ has_trigger_token() {
32
+ [ -n "${TRIGGER_TOKENS:-}" ] || return 0
33
+ # Token iteration goes through newline-split + `read`, NOT `for _tok in
34
+ # $TRIGGER_TOKENS`: zsh does not word-split an unquoted expansion, so the
35
+ # `for` form iterated ONCE with the whole list as a single token there — and
36
+ # a token that can never match is a gate that never fires. Caught by the
37
+ # bash×zsh differential suite.
38
+ local _input="$1" _tok _nl=$'\n'
39
+ local _toks="${TRIGGER_TOKENS// /$_nl}"
40
+ while IFS= read -r _tok; do
41
+ [ -n "$_tok" ] || continue
42
+ case "$_input" in *"$_tok"*) return 0 ;; esac
43
+ done <<< "$_toks"
44
+ return 1
45
+ }
46
+
17
47
  is_scan_trigger() {
18
48
  # Pre-expanded newline: zsh does NOT expand $'\n' in the REPLACEMENT of
19
49
  # ${var//pat/repl} (it inserts the literal characters), so an inline $'\n'
@@ -44,18 +74,7 @@ is_scan_trigger() {
44
74
  # `for` form iterated ONCE with the whole list as a single token there — and
45
75
  # a token that can never match is a gate that never fires. Caught by the
46
76
  # bash×zsh differential suite; same class as the $'\n' pitfall above.
47
- if [ -n "${TRIGGER_TOKENS:-}" ]; then
48
- local _tok _hit="" _toks="${TRIGGER_TOKENS// /$nl}"
49
- while IFS= read -r _tok; do
50
- [ -n "$_tok" ] || continue
51
- case "$input" in *"$_tok"*)
52
- _hit=1
53
- break
54
- ;;
55
- esac
56
- done <<< "$_toks"
57
- [ -n "$_hit" ] || return 1
58
- fi
77
+ has_trigger_token "$input" || return 1
59
78
  # FIX B: join `\<newline>` continuations into a space FIRST, so a command
60
79
  # split across lines with a trailing backslash stays ONE logical segment
61
80
  # (otherwise the subcommand/flag lands in a segment not starting with git).
@@ -75,6 +75,30 @@ if navori_audit_consume_armed "$session_id" "$cwd" "$audits_root"; then
75
75
  printf 'navori: audit-mode ACTIVE from this message on (armed via navori audit --arm; the hook ran --start %s).\n' "$session_id"
76
76
  fi
77
77
 
78
+ # ─── audit.mode = always (cobertura sin acordarse) ───────────────────────────
79
+ # `opt-in` keeps the historical behaviour: nothing is recorded until someone runs
80
+ # `navori audit --start|--arm`. `always` starts the recorder on this session's
81
+ # first prompt instead.
82
+ #
83
+ # It exists because opt-in coverage was measured and it is thin: of 187 real
84
+ # sessions only 54 carried a log, so the instrument saw 39% of the work — and the
85
+ # two arms of a controlled A/B sat at 0%, which is the one place the measurement
86
+ # was supposed to decide something. Same failure mode as the release tag that got
87
+ # skipped twice: a step that waits on a human remembering it.
88
+ #
89
+ # The `! -f` guard is what makes this idempotent AND free: once the log exists
90
+ # the branch is never taken again, and in `opt-in` repos the string compare costs
91
+ # nothing. Failure to start is silent and leaves the session unrecorded, exactly
92
+ # as if the mode were off — a recorder may never be the reason a prompt fails.
93
+ audit_mode={{shq:audit.mode}}
94
+ if [ ! -f "$log_file" ] && [ "$audit_mode" = "always" ] && command -v navori >/dev/null 2>&1; then
95
+ if navori audit --start "$session_id" --cwd "$cwd" >/dev/null 2>&1; then
96
+ # Same reasoning as the armed branch: a UserPromptSubmit hook's stdout is
97
+ # injected as context, so the model learns it is being recorded as it starts.
98
+ printf 'navori: audit-mode ACTIVE from this message on (audit.mode=always; no --arm needed).\n'
99
+ fi
100
+ fi
101
+
78
102
  # Not marked → not recording. This is also what makes the hook free outside
79
103
  # audit-mode: one stat and out.
80
104
  [ -f "$log_file" ] || exit 0
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PreToolUse(Bash): a `gh pr create` that did NOT come from the
4
+ # `commit-pr-pilot` is raised to a user confirmation.
5
+ #
6
+ # WHY (#705): the pilot is the single owner of commit+PR, and it is invoked on
7
+ # 15% of the PRs this harness opens. The rate is not uniform — one park repo
8
+ # runs at 48%, and the repo that PUBLISHES the pilot sat at 0 of 101. The
9
+ # confound was checked and does not hold: in the sessions where subagents were
10
+ # demonstrably available and used, the pilot was still never called. The pilot
11
+ # is not skipped; its antechamber is never entered, because `gh pr create` falls
12
+ # out of whatever the main agent was already doing and nothing interrupts it.
13
+ #
14
+ # So this hook interrupts, and does no more than that. It does NOT block: a
15
+ # session where the operator forbids subagents has no way to reach the pilot,
16
+ # and a hook that made PRs impossible there would be worse than the deviation it
17
+ # corrects. `ask` keeps the decision with the human while removing the one thing
18
+ # measured to fail — a layer that only suggests.
19
+ set -euo pipefail
20
+
21
+ # Command extraction (payload → $cmd). Shared body, single source of truth.
22
+ # navori:include extract-cmd
23
+
24
+ # Gate to `gh pr create` only. $TRIGGER_RE is consumed by the shared detector
25
+ # inlined below, which splits compound commands on && || ; | and matches at a
26
+ # segment START — so `git push … && gh pr create …` is caught and an
27
+ # `echo "gh pr create"` is not.
28
+ TRIGGER_RE='^gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)'
29
+ # Literal substring every branch of $TRIGGER_RE needs, read by the fast path in
30
+ # the shared detector (spec 0016). `create` rather than `gh`: both are necessary
31
+ # conditions, and the rarer one skips the fork on more commands. Keep NEXT to
32
+ # the regex — a branch added there without its token here loses the shortcut.
33
+ TRIGGER_TOKENS='create'
34
+ # navori:include gate-trigger
35
+
36
+ # THE CHEAP GATE, and it comes before everything that costs a process.
37
+ #
38
+ # This hook fires on EVERY Bash call and does real work on almost none of them,
39
+ # and until this line it paid for that privilege twice: `extract_cmd` forks jq
40
+ # (or node) to read the command, and the `skip` record forks jq again to write
41
+ # "I ran and it was not a PR". Measured on a `git status` payload: 24.8 ms per
42
+ # Bash call, half of the ~48 ms that already forced `audit-log.sh` to be
43
+ # redesigned.
44
+ #
45
+ # `has_trigger_token` answers from the payload navori already has in memory,
46
+ # with no fork at all, and its answer is a proof rather than a guess: the
47
+ # command is a substring of the payload, and JSON escaping cannot break a token
48
+ # apart. No token in the payload → no segment can match → nothing here concerns
49
+ # this hook.
50
+ #
51
+ # WHAT IS GIVEN UP: the `skip` record for those calls. It says "the hook ran and
52
+ # the command was not a PR" — 99.9% of its firings — and it cost two forks to
53
+ # produce. Every record that carries information (`ask`, `allow`) is still
54
+ # written below. Same trade as #696, for the same reason.
55
+ has_trigger_token "${payload:-}" || exit 0
56
+
57
+ cmd=$(extract_cmd)
58
+
59
+ navori_audit_name="pr-pilot-confirm"
60
+ navori_audit_phase="PreToolUse"
61
+ navori_audit_tool="Bash"
62
+ # Fail-open no-ops, overwritten by the real definitions the include brings in.
63
+ # Same contract as the sibling gates: a recorder may never kill what it observes.
64
+ navori_audit_begin() { :; }
65
+ navori_audit_log() { :; }
66
+ # navori:include audit-log
67
+ navori_audit_begin
68
+
69
+ # The verdict is derived once, in a trap, rather than by a call per branch —
70
+ # this fires on EVERY Bash call and does real work on almost none of them.
71
+ navori_audit_verdict="skip"
72
+ navori_audit_reason="el comando no abre un PR"
73
+ navori_audit_on_exit() {
74
+ navori_audit_log "$navori_audit_verdict" "$navori_audit_reason" || true
75
+ return 0
76
+ }
77
+ trap navori_audit_on_exit EXIT
78
+
79
+ # An EMPTY $cmd means nothing could be read from the tool input, not "some
80
+ # command that isn't a PR". Unlike the quality gate, the fail-open direction
81
+ # here is to stay quiet: this hook's worst case is a false confirmation prompt
82
+ # on every Bash call, which would train the user to dismiss it unread — and a
83
+ # prompt nobody reads is worth less than no prompt at all.
84
+ [ -n "$cmd" ] || exit 0
85
+ is_scan_trigger "$cmd" || exit 0
86
+
87
+ # Who is running it. Inside a subagent the host sends a real `agent_id` on the
88
+ # tool phases; the main thread sends none. Measured over the park's audit logs:
89
+ # 10,879 events carried a real id and every one of them resolved to a subagent.
90
+ #
91
+ # Read HERE, not at the top: `payload_field` may spawn a process, and by this
92
+ # line we already know the command is the rare one that needs the answer.
93
+ #
94
+ # It names SOME subagent, not specifically the pilot — a `researcher` opening a
95
+ # PR would pass. That is deliberate: this is a routing nudge, not a security
96
+ # boundary, and the detector cannot see through `sh -c` either.
97
+ navori_pr_agent=$(payload_field agent_id)
98
+ [ -n "$navori_pr_agent" ] || navori_pr_agent=$(payload_field subagent_id)
99
+ if [ -n "$navori_pr_agent" ]; then
100
+ navori_audit_verdict="allow"
101
+ navori_audit_reason="el PR viene de un subagente"
102
+ exit 0
103
+ fi
104
+
105
+ navori_audit_verdict="ask"
106
+ navori_audit_reason="PR abierto fuera del commit-pr-pilot"
107
+
108
+ # `ask` routes the call to the user instead of resolving it. The reason is what
109
+ # they read, so it says what the pilot adds and how to get it — a prompt that
110
+ # only says "are you sure" is a tax, not a routing signal.
111
+ #
112
+ # jq builds it: the reason travels inside JSON and a hand-rolled string would
113
+ # break on the first quote. No jq (not preinstalled on macOS) → stay silent
114
+ # rather than emit malformed JSON, which the host rejects with a wall of schema
115
+ # text that teaches the user to ignore this hook.
116
+ command -v jq >/dev/null 2>&1 || exit 0
117
+
118
+ # The message lands in its own assignment rather than inline in the jq call.
119
+ # A heredoc nested inside `"$( ... )"` is parsed by the shell BEFORE the quoted
120
+ # delimiter takes effect for the outer context, so the apostrophe in "repo's"
121
+ # and the backticks around `gh pr create` opened quotes that were never closed
122
+ # and the file failed `bash -n` outright. Caught by the syntax check; it would
123
+ # have shipped a hook that cannot run to every repo in the park.
124
+ navori_pr_reason=$(cat <<'MSG'
125
+ [navori] this `gh pr create` does not come from the commit-pr-pilot.
126
+
127
+ The pilot is the single owner of commit+PR: it applies the title/body format
128
+ this repo uses and runs the git/gh pre-flight before opening anything.
129
+
130
+ Delegate it with the Agent tool (subagent_type: commit-pr-pilot), or confirm to
131
+ open this PR by hand — a rollout PR, a revert, or a session where subagents are
132
+ unavailable are all legitimate reasons to do so.
133
+ MSG
134
+ )
135
+
136
+ jq -cn --arg reason "$navori_pr_reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:$reason}}'
137
+ exit 0