navori 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +22 -4
  2. package/dist/assets/core/core-assets/agents/explorer.md +2 -2
  3. package/dist/assets/core/core-assets/agents/implementer.md +2 -2
  4. package/dist/assets/core/core-assets/agents/leader.md +3 -1
  5. package/dist/assets/core/core-assets/agents/researcher.md +2 -2
  6. package/dist/assets/core/core-assets/agents/reviewer.md +17 -6
  7. package/dist/assets/core/core-assets/agents/ticket-audit.md +1 -1
  8. package/dist/assets/core/core-assets/hooks/_partials/extract-cmd.sh +21 -0
  9. package/dist/assets/core/core-assets/hooks/_partials/gate-trigger.sh +59 -0
  10. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +8 -19
  11. package/dist/assets/core/core-assets/hooks/precompact-session-summary.sh +34 -0
  12. package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +20 -76
  13. package/dist/assets/core/core-assets/hooks/session-start-context.sh +17 -1
  14. package/dist/assets/core/core-assets/hooks/stop-verify-reminder.sh +45 -0
  15. package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +72 -0
  16. package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
  17. package/dist/assets/core/core-assets/managed/codex-cross-review.md +14 -0
  18. package/dist/assets/core/core-assets/managed/orquestacion.md +12 -1
  19. package/dist/assets/core/core-assets/skills/dominio.md +67 -0
  20. package/dist/assets/core/core-assets/skills/ticket-intake.md +3 -3
  21. package/dist/assets/plugins/acli/plugin.json +14 -2
  22. package/dist/assets/plugins/codegraph/plugin.json +2 -1
  23. package/dist/assets/plugins/engram/plugin.json +12 -5
  24. package/dist/assets/plugins/gh/plugin.json +2 -1
  25. package/dist/assets/plugins/jscpd/plugin.json +2 -1
  26. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +22 -80
  27. package/dist/assets/plugins/semgrep/plugin.json +4 -2
  28. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +23 -82
  29. package/dist/index.js +389 -315
  30. package/package.json +3 -2
@@ -40,24 +40,42 @@ gh auth status # gh authenticated
40
40
 
41
41
  If the harness is active, identify THIS feature's review: `.claude/progress/review_<feature>.md`, with `<feature>` the id you received in your brief. A broad glob (`review_*.md`) over all reviews is not valid — it's not enough that some review with `APPROVED` exists in the directory, it has to be this feature's.
42
42
 
43
- 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. If the review lists the files it reviewed, compare them against `git diff --name-only`: if there are touched files that do NOT appear in that list, the review 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.
43
+ 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 `git diff --name-only` 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.
44
44
 
45
45
  <!-- This file-coverage rule lives here only; `skills/pr-create.md` is a pointer to this agent (single owner of the PR flow). -->
46
46
 
47
47
 
48
48
  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`.
49
49
 
50
- **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`). Before committing, confirm no approved file drifted since the review (rebase, human tweak, follow-up edit):
50
+ **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):
51
51
 
52
52
  ```bash
53
+ # 1) COVERAGE: shipping files the receipt never listed → reviewer never saw them.
54
+ # EXACT same diff set the reviewer captured — tracked-vs-target + untracked,
55
+ # minus the harness's own progress/ files (same grep the receipt applies) — so
56
+ # the sets line up 1:1 with no spurious mismatches. A git-persisted progress/
57
+ # update never counts as "uncovered"; deletions DO stay in the set (the receipt
58
+ # records them as `deleted <path>`) so a removed file can't ship unreviewed.
59
+ comm -23 \
60
+ <({ git diff --name-only "origin/{{prTarget}}"; git ls-files --others --exclude-standard; } \
61
+ | sort -u | grep -vE '^(\.claude/progress/|progress/)') \
62
+ <(grep -v '^#' .claude/progress/receipt.txt | sed 's/^[^ ]* //' | sort -u)
63
+
64
+ # 2) DRIFT: a reviewed file whose bytes changed since the review. A `deleted`
65
+ # marker means the reviewer signed off on the removal → drift only if the file
66
+ # came back.
53
67
  while IFS= read -r line; do
54
68
  case "$line" in ''|'#'*) continue ;; esac
55
69
  blob=${line%% *}; path=${line#* }
56
- [ "$(git hash-object "$path" 2>/dev/null)" = "$blob" ] || echo "DRIFT: $path"
70
+ if [ "$blob" = deleted ]; then
71
+ [ -e "$path" ] && echo "DRIFT: $path (reappeared since review)"
72
+ else
73
+ [ "$(git hash-object "$path" 2>/dev/null)" = "$blob" ] || echo "DRIFT: $path"
74
+ fi
57
75
  done < .claude/progress/receipt.txt
58
76
  ```
59
77
 
