navori 0.4.1 → 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 (28) hide show
  1. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +16 -6
  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/quality-gate-pre-commit.sh +20 -76
  12. package/dist/assets/core/core-assets/hooks/session-start-context.sh +17 -1
  13. package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +1 -1
  14. package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
  15. package/dist/assets/core/core-assets/managed/codex-cross-review.md +14 -0
  16. package/dist/assets/core/core-assets/managed/orquestacion.md +4 -2
  17. package/dist/assets/core/core-assets/skills/dominio.md +67 -0
  18. package/dist/assets/core/core-assets/skills/ticket-intake.md +3 -3
  19. package/dist/assets/plugins/acli/plugin.json +14 -2
  20. package/dist/assets/plugins/codegraph/plugin.json +2 -1
  21. package/dist/assets/plugins/engram/plugin.json +10 -3
  22. package/dist/assets/plugins/gh/plugin.json +2 -1
  23. package/dist/assets/plugins/jscpd/plugin.json +2 -1
  24. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +22 -80
  25. package/dist/assets/plugins/semgrep/plugin.json +4 -2
  26. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +23 -82
  27. package/dist/index.js +381 -313
  28. package/package.json +3 -2
@@ -47,21 +47,31 @@ Open that specific file and confirm its verdict is `APPROVED` and that its scope
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`, one `<blob-sha> <path>` line per reviewed file). 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):
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
53
  # 1) COVERAGE: shipping files the receipt never listed → reviewer never saw them.
54
- # Same diff set the reviewer captured (tracked vs target + untracked), so the
55
- # sets line up 1:1 with no spurious mismatches.
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.
56
59
  comm -23 \
57
- <({ git diff --name-only "origin/{{prTarget}}"; git ls-files --others --exclude-standard; } | sort -u) \
60
+ <({ git diff --name-only "origin/{{prTarget}}"; git ls-files --others --exclude-standard; } \
61
+ | sort -u | grep -vE '^(\.claude/progress/|progress/)') \
58
62
  <(grep -v '^#' .claude/progress/receipt.txt | sed 's/^[^ ]* //' | sort -u)
59
63
 
60
- # 2) DRIFT: a reviewed file whose bytes changed since the review.
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.
61
67
  while IFS= read -r line; do
62
68
  case "$line" in ''|'#'*) continue ;; esac
63
69
  blob=${line%% *}; path=${line#* }
64
- [ "$(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
65
75
  done < .claude/progress/receipt.txt
66
76
  ```
67
77
 
@@ -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
@@ -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).
@@ -47,7 +47,7 @@ shopt -s nullglob
47
47
  for f in "$dir"/impl_*.md; do
48
48
  if is_blank "$f"; then
49
49
  note "$(basename "$f") vacío"
50
- elif ! grep -qi '^status:' "$f" 2>/dev/null; then
50
+ elif ! grep -qiE '^\*{0,2}status:?\*{0,2}' "$f" 2>/dev/null; then
51
51
  note "$(basename "$f") sin línea 'Status:'"
52
52
  fi
53
53
  done
@@ -6,4 +6,4 @@ Before closing the session:
6
6
  2. **History**: add an entry in `progress/history.md` with `## YYYY-MM-DD HH:MM <agent> — <summary>` + changes + gate status.
7
7
  3. **Clear current**: leave `progress/current.md` at `idle` or with the explicit next step.
8
8
  4. **No temporaries**: delete scratch files; don't leave `console.log`, `debugger`, or commented-out code.
