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.
- package/dist/assets/core/core-assets/agents/implementer.md +2 -2
- package/dist/assets/core/core-assets/agents/reviewer.md +2 -2
- package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +6 -1
- package/dist/assets/core/core-assets/hooks/_partials/bash-outcome.sh +94 -7
- package/dist/assets/core/core-assets/hooks/_partials/hook-input.sh +33 -0
- package/dist/assets/core/core-assets/hooks/general-purpose-confirm.sh +15 -1
- package/dist/assets/core/core-assets/hooks/master-accept-confirm.sh +15 -3
- package/dist/assets/core/core-assets/hooks/master-plan-context.sh +7 -1
- package/dist/assets/core/core-assets/hooks/plan-gate.sh +5 -3
- package/dist/assets/core/core-assets/hooks/pr-publisher-confirm.sh +16 -0
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +11 -3
- package/dist/assets/core/core-assets/hooks/role-guard.sh +263 -0
- package/dist/assets/core/core-assets/hooks/routing-watch.sh +17 -0
- package/dist/assets/core/core-assets/managed/orquestacion.md +2 -2
- package/dist/assets/core/core-assets/managed/sdd.md +1 -1
- package/dist/assets/core/core-assets/skills/debug-failure.md +1 -1
- package/dist/assets/core/core-assets/skills/master-plan.md +13 -13
- package/dist/assets/core/core-assets/skills/spec-bootstrap.md +1 -1
- package/dist/assets/core/core-assets/skills/verify-before-done.md +2 -2
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +3 -3
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +2 -2
- package/dist/assets/plugins/tgrep/managed/guard-destructive-search-lane.sh +3 -1
- package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +4 -2
- package/dist/index.js +256 -250
- 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:
|
|
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
|
|
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:
|
|
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
|
|
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)
|
|
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.
|
|
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 (
|
|
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 (
|
|
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}
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
#
|
|
31
|
-
#
|
|
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:-}}" ||
|
|
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
|