navori 0.11.1 → 0.11.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 (25) hide show
  1. package/dist/assets/core/core-assets/agents/implementer.md +2 -2
  2. package/dist/assets/core/core-assets/agents/reviewer.md +2 -2
  3. package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +6 -1
  4. package/dist/assets/core/core-assets/hooks/_partials/bash-outcome.sh +94 -7
  5. package/dist/assets/core/core-assets/hooks/_partials/hook-input.sh +33 -0
  6. package/dist/assets/core/core-assets/hooks/general-purpose-confirm.sh +15 -1
  7. package/dist/assets/core/core-assets/hooks/master-accept-confirm.sh +15 -3
  8. package/dist/assets/core/core-assets/hooks/master-plan-context.sh +7 -1
  9. package/dist/assets/core/core-assets/hooks/plan-gate.sh +5 -3
  10. package/dist/assets/core/core-assets/hooks/pr-publisher-confirm.sh +16 -0
  11. package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +11 -3
  12. package/dist/assets/core/core-assets/hooks/role-guard.sh +263 -0
  13. package/dist/assets/core/core-assets/hooks/routing-watch.sh +17 -0
  14. package/dist/assets/core/core-assets/managed/orquestacion.md +2 -2
  15. package/dist/assets/core/core-assets/managed/sdd.md +1 -1
  16. package/dist/assets/core/core-assets/skills/debug-failure.md +1 -1
  17. package/dist/assets/core/core-assets/skills/master-plan.md +13 -13
  18. package/dist/assets/core/core-assets/skills/spec-bootstrap.md +1 -1
  19. package/dist/assets/core/core-assets/skills/verify-before-done.md +2 -2
  20. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +3 -3
  21. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +2 -2
  22. package/dist/assets/plugins/tgrep/managed/guard-destructive-search-lane.sh +3 -1
  23. package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +4 -2
  24. package/dist/index.js +256 -250
  25. package/package.json +1 -1
@@ -5,7 +5,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash
5
5
  model: {{models.implementer}}
6
6
  effort: {{effort.implementer}}
7
7
  maxTurns: 160
8
- maxWords: 2433
8
+ maxWords: 2448
9
9
  ---
10
10
 
11
11
  # Implementer Agent
@@ -41,7 +41,7 @@ You execute **a single** task from start to verification. You don't orchestrate,
41
41
  {{qualityGate.fast}}
42
42
  ```
43
43
 
44
- If it fails: fix it and re-run. Don't return with red. You are the single owner of this gate run: never share it with another process, never poll `pgrep`/`ps` for it, and a timeout is never a success signal. If the gate can outlive the Bash timeout, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its chained steps one by one in the foreground, never background them (no shell `&`, no `run_in_background`, no `Monitor`) — you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait. When you can't explain WHY it failed, apply `.claude/skills/debug-failure/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, that same skill's hypothesis re-check governs instead of throwing a third patch.
44
+ If it fails: fix it and re-run. Don't return with red. You are the single owner of this gate run: never share it with another process, never poll `pgrep`/`ps` for it, and a timeout is never a success signal. If the gate can outlive the Bash timeout, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its chained steps one by one in the foreground, never background them (<!-- navori:if-not onCodex -->no shell `&`, no `run_in_background`, no `Monitor`<!-- /navori:if-not --><!-- navori:if onCodex -->no shell `&`, no background execution<!-- /navori:if -->) — you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait. When you can't explain WHY it failed, apply `.claude/skills/debug-failure/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, that same skill's hypothesis re-check governs instead of throwing a third patch.
45
45
  5. **UI**: for screen changes, the default evidence is the repo's tests plus a correct diff — **do NOT spin up a browser or dev server automatically**. Visual/browser validation is **optional and strictly on-request**: run it only when the user explicitly asks to check the UI in this prompt, and then drive the repo's browser-automation tool if one is set up (e.g. `playwright-cli`, whose installer ships its own skill).
46
46
  6. **No commits** without the `reviewer`'s approval. When you finish, <!-- navori:if-not scribeOwnsMarkdown -->write the report<!-- /navori:if-not --><!-- navori:if scribeOwnsMarkdown -->write your JSON evidence<!-- /navori:if --> and return the reference.
47
47
  <!-- navori:if planTiers -->
@@ -4,7 +4,7 @@ description: Strict reviewer — approves or rejects a diff against CLAUDE.md an
4
4
  tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.reviewer}}
6
6
  effort: {{effort.reviewer}}
7
- maxWords: 2372
7
+ maxWords: 2387
8
8
  ---
9
9
 
10
10
  # Reviewer Agent
@@ -80,7 +80,7 @@ Apply `.claude/skills/review-diff/SKILL.md` — the full checklist by dimensions
80
80
  {{qualityGate.full}}
81
81
  ```