9
- 5. **Conventional commit**: `feat|fix|chore|docs(scope): message`, Spanish MX, atomic. Never commit `.claude/` or `CLAUDE.md`.
9
+ 5. **Conventional commit**: `feat|fix|chore|docs(scope): message`, atomic, in the language defined by the config's `commits`. Never commit `.claude/` or `CLAUDE.md`.
@@ -0,0 +1,14 @@
1
+ ## Cross-model review (Codex second opinion)
2
+
3
+ This repo renders the `codex` engine, so a second opinion from a **different provider** is one command away. After your `reviewer` approves a non-trivial diff — or on any change touching a critical area — you MAY have Codex review the SAME diff against this repo's own standards (already rendered in `AGENTS.md` + `.codex/agents/reviewer.toml`):
4
+
5
+ ```bash
6
+ CODEX_HOME=$(pwd)/.codex codex exec --sandbox read-only "revisa el diff origin/{{prTarget}}...HEAD según los estándares del repo"
7
+ ```
8
+
9
+ - **Read-only:** Codex inspects, never edits or commits, and needs no approvals.
10
+ - The verdict lands on **stdout**; progress noise goes to stderr.
11
+ - Auth via `CODEX_API_KEY` or a prior `codex login`. Don't pass `--model` — Codex's default is correct.
12
+ - **Advisory, not a gate:** a second lens on the diff. Weigh its findings against your `reviewer`'s and decide; it doesn't block the PR on its own.
13
+
14
+ Reach for it in `criticalAreas`, on high-blast-radius changes, or when the user asks for a cross-check — not on every trivial diff.
@@ -37,7 +37,7 @@ Fan-out is a lever, not a toll — so when you do delegate, hand the smallest en
37
37
 
38
38
  - **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.
39
39
  - **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.
40
- - **Tier by sub-task, not by round.** Mechanical work low tier; judgment (design, security regex, removal semantics) → high tier; a single fix round can mix both. See `docs/recipes/model-tiering.md`.
40
+ - **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).
41
41
  - **One-pass review on small/medium diffs.** Fix minor review findings yourself instead of spending a re-verification round; reserve a re-review for a risky fix that touched shared machinery.
42
42
 
43
43
  ### Continuous execution (don't pause between tasks)
@@ -46,4 +46,6 @@ Once the plan/scope is approved (R2+), execute ALL sub-tasks without confirming
46
46
 
47
47
  ### Synthesis without broken telephone
48
48
 