60
- Any `DRIFT` line — or a missing `receipt.txt` for a reviewed (R2+) change means the approval no longer covers the current diff. Abort, don't commit, and send it back to the `reviewer` to re-approve over the current bytes. (The pre-commit hook re-checks the staged set mechanically as a backstop; catching it here is earlier and clearer.)
78
+ 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, don't commit, and send it back to the `reviewer` to cover/re-approve over the current bytes. It's not enough to mention the gap and carry on. (The pre-commit hook re-checks the staged set for drift mechanically as a backstop; catching both here is earlier and clearer.)
61
79
 
62
80
  **R1 exception (trivial diff, no reviewer):** a genuine R1 change (1–3 files, mechanical or a bugfix with a clear cause, 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). This waiver is ONLY for a real R1 diff; anything R2+ (4+ files, or 2+ non-trivial files) still requires the APPROVED review.
63
81
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: explorer
3
3
  description: Broad map of an area or module of the repo. Returns structure, dependencies, and entry points. Does not modify code.
4
- tools: Read, Glob, Grep, Bash
4
+ tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.explorer}}
6
6
  effort: {{effort.explorer}}
7
7
  ---
@@ -25,7 +25,7 @@ If the question is specific ("where is X?"), it's not you — it's `researcher`.
25
25
 
26
26
  1. Read `CLAUDE.md` to understand the repo's conventions.
27
27
  2. Define the scope: a folder, a logical module, a file pattern. The orchestrator should hand it to you precisely; if it arrives ambiguous, return `blocked` naming the options (folder X / module Y / pattern Z) so it re-sends it scoped — don't guess.
28
- 3. Walk from the entry points (routes, module root exports, `index.ts`) toward the leaves. For each level, list files and their brief role. Apply `.claude/skills/structural-search.md` to locate shapes and entry points without reading whole files.
28
+ 3. Walk from the entry points (routes, module root exports, `index.ts`) toward the leaves. For each level, list files and their brief role. Apply `.claude/skills/structural-search/SKILL.md` to locate shapes and entry points without reading whole files.
29
29
  4. Identify reverse dependencies: which external modules consume this module? That indicates the "blast radius" of changing something here.
30
30
  5. Write `.claude/progress/explore_<area>.md`:
31
31
 
@@ -26,7 +26,7 @@ You execute **a single** task from start to verification. You don't orchestrate,
26
26
  ```
27
27
 
28
28
  - `Expected files: <list>`
29
- 3. **Implement** following the repo's flow (the leader's "Project rules" define the concrete pattern: layers, libs, paths, naming). To locate the code to touch, apply `.claude/skills/structural-search.md`: open only the confirmed span, don't read whole files by reflex.
29
+ 3. **Implement** following the repo's flow (the leader's "Project rules" define the concrete pattern: layers, libs, paths, naming). To locate the code to touch, apply `.claude/skills/structural-search/SKILL.md`: open only the confirmed span, don't read whole files by reflex.
30
30
  4. **Quality gate** (mandatory before returning):
31
31
 
32
32
  ```bash
@@ -70,7 +70,7 @@ No speculative abstractions: no interface / layer / flag with a single "just in
70
70
 
71
71
  ## Evidence-based completion (gate before the report)
72
72
 
73
- Before returning `done -> .claude/progress/impl_<feature>.md`, apply `.claude/skills/verify-before-done.md`. Summary of the Iron Law:
73
+ Before returning `done -> .claude/progress/impl_<feature>.md`, apply `.claude/skills/verify-before-done/SKILL.md`. Summary of the Iron Law:
74
74
 
75
75
  | Claim you're going to make | Required output | Not sufficient |
76
76
  |---|---|---|
@@ -93,10 +93,12 @@ Expected files:
93
93
 
94
94
  - `.claude/progress/audit_ticket_<TICKET-ID>.md` — deep analysis of one ticket (`ticket-audit`)
95
95
  - `.claude/progress/audit_deep_<scope>.md` — deep read-only audit of a module/area/repo with no ticket (`auditor`)
96
+ - `.claude/progress/plan_<scope>.md` — the `auditor`'s prioritized plan that accompanies a deep audit
96
97
  - `.claude/progress/explore_<topic>.md` — broad map (`explorer`)
97
98
  - `.claude/progress/research_<question>.md` — scoped question (`researcher`)
98
99
  - `.claude/progress/impl_<feature>.md` — the `implementer`'s report (includes its `Status: DONE | BLOCKED`)
99
100
  - `.claude/progress/review_<feature>.md` — the `reviewer`'s verdict
101
+ - `.claude/progress/receipt.txt` — the `reviewer`'s content receipt on `APPROVED` (binds the diff to the reviewed bytes; consumed by `commit-pr-pilot`)
100
102
 