82
82
 
83
- Read it in full to verify (exit code + failure count), but leave only `exit 0` + the summary line in the report (e.g. `N passed`); when red, only the failing tail. Don't drag the full verbose log turn to turn. This evidence —green gate over the final diff, this cycle— is what the `publisher` reuses so it does **not** re-run the gate, so it must be fresh and over the diff that's going to be committed. You are the single owner of this gate run: the only handle that exists is this Bash call itself, correlated to the diff you're reviewing this turn — never share it with another process, and never poll `pgrep`/`ps` for it (it also matches other sessions' commands and never exits). A timeout is never a success signal. Run it in the foreground with the Bash tool's max `timeout`; if `{{qualityGate.full}}` can exceed it, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its `&&`-chained steps one by one in the foreground, each under the timeout — never background it (no shell `&`, no `run_in_background`, no `Monitor`), you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait.
83
+ Read it in full to verify (exit code + failure count), but leave only `exit 0` + the summary line in the report (e.g. `N passed`); when red, only the failing tail. Don't drag the full verbose log turn to turn. This evidence —green gate over the final diff, this cycle— is what the `publisher` reuses so it does **not** re-run the gate, so it must be fresh and over the diff that's going to be committed. You are the single owner of this gate run: the only handle that exists is this Bash call itself, correlated to the diff you're reviewing this turn — never share it with another process, and never poll `pgrep`/`ps` for it (it also matches other sessions' commands and never exits). A timeout is never a success signal. Run it in the foreground with the Bash tool's max `timeout`; if `{{qualityGate.full}}` can exceed it, follow `.claude/skills/verify-before-done/SKILL.md`'s subagent row: run its `&&`-chained steps one by one in the foreground, each under the timeout — never background it (<!-- navori:if-not onCodex -->no shell `&`, no `run_in_background`, no `Monitor`<!-- /navori:if-not --><!-- navori:if onCodex -->no shell `&`, no background execution<!-- /navori:if -->), you won't be re-woken to read the result. If no chained step fits under any foreground timeout, stop and report `BLOCKED` instead of improvising a background wait.
84
84
 
85
85
  Don't gate a screen change on browser validation by default. Only if the user explicitly requested a visual/browser check and it wasn't done do you mark it incomplete — otherwise the diff + the repo's tests are the gate.
86
86
 