49
- Instruct subagents to **write to `.claude/progress/<file>.md`**; you receive only `done -> file`. That folder is ONLY for ephemeral agent handoffs (`audit_*`, `explore_*`, `research_*`, `impl_*`, `review_*`); **session state** (task, plan, blockers) lives in `progress/current.md` (root, git-persisted) and you consolidate it, never the subagents — each `implementer` reports its state (including `blocked`) in its own `impl_<feature>.md`. Verify the diff/evidence yourself; don't trust the report blindly. To close the cycle, invoke `commit-pr-pilot` — when `review_<feature>.md` says `APPROVED` (R2+), or directly for a trivial R1 diff that never went through a `reviewer`. The pilot gates the PR on `{{qualityGate.full}}` (green over the shipping diff — the reviewer's Pass-2 evidence in R2+, or the pilot's own run in R1). Pre-flight: clean working tree, not on `{{branchBase}}`, `gh auth status` ok. If `CHANGES_REQUESTED`, 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.
49
+ Instruct subagents to **write to `.claude/progress/<file>.md`**; you receive only `done -> file`. That folder is ONLY for ephemeral agent handoffs (`audit_*`, `plan_*`, `explore_*`, `research_*`, `impl_*`, `review_*`, `receipt.txt`); **session state** (task, plan, blockers) lives in `progress/current.md` (root, git-persisted) and you consolidate it, never the subagents — each `implementer` reports its state (including `blocked`) in its own `impl_<feature>.md`. Verify the diff/evidence yourself; don't trust the report blindly. To close the cycle, invoke `commit-pr-pilot` — when `review_<feature>.md` says `APPROVED` (R2+), or directly for a trivial R1 diff that never went through a `reviewer`. The pilot gates the PR on `{{qualityGate.full}}` (green over the shipping diff — the reviewer's Pass-2 evidence in R2+, or the pilot's own run in R1). Pre-flight: not on `{{branchBase}}`, `gh auth status` ok (no clean-working-tree check — the pilot's trigger IS the uncommitted diff, and the pilot owns that commit). If `CHANGES_REQUESTED`, 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.
50
+
51
+ **Second opinion (post-`APPROVED`).** On a non-trivial diff — or any change touching a critical area — if this repo also renders the `codex` engine, a review from a **different provider** is one command away: see the **Cross-model review** sub-block in `.claude/agents/leader.md`.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: dominio
3
+ description: Use when you discover — or need — a durable fact that spans multiple repos of a workspace (data model, business rule, migration, cross-service contract, shared gotcha). The Dominio is the workspace's canonical knowledge base; read it before assuming a model, and promote such facts into it instead of only saving to session memory.
4
+ type: reference
5
+ ---
6
+
7
+ # dominio — the workspace knowledge base
8
+
9
+ Canonical, cross-repo facts for a workspace live as markdown under
10
+ `~/.navori/workspaces/<name>/dominio/`: one file per entry (`<id>.md`) plus a
11
+ derived index (`DOMINIO.md`) that is injected at session start. This is where a
12
+ fact like *"`coach`/`coachee` no longer exist — it's `user-profile.kind`"* lives
13
+ once for every repo, so it isn't relearned wrong in each one.
14
+
15
+ ## Read first
16
+
17
+ The Dominio index is injected at the top of each session for repos that belong to
18
+ a workspace. **Before assuming a data model, business rule or cross-service
19
+ contract, check it.** Open the full entry (`navori dominio show <id>`, or read
20
+ `workspace://<name>/dominio/<id>.md`) when you need the detail behind an index
21
+ line.
22
+
23
+ ## When to promote a fact (all THREE must hold)
24
+
25
+ 1. **Durable** — it won't change next sprint; a structural fact, not a transient state.
26
+ 2. **Transversal** — it applies to **≥2 repos** of the workspace. Test: *"would an agent in ANOTHER repo get this wrong without it?"*
27
+ 3. **Canonical** — it's a fact/rule, not a task, a log, or an opinion.
28
+
29
+ If it passes, write it to the Dominio (below). If not, it belongs elsewhere.
30
+
31
+ ### Do NOT put in the Dominio
32
+
33
+ Ticket status / progress / TODOs → `progress/`. Session scratch → engram.
34
+ Single-repo detail → that repo's `CLAUDE.md`. Personal preferences → engram.
35
+ Volatile values (versions, counts). **Secrets — never.**
36
+
37
+ ## How to write an entry
38
+
39
+ Create `~/.navori/workspaces/<name>/dominio/<slug>.md` (`<slug>` is a stable
40
+ kebab-case id = the filename). Keep it to **one fact, short**:
41
+
42
+ ```markdown
43
+ ---
44
+ id: user-profile-model
45
+ title: Modelo user-profile
46
+ type: migration # architecture | business-rule | migration | gotcha | glossary
47
+ applies-to: [nexus, webapp, dashboard, mobile] # repos, or "all"
48
+ status: canonical # canonical | deprecated | superseded
49
+ supersedes: [] # ids this entry replaces
50
+ updated: 2026-07-30
51
+ updated_by: <you>
52
+ ---
53
+
54
+ <the fact>. **Por qué:** <reason>. **Cómo aplica:** <what to do differently>.
55
+ ```
56
+
57
+ Then run `navori dominio reindex` to refresh the index.
58
+
59
+ ## Curate — update, don't pile up
60
+
61
+ - **Update > duplicate.** Search existing entries first (`navori dominio list`);
62
+ edit the matching one and bump `updated`, don't add a second.
63
+ - **Retire, don't delete.** When a fact is replaced, set the old entry
64
+ `status: superseded` and point the new one's `supersedes:` at it — the history
65
+ keeps an agent from rediscovering the old model.
66
+ - **`navori dominio doctor`** validates coherence (all warnings). Reindex after
67
+ any change.