101
103
  **Path separation (don't mix):** `.claude/progress/` is ONLY for these ephemeral handoffs between agents. The **session state** (current task, plan, blockers) lives in `progress/current.md` (repo root, persists in git) and you consolidate it **YOU, only**: subagents never write it. When an `implementer` reports `blocked` in its `impl_<feature>.md`, you record the blocker in `progress/current.md` along with the next step.
102
104
 
@@ -105,7 +107,7 @@ Expected files:
105
107
  When `.claude/progress/review_<feature>.md` contains `APPROVED`:
106
108
 
107
109
  1. Invoke `commit-pr-pilot` to draft the title + body following the repo's format and open the PR.
108
- 2. Pre-flight on you before invoking: clean working tree, you're not on `{{branchBase}}`, `{{qualityGate.fast}}` green this turn, `gh auth status` ok.
110
+ 2. Pre-flight on you before invoking: you're not on `{{branchBase}}`, `{{qualityGate.fast}}` green this turn, `gh auth status` ok. (Do NOT require a clean working tree — the pilot's trigger IS an uncommitted diff ready to commit, and the pilot, not you, owns that commit.)
109
111
  3. Return to the user only the PR URL + title.
110
112
 
111
113
  If the review returned `CHANGES_REQUESTED`, do NOT invoke `commit-pr-pilot`: launch another `implementer` with the list of changes and restart the cycle.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: researcher
3
3
  description: Read-only investigation of a scoped question. Reads the repo, writes findings to a file. Does not modify code.
4
- tools: Read, Glob, Grep, Bash
4
+ tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.researcher}}
6
6
  effort: {{effort.researcher}}
7
7
  ---
@@ -28,7 +28,7 @@ If the question is broad ("map the whole module X for me"), it's not you — it'
28
28
  3. Run the search:
29
29
  - Primary method: the native `Grep` (content) and `Glob` (files by name/pattern) tools. They're read-only, fast (ripgrep), and don't ask for permission.
30
30
  - Fallback only for what the tools don't cover (git history with `git grep`, FS metadata with `find`): shell commands. Chained with pipes/redirects they ask for confirmation, so reserve the shell for when `Grep`/`Glob` fall short.
31
- - For semantic questions (not just string match), apply `.claude/skills/structural-search.md`: locate the right region and open only the confirmed span; don't read whole files by reflex.
31
+ - For semantic questions (not just string match), apply `.claude/skills/structural-search/SKILL.md`: locate the right region and open only the confirmed span; don't read whole files by reflex.
32
32
  4. Validate each finding: open the file, confirm the match means what it seems (sometimes a `grep` matches comments or strings unrelated to the concept).
33
33
  5. Write `.claude/progress/research_<question-slug>.md`:
34
34
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: reviewer
3
3
  description: Strict reviewer. Approves or rejects the implementer's work against CLAUDE.md. Does not edit code.
4
- tools: Read, Glob, Grep, Bash
4
+ tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.reviewer}}
6
6
  effort: {{effort.reviewer}}
7
7
  ---
@@ -26,11 +26,16 @@ You are a strict reviewer. Your only function is to **approve or reject**. You d
26
26
  git status --short
27
27
  git fetch origin {{prTarget}} --quiet
28
28
  git diff --stat
29
- git diff origin/{{prTarget}}...HEAD
29
+ # two-dot: the FULL working tree vs the target (committed AND uncommitted),
30
+ # the exact set the receipt fingerprints below. Three-dot (`...HEAD`) would show
31
+ # only committed changes, but in the harness the diff is still uncommitted — so
32
+ # the review command would read empty while the receipt signs the working tree.
33
+ git diff "origin/{{prTarget}}"
34
+ git ls-files --others --exclude-standard # untracked files (new, not yet staged)
30
35
  ```
31
36
 
32
37
  3. **Re-review** (if there's already a `.claude/progress/review_<feature>.md` from a previous cycle): focus the *reading* on (a) that the issues listed there are resolved and (b) the files the `implementer` reports having touched in this cycle (`impl_<feature>.md`). Don't re-review from scratch the already-approved code that didn't change; the full quality gate is still run anyway — a change can break something outside the delta.
33
- 4. Apply `.claude/skills/verify-before-done.md` to every `[x]` that depends on evidence. The quality gate is run **this turn, in Pass 2** (not before: a `SPEC_MISS` in Pass 1 doesn't need it — don't spend the gate on a diff you're going to reject on spec). Don't assume from the implementer's cached report.
38
+ 4. Apply `.claude/skills/verify-before-done/SKILL.md` to every `[x]` that depends on evidence. The quality gate is run **this turn, in Pass 2** (not before: a `SPEC_MISS` in Pass 1 doesn't need it — don't spend the gate on a diff you're going to reject on spec). Don't assume from the implementer's cached report.
34
39
 
35
40
  ### Pass 1 — Spec compliance
36
41
 
@@ -52,7 +57,7 @@ Does the diff do EXACTLY what was asked? You don't review style yet.
52
57
 
53
58
  Does the code match the repo's conventions? Here you do review style/naming/types.
54
59
 
55
- Apply `.claude/skills/review-diff.md` — the full checklist by dimensions, with severities. Its CRITICAL/HIGH map to the ≥80 issues below; MEDIUM to the informational observations. Summary of the minimum to validate against `CLAUDE.md` and the leader's "Project rules":
60
+ Apply `.claude/skills/review-diff/SKILL.md` — the full checklist by dimensions, with severities. Its CRITICAL/HIGH map to the ≥80 issues below; MEDIUM to the informational observations. Summary of the minimum to validate against `CLAUDE.md` and the leader's "Project rules":
56
61
 
57
62
  - **Conventions**: naming, path aliases, folder structure.
58
63
  - **Centralized types**: no inline `type`/`interface` where the convention says "outside".
@@ -87,12 +92,18 @@ printf '# navori-receipt v1 feature=<feature>\n' > .claude/progress/receipt.txt
87
92
  | sort -u \
88
93
  | grep -vE '^(\.claude/progress/|progress/)' \
89
94
  | while IFS= read -r f; do
90
- [ -f "$f" ] && printf '%s %s\n' "$(git hash-object "$f")" "$f"
95
+ if [ -f "$f" ]; then
96
+ printf '%s %s\n' "$(git hash-object "$f")" "$f" # live file → blob sha
97
+ else
98
+ printf 'deleted %s\n' "$f" # removed file → deletion marker
99
+ fi
91
100
  done >> .claude/progress/receipt.txt
92
101
  ```