@@ -249,6 +249,9 @@ navori_audit_log() {
249
249
  # must treat "names nobody" as invalid data rather than as a different agent
250
250
  # (`ownerOf` in `lib/audit/parse.ts` is where that rule lives).
251
251
 
252
+ # Optional third argument `kind` (hard | ask | advisory): how binding the
253
+ # outcome was. Additive: callers that pass nothing keep today's records, and
254
+ # readers treat a missing `kind` as unclassified.
252
255
  printf '%s\n' "$(jq -cn \
253
256
  --arg name "${navori_audit_name:-unknown}" \
254
257
  --arg phase "${navori_audit_phase:-unknown}" \
@@ -258,13 +261,15 @@ navori_audit_log() {
258
261
  --arg src "${navori_audit_source:-core}" \
259
262
  --arg agent "${navori_audit_agent:-}" \
260
263
  --arg toolUseId "${navori_audit_tool_use_id:-}" \
264
+ --arg kind "${3:-}" \
261
265
  --argjson ms "$navori_audit_ms" \
262
266
  --argjson tsMs "$navori_audit_end" \
263
267
  '{tsMs:$tsMs,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
264
268
  + (if $tool == "" then {} else {tool:$tool} end)
265
269
  + (if $reason == "" then {} else {reason:$reason} end)
266
270
  + (if $agent == "" then {} else {agentId:$agent} end)
267
- + (if $toolUseId == "" then {} else {toolUseId:$toolUseId} end)' 2>/dev/null)" \
271
+ + (if $toolUseId == "" then {} else {toolUseId:$toolUseId} end)
272
+ + (if $kind == "" then {} else {kind:$kind} end)' 2>/dev/null)" \
268
273
  >> "$navori_audit_file" 2>/dev/null
269
274
 
270
275
  return 0
@@ -88,8 +88,16 @@ navori_json_str() {
88
88
  # Repeat-failure state is shared by the PostToolUseFailure watcher and the
89
89
  # existing success lane. A successful Bash call only pays for Node when its
90
90
  # session already has a failure-state file; ordinary calls use builtins only.
91
+ #
92
+ # `action` is `reset`/`failure` (Claude: the event says which) or `codex`
93
+ # (spec 0041 T12: Codex fires PostToolUse on failure too, so the OUTCOME is read
94
+ # from the rollout, `$2` = its path, `$3` = the call's `tool_use_id`).
91
95
  navori_bash_failure_state() {
92
96
  local action=$1 sid=${CLAUDE_CODE_SESSION_ID:-} dir file
97
+ if [ "$action" = codex ]; then
98
+ navori_plain_field session_id
99
+ sid=$navori_field
100
+ fi
93
101
  case "$sid" in "" | *[!A-Za-z0-9._-]*) return 0 ;; esac
94
102
  [ -n "${nv_project_dir:-}" ] || return 0
95
103
  dir=$nv_project_dir/.navori/state/hooks/bash-outcome-watch
@@ -99,24 +107,58 @@ navori_bash_failure_state() {
99
107
  fi
100
108
  command -v node >/dev/null 2>&1 || return 0
101
109
  NV_BASH_OUTCOME_ACTION=$action NV_BASH_OUTCOME_DIR=$dir NV_BASH_OUTCOME_FILE=$file \
110
+ NV_BASH_OUTCOME_SID=$sid NV_BASH_OUTCOME_TRANSCRIPT=${2:-} NV_BASH_OUTCOME_TOOL_USE=${3:-} \
102
111
  NV_BASH_OUTCOME_ROOT=$nv_project_dir node -e '
103
112
  const fs = require("node:fs");
104
113
  const path = require("node:path");
105
114
  const crypto = require("node:crypto");
106
115
  const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
107
116
  const env = process.env;
117
+ // Codex outcome (spec 0041 T12): the exit code of THIS call lives in the rollout
118
+ // record `event_msg`/`item_completed` whose `item.id` is the `tool_use_id` of the call
119
+ // (probe V4). Bounded to the tail of the file; only `exit_code` is read, never
120
+ // the output fields. Anything missing or unreadable -> null -> silence.
121
+ const codexExit = () => {
122
+ const max = 1048576;
123
+ const fd = fs.openSync(env.NV_BASH_OUTCOME_TRANSCRIPT, "r");
124
+ try {
125
+ const size = fs.fstatSync(fd).size;
126
+ const len = Math.min(size, max);
127
+ const buf = Buffer.alloc(len);
128
+ fs.readSync(fd, buf, 0, len, size - len);
129
+ const lines = buf.toString("utf8").split("\n");
130
+ for (let i = lines.length - 1; i >= 0; i--) {
131
+ const line = lines[i];
132
+ if (!line.includes(env.NV_BASH_OUTCOME_TOOL_USE) || !line.includes("item_completed")) continue;
133
+ let record;
134
+ try { record = JSON.parse(line); } catch { continue; }
135
+ const item = record?.payload?.item;
136
+ if (record?.type !== "event_msg" || record?.payload?.type !== "item_completed") continue;
137
+ if (item?.type !== "CommandExecution" || item?.id !== env.NV_BASH_OUTCOME_TOOL_USE) continue;
138
+ return Number.isInteger(item.exit_code) ? item.exit_code : null;
139
+ }
140
+ return null;
141
+ } finally { fs.closeSync(fd); }
142
+ };
108
143
  let input = "";
109
144
  process.stdin.setEncoding("utf8");
110
145
  process.stdin.on("data", (chunk) => { input += chunk; });
111
146
  process.stdin.on("end", () => {
112
147
  try {
113
148
  const payload = JSON.parse(input);
114
- if (payload.tool_name !== "Bash" || payload.session_id !== env.CLAUDE_CODE_SESSION_ID) return;
149
+ if (payload.tool_name !== "Bash" || payload.session_id !== env.NV_BASH_OUTCOME_SID) return;
115
150
  if (payload.is_interrupt === true || payload.tool_response?.is_interrupt === true) return;
116
151
  const command = payload.tool_input?.command;
117
152
  const cwd = payload.cwd;
118
153
  if (typeof command !== "string" || !command.trim() || typeof cwd !== "string" || !cwd) return;
119
154
  const agent = typeof payload.agent_id === "string" ? payload.agent_id : "";
155
+ let action = env.NV_BASH_OUTCOME_ACTION;
156
+ let exitCode = null;
157
+ if (action === "codex") {
158
+ exitCode = codexExit();
159
+ if (exitCode === null) return;
160
+ action = exitCode === 0 ? "reset" : "failure";
161
+ }
120
162
  const key = digest(JSON.stringify([command.trim().replace(/\s+/g, " "), cwd, agent]));
121
163
  const root = path.resolve(env.NV_BASH_OUTCOME_ROOT);
122
164
  const dir = env.NV_BASH_OUTCOME_DIR;
@@ -125,21 +167,21 @@ process.stdin.on("end", () => {
125
167
  for (const component of [root, path.join(root, ".navori"), path.join(root, ".navori/state"), path.join(root, ".navori/state/hooks"), dir]) {
126
168
  if (fs.existsSync(component)) {
127
169
  if (!fs.lstatSync(component).isDirectory() || fs.lstatSync(component).isSymbolicLink()) return;
128
- } else if (env.NV_BASH_OUTCOME_ACTION === "failure") {
170
+ } else if (action === "failure") {
129
171
  fs.mkdirSync(component);
130
172
  } else return;
131
173
  }
132
174
  if (fs.existsSync(file) && (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())) return;
133
175
  let rows = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)) : [];
134
176
  if (!Array.isArray(rows) || rows.some((row) => typeof row.key !== "string" || typeof row.sig !== "string" || typeof row.count !== "number" || typeof row.epoch !== "number")) return;
135
- if (env.NV_BASH_OUTCOME_ACTION === "reset") {
177
+ if (action === "reset") {
136
178
  const next = rows.filter((row) => row.key !== key);
137
179
  if (next.length === rows.length) return;
138
180
  rows = next;
139
181
  } else {
140
- const error = payload.error;
182
+ const error = exitCode === null ? payload.error : "";
141
183
  if (typeof error !== "string") return;
142
- const match = /exit code\s+(\d+)/i.exec(error);
184
+ const match = exitCode === null ? /exit code\s+(\d+)/i.exec(error) : [null, String(exitCode)];
143
185
  if (!match) return;
144
186
  const code = match[1];
145
187
  const escapedRoot = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -154,7 +196,7 @@ process.stdin.on("end", () => {
154
196
  .replace(/\b\d+(?:\.\d+)?\s?(?:ms|seconds?|minutes?)\b/gi, "<d>")
155
197
  .replace(/\b[0-9a-f]{8,}\b/gi, "<h>")
156
198
  .split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 20).join("\n");
157
- if (!body) return;
199
+ if (!body && exitCode === null) return;
158
200
  const sig = digest(code + "\n" + body);
159
201
  const previous = rows.find((row) => row.key === key);
160
202
  const count = previous?.sig === sig ? Math.min(previous.count + 1, 3) : 1;
@@ -163,7 +205,7 @@ process.stdin.on("end", () => {
163
205
  rows.push({ key, sig, count, notified: notified || count === 3, epoch: Date.now() });
164
206
  if (count === 3 && !notified) {
165
207
  const first = body.split("\n")[0].slice(0, 100);
166
- const note = `navori: el comando ${command.trim().slice(0, 100)} falló 3 veces con la misma firma (exit ${code}: ${first}). Cambia de enfoque con debug-failure o escala al usuario. Si el rojo es intencional, ignora esta nota.`;
208
+ const note = `navori: el comando ${command.trim().slice(0, 100)} falló 3 veces con la misma firma (exit ${code}${first ? ": " + first : ""}). Cambia de enfoque con debug-failure o escala al usuario. Si el rojo es intencional, ignora esta nota.`;
167
209
  var output = note.slice(0, 400);
168
210
  }
169
211
  }
@@ -179,6 +221,51 @@ process.stdin.on("end", () => {
179
221
  return 0
180
222
  }
181
223
 
224
+ # A plain top-level string field of the raw payload, with builtins only (no
225
+ # fork), into `navori_field`. Matches `"key":"value"` with an unescaped quote
226
+ # before the colon, which a `tool_response` cannot forge (inside it every quote is
227
+ # `\"`). Empty when absent, spaced or escaped: the caller treats that as silence.
228
+ navori_plain_field() {
229
+ local needle="\"$1\":\"" rest
230
+ navori_field=
231
+ case "$payload" in *"$needle"*) ;; *) return 0 ;; esac
232
+ rest=${payload#*"$needle"}
233
+ navori_field=${rest%%\"*}
234
+ case "$navori_field" in *\\*) navori_field= ;; esac
235
+ return 0
236
+ }
237
+
238
+ # Spec 0041 T12 (R11): the Codex Bash outcome lane, run by `routing-watch.sh`
239
+ # on `PostToolUse(Bash)` under Codex. Codex fires PostToolUse on failure too, so
240
+ # the exit code is read from the rollout (`transcript_path`): a fixed-string grep
241
+ # over the BOUNDED tail finds the `item_completed` record of this `tool_use_id`.
242
+ # A success with no failure state costs no Node; everything else (a failure, or a
243
+ # success that must reset a counter) goes through `navori_bash_failure_state`,
244
+ # which parses only `exit_code`. FAIL-OPEN and silent: no rollout, no matching
245
+ # record or an unreadable one means no advice. Prints the advice, if any.
246
+ navori_bash_codex_outcome() {
247
+ local tid tp sid line
248
+ navori_plain_field tool_use_id
249
+ tid=$navori_field
250
+ navori_plain_field transcript_path
251
+ tp=$navori_field
252
+ navori_plain_field session_id
253
+ sid=$navori_field
254
+ case "$tid" in "" | *[!A-Za-z0-9._:-]*) return 0 ;; esac
255
+ case "$sid" in "" | *[!A-Za-z0-9._-]*) return 0 ;; esac
256
+ case "$tp" in *.jsonl) ;; *) return 0 ;; esac
257
+ [ -f "$tp" ] && [ -r "$tp" ] || return 0
258
+ line=$(tail -c 1048576 "$tp" 2>/dev/null | grep -F -- "$tid" 2>/dev/null | grep -F '"item_completed"' 2>/dev/null | tail -n 1)
259
+ [ -n "$line" ] || return 0
260
+ case "$line" in
261
+ *'"exit_code":0,'* | *'"exit_code":0}'*)
262
+ [ -f "${nv_project_dir:-}/.navori/state/hooks/bash-outcome-watch/$sid" ] || return 0
263
+ ;;
264
+ esac
265
+ navori_bash_failure_state codex "$tp" "$tid"
266
+ return 0
267
+ }
268
+
182
269
  # Called by `routing-watch.sh` once it knows the tool is `Bash`, and only under
183
270
  # `claude-post-tool-use` (Codex fires PostToolUse on failure too, so no signal).
184
271
  navori_bash_success_lane() {
@@ -90,6 +90,39 @@ nv_subagent_type() {
90
90
  fi
91
91
  }
92
92
 
93
+ # The agent a spawn call TARGETS, never the one that makes the call (spec 0041
94
+ # H16). Claude: `tool_input.subagent_type`. Codex PreToolUse/PostToolUse of a
95
+ # spawn: `tool_input.agent_type` only — the top-level `agent_type` there is the
96
+ # CALLER's. On `SubagentStop` the top-level `agent_type` IS the finishing
97
+ # subagent, so that event reads it instead.
98
+ nv_spawn_target_type() {
99
+ if [ "$nv_engine" = codex ]; then
100
+ case "$(payload_field hook_event_name)" in
101
+ SubagentStop) payload_field agent_type ;;
102
+ *) payload_field tool_input.agent_type ;;
103
+ esac
104
+ else
105
+ payload_field tool_input.subagent_type
106
+ fi
107
+ }
108
+
109
+ # The agent RUNNING the current event: the top-level `agent_type`, which the
110
+ # host adds only inside a subagent (empty on the main thread). Never confuse it
111
+ # with `nv_spawn_target_type` (H16). `nv_subagent_type` above keeps its legacy
112
+ # mixed behaviour for existing hooks; new code uses these two helpers.
113
+ nv_event_agent_type() {
114
+ payload_field agent_type
115
+ }
116
+
117
+ # True for the spawn tool: Codex V1 `spawn_agent`, or any V2 name that ends in
118
+ # `spawn_agent` (V2 flattens the namespace into the tool name).
119
+ nv_is_spawn_tool() {
120
+ case "$(payload_field tool_name)" in
121
+ *spawn_agent) return 0 ;;
122
+ *) return 1 ;;
123
+ esac
124
+ }
125
+
93
126
  # Deliberately NO `nv_emit_context` helper here. `hook-output-contract.test.ts`
94
127
  # ("los partials no hablan con el host, solo escriben al log") holds every
95
128
  # `_partials/*.sh` file to zero host-output vocabulary — a partial is inlined
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # PreToolUse(Agent): dispatching `general-purpose` is raised to a user
3
+ # PreToolUse(Agent|spawn_agent): dispatching `general-purpose` is raised to a user
4
4
  # confirmation that names the `scout` (spec 0039 R40).
5
5
  #
6
6
  # WHY: `general-purpose` is the host's catch-all subagent. For read-only
@@ -44,9 +44,23 @@ navori_audit_on_exit() {
44
44
  }
45
45
  trap navori_audit_on_exit EXIT
46
46
 
47
+ # Claude names the child `subagent_type`; Codex's `spawn_agent` names it `agent_type`.
47
48
  navori_gp_type=$(payload_field tool_input.subagent_type)
49
+ [ -n "$navori_gp_type" ] || navori_gp_type=$(payload_field tool_input.agent_type)
48
50
  [ "$navori_gp_type" = "general-purpose" ] || exit 0
49
51
 
52
+ # Codex hooks cannot emit `ask`, and a `prompt` rule does not confirm inside
53
+ # subagents (live probe V1, spec 0041 R10): the Codex copy denies instead,
54
+ # decided by `$0` like `comment-draft-confirm.sh`. Never an allow.
55
+ case "$0" in
56
+ *".codex/hooks/"*)
57
+ navori_audit_verdict="deny"
58
+ navori_audit_reason="general-purpose despachado; scout cubre lectura e investigación"
59
+ printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[navori] general-purpose is dispatched here and Codex hooks cannot prompt. For read-only exploration or web research dispatch the scout instead; if this task really needs general-purpose, ask the user to confirm and re-dispatch."}}'
60
+ exit 0
61
+ ;;
62
+ esac
63
+
50
64
  navori_audit_verdict="ask"
51
65
  navori_audit_reason="general-purpose despachado; scout cubre lectura e investigación"
52
66
 
@@ -61,6 +61,18 @@ is_scan_trigger "$cmd" || exit 0
61
61
 
62
62
  # Fixed by construction: no repository content reaches JSON, so this still
63
63
  # works when jq and node are unavailable.
64
- navori_audit_verdict="ask"
65
- navori_audit_reason="manual approval or stage close"
66
- printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"[navori] this records that you approved a manual criterion. Confirm only if you reviewed it."}}'
64
+ # Codex parses `permissionDecision` but drops `"ask"` (the call proceeds), so
65
+ # the Codex copy denies instead: deny-as-confirmation, decided by `$0` like
66
+ # `comment-draft-confirm.sh` (spec 0041 D11). Never an allow.
67
+ case "$0" in
68
+ *".codex/hooks/"*)
69
+ navori_audit_verdict="deny"
70
+ navori_audit_reason="manual approval or stage close"
71
+ printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[navori] this records that the user approved a manual criterion, and Codex hooks cannot prompt. Show the user what it approves and let them confirm and run the command themselves."}}'
72
+ ;;
73
+ *)
74
+ navori_audit_verdict="ask"
75
+ navori_audit_reason="manual approval or stage close"
76
+ printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"[navori] this records that you approved a manual criterion. Confirm only if you reviewed it."}}'
77
+ ;;
78
+ esac
@@ -21,7 +21,13 @@ trap navori_audit_on_exit EXIT
21
21
 
