@ccoalm/ccl-skills 0.3.0 → 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/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +34 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_abort_leak_state_helpers.sh +148 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +7 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate_abort_leak.sh +271 -34
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +8 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +7 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +3 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +7 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +29 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +28 -29
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +153 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +23 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +303 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +222 -91
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +6 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +6 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_verdict_differential.sh +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_round_attribution.sh +12 -12
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +455 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_source_refuted.sh +20 -20
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_liveness_predicate_gate.sh +288 -0
- package/dist/assets/release.json +39 -24
- package/package.json +1 -1
package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py
CHANGED
|
@@ -12,7 +12,7 @@ fallback. Exit 3 means a JSON object/envelope was present but carried no classif
|
|
|
12
12
|
error; callers must not grep its model-controlled payload as transport diagnostics.
|
|
13
13
|
|
|
14
14
|
Reason tokens printed on exit 0:
|
|
15
|
-
auth -> not logged in / login required
|
|
15
|
+
auth -> not logged in / login required / explicit 401 auth failure
|
|
16
16
|
quota:<text> -> rate limit / quota / api_error_status 429
|
|
17
17
|
permission_denied -> tool permission denials present
|
|
18
18
|
error:<subtype>:<text> -> generic errored envelope
|
|
@@ -24,6 +24,10 @@ import re
|
|
|
24
24
|
import sys
|
|
25
25
|
|
|
26
26
|
NOT_LOGGED_IN = re.compile(r"(^|[^a-z])not logged in([^a-z]|$)|please run /login", re.I)
|
|
27
|
+
AUTH_FAILURE = re.compile(
|
|
28
|
+
r"^\s*failed to authenticate\.\s+api error:\s*401\b",
|
|
29
|
+
re.I,
|
|
30
|
+
)
|
|
27
31
|
QUOTA = re.compile(r"rate limit exceeded|quota exceeded|hit your limit", re.I)
|
|
28
32
|
|
|
29
33
|
|
|
@@ -32,17 +36,44 @@ def result_text(env: dict) -> str:
|
|
|
32
36
|
return result if isinstance(result, str) else ""
|
|
33
37
|
|
|
34
38
|
|
|
39
|
+
def api_status_int(env: dict) -> int | None:
|
|
40
|
+
"""Normalize `api_error_status` before comparing it to a number.
|
|
41
|
+
|
|
42
|
+
A bare `== 401` matches only a Python int, so a transport that serializes the
|
|
43
|
+
status as `"401"` silently misses the arm — and then the text arm misses too
|
|
44
|
+
unless the wording is the one pinned phrase, re-creating the generic
|
|
45
|
+
`error:success` false negative this classifier exists to remove. The sibling
|
|
46
|
+
`parse_probe_result.py` already normalizes the same field (its accepted set
|
|
47
|
+
carries `"0"`, so a string status is an observed shape here, not a
|
|
48
|
+
hypothetical); this mirrors that existing standard rather than inventing one.
|
|
49
|
+
"""
|
|
50
|
+
status = env.get("api_error_status")
|
|
51
|
+
if isinstance(status, bool):
|
|
52
|
+
# `int(True)` is 1, so a boolean would silently become a status code.
|
|
53
|
+
return None
|
|
54
|
+
if isinstance(status, int):
|
|
55
|
+
return status
|
|
56
|
+
if isinstance(status, str) and re.fullmatch(r"\s*\d+\s*", status):
|
|
57
|
+
return int(status)
|
|
58
|
+
# Anything else — float, None, object, non-numeric text — is not a status.
|
|
59
|
+
# `int()` on a float truncates (401.9 -> 401), which would let a value that
|
|
60
|
+
# is not a status code select the auth arm; refuse instead of coercing.
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
35
64
|
def classify(env: dict) -> str | None:
|
|
36
65
|
text = result_text(env)
|
|
37
66
|
if env.get("permission_denials"):
|
|
38
67
|
return "permission_denied"
|
|
39
|
-
api_status = env
|
|
68
|
+
api_status = api_status_int(env)
|
|
69
|
+
if api_status == 401:
|
|
70
|
+
return "auth"
|
|
40
71
|
if api_status == 429:
|
|
41
72
|
snippet = " ".join(text.split())[:200]
|
|
42
73
|
return f"quota:{snippet}" if snippet else "quota:rate limit or quota exceeded"
|
|
43
74
|
subtype = env.get("subtype")
|
|
44
75
|
errored = env.get("is_error") is True or subtype not in (None, "success")
|
|
45
|
-
if errored and NOT_LOGGED_IN.search(text):
|
|
76
|
+
if errored and (NOT_LOGGED_IN.search(text) or AUTH_FAILURE.search(text)):
|
|
46
77
|
return "auth"
|
|
47
78
|
if errored and QUOTA.search(text):
|
|
48
79
|
snippet = " ".join(text.split())[:200]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pins the abort-leak probe's process-state vocabulary and the one helper that consumes
|
|
3
|
+
# it, against real processes in each state.
|
|
4
|
+
#
|
|
5
|
+
# The probe exists because three spellings of "is it there?" disagreed about one pid.
|
|
6
|
+
# The fix was to give the whole probe a single vocabulary — so the vocabulary itself is
|
|
7
|
+
# now load-bearing, and every consumer must agree with it. That is not free: adding
|
|
8
|
+
# `stopped` as a third state silently changed the meaning of every consumer that had been
|
|
9
|
+
# written when there were two, and `await_suite_exit` went on treating "not live" as
|
|
10
|
+
# "gone" — which would have called a STOPPED suite exited and let leg 2 pass while the
|
|
11
|
+
# suite it was supposed to have killed was still sitting there.
|
|
12
|
+
#
|
|
13
|
+
# bash skills/code-review/scripts/test_abort_leak_state_helpers.sh
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
17
|
+
PROBE="$DIR/test_review_gate_abort_leak.sh"
|
|
18
|
+
[ -f "$PROBE" ] || { echo "FAIL: probe not found: $PROBE" >&2; exit 1; }
|
|
19
|
+
|
|
20
|
+
pass=0; fail=0
|
|
21
|
+
note() { fail=$((fail+1)); printf 'FAIL %s\n' "$1" >&2; }
|
|
22
|
+
ok() { pass=$((pass+1)); }
|
|
23
|
+
|
|
24
|
+
TMP=$(mktemp -d "${TMPDIR:-/tmp}/abort-leak-helpers.XXXXXX") || exit 1
|
|
25
|
+
cleanup_all() {
|
|
26
|
+
local p
|
|
27
|
+
for p in ${KIDS:-}; do kill -CONT "$p" 2>/dev/null; kill -KILL "$p" 2>/dev/null; done
|
|
28
|
+
rm -rf "$TMP"
|
|
29
|
+
}
|
|
30
|
+
KIDS=""
|
|
31
|
+
# Own it only while it is outstanding. A pid that has been reaped is a number the OS is
|
|
32
|
+
# free to hand to somebody else, so leaving it in KIDS turns the EXIT trap into a signal
|
|
33
|
+
# aimed at a stranger — and this repo runs suites eight-way parallel, so the stranger is
|
|
34
|
+
# plausibly another lane. Same rule the probe under test applies before it signals
|
|
35
|
+
# anything: prove current ownership, never trust a cached pid.
|
|
36
|
+
drop_kid() {
|
|
37
|
+
local keep="" k
|
|
38
|
+
for k in ${KIDS:-}; do [ "$k" = "$1" ] || keep="$keep $k"; done
|
|
39
|
+
KIDS="$keep"
|
|
40
|
+
}
|
|
41
|
+
reap_kid() { # reap_kid <pid> — stop owning it the moment it is reaped
|
|
42
|
+
kill -CONT "$1" 2>/dev/null
|
|
43
|
+
kill -KILL "$1" 2>/dev/null
|
|
44
|
+
wait "$1" 2>/dev/null
|
|
45
|
+
drop_kid "$1"
|
|
46
|
+
}
|
|
47
|
+
trap cleanup_all EXIT
|
|
48
|
+
trap 'exit 130' INT
|
|
49
|
+
trap 'exit 143' TERM HUP
|
|
50
|
+
|
|
51
|
+
# Lift the helpers out rather than sourcing the probe, which would run both legs.
|
|
52
|
+
helpers=$TMP/helpers.sh
|
|
53
|
+
{
|
|
54
|
+
echo '#!/usr/bin/env bash'
|
|
55
|
+
echo 'set -uo pipefail'
|
|
56
|
+
awk '/^wrapper_state\(\) \{/{p=1} p; p && /^\}$/{exit}' "$PROBE"
|
|
57
|
+
awk '/^await_suite_exit\(\) \{/{p=1} p; p && /^\}$/{exit}' "$PROBE"
|
|
58
|
+
} > "$helpers"
|
|
59
|
+
grep -q 'wrapper_state()' "$helpers" || { echo "FAIL: could not lift wrapper_state (moved?)" >&2; exit 1; }
|
|
60
|
+
grep -q 'await_suite_exit()' "$helpers" || { echo "FAIL: could not lift await_suite_exit (moved?)" >&2; exit 1; }
|
|
61
|
+
# shellcheck disable=SC1090
|
|
62
|
+
. "$helpers"
|
|
63
|
+
|
|
64
|
+
# --- states -----------------------------------------------------------------
|
|
65
|
+
sleep 300 & live_pid=$!; KIDS="$KIDS $live_pid"
|
|
66
|
+
[ "$(wrapper_state "$live_pid")" = live ] && ok || note "a running child must read live, got $(wrapper_state "$live_pid")"
|
|
67
|
+
|
|
68
|
+
kill -STOP "$live_pid" 2>/dev/null
|
|
69
|
+
stopped_seen=""
|
|
70
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
71
|
+
stopped_seen="$(wrapper_state "$live_pid")"
|
|
72
|
+
[ "$stopped_seen" = stopped ] && break
|
|
73
|
+
sleep 0.2
|
|
74
|
+
done
|
|
75
|
+
[ "$stopped_seen" = stopped ] && ok || note "a stopped child must read stopped, got $stopped_seen"
|
|
76
|
+
|
|
77
|
+
# THE regression: a stopped process is still present. Treating "not live" as gone is how
|
|
78
|
+
# a suite that was never killed reads as exited.
|
|
79
|
+
GRACE=2
|
|
80
|
+
suite_pid="$live_pid"
|
|
81
|
+
if await_suite_exit; then
|
|
82
|
+
note "await_suite_exit reported a STOPPED child as exited — 'not live' is not 'gone'"
|
|
83
|
+
else
|
|
84
|
+
ok
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
reap_kid "$live_pid"
|
|
88
|
+
suite_pid="$live_pid"
|
|
89
|
+
if await_suite_exit; then ok; else note "await_suite_exit must report a reaped child as gone"; fi
|
|
90
|
+
|
|
91
|
+
# A never-existing pid, and a non-numeric one, are both absent rather than an error.
|
|
92
|
+
[ "$(wrapper_state 999999999)" = absent ] && ok || note "an unused pid must read absent"
|
|
93
|
+
[ "$(wrapper_state "")" = absent ] && ok || note "an empty pid must read absent"
|
|
94
|
+
[ "$(wrapper_state notanumber)" = absent ] && ok || note "a non-numeric pid must read absent"
|
|
95
|
+
|
|
96
|
+
# A zombie: exited, not yet reaped. It answers kill -0 and reports a ppid, which is the
|
|
97
|
+
# whole reason this vocabulary exists.
|
|
98
|
+
python3 -c '
|
|
99
|
+
import os, sys, time
|
|
100
|
+
pid = os.fork()
|
|
101
|
+
if pid == 0:
|
|
102
|
+
os._exit(0)
|
|
103
|
+
sys.stdout.write(str(pid) + "\n")
|
|
104
|
+
sys.stdout.flush()
|
|
105
|
+
time.sleep(6)
|
|
106
|
+
' >"$TMP/zombie.out" 2>/dev/null &
|
|
107
|
+
zparent=$!; KIDS="$KIDS $zparent"
|
|
108
|
+
zpid=""
|
|
109
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
110
|
+
zpid="$(head -1 "$TMP/zombie.out" 2>/dev/null || true)"
|
|
111
|
+
[ -n "$zpid" ] && break
|
|
112
|
+
sleep 0.3
|
|
113
|
+
done
|
|
114
|
+
if [ -n "$zpid" ]; then
|
|
115
|
+
zstate=""
|
|
116
|
+
for _ in 1 2 3 4 5; do
|
|
117
|
+
zstate="$(wrapper_state "$zpid")"
|
|
118
|
+
[ "$zstate" = zombie ] && break
|
|
119
|
+
sleep 0.3
|
|
120
|
+
done
|
|
121
|
+
[ "$zstate" = zombie ] && ok || note "an unreaped corpse must read zombie, got $zstate"
|
|
122
|
+
# And it must NOT read live, which is the exact confusion that red CI three times.
|
|
123
|
+
[ "$zstate" != live ] && ok || note "a corpse must never read live"
|
|
124
|
+
else
|
|
125
|
+
note "could not construct a zombie to test against"
|
|
126
|
+
fi
|
|
127
|
+
reap_kid "$zparent"
|
|
128
|
+
|
|
129
|
+
# A `ps` that cannot run is not a process that is gone. Shadow ps with a stub that
|
|
130
|
+
# exits 127 and prove the state reads unknown, and that await_suite_exit does NOT then
|
|
131
|
+
# report the suite exited — the expensive direction of this error.
|
|
132
|
+
psdir="$TMP/fakebin"; mkdir -p "$psdir"
|
|
133
|
+
printf '#!/bin/sh\nexit 127\n' > "$psdir/ps"; chmod +x "$psdir/ps"
|
|
134
|
+
sleep 300 & unknown_pid=$!; KIDS="$KIDS $unknown_pid"
|
|
135
|
+
unknown_state="$(PATH="$psdir:$PATH" wrapper_state "$unknown_pid")"
|
|
136
|
+
[ "$unknown_state" = unknown ] && ok || note "a failing ps must read unknown, got $unknown_state"
|
|
137
|
+
GRACE=2
|
|
138
|
+
suite_pid="$unknown_pid"
|
|
139
|
+
if PATH="$psdir:$PATH" await_suite_exit; then
|
|
140
|
+
note "await_suite_exit reported exited while ps was broken — a tool failure is not an exit"
|
|
141
|
+
else
|
|
142
|
+
ok
|
|
143
|
+
fi
|
|
144
|
+
reap_kid "$unknown_pid"
|
|
145
|
+
|
|
146
|
+
printf 'abort_leak_state_helpers: pass=%s fail=%s\n' "$pass" "$fail"
|
|
147
|
+
[ "$fail" -eq 0 ] || exit 1
|
|
148
|
+
echo "abort_leak_state_helpers_ok"
|
|
@@ -49,6 +49,34 @@ expect_no_envelope() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
expect_token '{"type":"result","subtype":"error","is_error":true,"result":"Not logged in. Please run /login"}' auth
|
|
52
|
+
expect_token '{"type":"result","subtype":"success","is_error":true,"result":"Failed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue."}' auth
|
|
53
|
+
expect_token '{"type":"result","subtype":"success","is_error":true,"result":" \r\nFailed to authenticate. API Error: 401 OAuth access token has expired."}' auth
|
|
54
|
+
expect_token '{"type":"result","subtype":"success","is_error":false,"api_error_status":401,"result":"authentication rejected"}' auth
|
|
55
|
+
# String-serialized transport status. A bare `== 401` matches only an int, so this
|
|
56
|
+
# shape used to fall through BOTH arms and land on the generic `error:success`
|
|
57
|
+
# token — the exact false negative this change removes. Pinned per status so the
|
|
58
|
+
# normalization cannot be dropped for one arm while the other keeps its fixture.
|
|
59
|
+
expect_token '{"type":"result","subtype":"success","is_error":false,"api_error_status":"401","result":"authentication rejected"}' auth
|
|
60
|
+
expect_token '{"type":"result","subtype":"success","is_error":false,"api_error_status":"429","result":"slow down"}' quota:
|
|
61
|
+
# A non-numeric status is not a status: it must not be coerced into either arm.
|
|
62
|
+
expect_token '{"type":"result","subtype":"error","is_error":true,"api_error_status":"unauthorized","result":"boom"}' error:
|
|
63
|
+
# Nor is a bool or a float. `int(True)` is 1 and `int(401.9)` truncates to 401,
|
|
64
|
+
# so a permissive coercion would let a value that is not a status code select the
|
|
65
|
+
# auth arm — pinned in both directions rather than left to the reader.
|
|
66
|
+
expect_token '{"type":"result","subtype":"error","is_error":true,"api_error_status":true,"result":"boom"}' error:
|
|
67
|
+
expect_token '{"type":"result","subtype":"error","is_error":true,"api_error_status":401.9,"result":"boom"}' error:
|
|
68
|
+
# The TEXT arm deliberately still requires an errored envelope, while the
|
|
69
|
+
# STRUCTURED arm does not. The asymmetry is the trust boundary: `api_error_status`
|
|
70
|
+
# is a transport field, `result` is model-controlled, so payload prose needs a
|
|
71
|
+
# second signal before it can select an auth decision. A challenge argued this
|
|
72
|
+
# shape reproduces the observed `error:success` defect; it does not — it exits 3
|
|
73
|
+
# ("envelope present, nothing classifiable"), a different path the caller is
|
|
74
|
+
# already told not to raw-text grep. Pinned so the boundary is a decision on
|
|
75
|
+
# record rather than an accident, in both directions.
|
|
76
|
+
expect_clean '{"type":"result","subtype":"success","is_error":false,"result":"Failed to authenticate. API Error: 401 OAuth access token has expired."}'
|
|
77
|
+
expect_token '{"type":"result","subtype":"error","is_error":true,"result":"P1: authentication failed when the reviewed session expires"}' error:
|
|
78
|
+
expect_token '{"type":"result","subtype":"error","is_error":true,"result":"Failed to authenticate after a dependency timeout. API Error: 500"}' error:
|
|
79
|
+
expect_token '{"type":"result","subtype":"error","is_error":true,"result":"review preamble\nFailed to authenticate. API Error: 401 OAuth access token has expired."}' error:
|
|
52
80
|
expect_token '{"type":"result","subtype":"error","is_error":true,"result":"API Error: rate limit exceeded"}' quota:
|
|
53
81
|
expect_token '{"type":"result","subtype":"success","is_error":false,"api_error_status":429,"result":"slow down"}' quota:
|
|
54
82
|
expect_token '{"type":"result","subtype":"success","is_error":false,"permission_denials":["Read"],"result":""}' permission_denied
|
|
@@ -105,6 +105,13 @@ run_fail challenge '{"subtype":"success","is_error":false,"structured_output":{"
|
|
|
105
105
|
run_fail challenge '{"type":"result","structured_output":{"mode":"challenge","findings":[]}}'
|
|
106
106
|
run_fail challenge '{"structured_output":{"mode":"challenge","findings":[]}}'
|
|
107
107
|
run_fail challenge '{"type":"result","subtype":"success","is_error":false,"api_error_status":429,"terminal_reason":"completed","structured_output":{"mode":"challenge","findings":[]}}'
|
|
108
|
+
# Twin of the 429 row for the status the envelope classifier now maps to `auth`.
|
|
109
|
+
# The ordering between this parser and that classifier is what decides whether a
|
|
110
|
+
# transport-flagged envelope can still be read as a completed verdict, and until
|
|
111
|
+
# now only 429 pinned it. Pinned here rather than left to prose: an envelope
|
|
112
|
+
# carrying ANY api_error_status is not a clean verdict, so adding an auth arm
|
|
113
|
+
# downstream cannot turn one into a verdict-dropping regression unnoticed.
|
|
114
|
+
run_fail challenge '{"type":"result","subtype":"success","is_error":false,"api_error_status":401,"terminal_reason":"completed","structured_output":{"mode":"challenge","findings":[]}}'
|
|
108
115
|
run_fail challenge '{"type":"result","subtype":"success","is_error":false,"terminal_reason":"max_turns","structured_output":{"mode":"challenge","findings":[]}}'
|
|
109
116
|
run_fail challenge '{"type":"result","subtype":"success","is_error":false,"permission_denials":["Read"],"structured_output":{"mode":"challenge","findings":[]}}'
|
|
110
117
|
run_fail challenge '{"type":"assistant","is_error":true,"structured_output":{"mode":"challenge","findings":[]}}'
|
package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh
CHANGED
|
@@ -658,7 +658,15 @@ case "$behavior" in
|
|
|
658
658
|
# group — it is TERM-immune and lives in its own session — so an unbounded loop
|
|
659
659
|
# is a wrapper that survives for days. Measured before this bound: 39 hours.
|
|
660
660
|
hang_left="${REVIEW_GATE_TEST_HANG_SECONDS:-300}"
|
|
661
|
+
hang_bound="$hang_left"
|
|
661
662
|
while [ "$hang_left" -gt 0 ]; do sleep 1; hang_left=$((hang_left-1)); done
|
|
663
|
+
# Record that the BOUND is what ended this process. Nothing else writes this file,
|
|
664
|
+
# and a signalled process never reaches the line, so its presence is a fact about
|
|
665
|
+
# which code path ran rather than an observation of when. The abort probe reads it
|
|
666
|
+
# to tell "the fixture's own bound expired" apart from "some reaper got here first"
|
|
667
|
+
# — a distinction it used to make by checking whether the process was still alive
|
|
668
|
+
# at one instant, which a corpse awaiting reaping answers wrongly.
|
|
669
|
+
printf '%s\n' "$hang_bound" >"$state/claude_hang_bound_reached"
|
|
662
670
|
) &
|
|
663
671
|
wait "$!"
|
|
664
672
|
;;
|
|
@@ -816,7 +824,12 @@ case "$behavior" in
|
|
|
816
824
|
# Bounded for the same reason as the claude stub's hang: an abort that removes
|
|
817
825
|
# the controller leaves this TERM-immune process with no other reaper.
|
|
818
826
|
hang_left="${REVIEW_GATE_TEST_HANG_SECONDS:-300}"
|
|
827
|
+
hang_bound="$hang_left"
|
|
819
828
|
while [ "$hang_left" -gt 0 ]; do sleep 1; hang_left=$((hang_left-1)); done
|
|
829
|
+
# Same bound-reached record as the claude stub, per client: the abort probe points
|
|
830
|
+
# its two jobs at different stubs, so each stub must be able to say for itself that
|
|
831
|
+
# its own bound is what ended it.
|
|
832
|
+
printf '%s\n' "$hang_bound" >"$state/${client}_hang_bound_reached"
|
|
820
833
|
;;
|
|
821
834
|
auth)
|
|
822
835
|
if [ "$host_attempted" = 1 ]; then
|