@ccoalm/ccl-skills 0.3.0 → 0.5.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.
Files changed (79) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +34 -3
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_abort_leak_state_helpers.sh +148 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +28 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +7 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +13 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate_abort_leak.sh +271 -34
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-data-acquisition.md +3 -1
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-disclosure-channels.md +2 -0
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +12 -12
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +8 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +7 -8
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +12 -0
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +3 -2
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +7 -6
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +18 -0
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +47 -2
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +28 -29
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +165 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +23 -10
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +303 -1
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +222 -91
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +12 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +85 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +123 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +6 -1
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_verdict_differential.sh +1 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_round_attribution.sh +12 -12
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +455 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_source_refuted.sh +20 -20
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_liveness_predicate_gate.sh +288 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +4 -4
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/deliverable-doc-genre-skeletons.md +133 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/doc-charter-first.md +2 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/figure-and-table-craft.md +318 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/AGENTS.md +46 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/doc-lint.py +246 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/figure-lint.py +1092 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/mutation_probe.sh +100 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/test_figure_and_doc_lint.sh +375 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/control.md +10 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/empty-header.md +6 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fake-header.md +13 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fenced-noise.md +14 -0
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-dangling.md +5 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan-captioned.md +11 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan.md +9 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig.png +0 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/imbalance.md +41 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/no-unit.md +8 -0
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/should-be-chart.md +11 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/tables-only-clean.md +35 -0
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/unfilled.md +7 -0
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/wide-table.md +5 -0
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/bad-viewbox.svg +9 -0
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/blackmarker.svg +9 -0
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/control.svg +12 -0
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/crossings.svg +12 -0
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/cvd-confusable.svg +9 -0
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/decorative-line.svg +10 -0
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-no-arrow.svg +11 -0
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-vague.svg +12 -0
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-contract.json +21 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-is-a-list.svg +12 -0
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/flow-mixed.svg +13 -0
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/low-contrast.svg +12 -0
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/malformed.svg +1 -0
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-aria.svg +9 -0
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-group.svg +10 -0
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-legend.svg +9 -0
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-title.svg +12 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-viewbox.svg +9 -0
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/offcontract-shape.svg +13 -0
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/overflow.svg +13 -0
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/transformed.svg +9 -0
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/ungrouped-card.svg +12 -0
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/unlabeled-edge.svg +13 -0
  78. package/dist/assets/release.json +276 -31
  79. package/package.json +1 -1
@@ -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.get("api_error_status")
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":[]}}'
@@ -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