93
102
 
94
103
  It captures the working-tree bytes under review (committed **and** uncommitted). The `grep -v` drops the harness's own ephemeral progress files (the receipt, `impl_*`, `review_*`) — they never get committed, so fingerprinting them would be self-referential noise. Skip the whole step for `CHANGES_REQUESTED` — a rejected diff has nothing to bind.
95
104
 
105
+ A **removed** file has no bytes to hash, so it's recorded as `deleted <path>` instead of a blob sha. Keeping the deletion **in** the receipt is what closes the RDD cycle: the `commit-pr-pilot` coverage check is path-based, so it still sees the path (a deletion can't ship unreviewed), and both its drift check and the pre-commit hook read the `deleted` marker as "must stay absent" — flagging drift only if the file reappears. The shipping set the pilot compares against is then byte-for-byte the set you signed here (same `grep -vE`, deletions included), so a git-persisted `progress/` update or a removed file never shows up as "uncovered" and livelocks the close.
106
+
96
107
  ### Confidence scoring per finding (Pass 2)
97
108
 
98
109
  Each issue is scored 0-100. Only issues ≥80 block APPROVED. Issues 50-79 are listed as "informational observations" (they don't block). <50 = don't report.
@@ -164,7 +175,7 @@ CHANGES_REQUESTED -> .claude/progress/review_<feature>.md
164
175
 
165
176
  - ❌ Never skip Pass 1 (spec compliance). If the code is pretty but doesn't do what was asked, it's `CHANGES_REQUESTED`.
166
177
  - ❌ Never include as a blocker (in "Issues ≥80") a finding with confidence <80.
167
- - ✅ Apply `.claude/skills/verify-before-done.md` before marking APPROVED: each `[x]` must be backed by evidence run this turn (not from the implementer's cached report).
178
+ - ✅ Apply `.claude/skills/verify-before-done/SKILL.md` before marking APPROVED: each `[x]` must be backed by evidence run this turn (not from the implementer's cached report).
168
179
  - ❌ Never approve with `{{qualityGate.full}}` red.
169
180
  - ❌ Never approve if the new code **adds new errors or warnings** vs baseline.
170
181
  - ❌ Never approve new code with explicit or implicit `any` without a valid `// any justified: <reason>`.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ticket-audit
3
3
  description: Deep analysis of a complex ticket before implementing. Produces audit_ticket_<ID>.md with root cause, affected areas, and a decomposition plan.
4
- tools: Read, Glob, Grep, Bash
4
+ tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.ticketAudit}}
6
6
  effort: {{effort.ticketAudit}}
7
7
  ---
@@ -0,0 +1,21 @@
1
+ # Shared hook boilerplate — inlined into each hook at render time (see the
2
+ # include directive in the source scripts + lib/hook-includes.ts). Single source
3
+ # of truth for the sibling gate scripts; DO NOT copy this body back into a hook
4
+ # by hand (that is the drift #225/#261 removed).
5
+ #
6
+ # PreToolUse(Bash) passes the tool input on stdin. Extract .tool_input.command
7
+ # WITHOUT hard-depending on jq (NOT preinstalled on macOS): try jq, then node
8
+ # (Claude Code's own runtime), then a best-effort sed unwrap. No command
9
+ # extracted → empty $cmd, and each caller decides what that means (the gate
10
+ # scripts scan defensively; guard-destructive waves the command through).
11
+ payload=$(cat)
12
+ extract_cmd() {
13
+ if command -v jq >/dev/null 2>&1; then
14
+ printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null && return 0
15
+ fi
16
+ if command -v node >/dev/null 2>&1; then
17
+ printf '%s' "$payload" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command??""))}catch{}})' 2>/dev/null && return 0
18
+ fi
19
+ printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
20
+ }
21
+ cmd=$(extract_cmd)
@@ -0,0 +1,59 @@
1
+ # Shared gate detector — inlined into each hook at render time (see the include
2
+ # directive in the source scripts + lib/hook-includes.ts). The caller MUST set
3
+ # $TRIGGER_RE (an ERE) before the include; it decides which git ops this hook
4
+ # gates. Single source of truth for the FIX B/C wrapper-peeling logic; DO NOT
5
+ # copy this body into a hook by hand.
6
+ #
7
+ # Detect whether $1 (a possibly-compound command) invokes a gated operation.
8
+ # Splits $1 on the shell separators && || ; | and newlines, strips leading
9
+ # whitespace plus wrapper words (`(`, `\`, `command `) and `VAR=value` env
10
+ # prefixes from each segment, and returns 0 if ANY segment STARTS with a gated
11
+ # `git …` invocation on a word boundary (matched by $TRIGGER_RE). Replaces
12
+ # literal-prefix `case` matching, which silently skipped the gate for
13
+ # `cd x && git commit`, `echo y; git push`, or a leading space (#88: NEVER skip
14
+ # the gate silently). Matching a segment START means a quoted `echo "git commit"`
15
+ # does NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`,
16
+ # or obfuscation — a seatbelt, not a sandbox.
17
+ is_scan_trigger() {
18
+ local input="$1" segment
19
+ # FIX B: join `\<newline>` continuations into a space FIRST, so a command
20
+ # split across lines with a trailing backslash stays ONE logical segment
21
+ # (otherwise the subcommand/flag lands in a segment not starting with git).
22
+ input="${input//\\$'\n'/ }"
23
+ input="${input//&&/$'\n'}"
24
+ input="${input//||/$'\n'}"
25
+ input="${input//;/$'\n'}"
26
+ input="${input//|/$'\n'}"
27
+ # `<<<` feeds the already-expanded value as data — no re-evaluation — so a
28
+ # command that contains backticks/$() is inspected, never executed.
29
+ while IFS= read -r segment; do
30
+ segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
31
+ # FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
32
+ # `VAR=val git …` all reduce to a plain `git …` before matching.
33
+ while [[ "$segment" == \(* ]]; do # strip leading ( runs
34
+ segment="${segment#\(}"
35
+ segment="${segment#"${segment%%[![:space:]]*}"}"
36
+ done
37
+ segment="${segment#\\}" # strip a leading backslash (\git)
38
+ while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
39
+ case "$segment" in
40
+ *[[:space:]]*)
41
+ segment="${segment#*[[:space:]]}"
42
+ segment="${segment#"${segment%%[![:space:]]*}"}"
43
+ ;;
44
+ *) segment=""; break ;;
45
+ esac
46
+ done
47
+ if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
48
+ segment="${segment#command }"
49
+ segment="${segment#"${segment%%[![:space:]]*}"}"
50
+ fi
51
+ # FIX C: allow git global options between `git` and the subcommand
52
+ # (`git -c k=v commit`, `git -C /repo push`). $TRIGGER_RE's trailing boundary
53
+ # keeps `git commitgraph` / `git config …` from matching.
54
+ if printf '%s' "$segment" | grep -qE "$TRIGGER_RE"; then
55
+ return 0
56
+ fi
57
+ done <<< "$input"
58
+ return 1
59
+ }
@@ -21,27 +21,16 @@
21
21
  # bottom — `$cmd` is already parsed and in scope there.
