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