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.
- package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +14 -18
- package/dist/assets/core/core-assets/agents/implementer.md +1 -1
- package/dist/assets/core/core-assets/agents/leader.md +10 -7
- package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +56 -5
- package/dist/assets/core/core-assets/hooks/_partials/classify-source.sh +52 -0
- package/dist/assets/core/core-assets/hooks/_partials/gate-trigger.sh +31 -12
- package/dist/assets/core/core-assets/hooks/audit-mode-trigger.sh +24 -0
- package/dist/assets/core/core-assets/hooks/pr-pilot-confirm.sh +137 -0
- package/dist/assets/core/core-assets/hooks/routing-watch.sh +90 -15
- package/dist/assets/core/core-assets/lib-skills/zod-validation.md +5 -4
- package/dist/assets/core/core-assets/managed/cierre-sesion.md +2 -2
- package/dist/assets/core/core-assets/managed/operaciones-seguras.md +1 -1
- package/dist/assets/core/core-assets/managed/orquestacion.md +33 -23
- package/dist/assets/core/core-assets/skills/debug-error.md +15 -10
- package/dist/assets/core/core-assets/skills/review-diff.md +4 -0
- package/dist/assets/core/core-assets/skills/solution-design.md +2 -2
- package/dist/assets/core/core-assets/skills/structural-search.md +7 -1
- package/dist/assets/core/core-assets/skills/ticket-intake.md +6 -2
- package/dist/assets/core/core-assets/skills/verify-before-done.md +1 -1
- package/dist/assets/plugins/codegraph/managed/codegraph-protocol.md +1 -1
- package/dist/assets/plugins/engram/managed/engram-protocol.md +1 -1
- package/dist/assets/plugins/engram/skills/engram-leader.md +2 -2
- package/dist/assets/plugins/tgrep/managed/tgrep-protocol.md +1 -1
- package/dist/assets/plugins/tgrep/plugin.json +15 -1
- package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +361 -0
- package/dist/assets/plugins/tgrep/scripts/tgrep-search.sh +93 -0
- package/dist/assets/plugins/tgrep/scripts/tgrep-session.sh +7 -1
- package/dist/assets/plugins/tgrep/skills/tgrep-rung.md +2 -2
- package/dist/index.js +390 -362
- package/package.json +2 -2
- 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
|
|
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;
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<!--
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
-
|
|
119
|
-
-
|
|
120
|
-
- **
|
|
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
|
-
|
|
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
|
-
- **
|
|
133
|
-
- **
|
|
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
|
|
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.
|
|
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
|
-
|
|
|
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
|
|
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 →
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|