22
22
  set -euo pipefail
23
23
 
24
- # Extract .tool_input.command from the PreToolUse payload WITHOUT hard-depending
25
- # on jq (NOT preinstalled on macOS — a missing jq used to make this guard wave
26
- # every command through). Try jq, then node (Claude Code's own runtime), and if
27
- # no JSON parser is on PATH fall back to a best-effort sed unwrap so the guard
28
- # still inspects the command instead of failing open.
29
- payload=$(cat)
30
- extract_cmd() {
31
- if command -v jq >/dev/null 2>&1; then
32
- printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null && return 0
33
- fi
34
- if command -v node >/dev/null 2>&1; then
35
- printf '%s' "$payload" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command??""))}catch{}})' 2>/dev/null && return 0
36
- fi
37
- # No JSON parser on PATH: pull the "command" string out with sed. Best-effort
38
- # (won't handle a literal embedded quote), but far better than failing open.
39
- printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
40
- }
41
- cmd=$(extract_cmd)
24
+ # Command extraction (payload → $cmd). Shared body, single source of truth.
25
+ # A missing JSON parser used to make this guard wave every command through; the
26
+ # shared extractor falls back to sed so it still inspects the command.
27
+ # navori:include extract-cmd
42
28
  [ -z "$cmd" ] && exit 0
43
29
 
