agent-composer 0.3.1 → 0.4.0
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/README.md +495 -180
- package/composer.config.schema.json +206 -2
- package/dist/cli/cleanup.d.ts +24 -0
- package/dist/cli/cleanup.js +151 -0
- package/dist/cli/cleanup.js.map +1 -0
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +244 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/goal.d.ts +28 -0
- package/dist/cli/goal.js +251 -0
- package/dist/cli/goal.js.map +1 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.js +31 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +116 -21
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/initArgs.d.ts +16 -0
- package/dist/cli/initArgs.js +19 -0
- package/dist/cli/initArgs.js.map +1 -0
- package/dist/cli/installGitHook.d.ts +7 -0
- package/dist/cli/installGitHook.js +61 -0
- package/dist/cli/installGitHook.js.map +1 -0
- package/dist/cli/mode.d.ts +6 -0
- package/dist/cli/mode.js +25 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/cli/status.d.ts +105 -0
- package/dist/cli/status.js +400 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/modes.d.ts +10 -0
- package/dist/config/modes.js +26 -0
- package/dist/config/modes.js.map +1 -0
- package/dist/config/oracleRole.d.ts +10 -0
- package/dist/config/oracleRole.js +11 -0
- package/dist/config/oracleRole.js.map +1 -0
- package/dist/config/schema.d.ts +246 -0
- package/dist/config/schema.js +127 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/evolve/reflection.d.ts +9 -0
- package/dist/evolve/reflection.js +14 -0
- package/dist/evolve/reflection.js.map +1 -1
- package/dist/evolve/runner.d.ts +2 -1
- package/dist/evolve/runner.js +2 -1
- package/dist/evolve/runner.js.map +1 -1
- package/dist/index.js +115 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicCompatibleProvider.d.ts +13 -1
- package/dist/providers/AnthropicCompatibleProvider.js +115 -9
- package/dist/providers/AnthropicCompatibleProvider.js.map +1 -1
- package/dist/providers/CLIProvider.d.ts +18 -0
- package/dist/providers/CLIProvider.js +265 -62
- package/dist/providers/CLIProvider.js.map +1 -1
- package/dist/providers/IProvider.d.ts +12 -0
- package/dist/providers/SpendGuardProvider.d.ts +32 -0
- package/dist/providers/SpendGuardProvider.js +98 -0
- package/dist/providers/SpendGuardProvider.js.map +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +17 -2
- package/dist/registry.js.map +1 -1
- package/dist/server/activeRuns.d.ts +17 -0
- package/dist/server/activeRuns.js +114 -0
- package/dist/server/activeRuns.js.map +1 -0
- package/dist/server/codexLifecycleRunner.d.ts +29 -0
- package/dist/server/codexLifecycleRunner.js +188 -0
- package/dist/server/codexLifecycleRunner.js.map +1 -0
- package/dist/server/configMutation.d.ts +22 -0
- package/dist/server/configMutation.js +121 -0
- package/dist/server/configMutation.js.map +1 -0
- package/dist/server/handoffContext.d.ts +1 -0
- package/dist/server/handoffContext.js +12 -0
- package/dist/server/handoffContext.js.map +1 -0
- package/dist/server/progress.d.ts +24 -0
- package/dist/server/progress.js +109 -0
- package/dist/server/progress.js.map +1 -0
- package/dist/server/toolDescriptions.d.ts +60 -0
- package/dist/server/toolDescriptions.js +134 -0
- package/dist/server/toolDescriptions.js.map +1 -0
- package/dist/server.d.ts +19 -25
- package/dist/server.js +87 -377
- package/dist/server.js.map +1 -1
- package/dist/tools/audit.d.ts +2 -0
- package/dist/tools/audit.js +66 -0
- package/dist/tools/audit.js.map +1 -0
- package/dist/tools/code.d.ts +2 -0
- package/dist/tools/code.js +160 -0
- package/dist/tools/code.js.map +1 -0
- package/dist/tools/codexLifecycle.d.ts +2 -0
- package/dist/tools/codexLifecycle.js +206 -0
- package/dist/tools/codexLifecycle.js.map +1 -0
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.js +183 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/context.d.ts +31 -0
- package/dist/tools/context.js +2 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/goal.d.ts +2 -0
- package/dist/tools/goal.js +159 -0
- package/dist/tools/goal.js.map +1 -0
- package/dist/tools/handoff.d.ts +2 -0
- package/dist/tools/handoff.js +57 -0
- package/dist/tools/handoff.js.map +1 -0
- package/dist/tools/oracle.d.ts +2 -0
- package/dist/tools/oracle.js +248 -0
- package/dist/tools/oracle.js.map +1 -0
- package/dist/tools/research.d.ts +2 -0
- package/dist/tools/research.js +51 -0
- package/dist/tools/research.js.map +1 -0
- package/dist/tools/review.d.ts +2 -0
- package/dist/tools/review.js +233 -0
- package/dist/tools/review.js.map +1 -0
- package/dist/tools/route.d.ts +2 -0
- package/dist/tools/route.js +69 -0
- package/dist/tools/route.js.map +1 -0
- package/dist/tools/session.d.ts +2 -0
- package/dist/tools/session.js +37 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/status.d.ts +2 -0
- package/dist/tools/status.js +34 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/workflow.d.ts +2 -0
- package/dist/tools/workflow.js +27 -0
- package/dist/tools/workflow.js.map +1 -0
- package/dist/util/applyFileBlocks.d.ts +18 -0
- package/dist/util/applyFileBlocks.js +163 -0
- package/dist/util/applyFileBlocks.js.map +1 -0
- package/dist/util/asyncControl.d.ts +14 -0
- package/dist/util/asyncControl.js +106 -0
- package/dist/util/asyncControl.js.map +1 -0
- package/dist/util/auditLog.d.ts +56 -0
- package/dist/util/auditLog.js +232 -0
- package/dist/util/auditLog.js.map +1 -0
- package/dist/util/codexLifecycle.d.ts +55 -0
- package/dist/util/codexLifecycle.js +102 -0
- package/dist/util/codexLifecycle.js.map +1 -0
- package/dist/util/codexLifecycleJob.d.ts +209 -0
- package/dist/util/codexLifecycleJob.js +360 -0
- package/dist/util/codexLifecycleJob.js.map +1 -0
- package/dist/util/composerDisabled.d.ts +6 -0
- package/dist/util/composerDisabled.js +27 -0
- package/dist/util/composerDisabled.js.map +1 -0
- package/dist/util/dispatchHint.d.ts +5 -3
- package/dist/util/dispatchHint.js +62 -2
- package/dist/util/dispatchHint.js.map +1 -1
- package/dist/util/goal.d.ts +132 -0
- package/dist/util/goal.js +616 -0
- package/dist/util/goal.js.map +1 -0
- package/dist/util/goalReport.d.ts +51 -0
- package/dist/util/goalReport.js +164 -0
- package/dist/util/goalReport.js.map +1 -0
- package/dist/util/jobPolling.d.ts +9 -0
- package/dist/util/jobPolling.js +17 -0
- package/dist/util/jobPolling.js.map +1 -0
- package/dist/util/oracleJob.d.ts +66 -0
- package/dist/util/oracleJob.js +295 -0
- package/dist/util/oracleJob.js.map +1 -0
- package/dist/util/oracleLock.d.ts +38 -0
- package/dist/util/oracleLock.js +182 -0
- package/dist/util/oracleLock.js.map +1 -0
- package/dist/util/reviewDiff.d.ts +8 -0
- package/dist/util/reviewDiff.js +29 -0
- package/dist/util/reviewDiff.js.map +1 -0
- package/dist/util/reviewJob.d.ts +57 -0
- package/dist/util/reviewJob.js +207 -0
- package/dist/util/reviewJob.js.map +1 -0
- package/dist/util/workflowPlan.d.ts +24 -0
- package/dist/util/workflowPlan.js +49 -0
- package/dist/util/workflowPlan.js.map +1 -0
- package/package.json +8 -1
- package/plugin/composer-mastermind/commands/evolve.md +4 -0
- package/plugin/composer-mastermind/hooks/boundary_guard.sh +43 -2
- package/plugin/composer-mastermind/hooks/codex_warm_review.sh +161 -9
- package/plugin/composer-mastermind/hooks/learn.sh +172 -32
- package/plugin/composer-mastermind/hooks/precommit_codex_review.sh +430 -62
- package/plugin/composer-mastermind/skills/composer-mastermind/SKILL.md +184 -4
- package/scripts/composer-oracle-router-safe.sh +47 -0
- package/scripts/composer-statusline-segment.mjs +40 -0
- package/scripts/oracle-codex-handoff-safe.sh +49 -0
- package/scripts/oracle-plan-mcp.sh +66 -0
- package/scripts/oracle-pro-safe.sh +471 -0
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
# allow the commit unless codexReview.preCommitHook.failClosed is true.
|
|
9
9
|
# Config keys:
|
|
10
10
|
# codexReview.preCommitCommand, scope, base, model
|
|
11
|
-
# codexReview.preCommitHook.enabled, blockOnSeverity, timeoutMs, failClosed
|
|
11
|
+
# codexReview.preCommitHook.enabled, blockOnSeverity, timeoutMs, failClosed, maxConsecutiveBlocks
|
|
12
12
|
# codexReview.warmCache.enabled, maxAgeMinutes
|
|
13
13
|
# codexReview.notify.desktop
|
|
14
14
|
|
|
15
15
|
set -u
|
|
16
16
|
|
|
17
|
-
RUN_LOG="
|
|
17
|
+
RUN_LOG="${RUN_LOG:-/tmp/composer-codex-review-log.jsonl}"
|
|
18
|
+
PRECOMMIT_DEFAULT_TIMEOUT_MS=120000
|
|
19
|
+
PRECOMMIT_MIN_HARD_CAP_MS=120000
|
|
20
|
+
PRECOMMIT_MAX_HARD_CAP_MS=180000
|
|
18
21
|
|
|
19
22
|
composer_disabled() {
|
|
20
23
|
case "${COMPOSER_ENABLED:-}" in
|
|
@@ -45,6 +48,10 @@ json_escape_fallback() {
|
|
|
45
48
|
|
|
46
49
|
emit_json() {
|
|
47
50
|
local message="$1"
|
|
51
|
+
if [[ "${GITHOOK:-0}" == "1" ]]; then
|
|
52
|
+
printf '%s\n' "$message" >&2
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
48
55
|
jq -nc --arg systemMessage "$message" \
|
|
49
56
|
'{systemMessage:$systemMessage,suppressOutput:true}' 2>/dev/null \
|
|
50
57
|
|| printf '{"systemMessage":"%s","suppressOutput":true}\n' "$(json_escape_fallback "$message")"
|
|
@@ -53,6 +60,11 @@ emit_json() {
|
|
|
53
60
|
emit_deny() {
|
|
54
61
|
local reason="$1"
|
|
55
62
|
local message="${2:-$reason}"
|
|
63
|
+
if [[ "${GITHOOK:-0}" == "1" ]]; then
|
|
64
|
+
printf '%s\n' "$message" >&2
|
|
65
|
+
printf 'commit blocked by Codex pre-commit review: %s\n' "$reason" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
56
68
|
jq -nc --arg r "$reason" --arg systemMessage "$message" \
|
|
57
69
|
'{systemMessage:$systemMessage,suppressOutput:true,hookSpecificOutput:{hookEventName:"PreToolUse", permissionDecision:"deny", permissionDecisionReason:$r}}' 2>/dev/null \
|
|
58
70
|
|| printf '{"systemMessage":"%s","suppressOutput":true,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$(json_escape_fallback "$message")" "$(json_escape_fallback "$reason")"
|
|
@@ -67,6 +79,8 @@ append_run_log() {
|
|
|
67
79
|
local findings="$5"
|
|
68
80
|
local scope="$6"
|
|
69
81
|
local diff_hash="$7"
|
|
82
|
+
local reason_code="${8:-}"
|
|
83
|
+
local stage="${9:-precommit_codex_review}"
|
|
70
84
|
jq -nc \
|
|
71
85
|
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
72
86
|
--arg verdict "$verdict" \
|
|
@@ -74,9 +88,11 @@ append_run_log() {
|
|
|
74
88
|
--arg source "$source" \
|
|
75
89
|
--arg scope "$scope" \
|
|
76
90
|
--arg diff_hash "$diff_hash" \
|
|
91
|
+
--arg reason_code "$reason_code" \
|
|
92
|
+
--arg stage "$stage" \
|
|
77
93
|
--argjson duration_ms "${duration_ms:-0}" \
|
|
78
94
|
--argjson findings "${findings:-0}" \
|
|
79
|
-
'{ts:$ts,verdict:$verdict,decision:$decision,source:$source,duration_ms:$duration_ms,findings:$findings,scope:$scope,diff_hash:$diff_hash}' \
|
|
95
|
+
'{ts:$ts,verdict:$verdict,decision:$decision,source:$source,duration_ms:$duration_ms,elapsed_wall_ms:$duration_ms,findings:$findings,scope:$scope,diff_hash:$diff_hash,stage:$stage,reason_code:(if $reason_code == "" then null else $reason_code end)}' \
|
|
80
96
|
>> "$RUN_LOG" 2>/dev/null || true
|
|
81
97
|
}
|
|
82
98
|
|
|
@@ -84,16 +100,41 @@ fail_review() {
|
|
|
84
100
|
local fail_closed="$1"
|
|
85
101
|
local reason="$2"
|
|
86
102
|
local duration_ms="${3:-0}"
|
|
103
|
+
local reason_code="${4:-review_unavailable}"
|
|
87
104
|
if [[ "$fail_closed" == "true" ]]; then
|
|
88
|
-
append_run_log "error" "deny" "sync" "$duration_ms" 0 "${SCOPE:-}" "${DIFF_HASH:-}"
|
|
105
|
+
append_run_log "error" "deny" "sync" "$duration_ms" 0 "${SCOPE:-}" "${DIFF_HASH:-}" "$reason_code"
|
|
89
106
|
emit_deny "codex pre-commit review unavailable (fail-closed): $reason" "⛔ Codex review unavailable (fail-closed): $reason"
|
|
90
107
|
fi
|
|
91
108
|
printf 'codex pre-commit review skipped: %s\n' "$reason" >&2
|
|
92
|
-
append_run_log "skip" "allow" "sync" "$duration_ms" 0 "${SCOPE:-}" "${DIFF_HASH:-}"
|
|
109
|
+
append_run_log "skip" "allow" "sync" "$duration_ms" 0 "${SCOPE:-}" "${DIFF_HASH:-}" "$reason_code"
|
|
93
110
|
emit_json "⚠️ Codex pre-commit review skipped: $reason — commit allowed (fail-open)"
|
|
94
111
|
exit 0
|
|
95
112
|
}
|
|
96
113
|
|
|
114
|
+
resolve_precommit_hard_cap_ms() {
|
|
115
|
+
local configured="${COMPOSER_PRECOMMIT_HOOK_MAX_TIMEOUT_MS:-$PRECOMMIT_MAX_HARD_CAP_MS}"
|
|
116
|
+
case "$configured" in
|
|
117
|
+
''|*[!0-9]*) configured="$PRECOMMIT_MAX_HARD_CAP_MS" ;;
|
|
118
|
+
esac
|
|
119
|
+
if [[ "$configured" -lt "$PRECOMMIT_MIN_HARD_CAP_MS" ]]; then
|
|
120
|
+
configured="$PRECOMMIT_MIN_HARD_CAP_MS"
|
|
121
|
+
elif [[ "$configured" -gt "$PRECOMMIT_MAX_HARD_CAP_MS" ]]; then
|
|
122
|
+
configured="$PRECOMMIT_MAX_HARD_CAP_MS"
|
|
123
|
+
fi
|
|
124
|
+
printf '%s\n' "$configured"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
normalize_precommit_timeout_ms() {
|
|
128
|
+
local requested="$1"
|
|
129
|
+
local cap
|
|
130
|
+
cap="$(resolve_precommit_hard_cap_ms)"
|
|
131
|
+
if [[ "$requested" -gt "$cap" ]]; then
|
|
132
|
+
printf '%s\n' "$cap"
|
|
133
|
+
else
|
|
134
|
+
printf '%s\n' "$requested"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
rank_severity() {
|
|
98
139
|
case "$1" in
|
|
99
140
|
critical) printf '4' ;;
|
|
@@ -128,6 +169,14 @@ hash_stdin_16() {
|
|
|
128
169
|
fi
|
|
129
170
|
}
|
|
130
171
|
|
|
172
|
+
git_timeout() {
|
|
173
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
174
|
+
timeout 5 git "$@"
|
|
175
|
+
else
|
|
176
|
+
git "$@"
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
131
180
|
compute_repo_hash() {
|
|
132
181
|
printf '%s' "$1" | hash_stdin_16
|
|
133
182
|
}
|
|
@@ -174,12 +223,12 @@ compute_diff_hash() {
|
|
|
174
223
|
local base_ref merge_base branch_diff
|
|
175
224
|
# blockOnSeverity is excluded because threshold is re-applied at gate-read time from the cached findings array.
|
|
176
225
|
{
|
|
177
|
-
|
|
178
|
-
|
|
226
|
+
git_timeout -C "$root" diff HEAD 2>/dev/null
|
|
227
|
+
git_timeout -C "$root" diff --cached 2>/dev/null
|
|
179
228
|
if [[ "$scope" == "branch" ]]; then
|
|
180
|
-
if base_ref="$(
|
|
181
|
-
&& merge_base="$(
|
|
182
|
-
&& branch_diff="$(
|
|
229
|
+
if base_ref="$(git_timeout -C "$root" rev-parse --verify "${base}^{commit}" 2>/dev/null)" \
|
|
230
|
+
&& merge_base="$(git_timeout -C "$root" merge-base "$base" HEAD 2>/dev/null)" \
|
|
231
|
+
&& branch_diff="$(git_timeout -C "$root" diff "$base...HEAD" 2>/dev/null)"; then
|
|
183
232
|
printf '\ncomposer-codex-review-branch\nbaseRef=%s\nmergeBase=%s\n' "$base_ref" "$merge_base"
|
|
184
233
|
printf '%s' "$branch_diff"
|
|
185
234
|
printf '\n'
|
|
@@ -191,7 +240,7 @@ compute_diff_hash() {
|
|
|
191
240
|
|
|
192
241
|
find_git_root() {
|
|
193
242
|
local start="${CLAUDE_PROJECT_DIR:-.}"
|
|
194
|
-
|
|
243
|
+
git_timeout -C "$start" rev-parse --show-toplevel 2>/dev/null || git_timeout rev-parse --show-toplevel 2>/dev/null
|
|
195
244
|
}
|
|
196
245
|
|
|
197
246
|
find_codex_plugin_root() {
|
|
@@ -226,22 +275,56 @@ find_codex_plugin_root() {
|
|
|
226
275
|
fi
|
|
227
276
|
}
|
|
228
277
|
|
|
278
|
+
# kill_tree: fallback teardown when no process group could be created. STOP
|
|
279
|
+
# each node BEFORE enumerating its children so it cannot fork a new child
|
|
280
|
+
# mid-teardown (the race), then CONT so a delivered TERM is actually acted
|
|
281
|
+
# on. Note: a descendant that has already reparented (double-fork) is not
|
|
282
|
+
# reachable via pgrep -P walking — the process-group path below handles that.
|
|
283
|
+
kill_tree() {
|
|
284
|
+
local sig="$1" root="$2" child
|
|
285
|
+
kill -STOP "$root" 2>/dev/null || true
|
|
286
|
+
for child in $(pgrep -P "$root" 2>/dev/null); do
|
|
287
|
+
kill_tree "$sig" "$child"
|
|
288
|
+
done
|
|
289
|
+
kill -"$sig" "$root" 2>/dev/null || true
|
|
290
|
+
kill -CONT "$root" 2>/dev/null || true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# teardown_reviewer: prefer an ATOMIC process-group signal (kill -SIG -PGID)
|
|
294
|
+
# when the reviewer leads its own group — this also reaches descendants that
|
|
295
|
+
# reparented away from the immediate child. Fall back to kill_tree otherwise.
|
|
296
|
+
teardown_reviewer() {
|
|
297
|
+
local sig="$1" pid="$2" pgid_mode="$3"
|
|
298
|
+
if [[ "$pgid_mode" == "1" ]] && kill -"$sig" "-$pid" 2>/dev/null; then
|
|
299
|
+
return 0
|
|
300
|
+
fi
|
|
301
|
+
kill_tree "$sig" "$pid"
|
|
302
|
+
}
|
|
303
|
+
|
|
229
304
|
run_reviewer() {
|
|
230
305
|
local timeout_seconds="$1"
|
|
231
306
|
shift
|
|
232
|
-
if [[ "${COMPOSER_FORCE_BASH_TIMEOUT:-}" != "1" ]] && command -v timeout >/dev/null 2>&1; then
|
|
233
|
-
timeout "$timeout_seconds" "$@"
|
|
234
|
-
return $?
|
|
235
|
-
fi
|
|
236
|
-
if [[ "${COMPOSER_FORCE_BASH_TIMEOUT:-}" != "1" ]] && command -v gtimeout >/dev/null 2>&1; then
|
|
237
|
-
gtimeout "$timeout_seconds" "$@"
|
|
238
|
-
return $?
|
|
239
|
-
fi
|
|
240
307
|
|
|
241
|
-
local pid watchdog status marker
|
|
308
|
+
local pid watchdog status marker pgid_mode
|
|
242
309
|
marker="${TMPDIR:-/tmp}/composer-timeout.$$.$RANDOM"
|
|
243
|
-
|
|
244
|
-
|
|
310
|
+
# Launch the reviewer as the leader of a NEW process group so the watchdog
|
|
311
|
+
# can signal the whole subtree atomically (catching reparented grandchildren
|
|
312
|
+
# that hold the stdout pipe). setsid (Linux) or perl setpgrp (portable;
|
|
313
|
+
# ships on macOS where setsid does not) make the child its own group leader.
|
|
314
|
+
# If neither exists, fall back to in-group launch + kill_tree teardown.
|
|
315
|
+
pgid_mode=0
|
|
316
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
317
|
+
setsid "$@" &
|
|
318
|
+
pid=$!
|
|
319
|
+
pgid_mode=1
|
|
320
|
+
elif command -v perl >/dev/null 2>&1; then
|
|
321
|
+
perl -e 'setpgrp(0,0); exec @ARGV or die "exec failed: $!"' "$@" &
|
|
322
|
+
pid=$!
|
|
323
|
+
pgid_mode=1
|
|
324
|
+
else
|
|
325
|
+
"$@" &
|
|
326
|
+
pid=$!
|
|
327
|
+
fi
|
|
245
328
|
(
|
|
246
329
|
sleeper=""
|
|
247
330
|
trap '[[ -n "$sleeper" ]] && kill "$sleeper" 2>/dev/null || true; exit 0' TERM INT
|
|
@@ -249,9 +332,9 @@ run_reviewer() {
|
|
|
249
332
|
sleeper=$!
|
|
250
333
|
wait "$sleeper" 2>/dev/null || exit 0
|
|
251
334
|
printf '1' >"$marker" 2>/dev/null || true
|
|
252
|
-
|
|
335
|
+
teardown_reviewer TERM "$pid" "$pgid_mode"
|
|
253
336
|
sleep 5
|
|
254
|
-
|
|
337
|
+
teardown_reviewer KILL "$pid" "$pgid_mode"
|
|
255
338
|
) &
|
|
256
339
|
watchdog=$!
|
|
257
340
|
wait "$pid"
|
|
@@ -312,6 +395,68 @@ write_cache() {
|
|
|
312
395
|
> "$tmp" 2>/dev/null && chmod 600 "$tmp" 2>/dev/null && mv "$tmp" "$cache_file" 2>/dev/null || rm -f "$tmp"
|
|
313
396
|
}
|
|
314
397
|
|
|
398
|
+
compute_work_hash() {
|
|
399
|
+
local root="$1"
|
|
400
|
+
local base="$2"
|
|
401
|
+
local branch
|
|
402
|
+
branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || printf 'unknown')"
|
|
403
|
+
printf 'branch=%s\nbase=%s\n' "$branch" "$base" | hash_stdin_16
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
read_block_counter() {
|
|
407
|
+
local counter_file="$1"
|
|
408
|
+
if ! cache_file_is_trusted "$counter_file"; then
|
|
409
|
+
printf '0'
|
|
410
|
+
return 0
|
|
411
|
+
fi
|
|
412
|
+
jq -r 'if (.count | type) == "number" and (.count >= 0) then (.count | floor) else 0 end' \
|
|
413
|
+
"$counter_file" 2>/dev/null || printf '0'
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
write_block_counter() {
|
|
417
|
+
local counter_file="$1"
|
|
418
|
+
local count="$2"
|
|
419
|
+
local tmp="${counter_file}.$$"
|
|
420
|
+
jq -nc \
|
|
421
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
422
|
+
--argjson count "$count" \
|
|
423
|
+
'{count:$count,ts:$ts}' \
|
|
424
|
+
> "$tmp" 2>/dev/null && chmod 600 "$tmp" 2>/dev/null && mv "$tmp" "$counter_file" 2>/dev/null || rm -f "$tmp"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
reset_block_counter() {
|
|
428
|
+
local counter_file="${BLOCK_COUNTER_FILE:-}"
|
|
429
|
+
[[ -n "$counter_file" ]] || return 0
|
|
430
|
+
rm -f "$counter_file" 2>/dev/null || true
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
maybe_allow_after_block_cap() {
|
|
434
|
+
local source="$1"
|
|
435
|
+
local duration_ms="$2"
|
|
436
|
+
local findings="$3"
|
|
437
|
+
local duration_s="$4"
|
|
438
|
+
|
|
439
|
+
[[ "${MAX_CONSECUTIVE_BLOCKS:-0}" =~ ^[0-9]+$ ]] || return 1
|
|
440
|
+
[[ "$MAX_CONSECUTIVE_BLOCKS" -gt 0 ]] || return 1
|
|
441
|
+
[[ -n "${BLOCK_COUNTER_FILE:-}" ]] || return 1
|
|
442
|
+
|
|
443
|
+
local current_count next_count
|
|
444
|
+
current_count="$(read_block_counter "$BLOCK_COUNTER_FILE")"
|
|
445
|
+
[[ "$current_count" =~ ^[0-9]+$ ]] || current_count=0
|
|
446
|
+
|
|
447
|
+
if [[ "$current_count" -ge "$MAX_CONSECUTIVE_BLOCKS" ]]; then
|
|
448
|
+
reset_block_counter
|
|
449
|
+
notify_desktop "Codex pre-commit review allowed after oscillation cap"
|
|
450
|
+
append_run_log "needs-attention" "allow-cap" "$source" "$duration_ms" "$findings" "$SCOPE" "${DIFF_HASH:-}"
|
|
451
|
+
emit_json "⚠️ Codex gate: allowed after ${current_count} consecutive blocks (oscillation cap reached) — review the diff manually / see the PR (${duration_s}s)"
|
|
452
|
+
exit 0
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
next_count=$((current_count + 1))
|
|
456
|
+
write_block_counter "$BLOCK_COUNTER_FILE" "$next_count"
|
|
457
|
+
return 1
|
|
458
|
+
}
|
|
459
|
+
|
|
315
460
|
review_from_cache() {
|
|
316
461
|
local cache_file="$1"
|
|
317
462
|
jq -c '{verdict:(.verdict // "error"), summary:(.summary // ""), findings:(if (.findings | type) == "array" then .findings else [] end), next_steps:[]}' "$cache_file" 2>/dev/null
|
|
@@ -322,22 +467,171 @@ cache_age_minutes() {
|
|
|
322
467
|
jq -r '((now - (.ts | fromdateiso8601)) / 60 | floor)' "$cache_file" 2>/dev/null || printf '0'
|
|
323
468
|
}
|
|
324
469
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
470
|
+
review_parse_error_message() {
|
|
471
|
+
local review_output="$1"
|
|
472
|
+
jq -r '
|
|
473
|
+
def parsed_json($value):
|
|
474
|
+
if ($value | type) == "string" then (($value | fromjson?) // {}) else {} end;
|
|
475
|
+
def first_error($items):
|
|
476
|
+
[
|
|
477
|
+
$items[]
|
|
478
|
+
| select(. != null and . != false and ((. | tostring) | length) > 0)
|
|
479
|
+
]
|
|
480
|
+
| first // "";
|
|
481
|
+
|
|
482
|
+
(parsed_json(.rawOutput? // null)) as $rawOutputJson
|
|
483
|
+
| (parsed_json(.codex.stdout? // null)) as $codexStdoutJson
|
|
484
|
+
| first_error([
|
|
485
|
+
.parseError?,
|
|
486
|
+
.result.parseError?,
|
|
487
|
+
.review?.parseError?,
|
|
488
|
+
.review?.result?.parseError?,
|
|
489
|
+
.data?.parseError?,
|
|
490
|
+
.data?.result?.parseError?,
|
|
491
|
+
.output?.parseError?,
|
|
492
|
+
.output?.result?.parseError?,
|
|
493
|
+
.response?.parseError?,
|
|
494
|
+
.response?.result?.parseError?,
|
|
495
|
+
.payload?.parseError?,
|
|
496
|
+
.payload?.result?.parseError?,
|
|
497
|
+
$rawOutputJson.parseError?,
|
|
498
|
+
$rawOutputJson.result.parseError?,
|
|
499
|
+
$codexStdoutJson.parseError?,
|
|
500
|
+
$codexStdoutJson.result.parseError?
|
|
501
|
+
])
|
|
502
|
+
' <<<"$review_output" 2>/dev/null
|
|
330
503
|
}
|
|
331
504
|
|
|
332
505
|
normalize_review_output() {
|
|
333
506
|
local review_output="$1"
|
|
334
507
|
jq -c '
|
|
335
|
-
def
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
508
|
+
def parsed_json($value):
|
|
509
|
+
if ($value | type) == "string" then (($value | fromjson?) // {}) else {} end;
|
|
510
|
+
def first_value($items):
|
|
511
|
+
[
|
|
512
|
+
$items[]
|
|
513
|
+
| select(. != null and . != "")
|
|
514
|
+
]
|
|
515
|
+
| first // null;
|
|
516
|
+
def first_text($items):
|
|
517
|
+
[
|
|
518
|
+
$items[]
|
|
519
|
+
| select((. | type) == "string" and length > 0)
|
|
520
|
+
]
|
|
521
|
+
| first // "";
|
|
522
|
+
def first_array($items):
|
|
523
|
+
[
|
|
524
|
+
$items[]
|
|
525
|
+
| select((. | type) == "array")
|
|
526
|
+
]
|
|
527
|
+
| first // [];
|
|
528
|
+
|
|
529
|
+
(parsed_json(.rawOutput? // null)) as $rawOutputJson
|
|
530
|
+
| (parsed_json(.codex.stdout? // null)) as $codexStdoutJson
|
|
531
|
+
| (parsed_json(.stdout? // null)) as $stdoutJson
|
|
532
|
+
| {
|
|
533
|
+
verdict: first_value([
|
|
534
|
+
.result.verdict?,
|
|
535
|
+
.verdict?,
|
|
536
|
+
.review?.result?.verdict?,
|
|
537
|
+
.review?.verdict?,
|
|
538
|
+
.data?.result?.verdict?,
|
|
539
|
+
.data?.verdict?,
|
|
540
|
+
.output?.result?.verdict?,
|
|
541
|
+
.output?.verdict?,
|
|
542
|
+
.response?.result?.verdict?,
|
|
543
|
+
.response?.verdict?,
|
|
544
|
+
.payload?.result?.verdict?,
|
|
545
|
+
.payload?.verdict?,
|
|
546
|
+
$rawOutputJson.result.verdict?,
|
|
547
|
+
$rawOutputJson.verdict?,
|
|
548
|
+
$rawOutputJson.review?.result?.verdict?,
|
|
549
|
+
$rawOutputJson.review?.verdict?,
|
|
550
|
+
$codexStdoutJson.result.verdict?,
|
|
551
|
+
$codexStdoutJson.verdict?,
|
|
552
|
+
$codexStdoutJson.review?.result?.verdict?,
|
|
553
|
+
$codexStdoutJson.review?.verdict?,
|
|
554
|
+
$stdoutJson.result.verdict?,
|
|
555
|
+
$stdoutJson.verdict?
|
|
556
|
+
]),
|
|
557
|
+
summary: (first_text([
|
|
558
|
+
.result.summary?,
|
|
559
|
+
.summary?,
|
|
560
|
+
.review?.result?.summary?,
|
|
561
|
+
.review?.summary?,
|
|
562
|
+
.data?.result?.summary?,
|
|
563
|
+
.data?.summary?,
|
|
564
|
+
.output?.result?.summary?,
|
|
565
|
+
.output?.summary?,
|
|
566
|
+
.response?.result?.summary?,
|
|
567
|
+
.response?.summary?,
|
|
568
|
+
.payload?.result?.summary?,
|
|
569
|
+
.payload?.summary?,
|
|
570
|
+
$rawOutputJson.result.summary?,
|
|
571
|
+
$rawOutputJson.summary?,
|
|
572
|
+
$rawOutputJson.review?.result?.summary?,
|
|
573
|
+
$rawOutputJson.review?.summary?,
|
|
574
|
+
$codexStdoutJson.result.summary?,
|
|
575
|
+
$codexStdoutJson.summary?,
|
|
576
|
+
$codexStdoutJson.review?.result?.summary?,
|
|
577
|
+
$codexStdoutJson.review?.summary?,
|
|
578
|
+
$stdoutJson.result.summary?,
|
|
579
|
+
$stdoutJson.summary?
|
|
580
|
+
]) // ""),
|
|
581
|
+
findings: first_array([
|
|
582
|
+
.result.findings?,
|
|
583
|
+
.findings?,
|
|
584
|
+
.review?.result?.findings?,
|
|
585
|
+
.review?.findings?,
|
|
586
|
+
.data?.result?.findings?,
|
|
587
|
+
.data?.findings?,
|
|
588
|
+
.output?.result?.findings?,
|
|
589
|
+
.output?.findings?,
|
|
590
|
+
.response?.result?.findings?,
|
|
591
|
+
.response?.findings?,
|
|
592
|
+
.payload?.result?.findings?,
|
|
593
|
+
.payload?.findings?,
|
|
594
|
+
$rawOutputJson.result.findings?,
|
|
595
|
+
$rawOutputJson.findings?,
|
|
596
|
+
$rawOutputJson.review?.result?.findings?,
|
|
597
|
+
$rawOutputJson.review?.findings?,
|
|
598
|
+
$codexStdoutJson.result.findings?,
|
|
599
|
+
$codexStdoutJson.findings?,
|
|
600
|
+
$codexStdoutJson.review?.result?.findings?,
|
|
601
|
+
$codexStdoutJson.review?.findings?,
|
|
602
|
+
$stdoutJson.result.findings?,
|
|
603
|
+
$stdoutJson.findings?
|
|
604
|
+
]),
|
|
605
|
+
next_steps: first_array([
|
|
606
|
+
.result.next_steps?,
|
|
607
|
+
.next_steps?,
|
|
608
|
+
.review?.result?.next_steps?,
|
|
609
|
+
.review?.next_steps?,
|
|
610
|
+
.data?.result?.next_steps?,
|
|
611
|
+
.data?.next_steps?,
|
|
612
|
+
.output?.result?.next_steps?,
|
|
613
|
+
.output?.next_steps?,
|
|
614
|
+
.response?.result?.next_steps?,
|
|
615
|
+
.response?.next_steps?,
|
|
616
|
+
.payload?.result?.next_steps?,
|
|
617
|
+
.payload?.next_steps?,
|
|
618
|
+
$rawOutputJson.result.next_steps?,
|
|
619
|
+
$rawOutputJson.next_steps?,
|
|
620
|
+
$rawOutputJson.review?.result?.next_steps?,
|
|
621
|
+
$rawOutputJson.review?.next_steps?,
|
|
622
|
+
$codexStdoutJson.result.next_steps?,
|
|
623
|
+
$codexStdoutJson.next_steps?,
|
|
624
|
+
$codexStdoutJson.review?.result?.next_steps?,
|
|
625
|
+
$codexStdoutJson.review?.next_steps?,
|
|
626
|
+
$stdoutJson.result.next_steps?,
|
|
627
|
+
$stdoutJson.next_steps?
|
|
628
|
+
]),
|
|
629
|
+
raw_text: first_text([
|
|
630
|
+
.codex.stdout?,
|
|
631
|
+
.rawOutput?,
|
|
632
|
+
.stdout?,
|
|
633
|
+
.review?
|
|
634
|
+
])
|
|
341
635
|
}
|
|
342
636
|
' <<<"$review_output" 2>/dev/null
|
|
343
637
|
}
|
|
@@ -358,6 +652,7 @@ evaluate_review_output() {
|
|
|
358
652
|
case "$verdict" in
|
|
359
653
|
approve)
|
|
360
654
|
printf 'codex pre-commit review: approve\n' >&2
|
|
655
|
+
reset_block_counter
|
|
361
656
|
append_run_log "approve" "allow" "$source" "$duration_ms" 0 "$SCOPE" "${DIFF_HASH:-}"
|
|
362
657
|
if [[ "$source" == "cache" ]]; then
|
|
363
658
|
emit_json "✅ Codex pre-commit review: approve (cached, ${cache_age}m old)"
|
|
@@ -369,6 +664,11 @@ evaluate_review_output() {
|
|
|
369
664
|
needs-attention)
|
|
370
665
|
;;
|
|
371
666
|
*)
|
|
667
|
+
local raw_text_len
|
|
668
|
+
raw_text_len="$(jq -r '(.raw_text // "" | length)' <<<"$review_output" 2>/dev/null || printf '0')"
|
|
669
|
+
if [[ -z "$verdict" && "$raw_text_len" =~ ^[0-9]+$ && "$raw_text_len" -gt 0 ]]; then
|
|
670
|
+
fail_review "$FAIL_CLOSED" "structured review verdict missing; use codexReview.preCommitCommand=adversarial-review for mechanical gates" "$duration_ms"
|
|
671
|
+
fi
|
|
372
672
|
fail_review "$FAIL_CLOSED" "unknown review verdict: ${verdict:-missing}" "$duration_ms"
|
|
373
673
|
;;
|
|
374
674
|
esac
|
|
@@ -399,44 +699,88 @@ evaluate_review_output() {
|
|
|
399
699
|
| join(" | ")
|
|
400
700
|
' <<<"$review_output" 2>/dev/null || true)"
|
|
401
701
|
finding_summary="$(compact_text "$finding_summary")"
|
|
702
|
+
maybe_allow_after_block_cap "$source" "$duration_ms" "$finding_count" "$duration_s"
|
|
402
703
|
notify_desktop "Codex pre-commit review blocked: ${max_severity}"
|
|
403
704
|
append_run_log "needs-attention" "deny" "$source" "$duration_ms" "$finding_count" "$SCOPE" "${DIFF_HASH:-}"
|
|
404
705
|
emit_deny "Codex pre-commit review: needs-attention (>= $BLOCK_ON_SEVERITY). $summary | $finding_summary" "⛔ Codex pre-commit review: blocked (${max_severity}, ${duration_s}s)"
|
|
405
706
|
fi
|
|
406
707
|
|
|
407
708
|
printf 'codex pre-commit review: needs-attention but all findings below %s; allowing\n' "$BLOCK_ON_SEVERITY" >&2
|
|
709
|
+
reset_block_counter
|
|
408
710
|
append_run_log "needs-attention" "allow" "$source" "$duration_ms" "$finding_count" "$SCOPE" "${DIFF_HASH:-}"
|
|
409
711
|
emit_json "🟡 Codex pre-commit review: needs-attention below ${BLOCK_ON_SEVERITY} threshold — allowing (${duration_s}s)"
|
|
410
712
|
exit 0
|
|
411
713
|
}
|
|
412
714
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
715
|
+
GITHOOK=0
|
|
716
|
+
if [[ "${1:-}" == "--git-hook" || "${COMPOSER_PRECOMMIT_GITHOOK:-}" == "1" ]]; then
|
|
717
|
+
GITHOOK=1
|
|
416
718
|
fi
|
|
417
719
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
720
|
+
if [[ "$GITHOOK" == "1" ]]; then
|
|
721
|
+
# Real git pre-commit hook: git provides no PreToolUse JSON on stdin. jq is
|
|
722
|
+
# still needed to parse config; if it is missing, honor failClosed via Node.
|
|
723
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
724
|
+
PREFLIGHT_CONFIG_PATH="${COMPOSER_CONFIG:-${CLAUDE_PROJECT_DIR:-.}/composer.config.json}"
|
|
725
|
+
PREFLIGHT_FLAGS="0 0"
|
|
726
|
+
if [[ -f "$PREFLIGHT_CONFIG_PATH" ]] && command -v node >/dev/null 2>&1; then
|
|
727
|
+
PREFLIGHT_FLAGS="$(node -e 'try{const c=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const r=c.codexReview||{};const h=r.preCommitHook||{};process.stdout.write((r.enabled===true&&h.enabled===true?"1":"0")+" "+(h.failClosed===true?"1":"0"))}catch(e){process.stdout.write("0 0")}' "$PREFLIGHT_CONFIG_PATH" 2>/dev/null || printf '0 0')"
|
|
728
|
+
fi
|
|
729
|
+
if [[ "${PREFLIGHT_FLAGS%% *}" == "1" && "${PREFLIGHT_FLAGS##* }" == "1" ]]; then
|
|
730
|
+
emit_deny "codex pre-commit review unavailable: jq not installed (fail-closed)" "⛔ Codex pre-commit gate requires jq (fail-closed). Install jq (brew install jq)."
|
|
731
|
+
fi
|
|
732
|
+
printf 'codex pre-commit review skipped: jq not installed\n' >&2
|
|
733
|
+
exit 0
|
|
734
|
+
fi
|
|
735
|
+
else
|
|
736
|
+
INPUT="$(cat || true)"
|
|
737
|
+
if [[ -z "$INPUT" ]]; then
|
|
738
|
+
exit 0
|
|
739
|
+
fi
|
|
422
740
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if
|
|
428
|
-
|
|
429
|
-
|
|
741
|
+
# jq is required to parse the hook payload. If it is missing we cannot run the
|
|
742
|
+
# gate; honor failClosed via a jq-free (Node) config read so a configured
|
|
743
|
+
# fail-closed gate cannot silently fail open. Only an actual Bash `git commit`
|
|
744
|
+
# payload is gated; everything else is allowed through unchanged.
|
|
745
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
746
|
+
if grep -Eq '"tool_name"[[:space:]]*:[[:space:]]*"Bash"' <<<"$INPUT" \
|
|
747
|
+
&& grep -Eq 'git[^"]*commit' <<<"$INPUT"; then
|
|
748
|
+
PREFLIGHT_CONFIG_PATH="${COMPOSER_CONFIG:-${CLAUDE_PROJECT_DIR:-.}/composer.config.json}"
|
|
749
|
+
PREFLIGHT_FLAGS="0 0"
|
|
750
|
+
if [[ -f "$PREFLIGHT_CONFIG_PATH" ]] && command -v node >/dev/null 2>&1; then
|
|
751
|
+
PREFLIGHT_FLAGS="$(node -e 'try{const c=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const r=c.codexReview||{};const h=r.preCommitHook||{};process.stdout.write((r.enabled===true&&h.enabled===true?"1":"0")+" "+(h.failClosed===true?"1":"0"))}catch(e){process.stdout.write("0 0")}' "$PREFLIGHT_CONFIG_PATH" 2>/dev/null || printf '0 0')"
|
|
752
|
+
fi
|
|
753
|
+
if [[ "${PREFLIGHT_FLAGS%% *}" == "1" && "${PREFLIGHT_FLAGS##* }" == "1" ]]; then
|
|
754
|
+
emit_deny "codex pre-commit review unavailable: jq not installed (fail-closed)" "⛔ Codex pre-commit gate requires jq (fail-closed). Install jq (brew install jq)."
|
|
755
|
+
fi
|
|
756
|
+
fi
|
|
757
|
+
printf 'codex pre-commit review skipped: jq not installed\n' >&2
|
|
758
|
+
exit 0
|
|
759
|
+
fi
|
|
430
760
|
|
|
431
|
-
|
|
432
|
-
if [[ -z "$
|
|
433
|
-
|
|
434
|
-
fi
|
|
435
|
-
if
|
|
436
|
-
|
|
761
|
+
TOOL="$(jq -r '.tool_name // empty' <<<"$INPUT" 2>/dev/null || true)"
|
|
762
|
+
if [[ -z "$TOOL" ]]; then
|
|
763
|
+
exit 0
|
|
764
|
+
fi
|
|
765
|
+
if [[ "$TOOL" != "Bash" ]]; then
|
|
766
|
+
exit 0
|
|
767
|
+
fi
|
|
768
|
+
|
|
769
|
+
COMMAND_TEXT="$(jq -r '.tool_input.command // empty' <<<"$INPUT" 2>/dev/null || true)"
|
|
770
|
+
if [[ -z "$COMMAND_TEXT" ]]; then
|
|
771
|
+
exit 0
|
|
772
|
+
fi
|
|
773
|
+
if grep -Eq '(^|[^[:alnum:]])(commit-tree|commit-graph)([^[:alnum:]]|$)|--dry-run' <<<"$COMMAND_TEXT"; then
|
|
774
|
+
exit 0
|
|
775
|
+
fi
|
|
776
|
+
if ! grep -Eq '(^|[^[:alnum:]])git([[:space:]]|[[:space:]].*[[:space:]])commit([[:space:]]|$)' <<<"$COMMAND_TEXT"; then
|
|
777
|
+
exit 0
|
|
778
|
+
fi
|
|
437
779
|
fi
|
|
438
|
-
|
|
439
|
-
|
|
780
|
+
|
|
781
|
+
# Library mode: allow tests to source helpers without running the gate.
|
|
782
|
+
if [[ "${COMPOSER_PRECOMMIT_LIB_ONLY:-0}" == "1" ]]; then
|
|
783
|
+
return 0 2>/dev/null || exit 0
|
|
440
784
|
fi
|
|
441
785
|
|
|
442
786
|
CONFIG_PATH="${COMPOSER_CONFIG:-${CLAUDE_PROJECT_DIR:-.}/composer.config.json}"
|
|
@@ -460,8 +804,9 @@ SCOPE="$(jq -r '.codexReview.scope // "working-tree"' <<<"$CONFIG_JSON" 2>/dev/n
|
|
|
460
804
|
BASE="$(jq -r '.codexReview.base // "main"' <<<"$CONFIG_JSON" 2>/dev/null || printf 'main')"
|
|
461
805
|
CODEX_MODEL="$(jq -r '.codexReview.model // empty' <<<"$CONFIG_JSON" 2>/dev/null || true)"
|
|
462
806
|
BLOCK_ON_SEVERITY="$(jq -r '.codexReview.preCommitHook.blockOnSeverity // "high"' <<<"$CONFIG_JSON" 2>/dev/null || printf 'high')"
|
|
463
|
-
TIMEOUT_MS="$(jq -r
|
|
807
|
+
TIMEOUT_MS="$(jq -r ".codexReview.preCommitHook.timeoutMs // $PRECOMMIT_DEFAULT_TIMEOUT_MS" <<<"$CONFIG_JSON" 2>/dev/null || printf '%s' "$PRECOMMIT_DEFAULT_TIMEOUT_MS")"
|
|
464
808
|
FAIL_CLOSED="$(jq -r '.codexReview.preCommitHook.failClosed // false' <<<"$CONFIG_JSON" 2>/dev/null || printf 'false')"
|
|
809
|
+
MAX_CONSECUTIVE_BLOCKS="$(jq -r '(.codexReview.preCommitHook.maxConsecutiveBlocks // 0) | if type == "number" and . >= 0 and . == floor then . else 0 end' <<<"$CONFIG_JSON" 2>/dev/null || printf '0')"
|
|
465
810
|
WARM_CACHE_ENABLED="$(jq -r '.codexReview.warmCache.enabled // false' <<<"$CONFIG_JSON" 2>/dev/null || printf 'false')"
|
|
466
811
|
WARM_CACHE_MAX_AGE_MINUTES="$(jq -r '.codexReview.warmCache.maxAgeMinutes // 30' <<<"$CONFIG_JSON" 2>/dev/null || printf '30')"
|
|
467
812
|
NOTIFY_DESKTOP="$(jq -r '.codexReview.notify.desktop // false' <<<"$CONFIG_JSON" 2>/dev/null || printf 'false')"
|
|
@@ -477,6 +822,13 @@ esac
|
|
|
477
822
|
case "$TIMEOUT_MS" in
|
|
478
823
|
''|*[!0-9]*) fail_review "$FAIL_CLOSED" "invalid timeoutMs: $TIMEOUT_MS" ;;
|
|
479
824
|
esac
|
|
825
|
+
TIMEOUT_MS="$(normalize_precommit_timeout_ms "$TIMEOUT_MS")"
|
|
826
|
+
case "$MAX_CONSECUTIVE_BLOCKS" in
|
|
827
|
+
''|*[!0-9]*) MAX_CONSECUTIVE_BLOCKS=0 ;;
|
|
828
|
+
esac
|
|
829
|
+
if [[ "$(printf '%s' "$CODEX_MODEL" | tr '[:upper:]' '[:lower:]')" == "gpt-5.5-pro" ]]; then
|
|
830
|
+
fail_review "$FAIL_CLOSED" "gpt-5.5-pro is the ChatGPT-Pro/Oracle browser lane, not a Codex CLI model"
|
|
831
|
+
fi
|
|
480
832
|
case "$WARM_CACHE_MAX_AGE_MINUTES" in
|
|
481
833
|
''|*[!0-9]*) WARM_CACHE_MAX_AGE_MINUTES=30 ;;
|
|
482
834
|
esac
|
|
@@ -488,10 +840,19 @@ fi
|
|
|
488
840
|
|
|
489
841
|
DIFF_HASH=""
|
|
490
842
|
CACHE_FILE=""
|
|
843
|
+
BLOCK_COUNTER_FILE=""
|
|
844
|
+
GIT_ROOT="$(find_git_root || true)"
|
|
845
|
+
if [[ -n "$GIT_ROOT" && "$MAX_CONSECUTIVE_BLOCKS" -gt 0 ]]; then
|
|
846
|
+
REPO_HASH="$(compute_repo_hash "$GIT_ROOT" 2>/dev/null || true)"
|
|
847
|
+
WORK_HASH="$(compute_work_hash "$GIT_ROOT" "$BASE" 2>/dev/null || true)"
|
|
848
|
+
STATE_DIR="$(ensure_state_dir 2>/dev/null || true)"
|
|
849
|
+
if [[ -n "$REPO_HASH" && -n "$WORK_HASH" && -n "$STATE_DIR" ]]; then
|
|
850
|
+
BLOCK_COUNTER_FILE="$STATE_DIR/codex-review-blocks-${REPO_HASH}-${WORK_HASH}.json"
|
|
851
|
+
fi
|
|
852
|
+
fi
|
|
491
853
|
if [[ -n "${COMPOSER_CODEX_REVIEW_CMD:-}" ]]; then
|
|
492
854
|
: # Test seam commands are not equivalent reviewer policy, so they never read or write warm-cache verdicts.
|
|
493
855
|
elif [[ "$WARM_CACHE_ENABLED" == "true" ]]; then
|
|
494
|
-
GIT_ROOT="$(find_git_root || true)"
|
|
495
856
|
if [[ -n "$GIT_ROOT" ]]; then
|
|
496
857
|
REPO_HASH="$(compute_repo_hash "$GIT_ROOT" 2>/dev/null || true)"
|
|
497
858
|
DIFF_HASH="$(compute_diff_hash "$GIT_ROOT" "$REVIEW_COMMAND" "$SCOPE" "$BASE" "$CODEX_MODEL" 2>/dev/null || true)"
|
|
@@ -535,9 +896,15 @@ END_SECONDS="$(date +%s)"
|
|
|
535
896
|
DURATION_MS=$(( (END_SECONDS - START_SECONDS) * 1000 ))
|
|
536
897
|
|
|
537
898
|
if [[ "$REVIEW_STATUS" -eq 124 ]]; then
|
|
538
|
-
fail_review "
|
|
899
|
+
fail_review "true" "review timed out after ${TIMEOUT_SECONDS}s" "$DURATION_MS" "hook_timeout"
|
|
539
900
|
fi
|
|
540
901
|
if [[ "$REVIEW_STATUS" -ne 0 ]]; then
|
|
902
|
+
if [[ -n "$REVIEW_OUTPUT" ]] && jq -e . >/dev/null 2>&1 <<<"$REVIEW_OUTPUT"; then
|
|
903
|
+
PARSE_ERROR_MESSAGE="$(review_parse_error_message "$REVIEW_OUTPUT")"
|
|
904
|
+
if [[ -n "$PARSE_ERROR_MESSAGE" ]]; then
|
|
905
|
+
fail_review "$FAIL_CLOSED" "review parseError: $PARSE_ERROR_MESSAGE" "$DURATION_MS"
|
|
906
|
+
fi
|
|
907
|
+
fi
|
|
541
908
|
fail_review "$FAIL_CLOSED" "review command exited $REVIEW_STATUS" "$DURATION_MS"
|
|
542
909
|
fi
|
|
543
910
|
if [[ -z "$REVIEW_OUTPUT" ]]; then
|
|
@@ -546,8 +913,9 @@ fi
|
|
|
546
913
|
if ! jq -e . >/dev/null 2>&1 <<<"$REVIEW_OUTPUT"; then
|
|
547
914
|
fail_review "$FAIL_CLOSED" "review returned unparseable JSON" "$DURATION_MS"
|
|
548
915
|
fi
|
|
549
|
-
|
|
550
|
-
|
|
916
|
+
PARSE_ERROR_MESSAGE="$(review_parse_error_message "$REVIEW_OUTPUT")"
|
|
917
|
+
if [[ -n "$PARSE_ERROR_MESSAGE" ]]; then
|
|
918
|
+
fail_review "$FAIL_CLOSED" "review parseError: $PARSE_ERROR_MESSAGE" "$DURATION_MS"
|
|
551
919
|
fi
|
|
552
920
|
NORMALIZED_REVIEW_OUTPUT="$(normalize_review_output "$REVIEW_OUTPUT" || true)"
|
|
553
921
|
if [[ -z "$NORMALIZED_REVIEW_OUTPUT" ]] || ! jq -e . >/dev/null 2>&1 <<<"$NORMALIZED_REVIEW_OUTPUT"; then
|