22
22
  specs_dir={{shq:sdd.specsDir}}
23
23
  NAVORI_MASTER_LINE_BUDGET=600
24
- project_dir=${CLAUDE_PROJECT_DIR:-$PWD}
24
+ # Codex sets no CLAUDE_PROJECT_DIR: the root is the git toplevel of the hook's
25
+ # directory; `$0` tells the copies apart (spec 0041 R21, same pattern as
26
+ # `comment-draft-confirm.sh`).
27
+ case "$0" in
28
+ *".codex/hooks/"*) project_dir=$(git rev-parse --show-toplevel 2>/dev/null) || project_dir=$PWD ;;
29
+ *) project_dir=${CLAUDE_PROJECT_DIR:-$PWD} ;;
30
+ esac
25
31
  index_path=$project_dir/$specs_dir/_master/INDEX.md
26
32
  offer='En tu primera respuesta de la sesión, ofrece continuar con el plan maestro en una sola línea, sin interrumpir lo que el usuario pidió.'
27
33
 
@@ -26,9 +26,11 @@
26
26
  # below, in ONE invocation (a second `--help` probe would read the hook's
27
27
  # stdin payload twice, which a pipe cannot replay).
28
28
  #
29
- # CODEX. Spec 0035 defers registration of this hook: a live Codex smoke
30
- # created an implementer without a workplan despite PreToolUse registration.
31
- # The workplan procedure remains advisory until the host reliably blocks it.
29
+ # CODEX (spec 0041 R9). Registered on PreToolUse(spawn_agent$) as the last late
30
+ # row. Only `implementer` is gated, with the role from `tool_input.agent_type`.
31
+ # Codex V2 encrypts the spawn `message`, so the opening line then comes from the
32
+ # orchestrator's `.navori/state/handoffs/dispatch_<feature>.json` (TTL 10 min,
33
+ # exactly one fresh file); the decision logic stays in `navori plan gate`.
32
34
  #