44
- base="{{branchBase}}"
30
+ # branchBase is shell-quoted at render time via the shq: marker (#197): a
31
+ # hostile branchBase in navori.config.json lands here as an inert literal,
32
+ # never executable.
33
+ base={{shq:branchBase}}
45
34
 
46
35
  block() {
47
36
  echo "[navori] BLOQUEADO por guard-destructive: $1" >&2
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PreCompact lifecycle hook — session-summary reminder.
4
+ # Fires just before Claude Code compacts the conversation (manual `/compact` or
5
+ # automatic). Compaction summarizes and drops turn-by-turn detail, so anything
6
+ # not persisted durably (decisions, root-caused bugs, conventions) is at risk of
7
+ # being lost. This hook injects a reminder to persist a session summary before
8
+ # that detail is gone.
9
+ #
10
+ # DESIGN — advisory, never blocking. A shell hook can't itself run the model's
11
+ # memory tool — that's the model's action — so this hook is honest about its
12
+ # limit: it INJECTS a reminder via `hookSpecificOutput.additionalContext` (the
13
+ # same channel session-start uses) and lets the model act on it. It never returns
14
+ # `decision: block`, so it never stalls compaction. The reminder points at the
15
+ # engram session-summary tool when available, and falls back to a plain "jot key
16
+ # decisions into progress/current.md" so it's still useful in a repo without
17
+ # engram. It deliberately does NOT spell out engram's exact tool token verbatim —
18
+ # that token is a doctor invariant the engram plugin owns; naming it here would
19
+ # let a hook mask a gutted guidance block from `doctor`.
20
+ #
21
+ # The managed block is regenerated by `navori render`; do NOT edit by hand.
22
+ set -euo pipefail
23
+
24
+ cat >/dev/null 2>&1 || true # drain the PreCompact JSON on stdin (unused)
25
+
26
+ ctx="A punto de compactar el contexto: el detalle turno-a-turno se resumirá. Antes de perderlo, persiste un resumen de sesión con tu herramienta de memoria (engram: guarda el resumen de sesión) y/o anota las decisiones y los bugs con causa raíz de esta sesión en progress/current.md."
27
+
28
+ # Emit additionalContext safely: node (best escaping) → jq → give up (exit 0).
29
+ if command -v node >/dev/null 2>&1; then
30
+ CTX="$ctx" node -e 'process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"PreCompact",additionalContext:process.env.CTX}}))'
31
+ elif command -v jq >/dev/null 2>&1; then
32
+ jq -n --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreCompact",additionalContext:$ctx}}'
33
+ fi
34
+ exit 0
@@ -9,20 +9,8 @@
9
9
  # bottom — they keep `cmd` and the original exit codes in scope.
10
10
  set -euo pipefail
11
11
 
12
- # Extract .tool_input.command WITHOUT hard-depending on jq (not preinstalled on
13
- # macOS). Try jq, then node (Claude Code's own runtime), then a best-effort sed
14
- # unwrap. If nothing extracts a command $cmd stays empty and the gate skips.
15
- payload=$(cat)
16
- extract_cmd() {
17
- if command -v jq >/dev/null 2>&1; then
18
- printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null && return 0
19
- fi
20
- if command -v node >/dev/null 2>&1; then
21
- printf '%s' "$payload" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command??""))}catch{}})' 2>/dev/null && return 0
22
- fi
23
- printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
24
- }
25
- cmd=$(extract_cmd)
12
+ # Command extraction (payload → $cmd). Shared body, single source of truth.
13
+ # navori:include extract-cmd
26
14
 
27
15
  # Detect the project's REAL package manager from lockfiles / package.json, so a
28
16
  # gate command hardcoded to one PM (e.g. `pnpm run ...`) can still run in a repo
