oh-my-customcode 2.0.7 → 2.0.8
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 +3 -2
- package/dist/cli/index.js +28 -7
- package/dist/index.js +28 -7
- package/package.json +1 -1
- package/templates/.claude/hooks/hooks.json +28 -12
- package/templates/.claude/hooks/scripts/auto-continue-guard.sh +5 -5
- package/templates/.claude/hooks/scripts/auto-dev-token-tracker.sh +12 -1
- package/templates/.claude/hooks/scripts/bash-cd-advisor.sh +172 -0
- package/templates/.claude/hooks/scripts/fail-axis-cause-advisor.sh +2 -2
- package/templates/.claude/hooks/scripts/failure-ledger.sh +18 -0
- package/templates/.claude/hooks/scripts/file-change-validator.sh +6 -3
- package/templates/.claude/hooks/scripts/omcustom-auto-update.sh +19 -5
- package/templates/.claude/hooks/scripts/rule-deletion-guard.sh +118 -1
- package/templates/.claude/hooks/scripts/session-env-check.sh +20 -5
- package/templates/.claude/hooks/scripts/stall-detection-advisor.sh +9 -5
- package/templates/.claude/hooks/scripts/stuck-detector.sh +21 -4
- package/templates/.claude/hooks/scripts/subagent-failure-advisor.sh +48 -29
- package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +15 -8
- package/templates/.claude/ontology/agents.yaml +71 -7
- package/templates/.claude/ontology/graphs/agent-skill.json +64 -5
- package/templates/.claude/ontology/graphs/full-graph.json +236 -3
- package/templates/.claude/ontology/skills.yaml +65 -1
- package/templates/.claude/rules/MUST-enforcement-policy.md +3 -3
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +1 -1
- package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
- package/templates/.claude/settings.json +28 -12
- package/templates/.claude/skills/fsd/SKILL.md +10 -9
- package/templates/.claude/skills/goal/SKILL.md +5 -5
- package/templates/.claude/skills/omcustom-loop/SKILL.md +4 -4
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +13 -0
- package/templates/guides/claude-code/04-agent-skills.md +1 -1
- package/templates/guides/claude-code/15-version-compatibility.md +46 -46
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +13 -0
|
@@ -30,8 +30,20 @@ if ! [ -c /dev/tty ] 2>/dev/null; then
|
|
|
30
30
|
fi
|
|
31
31
|
|
|
32
32
|
# --- Configuration ---
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# Cache location. Deliberate difference from the TS resolver (src/utils/home.ts
|
|
34
|
+
# resolveHomeDir, which falls back to the OS/passwd home): when HOME is unset, empty or not
|
|
35
|
+
# an absolute path, this hook SKIPS the cache entirely (no read, no write, no mkdir) rather
|
|
36
|
+
# than resolving to "/.oh-my-customcode" or a passwd home (#1775).
|
|
37
|
+
CACHE_ENABLED=true
|
|
38
|
+
CACHE_DIR=""
|
|
39
|
+
CACHE_FILE=""
|
|
40
|
+
case "${HOME:-}" in
|
|
41
|
+
/*)
|
|
42
|
+
CACHE_DIR="$HOME/.oh-my-customcode"
|
|
43
|
+
CACHE_FILE="${CACHE_DIR}/self-update-cache.json"
|
|
44
|
+
;;
|
|
45
|
+
*) CACHE_ENABLED=false ;;
|
|
46
|
+
esac
|
|
35
47
|
CACHE_MAX_AGE=3600 # 1 hour in seconds
|
|
36
48
|
NPM_TIMEOUT=3 # seconds for npm view
|
|
37
49
|
INPUT_TIMEOUT=10 # seconds for user prompt
|
|
@@ -69,10 +81,12 @@ LATEST_VERSION=""
|
|
|
69
81
|
CACHE_HIT=false
|
|
70
82
|
|
|
71
83
|
# Ensure cache directory exists
|
|
72
|
-
|
|
84
|
+
if [ "$CACHE_ENABLED" = true ]; then
|
|
85
|
+
mkdir -p "$CACHE_DIR" 2>/dev/null
|
|
86
|
+
fi
|
|
73
87
|
|
|
74
88
|
# Check cache freshness
|
|
75
|
-
if [ -f "$CACHE_FILE" ]; then
|
|
89
|
+
if [ "$CACHE_ENABLED" = true ] && [ -f "$CACHE_FILE" ]; then
|
|
76
90
|
CACHE_TIMESTAMP=$(grep -o '"timestamp"[[:space:]]*:[[:space:]]*[0-9]*' "$CACHE_FILE" 2>/dev/null | grep -o '[0-9]*$')
|
|
77
91
|
CACHED_VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$CACHE_FILE" 2>/dev/null | grep -o '"[^"]*"$' | tr -d '"')
|
|
78
92
|
|
|
@@ -93,7 +107,7 @@ if [ "$CACHE_HIT" = false ]; then
|
|
|
93
107
|
LATEST_VERSION=$(timeout "$NPM_TIMEOUT" npm view "$PACKAGE_NAME" version 2>/dev/null || echo "")
|
|
94
108
|
|
|
95
109
|
# Update cache on successful fetch
|
|
96
|
-
if [ -n "$LATEST_VERSION" ]; then
|
|
110
|
+
if [ -n "$LATEST_VERSION" ] && [ "$CACHE_ENABLED" = true ]; then
|
|
97
111
|
NOW=$(date +%s)
|
|
98
112
|
cat > "$CACHE_FILE" << EOF
|
|
99
113
|
{
|
|
@@ -9,7 +9,31 @@
|
|
|
9
9
|
# destructive verb, destructive verb in a non-head position, unresolvable expansion in a target,
|
|
10
10
|
# opaque constructs such as eval / sh -c / $(...) / backticks / source, parse errors, unknown cwd,
|
|
11
11
|
# analyzer exceptions) -> strict legacy regex check (blocks if the pre-#1782 logic would block).
|
|
12
|
+
# Opaque markers are looked for outside quoted text (#1859): markers inside a quoted string whose last
|
|
13
|
+
# path component contains whitespace no longer force the fallback (the shell does not run them as a
|
|
14
|
+
# command); single-quoted `$(`/backticks are likewise ignored. `$(` and backticks inside double quotes,
|
|
15
|
+
# and any other quoted string (it may be the command, e.g. "eval", "/my dir/bash"), stay opaque.
|
|
16
|
+
# The fallback fires only if the raw command AND its masked copy both match.
|
|
12
17
|
# No python3: legacy regex check (needs jq). Neither python3 nor jq: fail-open (exit 0).
|
|
18
|
+
#
|
|
19
|
+
# Intended limitation (#1784): the blocked set is the one modelled above (v1.1.95, #1782: rm/unlink/
|
|
20
|
+
# rmdir/shred/truncate, mv, find, xargs, git rm|mv|clean, writing redirects); the exclusion of the
|
|
21
|
+
# overwrite/interpreter forms is recorded at
|
|
22
|
+
# https://github.com/baekenough/oh-my-customcode/issues/1784#issuecomment-6405528206 . Content-overwrite
|
|
23
|
+
# forms (cp /dev/null, tee, dd of=, sed -i, ln -sf, install, rsync --delete, tar --remove-files,
|
|
24
|
+
# git checkout -- / restore / stash), interpreter one-liners (perl -e, python -c, node -e) and shell
|
|
25
|
+
# aliases and git clean -f without -d run from an ancestor of a rule dir (e.g. the project root, where it can still remove untracked files inside the rule dirs; a target or cwd inside a rule dir is blocked) are NOT blocked: they are edit-class or interpreter forms whose blocking
|
|
26
|
+
# would raise false positives like #1859, and git history is the recovery path.
|
|
27
|
+
#
|
|
28
|
+
# Residual limits accepted with the #1859 change: (a) a quoted payload that carries an opaque marker
|
|
29
|
+
# and is handed to an executor the analyzer does not model (ssh, su -c, script -c, ..., or stdin piped
|
|
30
|
+
# to a shell: `| bash`, `| sh`) is no longer forced to the fallback, so it is not inspected - the same
|
|
31
|
+
# as such payloads without a marker always were; `env -S` / `--split-string` is likewise not parsed:
|
|
32
|
+
# unwrap() sends it to the legacy regex check only; (b) the quoted-option gap predates it: `bash "-c"
|
|
33
|
+
# ...` is not recognised as `-c`, because the marker pattern needs whitespace right before an unquoted
|
|
34
|
+
# `-`; (c) a quoted string is treated as text
|
|
35
|
+
# only when its last path component (after the final `/`) contains whitespace, so a lone quoted word
|
|
36
|
+
# or path to a binary stays visible and still falls back (conservative).
|
|
13
37
|
|
|
14
38
|
set -uo pipefail
|
|
15
39
|
|
|
@@ -259,6 +283,97 @@ def low(s):
|
|
|
259
283
|
return s.lower()
|
|
260
284
|
|
|
261
285
|
|
|
286
|
+
def _blank_quoted(body, double):
|
|
287
|
+
"""Blank the inside of one quoted string for the OPAQUE scan (#1859).
|
|
288
|
+
|
|
289
|
+
A string whose last path component (after the final `/`) contains whitespace cannot name a shell
|
|
290
|
+
binary or `eval`/`source`, so the markers in it are not run as a command by that shell: everything
|
|
291
|
+
is blanked, except `$(` and backticks inside double quotes (the shell expands those). This does
|
|
292
|
+
not make the text safe: a payload handed to another executor (ssh, su -c, ...) is not inspected.
|
|
293
|
+
Any other string may BE the command (`"eval"`, `'sh'`, `"/my dir/bash"`), so it stays visible;
|
|
294
|
+
only the `$(`/backtick of a single-quoted one, which the shell never expands, is neutralised.
|
|
295
|
+
"""
|
|
296
|
+
if not any(ch.isspace() for ch in body.rsplit('/', 1)[-1]):
|
|
297
|
+
return body if double else body.replace('$(', '_(').replace('`', '_')
|
|
298
|
+
if not double:
|
|
299
|
+
return '_' * len(body)
|
|
300
|
+
res, k = [], 0
|
|
301
|
+
while k < len(body):
|
|
302
|
+
if body.startswith('$(', k):
|
|
303
|
+
res.append('$(')
|
|
304
|
+
k += 2
|
|
305
|
+
else:
|
|
306
|
+
res.append(body[k] if body[k] == '`' else '_')
|
|
307
|
+
k += 1
|
|
308
|
+
return ''.join(res)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def mask_quoted(s):
|
|
312
|
+
"""Copy of `s` for the OPAQUE scan with non-executable quoted text blanked (#1859).
|
|
313
|
+
|
|
314
|
+
Mirrors the quote handling of lex(). When unsure (heredoc operator, unterminated quote) the rest
|
|
315
|
+
of the text is returned unmasked, so the scan is then at least as strict as on the raw command.
|
|
316
|
+
Unquoted text, comments and `$'...'` strings are never touched.
|
|
317
|
+
"""
|
|
318
|
+
out, n, i = [], len(s), 0
|
|
319
|
+
wordstart = True
|
|
320
|
+
while i < n:
|
|
321
|
+
c = s[i]
|
|
322
|
+
if c in ' \t\n':
|
|
323
|
+
out.append(c)
|
|
324
|
+
i += 1
|
|
325
|
+
wordstart = True
|
|
326
|
+
elif s.startswith('<<<', i):
|
|
327
|
+
out.append('<<<')
|
|
328
|
+
i += 3
|
|
329
|
+
wordstart = True
|
|
330
|
+
elif s.startswith('<<', i):
|
|
331
|
+
return ''.join(out) + s[i:] # heredoc body is skipped by lex(): quote state is unknowable
|
|
332
|
+
elif c in ';&|<>()':
|
|
333
|
+
out.append(c)
|
|
334
|
+
i += 1
|
|
335
|
+
wordstart = True
|
|
336
|
+
elif c == '#' and wordstart:
|
|
337
|
+
j = s.find('\n', i)
|
|
338
|
+
j = n if j < 0 else j
|
|
339
|
+
out.append(s[i:j])
|
|
340
|
+
i = j
|
|
341
|
+
elif c == '\\':
|
|
342
|
+
out.append(s[i:i + 2])
|
|
343
|
+
i += 2
|
|
344
|
+
wordstart = False
|
|
345
|
+
elif c == '$' and s.startswith("$'", i):
|
|
346
|
+
j = i + 2
|
|
347
|
+
while j < n and s[j] != "'":
|
|
348
|
+
j += 2 if s[j] == '\\' else 1
|
|
349
|
+
if j >= n:
|
|
350
|
+
return ''.join(out) + s[i:]
|
|
351
|
+
out.append(s[i:j + 1])
|
|
352
|
+
i = j + 1
|
|
353
|
+
wordstart = False
|
|
354
|
+
elif c == "'":
|
|
355
|
+
j = s.find("'", i + 1)
|
|
356
|
+
if j < 0:
|
|
357
|
+
return ''.join(out) + s[i:]
|
|
358
|
+
out.append("'" + _blank_quoted(s[i + 1:j], False) + "'")
|
|
359
|
+
i = j + 1
|
|
360
|
+
wordstart = False
|
|
361
|
+
elif c == '"':
|
|
362
|
+
j = i + 1
|
|
363
|
+
while j < n and s[j] != '"':
|
|
364
|
+
j += 2 if s[j] == '\\' and j + 1 < n and s[j + 1] in '$`"\\\n' else 1
|
|
365
|
+
if j >= n:
|
|
366
|
+
return ''.join(out) + s[i:]
|
|
367
|
+
out.append('"' + _blank_quoted(s[i + 1:j], True) + '"')
|
|
368
|
+
i = j + 1
|
|
369
|
+
wordstart = False
|
|
370
|
+
else:
|
|
371
|
+
out.append(c)
|
|
372
|
+
i += 1
|
|
373
|
+
wordstart = False
|
|
374
|
+
return ''.join(out)
|
|
375
|
+
|
|
376
|
+
|
|
262
377
|
def main(d, cmd):
|
|
263
378
|
start = d.get('cwd')
|
|
264
379
|
if isinstance(start, str) and os.path.isdir(start):
|
|
@@ -280,7 +395,9 @@ def main(d, cmd):
|
|
|
280
395
|
low(os.path.join(root, 'templates', '.claude', 'rules'))]
|
|
281
396
|
|
|
282
397
|
cmd = cmd.replace('\r', '').replace('\\\n', '')
|
|
283
|
-
|
|
398
|
+
# Fall back only when BOTH scans match: masking must never add a fallback the raw scan lacks
|
|
399
|
+
# (blanking `; | & \n` inside quotes can otherwise create an `sh ... -c` match, #1859 review F1).
|
|
400
|
+
if OPAQUE.search(cmd) and OPAQUE.search(mask_quoted(cmd)):
|
|
284
401
|
return 3
|
|
285
402
|
try:
|
|
286
403
|
toks = lex(cmd)
|
|
@@ -99,7 +99,14 @@ fi
|
|
|
99
99
|
|
|
100
100
|
# Drift Detection: compare git HEAD between sessions
|
|
101
101
|
DRIFT_STATUS="not-git"
|
|
102
|
-
|
|
102
|
+
# When HOME is unset, empty or not an absolute path, skip the session-state write entirely
|
|
103
|
+
# (no mkdir, no state file; drift stays "not-git") instead of aborting under set -eu or
|
|
104
|
+
# writing to "/.claude/..." (#1775; same rule as the self-update cache below).
|
|
105
|
+
HOME_ABSOLUTE=false
|
|
106
|
+
case "${HOME:-}" in
|
|
107
|
+
/*) HOME_ABSOLUTE=true ;;
|
|
108
|
+
esac
|
|
109
|
+
if [ "$HOME_ABSOLUTE" = true ] && command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
103
110
|
SESSION_STATE_DIR="$HOME/.claude/session-state"
|
|
104
111
|
mkdir -p "$SESSION_STATE_DIR"
|
|
105
112
|
|
|
@@ -132,8 +139,10 @@ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/n
|
|
|
132
139
|
fi
|
|
133
140
|
|
|
134
141
|
# --- CI Status Check ---
|
|
135
|
-
# Check last CI run status if gh CLI is available
|
|
136
|
-
|
|
142
|
+
# Check last CI run status if gh CLI is available.
|
|
143
|
+
# Skipped when HOME is not absolute (HOME_ABSOLUTE, set above): external CLIs keep state under
|
|
144
|
+
# $HOME and would write it into the cwd (the project dir) instead (#1775).
|
|
145
|
+
if [ "$HOME_ABSOLUTE" = true ] && command -v gh &>/dev/null; then
|
|
137
146
|
ci_status=$(gh run list --limit 1 --json conclusion -q '.[0].conclusion' 2>/dev/null || echo "unknown")
|
|
138
147
|
ci_name=$(gh run list --limit 1 --json name -q '.[0].name' 2>/dev/null || echo "unknown")
|
|
139
148
|
if [ "$ci_status" = "failure" ]; then
|
|
@@ -179,8 +188,14 @@ fi
|
|
|
179
188
|
# the hard-coded "latestVersion" lookup matched nothing. Read `latestVersion` first, then fall
|
|
180
189
|
# back to `version`. The `"version"` pattern includes the opening quote, so it cannot
|
|
181
190
|
# accidentally match the tail of `"latestVersion"`.
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
# Deliberate difference from the TS resolver (src/utils/home.ts resolveHomeDir falls back to
|
|
192
|
+
# the OS/passwd home): when HOME is unset, empty or not an absolute path, skip the cache
|
|
193
|
+
# entirely instead of reading "/.oh-my-customcode/..." (#1775).
|
|
194
|
+
CACHE_FILE=""
|
|
195
|
+
case "${HOME:-}" in
|
|
196
|
+
/*) CACHE_FILE="$HOME/.oh-my-customcode/self-update-cache.json" ;;
|
|
197
|
+
esac
|
|
198
|
+
if [ -n "$CACHE_FILE" ] && [ -f "$CACHE_FILE" ]; then
|
|
184
199
|
CACHED_LATEST=$(json_string_field "$CACHE_FILE" "latestVersion")
|
|
185
200
|
if [ -z "$CACHED_LATEST" ]; then
|
|
186
201
|
CACHED_LATEST=$(json_string_field "$CACHE_FILE" "version")
|
|
@@ -11,9 +11,11 @@ set -euo pipefail
|
|
|
11
11
|
# array/scalar payloads gave rc=5 (unguarded `jq` agent_type extraction); a well-formed payload with a
|
|
12
12
|
# 300KB `.description` gave rc=141 (5/5; 70KB rc=0; boundary near 81,920 bytes here, platform-dependent; `| head -c 80` SIGPIPE under pipefail). CC sends objects and
|
|
13
13
|
# SubagentStop has no `.description`, so these paths are not reached at runtime.
|
|
14
|
-
# Start-record `start_epoch
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Start-record `start_epoch` (also the still-running-agent `running_start`): a value that is not a
|
|
15
|
+
# positive decimal integer without a leading zero ("", "0", "null", "abc", "1792939922.5", "09", "-5")
|
|
16
|
+
# is treated as no start record: rc=0, nothing recorded for the completing agent, and the loop skips
|
|
17
|
+
# that running entry (measured on bash 3.2; before this check "abc"/"1792939922.5"/"09" gave rc=1 and
|
|
18
|
+
# "" recorded now_epoch, about 1.79e9, as the duration).
|
|
17
19
|
|
|
18
20
|
# ORDERING: This hook MUST run AFTER task-outcome-recorder.sh in hooks.json SubagentStop array.
|
|
19
21
|
# Reason: This script removes consumed entries from AGENT_START_FILE; task-outcome-recorder reads them first.
|
|
@@ -36,7 +38,9 @@ description=$(printf '%s\n' "$input" | jq -r '.description // ""' | head -c 80)
|
|
|
36
38
|
# --- 2. Calculate duration from start record ---
|
|
37
39
|
start_epoch=$(grep -F "\"agent_type\":\"${agent_type}\"" "$AGENT_START_FILE" 2>/dev/null | tail -1 | jq -r '.start_epoch // "0"' 2>/dev/null || echo "0")
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
# Only a positive decimal integer without a leading zero is a usable epoch; anything else
|
|
42
|
+
# ("", "0", "null", "abc", "1792939922.5", "09", negatives) is treated as no start record.
|
|
43
|
+
if ! [[ "$start_epoch" =~ ^[1-9][0-9]*$ ]]; then
|
|
40
44
|
printf '%s\n' "$input"
|
|
41
45
|
exit 0
|
|
42
46
|
fi
|
|
@@ -70,7 +74,7 @@ if [ "$completed_count" -ge 1 ]; then
|
|
|
70
74
|
running_desc=$(printf '%s\n' "$line" | jq -r '.description // ""' 2>/dev/null || true)
|
|
71
75
|
running_model=$(printf '%s\n' "$line" | jq -r '.model // "inherit"' 2>/dev/null || true)
|
|
72
76
|
|
|
73
|
-
if [ "$running_start"
|
|
77
|
+
if ! [[ "$running_start" =~ ^[1-9][0-9]*$ ]]; then continue; fi
|
|
74
78
|
|
|
75
79
|
elapsed=$((now_epoch - running_start))
|
|
76
80
|
|
|
@@ -545,6 +545,7 @@ extraction_mode=$(printf '%s' "$input" | jq -er '
|
|
|
545
545
|
elif ((.tool_name // "unknown" | type) != "string") then "legacy"
|
|
546
546
|
elif ((.tool_input != null) and ((.tool_input | type) != "object")) then "legacy"
|
|
547
547
|
elif ((.tool_output != null) and ((.tool_output | type) != "object")) then "legacy"
|
|
548
|
+
elif ((.tool_response != null) and ((.tool_response | type) != "object")) then "legacy"
|
|
548
549
|
elif ((.tool_input.file_path // "" | type) != "string")
|
|
549
550
|
or ((.tool_input.command // "" | type) != "string")
|
|
550
551
|
or ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "" | type) != "string")
|
|
@@ -579,6 +580,22 @@ _read_stuck_fields() {
|
|
|
579
580
|
|
|
580
581
|
TARGET_KEY_CAP=1000
|
|
581
582
|
|
|
583
|
+
# PostToolUse carries "tool_response", never "tool_output" (#1775; probe: CC
|
|
584
|
+
# 2.1.310, "claude -p", one run, model haiku). Measured Bash shape: object
|
|
585
|
+
# {interrupted,isImage,noOutputExpected,stderr,stdout}; no measured PostToolUse
|
|
586
|
+
# shape has is_error. Bash failures arrive via PostToolUseFailure (a Bash exit 3
|
|
587
|
+
# produced PostToolUseFailure with error "Exit code 3"), which this hook is not
|
|
588
|
+
# wired to, so the is_error path stays inert for Bash. Read tool_response first;
|
|
589
|
+
# ".tool_output" is a legacy fallback only. A non-object tool_response routes to
|
|
590
|
+
# the "legacy" extraction path (see extraction_mode) and is used as text only
|
|
591
|
+
# when it is a string.
|
|
592
|
+
JQ_IS_ERROR='((.tool_response | if type == "object" then .is_error else null end) // .tool_output.is_error // false)'
|
|
593
|
+
JQ_OUTPUT_PREVIEW='((.tool_response) as $r
|
|
594
|
+
| if ($r | type) == "object" and (($r.stdout | type) == "string") then $r.stdout
|
|
595
|
+
elif ($r | type) == "string" then $r
|
|
596
|
+
else (.tool_output.output // "") end
|
|
597
|
+
| tostring)[0:200]'
|
|
598
|
+
|
|
582
599
|
truncate_key() {
|
|
583
600
|
local v="$1"
|
|
584
601
|
if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
|
|
@@ -595,7 +612,7 @@ if [ "$extraction_mode" = "batched" ]; then
|
|
|
595
612
|
(if (.tool_name // "unknown") == "Bash"
|
|
596
613
|
then (.tool_input.command // .tool_input.file_path // "")
|
|
597
614
|
else (.tool_input.file_path // "") end), "\u0000",
|
|
598
|
-
|
|
615
|
+
'"$JQ_IS_ERROR"', "\u0000"'
|
|
599
616
|
[ "${#STUCK_FIELDS[@]}" -eq 3 ] || exit 1
|
|
600
617
|
tool_name="${STUCK_FIELDS[0]}"
|
|
601
618
|
if [ "$tool_name" = "Bash" ]; then
|
|
@@ -608,7 +625,7 @@ if [ "$extraction_mode" = "batched" ]; then
|
|
|
608
625
|
fi
|
|
609
626
|
is_error="${STUCK_FIELDS[2]}"
|
|
610
627
|
output_preview=$(printf '%s' "$input" \
|
|
611
|
-
| jq -r
|
|
628
|
+
| jq -r "$JQ_OUTPUT_PREVIEW" \
|
|
612
629
|
| head -c 200 || true)
|
|
613
630
|
_read_stuck_fields '(.tool_input.command // ""), "\u0000",
|
|
614
631
|
(if (.tool_name == "Edit" or .tool_name == "Write")
|
|
@@ -653,7 +670,7 @@ else
|
|
|
653
670
|
file_path=$(printf '%s' "$file_path_full" | head -c "$TARGET_KEY_CAP" || true)
|
|
654
671
|
target_key=$(truncate_key "$file_path_full")
|
|
655
672
|
fi
|
|
656
|
-
is_error=$(printf '%s' "$input" | jq -r
|
|
673
|
+
is_error=$(printf '%s' "$input" | jq -r "$JQ_IS_ERROR")
|
|
657
674
|
# "head -c 200" closes the pipe after 200 bytes. Once tool_output.output is
|
|
658
675
|
# larger than the pipe buffer (~64 KB), jq keeps writing, dies of SIGPIPE
|
|
659
676
|
# (exit 141), and "set -o pipefail" + "set -e" aborted the ENTIRE hook before
|
|
@@ -664,7 +681,7 @@ is_error=$(printf '%s' "$input" | jq -r '.tool_output.is_error // false')
|
|
|
664
681
|
# enforced by head (jq slices by codepoint, head by byte, and 200 codepoints
|
|
665
682
|
# are always >= 200 bytes, so the resulting bytes are unchanged).
|
|
666
683
|
output_preview=$(printf '%s' "$input" \
|
|
667
|
-
| jq -r
|
|
684
|
+
| jq -r "$JQ_OUTPUT_PREVIEW" \
|
|
668
685
|
| head -c 200 || true)
|
|
669
686
|
raw_command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
|
|
670
687
|
|
|
@@ -5,38 +5,57 @@ set -euo pipefail
|
|
|
5
5
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
6
6
|
|
|
7
7
|
# Subagent Failure Advisor Hook
|
|
8
|
-
# Trigger:
|
|
9
|
-
# Purpose: Advisory-only
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# (
|
|
24
|
-
#
|
|
8
|
+
# Trigger: PostToolUseFailure (matcher: Agent / Task tool)
|
|
9
|
+
# Purpose: Advisory-only. When an Agent/Task tool call fails, tell the model not to
|
|
10
|
+
# auto-continue as if the subagent had succeeded. Replaces the removed
|
|
11
|
+
# `type: "prompt"` SubagentStop hook (#1631), whose LLM judge was structurally
|
|
12
|
+
# always "not yet completed" and produced a Stop-hook-feedback re-injection loop.
|
|
13
|
+
# Why not SubagentStop (#1775): the SubagentStop payload has no tool_response /
|
|
14
|
+
# tool_output / is_error (measured keys on CC 2.1.310, `claude -p`, haiku, one run, see
|
|
15
|
+
# https://github.com/baekenough/oh-my-customcode/issues/1775#issuecomment-6404861043: agent_id, agent_transcript_path, agent_type,
|
|
16
|
+
# background_tasks, cwd, effort, hook_event_name, last_assistant_message,
|
|
17
|
+
# permission_mode, prompt_id, session_crons, session_id, stop_hook_active,
|
|
18
|
+
# transcript_path), so a failure check there can never fire.
|
|
19
|
+
# Input (PostToolUseFailure; measured on CC 2.1.310, `claude -p`, haiku, one run, unknown
|
|
20
|
+
# subagent_type; additionalContext delivery evidence is weak because the probe prompt named
|
|
21
|
+
# the marker; measured fields only: tool_name, is_interrupt, error (string), see
|
|
22
|
+
# https://github.com/baekenough/oh-my-customcode/issues/1775#issuecomment-6405197553).
|
|
23
|
+
# Other fields (tool_input sub-fields, duration_ms, tool_use_id, hook_event_name, ...) come from
|
|
24
|
+
# the PostToolUseFailure input table in https://code.claude.com/docs/en/hooks.md, not measured.
|
|
25
|
+
# Output: stdout JSON {"hookSpecificOutput":{"hookEventName":"PostToolUseFailure",
|
|
26
|
+
# "additionalContext": "..."}} -- observed as delivered to the model (weak evidence, see above).
|
|
27
|
+
# Never a `decision` field; the only explicit exit is exit 0. Silent for interrupts, non-Agent/Task tools,
|
|
28
|
+
# missing error, or non-object stdin.
|
|
25
29
|
|
|
26
30
|
input=$(cat)
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
# Single jq pass; any parse failure or non-object stdin -> silent exit 0.
|
|
33
|
+
fields=$(printf '%s' "$input" | jq -r '
|
|
34
|
+
if type == "object" then
|
|
35
|
+
[ (.tool_name // ""),
|
|
36
|
+
(if .is_interrupt == true then "1" else "0" end),
|
|
37
|
+
((.tool_input.subagent_type // "unknown") | tostring | .[0:80]),
|
|
38
|
+
((.tool_input.description // "") | tostring | gsub("[\n\r\t]"; " ") | .[0:120]),
|
|
39
|
+
((.error // "") | tostring | gsub("[\n\r\t]"; " ") | .[0:200])
|
|
40
|
+
] | join("\u001f")
|
|
41
|
+
else empty end' 2>/dev/null || true)
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // .agent_type // "unknown"' 2>/dev/null || echo "unknown")
|
|
32
|
-
error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output // ""' 2>/dev/null | head -c 200)
|
|
33
|
-
echo "" >&2
|
|
34
|
-
echo "--- [Subagent Failure Advisor] background subagent (${agent_type}) reported an error ---" >&2
|
|
35
|
-
echo " ${error_summary}" >&2
|
|
36
|
-
echo " If it FAILED, do NOT auto-continue -- report the failure and wait for user input." >&2
|
|
37
|
-
echo "-----------------------------------------------------------------------------------" >&2
|
|
38
|
-
fi
|
|
43
|
+
[ -n "$fields" ] || exit 0
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
IFS=$'\x1f' read -r tool_name is_interrupt subagent_type description error_text <<EOT
|
|
46
|
+
$fields
|
|
47
|
+
EOT
|
|
48
|
+
|
|
49
|
+
case "$tool_name" in
|
|
50
|
+
Agent|Task) ;;
|
|
51
|
+
*) exit 0 ;;
|
|
52
|
+
esac
|
|
53
|
+
[ "$is_interrupt" = "0" ] || exit 0
|
|
54
|
+
[ -n "$error_text" ] || exit 0
|
|
55
|
+
|
|
56
|
+
msg="[Subagent Failure Advisor] ${tool_name} call failed (subagent_type=${subagent_type}"
|
|
57
|
+
[ -z "$description" ] || msg="${msg}, description=${description}"
|
|
58
|
+
msg="${msg}): ${error_text}. Fix the arguments (e.g. the subagent_type) and retry once; if the failure is a permission denial or repeats, report it and do not pretend the subagent ran."
|
|
59
|
+
|
|
60
|
+
jq -n --arg ctx "$msg" '{hookSpecificOutput:{hookEventName:"PostToolUseFailure",additionalContext:$ctx}}' 2>/dev/null || true
|
|
42
61
|
exit 0
|
|
@@ -10,9 +10,10 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
10
10
|
# Protocol: stdin JSON -> process -> stdout pass-through
|
|
11
11
|
# Exit status (MEASURED, bash 3.2, `set -euo pipefail`): the only explicit exits are `exit 0`, but
|
|
12
12
|
# there is no trap, so it is NOT guaranteed. rc=0 for a well-formed payload, malformed JSON, empty
|
|
13
|
-
# stdin, array/scalar payloads and a corrupt AGENT_START_FILE
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# stdin, array/scalar payloads and a corrupt AGENT_START_FILE. A start_epoch that is not a positive
|
|
14
|
+
# decimal integer without a leading zero ("", "0", "null", "abc", "1792939922.5", "09", "-5") is
|
|
15
|
+
# treated as no start record: rc=0 and duration_seconds 0 (measured on bash 3.2; before this check
|
|
16
|
+
# "abc" gave rc=1 and "" recorded now_epoch, about 1.79e9, as the duration).
|
|
16
17
|
#
|
|
17
18
|
# MEASURED SubagentStop payload (CC 2.1.259 embedded hook schema; #1656 B):
|
|
18
19
|
# session_id, transcript_path, cwd, prompt_id?, permission_mode?, effort?,
|
|
@@ -35,8 +36,8 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
35
36
|
# Outcome caveat: SubagentStop exposes no failure signal, so entries recorded
|
|
36
37
|
# from that event are always outcome=success. There is no working hand-off for
|
|
37
38
|
# that gap today: SubagentStop payload carries no failure signal;
|
|
38
|
-
# subagent-failure-advisor.sh
|
|
39
|
-
#
|
|
39
|
+
# subagent-failure-advisor.sh reads `.error` from PostToolUseFailure (Agent/Task) payloads,
|
|
40
|
+
# not from SubagentStop, so this recorder still has no failure hand-off (#1656 G, #1775).
|
|
40
41
|
|
|
41
42
|
input=$(cat)
|
|
42
43
|
|
|
@@ -65,8 +66,11 @@ description=$(printf '%s\n' "$input" | jq -r '
|
|
|
65
66
|
|
|
66
67
|
# Extract skill name from description or prompt
|
|
67
68
|
skill_name=""
|
|
68
|
-
# `|| true`: a no-match grep exits 1 and `set -euo pipefail` would abort the hook
|
|
69
|
-
#
|
|
69
|
+
# `|| true`: a no-match grep exits 1 and `set -euo pipefail` would abort the hook.
|
|
70
|
+
# Two guards follow. The `|| true` at the end of the `grep -oiE '[a-z]+-[a-z]+...'` line inside the
|
|
71
|
+
# `if` below covers the case where the text passes the `grep -qiE` gate but not the name pattern.
|
|
72
|
+
# The `|| true` at the end of the later `grep -oiE 'Skill:...'` line covers a payload with no
|
|
73
|
+
# "Skill:" text, e.g. {"agent_type":"X"} (rc=1 without it).
|
|
70
74
|
# Empty stdin now exits at the non-object guard above.
|
|
71
75
|
if echo "$description" | grep -qiE '(skill:|routing|→.*skill)'; then
|
|
72
76
|
skill_name=$(echo "$description" | grep -oiE '[a-z]+-[a-z]+(-[a-z]+)*-?(routing|skill|practices|detection|decomposition|orchestration|pipeline|guards|cycle|plan|review|refactor|publish|version|audit|exec|analyze|bundle|report|setup|watch|lists|status|help|save|recall)' | head -1 || true)
|
|
@@ -159,7 +163,10 @@ AGENT_START_FILE="/tmp/.claude-agent-starts-${PPID}"
|
|
|
159
163
|
duration_seconds=0
|
|
160
164
|
if [ -f "$AGENT_START_FILE" ]; then
|
|
161
165
|
start_epoch=$(grep -F "\"agent_type\":\"${agent_type}\"" "$AGENT_START_FILE" 2>/dev/null | tail -1 | jq -r '.start_epoch // "0"' 2>/dev/null || echo "0")
|
|
162
|
-
|
|
166
|
+
# Only a positive decimal integer without a leading zero is a usable epoch. Anything else
|
|
167
|
+
# ("", "0", "null", "abc", "1792939922.5", "09", negatives) takes the no-start-record path
|
|
168
|
+
# (duration 0): bash arithmetic would otherwise read "" as 0 and record now_epoch as the duration.
|
|
169
|
+
if [[ "$start_epoch" =~ ^[1-9][0-9]*$ ]]; then
|
|
163
170
|
now_epoch=$(date +%s)
|
|
164
171
|
duration_seconds=$((now_epoch - start_epoch))
|
|
165
172
|
fi
|
|
@@ -9,16 +9,16 @@ classes:
|
|
|
9
9
|
agents: [lang-golang-expert, lang-python-expert, lang-rust-expert, lang-kotlin-expert, lang-typescript-expert, lang-java-expert]
|
|
10
10
|
description: "Programming language specialists"
|
|
11
11
|
BackendExpert:
|
|
12
|
-
agents: [be-fastapi-expert, be-springboot-expert, be-go-backend-expert, be-express-expert, be-nestjs-expert]
|
|
12
|
+
agents: [be-fastapi-expert, be-springboot-expert, be-go-backend-expert, be-express-expert, be-nestjs-expert, be-django-expert]
|
|
13
13
|
description: "Backend framework specialists"
|
|
14
14
|
FrontendExpert:
|
|
15
|
-
agents: [fe-vercel-agent, fe-vuejs-agent, fe-svelte-agent, fe-flutter-agent]
|
|
15
|
+
agents: [fe-vercel-agent, fe-vuejs-agent, fe-svelte-agent, fe-flutter-agent, fe-design-expert]
|
|
16
16
|
description: "Frontend framework specialists"
|
|
17
17
|
DataEngineer:
|
|
18
18
|
agents: [de-airflow-expert, de-dbt-expert, de-spark-expert, de-kafka-expert, de-snowflake-expert, de-pipeline-expert]
|
|
19
19
|
description: "Data engineering specialists"
|
|
20
20
|
DatabaseExpert:
|
|
21
|
-
agents: [db-postgres-expert, db-redis-expert, db-supabase-expert]
|
|
21
|
+
agents: [db-postgres-expert, db-redis-expert, db-supabase-expert, db-alembic-expert]
|
|
22
22
|
description: "Database specialists"
|
|
23
23
|
ToolingExpert:
|
|
24
24
|
agents: [tool-npm-expert, tool-optimizer, tool-bun-expert]
|
|
@@ -103,14 +103,14 @@ agents:
|
|
|
103
103
|
|
|
104
104
|
lang-java-expert:
|
|
105
105
|
class: LanguageExpert
|
|
106
|
-
description: "Expert Java
|
|
106
|
+
description: "Expert Java 25 LTS developer for modern Java with Virtual Threads, Pattern Matching, Record Patterns, and Sequenced Collections. Use for Java files (*.java, pom.xml, build.gradle), Java-related keywords, high-concurrency systems, data-centric APIs with Records, and migrating from older Java versions."
|
|
107
107
|
model: sonnet
|
|
108
108
|
memory: project
|
|
109
109
|
effort: high
|
|
110
|
-
skills: []
|
|
110
|
+
skills: [java-best-practices]
|
|
111
111
|
tools: [Read, Write, Edit, Grep, Glob, Bash]
|
|
112
|
-
summary: "Java
|
|
113
|
-
keywords: [java,
|
|
112
|
+
summary: "Java 25 LTS expert for modern features including virtual threads and pattern matching"
|
|
113
|
+
keywords: [java, java25, virtual-threads, pattern-matching, records, sequenced-collections]
|
|
114
114
|
file_patterns: ["*.java", "pom.xml", "build.gradle", "build.gradle.kts"]
|
|
115
115
|
|
|
116
116
|
be-fastapi-expert:
|
|
@@ -544,3 +544,67 @@ agents:
|
|
|
544
544
|
summary: "Task tracker with proactive reminders for TODO management"
|
|
545
545
|
keywords: [todo, tasks, reminders, tracking, deadlines, project-momentum]
|
|
546
546
|
file_patterns: ["TODO.md", "*.todo"]
|
|
547
|
+
|
|
548
|
+
agora-runner:
|
|
549
|
+
description: "Executes exactly one agora session step (start, one round, or finish) via the skill scripts and returns only a verdict summary. Never returns reviewer text, vendor attribution or sealed paths."
|
|
550
|
+
model: claude-sonnet-5-5
|
|
551
|
+
skills: [agora]
|
|
552
|
+
tools: [Bash, Read, Write, Glob]
|
|
553
|
+
|
|
554
|
+
be-django-expert:
|
|
555
|
+
class: BackendExpert
|
|
556
|
+
description: "Expert Django developer for production-ready Python web applications. Use for Django projects, models/views/templates, Django REST Framework, authentication, admin customization, and deployment optimization."
|
|
557
|
+
model: claude-sonnet-5-5
|
|
558
|
+
memory: local
|
|
559
|
+
effort: high
|
|
560
|
+
skills: [django-best-practices]
|
|
561
|
+
tools: [Read, Write, Edit, Grep, Glob, Bash]
|
|
562
|
+
|
|
563
|
+
db-alembic-expert:
|
|
564
|
+
class: DatabaseExpert
|
|
565
|
+
description: "Alembic migration specialist for generating, reviewing, fixing, and advising on SQLAlchemy database migrations"
|
|
566
|
+
model: claude-opus-5-5
|
|
567
|
+
memory: local
|
|
568
|
+
effort: high
|
|
569
|
+
skills: [alembic-best-practices, postgres-best-practices]
|
|
570
|
+
tools: [Read, Write, Edit, Grep, Glob, Bash]
|
|
571
|
+
|
|
572
|
+
fe-design-expert:
|
|
573
|
+
class: FrontendExpert
|
|
574
|
+
description: "Use for design system review, typography audit, color palette evaluation, motion design, and AI-generated design quality assessment"
|
|
575
|
+
model: claude-sonnet-5-5
|
|
576
|
+
memory: local
|
|
577
|
+
effort: medium
|
|
578
|
+
skills: [impeccable-design, web-design-guidelines, diagram-design]
|
|
579
|
+
tools: [Read, Write, Edit, Grep, Glob]
|
|
580
|
+
|
|
581
|
+
sec-codeql-expert:
|
|
582
|
+
description: "Expert security code analyst using CodeQL for vulnerability detection, call graph analysis, and SARIF output. Use for security audits, CVE triage, code pattern analysis, and vulnerability validation."
|
|
583
|
+
model: claude-opus-5-5
|
|
584
|
+
memory: local
|
|
585
|
+
effort: high
|
|
586
|
+
skills: [cve-triage, adversarial-review]
|
|
587
|
+
tools: [Read, Write, Grep, Bash]
|
|
588
|
+
|
|
589
|
+
slack-cli-expert:
|
|
590
|
+
description: "Expert Slack CLI developer for Slack app management, deployment, triggers, and workspace automation"
|
|
591
|
+
model: claude-sonnet-5-5
|
|
592
|
+
effort: medium
|
|
593
|
+
skills: []
|
|
594
|
+
tools: [Read, Write, Edit, Grep, Glob, Bash]
|
|
595
|
+
|
|
596
|
+
tracker-checkpoint:
|
|
597
|
+
description: "Pipeline execution state tracker with checkpoint persistence. Reads/writes /tmp/.claude-pipeline-*-{PPID}.json state files and validates state transitions. Used by dag-orchestration for resume-after-failure and pipeline-guards for quality gate state."
|
|
598
|
+
model: haiku
|
|
599
|
+
memory: local
|
|
600
|
+
effort: medium
|
|
601
|
+
skills: [dag-orchestration, pipeline-guards]
|
|
602
|
+
tools: [Read, Write, Edit, Bash, Glob, Grep]
|
|
603
|
+
|
|
604
|
+
wiki-curator:
|
|
605
|
+
description: "Dedicated wiki page CRUD agent — creates, updates, and maintains wiki/ markdown pages for the codebase knowledge base"
|
|
606
|
+
model: claude-sonnet-5-5
|
|
607
|
+
memory: local
|
|
608
|
+
effort: medium
|
|
609
|
+
skills: []
|
|
610
|
+
tools: [Read, Write, Edit, Glob, Grep, Bash]
|