33
35
  # The managed block below is regenerated by `navori render` and must NOT be
34
36
  # edited by hand.
@@ -95,6 +95,22 @@ is_scan_trigger "$cmd" || exit 0
95
95
  # It names SOME subagent, not specifically the publisher — a `scout` opening a
96
96
  # PR would pass. That is deliberate: this is a routing nudge, not a security
97
97
  # boundary, and the detector cannot see through `sh -c` either.
98
+ #
99
+ # CODEX (spec 0041 R10): Codex hooks cannot emit `ask` and a `.codex/rules`
100
+ # `prompt` rule only confirms on the main thread — it is silent inside the
101
+ # publisher subagent (live probe V1) — so the Codex copy denies instead:
102
+ # deny-as-confirmation, decided by `$0` like `comment-draft-confirm.sh`. It runs
103
+ # BEFORE the subagent allow, because the publisher is exactly where the
104
+ # unconfirmed `gh pr create` happens. Fixed by construction: no jq needed.
105
+ case "$0" in
106
+ *".codex/hooks/"*)
107
+ navori_audit_verdict="deny"
108
+ navori_audit_reason="gh pr create requires user confirmation (Codex)"
109
+ printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[navori] this `gh pr create` opens a PR and Codex hooks cannot prompt. Show the user the title and body, and let them confirm and run the command themselves."}}'
110
+ exit 0
111
+ ;;
112
+ esac
113
+
98
114
  navori_pr_agent=$(payload_field agent_id)