@@ -59,7 +47,8 @@ run_gate() {
59
47
  # --- content receipt (RDD) --------------------------------------------------
60
48
  # Backstop that binds the commit to the exact bytes the reviewer approved. The
61
49
  # reviewer writes a receipt (`<blob-sha> <path>` lines, one per approved file,
62
- # via `git hash-object`) when it marks APPROVED; the commit-pr-pilot recomputes
50
+ # via `git hash-object`, plus `deleted <path>` for a file it approved removing)
51
+ # when it marks APPROVED; the commit-pr-pilot recomputes
63
52
  # it before committing and consumes it after. THIS is the mechanical net for a
64
53
  # direct `git commit` that skips the pilot: if an approved file's content
65
54
  # drifted since the review (rebase, human tweak, follow-up edit), block. It
@@ -99,6 +88,10 @@ check_content_receipt() {
99
88
  path=${line#* } # path = the rest (may contain spaces)
100
89
  [ -n "$blob" ] && [ "$blob" != "$path" ] || continue
101
90
  printf '%s\n' "$commit_set" | grep -qxF "$path" || continue # not in this commit → ignore
91
+ if [ "$blob" = deleted ]; then # reviewer approved the removal
92
+ [ -e "$path" ] && drift="${drift} - ${path} (reappeared)"$'\n' # drift only if it came back
93
+ continue
94
+ fi
102
95
  now=$(git hash-object "$path" 2>/dev/null || true)
103
96
  if [ "$now" != "$blob" ]; then
104
97
  drift="${drift} - ${path}"$'\n'
@@ -108,75 +101,26 @@ check_content_receipt() {
108
101
  if [ -n "$drift" ]; then
109
102
  echo "[navori] APPROVED content changed since review (receipt mismatch). Commit BLOCKED." >&2
110
103
  printf '%s' "$drift" >&2
111
- echo "[navori] Re-run the reviewer over the current diff, or override with 'git commit --no-verify'." >&2
104
+ echo "[navori] Re-run the reviewer over the current diff. To bypass, run the commit yourself outside the agent." >&2
112
105
  echo "[navori] To clear a stale receipt: rm $receipt" >&2
113
106
  exit 2
114
107
  fi
115
108
  }
116
109
 
117
- # --- shared gate detection (keep IN SYNC across sibling hooks) --------------
118
- # Detect a `git commit` invocation anywhere in a (possibly
119
- # compound) command. Splits $1 on the shell separators && || ; | and newlines,
120
- # strips leading whitespace plus simple `VAR=value` env prefixes from each
121
- # segment, and returns 0 if ANY segment STARTS with `git commit` on
122
- # a word boundary. Replaces literal-prefix `case` matching, which silently
123
- # skipped the gate for `cd x && git commit`, `echo y; git commit`, or a leading
124
- # space (#88: NEVER skip the gate silently). Because it matches a segment START,
125
- # a quoted `echo "git commit"` does NOT trigger it. Known limitation: it cannot
126
- # see through `sh -c`, `eval`, or obfuscation — a seatbelt, not a sandbox.
127
- # The IDENTICAL function body lives in the sibling gate scripts (they render
128
- # standalone, so there is no shared lib to import):
129
- # plugins/jscpd/scripts/check-jscpd.sh
130
- # plugins/semgrep/scripts/check-semgrep.sh
131
- is_git_commit() {
132
- local input="$1" segment
133
- # FIX B: join `\<newline>` continuations into a space FIRST, so a command
134
- # split across lines with a trailing backslash stays ONE logical segment
135
- # (otherwise the subcommand/flag lands in a segment not starting with git).
136
- input="${input//\\$'\n'/ }"
137
- input="${input//&&/$'\n'}"
138
- input="${input//||/$'\n'}"
139
- input="${input//;/$'\n'}"
140
- input="${input//|/$'\n'}"
141
- # `<<<` feeds the already-expanded value as data — no re-evaluation — so a
142
- # command that contains backticks/$() is inspected, never executed.
143
- while IFS= read -r segment; do
144
- segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
145
- # FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
146
- # `VAR=val git …` all reduce to a plain `git …` before matching.
147
- while [[ "$segment" == \(* ]]; do # strip leading ( runs
148
- segment="${segment#\(}"
149
- segment="${segment#"${segment%%[![:space:]]*}"}"
150
- done
151
- segment="${segment#\\}" # strip a leading backslash (\git)
152
- while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
153
- case "$segment" in
154
- *[[:space:]]*)
155
- segment="${segment#*[[:space:]]}"
156
- segment="${segment#"${segment%%[![:space:]]*}"}"
157
- ;;
158
- *) segment=""; break ;;
159
- esac
160
- done
161
- if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
162
- segment="${segment#command }"
163
- segment="${segment#"${segment%%[![:space:]]*}"}"
164
- fi
165
- # FIX C: allow git global options between `git` and the subcommand
166
- # (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
167
- # `git commitgraph` / `git config …` from matching as commit.
168
- if printf '%s' "$segment" | grep -qE '^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'; then
169
- return 0
170
- fi
171
- done <<< "$input"
172
- return 1
173
- }
110
+ # Gate to `git commit` only. $TRIGGER_RE is consumed by the shared detector
111
+ # inlined below.
112
+ TRIGGER_RE='^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'
113
+ # navori:include gate-trigger
174
114
 
175
- if is_git_commit "$cmd"; then
115
+ if is_scan_trigger "$cmd"; then
176
116
  # Content-bind first: refuse to commit bytes that drifted from the approval
177
117
  # before spending the fast gate on them.
178
118
  check_content_receipt
179
- gate="{{qualityGate.fast}}"
119
+ # qualityGate.fast is shell-quoted at render time via the shq: marker (#197).
120
+ # The gate string is still `eval`'d by run_gate below (running the gate is the
121
+ # feature), but quoting it here means a hostile qualityGate.fast survives as one
122
+ # literal token instead of injecting commands at variable-assignment time.
123
+ gate={{shq:qualityGate.fast}}
180
124
  gate_bin="${gate%% *}"
181
125
  if command -v "$gate_bin" >/dev/null 2>&1; then
182
126
  run_gate "$gate"
@@ -192,7 +136,7 @@ if is_git_commit "$cmd"; then
192
136
  run_gate "$detected_pm ${gate#* }"
193
137
  else
194
138
  echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
195
- echo "[navori] Commit BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin' o usa 'git commit --no-verify' si de verdad quieres saltártelo." >&2
139
+ echo "[navori] Commit BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin', o si de verdad quieres saltártelo corre el commit tú mismo fuera del agente." >&2
196
140
  exit 2
197
141
  fi
198
142
  fi
@@ -29,7 +29,9 @@ add() { ctx="${ctx}${1}"$'\n'; }
29
29
 
30
30
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
31
31
  branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')
32
- base="{{branchBase}}"
32
+ # branchBase is shell-quoted at render time via the shq: marker (#197) so an
33
+ # untrusted branchBase can't inject a command here.
34
+ base={{shq:branchBase}}
33
35
  if [ "$branch" = "$base" ]; then
34
36
  add "Branch: ${branch} ⚠️ on the base branch — create a working branch before committing."
35
37
  else
@@ -59,6 +61,20 @@ if [ -n "$current" ]; then
59
61
  fi
60
62
  fi
61
63
 
64
+ # Workspace Dominio: canonical cross-repo knowledge for the workspace this repo
65
+ # belongs to (e.g. "coachee = user-profile.kind"), so agents don't relearn it
66
+ # wrong in every repo. The CLI owns the resolution (which workspace is cwd in +
67
+ # read the index); the hook stays dumb. Cheap pre-check first so the common
68
+ # no-workspace case never spawns the binary, and `|| true` so a missing/broken
69
+ # `navori` never blocks session startup. (spec 0011 §6.1)
70
+ if [ -d "$HOME/.navori/workspaces" ] && command -v navori >/dev/null 2>&1; then
71
+ dominio=$(navori dominio inject 2>/dev/null || true)
72
+ if [ -n "$dominio" ]; then
73
+ add ""
74
+ add "$dominio"
75
+ fi
76
+ fi
77
+
62
78
  [ -n "$ctx" ] || exit 0
63
79
 
64
80
  # Emit the JSON safely: node (best escaping) → jq → give up (exit 0, no context).
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Stop lifecycle hook — verify-before-done reminder (P4).
4
+ # Fires when the main agent finishes a turn. Its job is to nudge — NOT block —
5
+ # when the session is winding down with uncommitted work, so "I'm done" isn't
6
+ # claimed before the quality gate ran and the change is committed.
7
+ #
8
+ # DESIGN — advisory, never blocking. It emits a user-facing `systemMessage`
9
+ # only; it NEVER returns `decision: block`, so it can't trap the model in a
10
+ # "keep working" loop or force a turn the user didn't ask for. A hard,
11
+ # deterministic "did the model actually run verify-before-done?" gate is not
12
+ # reliably knowable from a shell hook (the model's intent isn't in the payload),
13
+ # so we intentionally downgrade to a reminder — see the PR for the rationale.
14
+ #
15
+ # OPT-IN. This hook is only wired into settings.json when
16
+ # `hooks.verifyOnStop` is true in navori.config.json (mirrors how
17
+ # `qualityGate.fast` gates the quality-gate hook). It stays OFF by default
18
+ # because a per-turn reminder can be noisy during active development; a repo
19
+ # that wants the nudge turns it on explicitly.
20
+ #
21
+ # The managed block is regenerated by `navori render`; do NOT edit by hand.
22
+ set -euo pipefail
23
+
24
+ cat >/dev/null 2>&1 || true # drain the Stop JSON on stdin (unused)
25
+
26
+ # Only meaningful inside a git repo.
27
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
28
+
29
+ # Hard evidence that work is unfinished: tracked files changed but not committed
30
+ # (staged or unstaged). Untracked files alone don't count — scratch/output files
31
+ # are common and shouldn't trigger the nudge.
32
+ if git diff --quiet --ignore-submodules HEAD 2>/dev/null; then
33
+ exit 0 # working tree clean vs HEAD → nothing to remind about
34
+ fi
35
+
36
+ msg="navori: cambios sin commitear en el árbol. Antes de dar la tarea por terminada, corre el quality gate (evidencia fresca este turno) y haz commit — verify-before-done (P4)."
37
+
38
+ # Emit the advisory as a user-facing systemMessage. node (Claude Code's own
39
+ # runtime) → jq → give up silently. systemMessage is non-blocking by contract.
40
+ if command -v node >/dev/null 2>&1; then
41
+ MSG="$msg" node -e 'process.stdout.write(JSON.stringify({systemMessage:process.env.MSG}))'
42
+ elif command -v jq >/dev/null 2>&1; then
43
+ jq -n --arg m "$msg" '{systemMessage:$m}'
44
+ fi
45
+ exit 0