99
115
  [ -n "$navori_pr_agent" ] || navori_pr_agent=$(payload_field subagent_id)
100
116
  if [ -n "$navori_pr_agent" ]; then
@@ -50,6 +50,9 @@ navori_audit_ran_gate=0
50
50
  navori_audit_skip_reason="el comando no es un commit"
51
51
  # Set when the gate had NO verdict (runner missing) and the user was asked (#1117).
52
52
  navori_audit_ask_reason=""
53
+ # Set right before each deliberate `exit 2`, so every block path records its own
54
+ # cause (#1117). Empty means the exit came from somewhere unforeseen.
55
+ navori_audit_block_reason=""
53
56
  navori_audit_on_exit() {
54
57
  navori_audit_code=$?
55
58
  # A cancelled hook reaches this trap with `$?` == 0 (#797), so the exit code
@@ -60,9 +63,9 @@ navori_audit_on_exit() {
60
63
  return 0
61
64
  fi
62
65
  if [ "$navori_audit_code" -ne 0 ]; then
63
- navori_audit_log "block" "el quality gate no paso o no pudo correr" || true
66
+ navori_audit_log "block" "${navori_audit_block_reason:-el quality gate no paso o no pudo correr}" hard || true
64
67
  elif [ -n "$navori_audit_ask_reason" ]; then
65
- navori_audit_log "ask" "$navori_audit_ask_reason" || true
68
+ navori_audit_log "ask" "$navori_audit_ask_reason" ask || true
66
69
  elif [ "$navori_audit_ran_gate" -eq 1 ]; then
67
70
  navori_audit_log "allow" "gate ejecutado y verde" || true
68
71
  else
@@ -108,6 +111,7 @@ run_gate() {
108
111
  navori_audit_log "gate-started" "inicio del quality gate" || true
109
112
  echo "[navori] running quality-gate fast: $1" >&2
110
113
  eval "$1" || {
114
+ navori_audit_block_reason="quality gate en rojo: '$1' fallo, commit abortado"
111
115
  echo "[navori] quality-gate fast failed. Commit aborted." >&2
112
116
  exit 2
113
117
  }
@@ -178,7 +182,10 @@ if [ "$run_needed" = 1 ]; then
178
182
  # Codex); when nothing resolves the substitution is empty and `cd ""` is a
179
183
  # no-op, so behavior outside a repo is unchanged.
180
184
  gate_root=$(navori_worktree)
181
- cd "${gate_root:-${nv_project_dir:-}}" || exit 2
185
+ cd "${gate_root:-${nv_project_dir:-}}" || {
186
+ navori_audit_block_reason="no se pudo entrar al arbol del commit '${gate_root:-${nv_project_dir:-}}'; el gate no corrio"
187
+ exit 2
188
+ }
182
189
  # qualityGate.fast is shell-quoted at render time via the shq: marker (#197).
183
190
  # The gate string is still `eval`'d by run_gate below (running the gate is the
184
191
  # feature), but quoting it here means a hostile qualityGate.fast survives as one
@@ -205,6 +212,7 @@ if [ "$run_needed" = 1 ]; then
205
212
  # The explicit engine test is the static proof (hook-claims-vs-scripts) that
206
213
  # the Codex path differs; `navori_can_ask` re-checks it with the payload.
207
214
  if [ "$nv_engine" = codex ] || ! navori_can_ask; then
215
+ navori_audit_block_reason="quality gate sin veredicto y sin forma de preguntar: '$gate_bin' no esta en PATH, commit bloqueado"
208
216
  echo "[navori] Commit BLOCKED to avoid skipping the gate silently. Install '$gate_bin', or if you really want to skip it run the commit yourself outside the agent." >&2
209
217
  exit 2
210
218
  fi