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
package/README.md
CHANGED
|
@@ -117,13 +117,14 @@ Soft default is 4 concurrent agents, hard cap 5. Agent Teams (shared task list,
|
|
|
117
117
|
|
|
118
118
|
| Category | Count | Agents |
|
|
119
119
|
|----------|-------|--------|
|
|
120
|
-
| Languages | 6 | lang-golang, lang-python, lang-rust, lang-kotlin, lang-typescript, lang-
|
|
120
|
+
| Languages | 6 | lang-golang, lang-python, lang-rust, lang-kotlin, lang-typescript, lang-java |
|
|
121
121
|
| Backend | 6 | be-fastapi, be-springboot, be-go-backend, be-express, be-nestjs, be-django |
|
|
122
122
|
| Frontend | 5 | fe-vercel, fe-vuejs, fe-svelte, fe-flutter, fe-design |
|
|
123
123
|
| Data Engineering | 6 | de-airflow, de-dbt, de-spark, de-kafka, de-snowflake, de-pipeline |
|
|
124
124
|
| Database | 4 | db-supabase, db-postgres, db-redis, db-alembic |
|
|
125
125
|
| Tooling | 4 | tool-npm, tool-optimizer, tool-bun, slack-cli |
|
|
126
|
-
| Architecture |
|
|
126
|
+
| Architecture | 2 | arch-documenter, arch-speckit |
|
|
127
|
+
| Agora | 1 | agora-runner |
|
|
127
128
|
| Infrastructure | 2 | infra-docker, infra-aws |
|
|
128
129
|
| QA | 3 | qa-planner, qa-writer, qa-engineer |
|
|
129
130
|
| Security | 1 | sec-codeql |
|
package/dist/cli/index.js
CHANGED
|
@@ -335,7 +335,7 @@ var init_package = __esm(() => {
|
|
|
335
335
|
workspaces: [
|
|
336
336
|
"packages/*"
|
|
337
337
|
],
|
|
338
|
-
version: "2.0.
|
|
338
|
+
version: "2.0.8",
|
|
339
339
|
description: "Batteries-included agent harness for Claude Code",
|
|
340
340
|
type: "module",
|
|
341
341
|
bin: {
|
|
@@ -17315,7 +17315,11 @@ import { basename as basename3, join as join8 } from "node:path";
|
|
|
17315
17315
|
// src/core/hook-group-merge.ts
|
|
17316
17316
|
var LEGACY_OMCUSTOM_DESCRIPTIONS = [
|
|
17317
17317
|
"Record agent outcomes + auto-continue workflow + R007/R008 drift advisory on subagent completion (autonomous-loop re-entry, #1545)",
|
|
17318
|
-
'Re-inject project CLAUDE.md into model context on session start/resume/clear and compact re-entry — matcher "*" covers all SessionStart sources including "compact" (#1617)'
|
|
17318
|
+
'Re-inject project CLAUDE.md into model context on session start/resume/clear and compact re-entry — matcher "*" covers all SessionStart sources including "compact" (#1617)',
|
|
17319
|
+
"Block dev servers outside tmux - ensures you can access logs",
|
|
17320
|
+
"Block creation of random .md files - keeps docs consolidated",
|
|
17321
|
+
"Record agent outcomes + auto-continue workflow + R007/R008 drift advisory + failure advisory on subagent completion (autonomous-loop re-entry, #1545; #1631 replaced the self-referential prompt-type judge hook with an advisory-only command hook)",
|
|
17322
|
+
"Validate externally modified files — advisory staleness warning (v2.1.83+)"
|
|
17319
17323
|
];
|
|
17320
17324
|
var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
|
|
17321
17325
|
var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
|
|
@@ -18199,6 +18203,16 @@ function analyzeMatcher(raw) {
|
|
|
18199
18203
|
}
|
|
18200
18204
|
return analyzeMixedAndExpr(ast, raw);
|
|
18201
18205
|
}
|
|
18206
|
+
var FILE_CHANGED_LITERAL_SEGMENT = /^(?!\.+$)[A-Za-z0-9._-]+$/;
|
|
18207
|
+
function analyzeFileChangedMatcher(raw) {
|
|
18208
|
+
if (raw === undefined)
|
|
18209
|
+
return null;
|
|
18210
|
+
const segments = raw.split("|");
|
|
18211
|
+
if (!segments.every((segment) => FILE_CHANGED_LITERAL_SEGMENT.test(segment))) {
|
|
18212
|
+
return null;
|
|
18213
|
+
}
|
|
18214
|
+
return { toolRegex: raw, fieldNode: null };
|
|
18215
|
+
}
|
|
18202
18216
|
function fieldNodeToJq(node) {
|
|
18203
18217
|
switch (node.kind) {
|
|
18204
18218
|
case "and":
|
|
@@ -18235,9 +18249,9 @@ function buildGuardedCommand(jqCond, originalCommand) {
|
|
|
18235
18249
|
var CLASSIFICATION_TABLE = [
|
|
18236
18250
|
{
|
|
18237
18251
|
event: "PreToolUse",
|
|
18238
|
-
description: "
|
|
18252
|
+
description: "Advise (additionalContext, never blocks) when a foreground dev server starts outside tmux/run_in_background - keeps logs accessible",
|
|
18239
18253
|
decision: "wrap-guard",
|
|
18240
|
-
reason:
|
|
18254
|
+
reason: "inline script is an advisory (always exit 0, additionalContext only); it re-parses tool_input.command for a dev command at command position and stays silent for tmux, `run_in_background: true`, backgrounded and mention-only commands, but the wrap-guard is kept as a cheap pre-filter so non-matching Bash calls do not spawn the POSIX-sh segment parser"
|
|
18241
18255
|
},
|
|
18242
18256
|
{
|
|
18243
18257
|
event: "PreToolUse",
|
|
@@ -18253,9 +18267,9 @@ var CLASSIFICATION_TABLE = [
|
|
|
18253
18267
|
},
|
|
18254
18268
|
{
|
|
18255
18269
|
event: "PreToolUse",
|
|
18256
|
-
description: "
|
|
18270
|
+
description: "Non-blocking notice on new .md/.txt files (exit 1) - keeps docs consolidated",
|
|
18257
18271
|
decision: "drop-condition",
|
|
18258
|
-
reason: "inline script re-parses tool_input.file_path via jq and re-tests both the `\\.(md|txt)$` and the README/CLAUDE/AGENT/SKILL exclusion with its own
|
|
18272
|
+
reason: "inline script re-parses tool_input.file_path via jq and re-tests both the `\\.(md|txt)$` and the README/CLAUDE/AGENT/SKILL exclusion with its own POSIX `case` patterns before printing its non-blocking exit-1 notice (per a CC 2.1.310 runtime probe, neither the tool block nor the stderr of an exit-1 PreToolUse hook reaches the model) — the matcher condition is fully redundant with the script body"
|
|
18259
18273
|
},
|
|
18260
18274
|
{
|
|
18261
18275
|
event: "PreToolUse",
|
|
@@ -18339,16 +18353,23 @@ function convertHookCommand(cmd, fieldNode, decision, warnCollector, context) {
|
|
|
18339
18353
|
command: buildGuardedCommand(jqCond, cmd.command)
|
|
18340
18354
|
};
|
|
18341
18355
|
}
|
|
18356
|
+
function warnNonLiteralFileChangedMatcher(event, rawMatcher, analysis, context, warnCollector) {
|
|
18357
|
+
if (event !== "FileChanged" || rawMatcher === undefined || analyzeFileChangedMatcher(rawMatcher) !== null) {
|
|
18358
|
+
return;
|
|
18359
|
+
}
|
|
18360
|
+
warnCollector.push(`hooks: ${context} — FileChanged matcher ${JSON.stringify(rawMatcher)} is not a |-separated list of literal filenames; FileChanged matchers are literal filenames (not tool-name regexes, globs, or expressions), so the converted matcher ${JSON.stringify(analysis.toolRegex)} would watch a file of that exact name — use e.g. "CLAUDE.md|bun.lock"`);
|
|
18361
|
+
}
|
|
18342
18362
|
function convertHooksEntry(event, entry, warnCollector) {
|
|
18343
18363
|
const context = `${event} / "${entry.description ?? "(no description)"}"`;
|
|
18344
18364
|
let analysis;
|
|
18345
18365
|
try {
|
|
18346
|
-
analysis = analyzeMatcher(entry.matcher);
|
|
18366
|
+
analysis = (event === "FileChanged" ? analyzeFileChangedMatcher(entry.matcher) : null) ?? analyzeMatcher(entry.matcher);
|
|
18347
18367
|
} catch (err) {
|
|
18348
18368
|
const message = err instanceof Error ? err.message : String(err);
|
|
18349
18369
|
warnCollector.push(`hooks: ${context} — matcher could not be auto-converted, entry EXCLUDED: ${message}`);
|
|
18350
18370
|
return null;
|
|
18351
18371
|
}
|
|
18372
|
+
warnNonLiteralFileChangedMatcher(event, entry.matcher, analysis, context, warnCollector);
|
|
18352
18373
|
let decision = "auto";
|
|
18353
18374
|
if (analysis.fieldNode !== null) {
|
|
18354
18375
|
const classification = findClassification(event, entry.description);
|
package/dist/index.js
CHANGED
|
@@ -1151,7 +1151,11 @@ import { basename, join as join3 } from "node:path";
|
|
|
1151
1151
|
// src/core/hook-group-merge.ts
|
|
1152
1152
|
var LEGACY_OMCUSTOM_DESCRIPTIONS = [
|
|
1153
1153
|
"Record agent outcomes + auto-continue workflow + R007/R008 drift advisory on subagent completion (autonomous-loop re-entry, #1545)",
|
|
1154
|
-
'Re-inject project CLAUDE.md into model context on session start/resume/clear and compact re-entry — matcher "*" covers all SessionStart sources including "compact" (#1617)'
|
|
1154
|
+
'Re-inject project CLAUDE.md into model context on session start/resume/clear and compact re-entry — matcher "*" covers all SessionStart sources including "compact" (#1617)',
|
|
1155
|
+
"Block dev servers outside tmux - ensures you can access logs",
|
|
1156
|
+
"Block creation of random .md files - keeps docs consolidated",
|
|
1157
|
+
"Record agent outcomes + auto-continue workflow + R007/R008 drift advisory + failure advisory on subagent completion (autonomous-loop re-entry, #1545; #1631 replaced the self-referential prompt-type judge hook with an advisory-only command hook)",
|
|
1158
|
+
"Validate externally modified files — advisory staleness warning (v2.1.83+)"
|
|
1155
1159
|
];
|
|
1156
1160
|
var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
|
|
1157
1161
|
var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
|
|
@@ -1788,6 +1792,16 @@ function analyzeMatcher(raw) {
|
|
|
1788
1792
|
}
|
|
1789
1793
|
return analyzeMixedAndExpr(ast, raw);
|
|
1790
1794
|
}
|
|
1795
|
+
var FILE_CHANGED_LITERAL_SEGMENT = /^(?!\.+$)[A-Za-z0-9._-]+$/;
|
|
1796
|
+
function analyzeFileChangedMatcher(raw) {
|
|
1797
|
+
if (raw === undefined)
|
|
1798
|
+
return null;
|
|
1799
|
+
const segments = raw.split("|");
|
|
1800
|
+
if (!segments.every((segment) => FILE_CHANGED_LITERAL_SEGMENT.test(segment))) {
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
return { toolRegex: raw, fieldNode: null };
|
|
1804
|
+
}
|
|
1791
1805
|
function fieldNodeToJq(node) {
|
|
1792
1806
|
switch (node.kind) {
|
|
1793
1807
|
case "and":
|
|
@@ -1824,9 +1838,9 @@ function buildGuardedCommand(jqCond, originalCommand) {
|
|
|
1824
1838
|
var CLASSIFICATION_TABLE = [
|
|
1825
1839
|
{
|
|
1826
1840
|
event: "PreToolUse",
|
|
1827
|
-
description: "
|
|
1841
|
+
description: "Advise (additionalContext, never blocks) when a foreground dev server starts outside tmux/run_in_background - keeps logs accessible",
|
|
1828
1842
|
decision: "wrap-guard",
|
|
1829
|
-
reason:
|
|
1843
|
+
reason: "inline script is an advisory (always exit 0, additionalContext only); it re-parses tool_input.command for a dev command at command position and stays silent for tmux, `run_in_background: true`, backgrounded and mention-only commands, but the wrap-guard is kept as a cheap pre-filter so non-matching Bash calls do not spawn the POSIX-sh segment parser"
|
|
1830
1844
|
},
|
|
1831
1845
|
{
|
|
1832
1846
|
event: "PreToolUse",
|
|
@@ -1842,9 +1856,9 @@ var CLASSIFICATION_TABLE = [
|
|
|
1842
1856
|
},
|
|
1843
1857
|
{
|
|
1844
1858
|
event: "PreToolUse",
|
|
1845
|
-
description: "
|
|
1859
|
+
description: "Non-blocking notice on new .md/.txt files (exit 1) - keeps docs consolidated",
|
|
1846
1860
|
decision: "drop-condition",
|
|
1847
|
-
reason: "inline script re-parses tool_input.file_path via jq and re-tests both the `\\.(md|txt)$` and the README/CLAUDE/AGENT/SKILL exclusion with its own
|
|
1861
|
+
reason: "inline script re-parses tool_input.file_path via jq and re-tests both the `\\.(md|txt)$` and the README/CLAUDE/AGENT/SKILL exclusion with its own POSIX `case` patterns before printing its non-blocking exit-1 notice (per a CC 2.1.310 runtime probe, neither the tool block nor the stderr of an exit-1 PreToolUse hook reaches the model) — the matcher condition is fully redundant with the script body"
|
|
1848
1862
|
},
|
|
1849
1863
|
{
|
|
1850
1864
|
event: "PreToolUse",
|
|
@@ -1928,16 +1942,23 @@ function convertHookCommand(cmd, fieldNode, decision, warnCollector, context) {
|
|
|
1928
1942
|
command: buildGuardedCommand(jqCond, cmd.command)
|
|
1929
1943
|
};
|
|
1930
1944
|
}
|
|
1945
|
+
function warnNonLiteralFileChangedMatcher(event, rawMatcher, analysis, context, warnCollector) {
|
|
1946
|
+
if (event !== "FileChanged" || rawMatcher === undefined || analyzeFileChangedMatcher(rawMatcher) !== null) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
warnCollector.push(`hooks: ${context} — FileChanged matcher ${JSON.stringify(rawMatcher)} is not a |-separated list of literal filenames; FileChanged matchers are literal filenames (not tool-name regexes, globs, or expressions), so the converted matcher ${JSON.stringify(analysis.toolRegex)} would watch a file of that exact name — use e.g. "CLAUDE.md|bun.lock"`);
|
|
1950
|
+
}
|
|
1931
1951
|
function convertHooksEntry(event, entry, warnCollector) {
|
|
1932
1952
|
const context = `${event} / "${entry.description ?? "(no description)"}"`;
|
|
1933
1953
|
let analysis;
|
|
1934
1954
|
try {
|
|
1935
|
-
analysis = analyzeMatcher(entry.matcher);
|
|
1955
|
+
analysis = (event === "FileChanged" ? analyzeFileChangedMatcher(entry.matcher) : null) ?? analyzeMatcher(entry.matcher);
|
|
1936
1956
|
} catch (err) {
|
|
1937
1957
|
const message = err instanceof Error ? err.message : String(err);
|
|
1938
1958
|
warnCollector.push(`hooks: ${context} — matcher could not be auto-converted, entry EXCLUDED: ${message}`);
|
|
1939
1959
|
return null;
|
|
1940
1960
|
}
|
|
1961
|
+
warnNonLiteralFileChangedMatcher(event, entry.matcher, analysis, context, warnCollector);
|
|
1941
1962
|
let decision = "auto";
|
|
1942
1963
|
if (analysis.fieldNode !== null) {
|
|
1943
1964
|
const classification = findClassification(event, entry.description);
|
|
@@ -2906,7 +2927,7 @@ var package_default = {
|
|
|
2906
2927
|
workspaces: [
|
|
2907
2928
|
"packages/*"
|
|
2908
2929
|
],
|
|
2909
|
-
version: "2.0.
|
|
2930
|
+
version: "2.0.8",
|
|
2910
2931
|
description: "Batteries-included agent harness for Claude Code",
|
|
2911
2932
|
type: "module",
|
|
2912
2933
|
bin: {
|
package/package.json
CHANGED
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "#!/bin/
|
|
20
|
+
"command": "#!/bin/sh\ninput=$(cat)\ncmd=$(printf '%s' \"$input\" | jq -r '.tool_input.command // \"\"' 2>/dev/null)\nbg=$(printf '%s' \"$input\" | jq -r '.tool_input.run_in_background // false' 2>/dev/null)\n\n# Advisory only: never blocks (always exit 0). Stay silent for background runs, tmux-wrapped commands,\n# mentions of the dev command in other commands, and variants like dev:test / develop / devtools.\n[ \"$bg\" = \"true\" ] && exit 0\nif printf '%s' \"$cmd\" | grep -Eq '^[[:space:]]*tmux([[:space:]]|$)'; then exit 0; fi\n\n# Drop quoted strings, then split into segments at && || ; | ( ) and newlines\nsegs=$(printf '%s' \"$cmd\" | sed -e 's/\"[^\"]*\"//g' -e \"s/'[^']*'//g\" -e 's/&&/;/g' -e 's/||/;/g' | tr ';|()\\n' '\\n\\n\\n\\n\\n')\n\n# Command position (optionally after VAR=value), word boundary after the dev command, segment not backgrounded with &\nhit=$(printf '%s\\n' \"$segs\" | grep -E '^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*(npm run dev|pnpm( run)? dev|yarn dev|bun run dev)([[:space:])]|$)' | grep -Ev '(^|[^>&])&([^&>]|$)' | head -n 1)\n\nif [ -n \"$hit\" ]; then\n printf '%s\\n' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"[Hook] 포그라운드 dev 서버 시작이 감지되었습니다. 로그 접근을 위해 run_in_background: true 로 실행하거나 tmux 세션(예: tmux new-session -d -s dev) 안에서 실행하는 것을 권장합니다. 이 알림은 차단하지 않습니다.\"}}'\nfi\nexit 0"
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
|
-
"description": "
|
|
23
|
+
"description": "Advise (additionalContext, never blocks) when a foreground dev server starts outside tmux/run_in_background - keeps logs accessible"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"matcher": "tool == \"Bash\" && tool_input.command matches \"(npm (install|test)|pnpm (install|test)|yarn (install|test)|bun (install|test)|cargo build|make|docker|pytest|vitest|playwright)\"",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"hooks": [
|
|
48
48
|
{
|
|
49
49
|
"type": "command",
|
|
50
|
-
"command": "#!/bin/
|
|
50
|
+
"command": "#!/bin/sh\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\ncase \"$file_path\" in\n *README.md|*CLAUDE.md|*AGENT.md|*SKILL.md) ;;\n *.md|*.txt)\n echo \"[Hook] NOTICE (non-blocking): new documentation file - the write is not blocked\" >&2\n echo \"[Hook] File: $file_path\" >&2\n echo \"[Hook] Prefer README.md or AGENT.md for documentation if this is not a deliberate new doc\" >&2\n exit 1\n ;;\nesac\n\nprintf '%s\\n' \"$input\""
|
|
51
51
|
}
|
|
52
52
|
],
|
|
53
|
-
"description": "
|
|
53
|
+
"description": "Non-blocking notice on new .md/.txt files (exit 1) - keeps docs consolidated"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"matcher": "tool == \"Edit\" || tool == \"Write\"",
|
|
@@ -102,6 +102,16 @@
|
|
|
102
102
|
],
|
|
103
103
|
"description": "Block rule file deletion — requires individual user confirmation per rule (R001 safety)"
|
|
104
104
|
},
|
|
105
|
+
{
|
|
106
|
+
"matcher": "tool == \"Bash\"",
|
|
107
|
+
"hooks": [
|
|
108
|
+
{
|
|
109
|
+
"type": "command",
|
|
110
|
+
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/bash-cd-advisor.sh\""
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"description": "Advise against top-level (non-subshell) cd in Bash commands; cd persists as session cwd (R005, #1834/#1816 H2)"
|
|
114
|
+
},
|
|
105
115
|
{
|
|
106
116
|
"matcher": "tool == \"Task\" || tool == \"Agent\"",
|
|
107
117
|
"hooks": [
|
|
@@ -274,13 +284,9 @@
|
|
|
274
284
|
{
|
|
275
285
|
"type": "command",
|
|
276
286
|
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\""
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
"type": "command",
|
|
280
|
-
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/subagent-failure-advisor.sh\""
|
|
281
287
|
}
|
|
282
288
|
],
|
|
283
|
-
"description": "Record agent outcomes +
|
|
289
|
+
"description": "Record agent outcomes + stall and consecutive-completion advisories + R007/R008 drift advisory on subagent completion — advisory-only, never continues or blocks the workflow (autonomous-loop re-entry, #1545; #1631 replaced the self-referential prompt-type judge hook with an advisory-only command hook; #1880)"
|
|
284
290
|
}
|
|
285
291
|
],
|
|
286
292
|
"CwdChanged": [
|
|
@@ -297,14 +303,14 @@
|
|
|
297
303
|
],
|
|
298
304
|
"FileChanged": [
|
|
299
305
|
{
|
|
300
|
-
"matcher": "
|
|
306
|
+
"matcher": "CLAUDE.md|bun.lock|yarn.lock|Cargo.lock|poetry.lock|package-lock.json|pnpm-lock.yaml",
|
|
301
307
|
"hooks": [
|
|
302
308
|
{
|
|
303
309
|
"type": "command",
|
|
304
310
|
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/file-change-validator.sh\""
|
|
305
311
|
}
|
|
306
312
|
],
|
|
307
|
-
"description": "
|
|
313
|
+
"description": "Warn on external changes to CLAUDE.md and the bun/yarn/Cargo/poetry/npm/pnpm lock files in the working directory — stderr shown to the user only, never blocks; the matcher lists literal filenames because CC registers each |-separated segment as a literal file to watch (v2.1.83+, #1775)"
|
|
308
314
|
}
|
|
309
315
|
],
|
|
310
316
|
"Notification": [
|
|
@@ -385,7 +391,7 @@
|
|
|
385
391
|
"hooks": [
|
|
386
392
|
{
|
|
387
393
|
"type": "command",
|
|
388
|
-
"command": "#!/bin/bash\ninput=$(cat)\ncmd=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.command')\n\nif echo \"$cmd\" | grep -qE 'gh pr create'; then\n output=$(printf '%s\\n' \"$input\" | jq -r '.tool_output.output // \"\"')\n pr_url=$(echo \"$output\" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+')\n \n if [ -n \"$pr_url\" ]; then\n echo \"[Hook] PR created: $pr_url\" >&2\n echo \"[Hook] Checking GitHub Actions status...\" >&2\n repo=$(echo \"$pr_url\" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\\1|')\n pr_num=$(echo \"$pr_url\" | sed -E 's|.*/pull/([0-9]+)|\\1|')\n echo \"[Hook] To review PR: gh pr review $pr_num --repo $repo\" >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\""
|
|
394
|
+
"command": "#!/bin/bash\ninput=$(cat)\ncmd=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.command')\n\nif echo \"$cmd\" | grep -qE 'gh pr create'; then\n output=$(printf '%s\\n' \"$input\" | jq -r '.tool_response.stdout // .tool_output.output // \"\"')\n pr_url=$(echo \"$output\" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+')\n \n if [ -n \"$pr_url\" ]; then\n echo \"[Hook] PR created: $pr_url\" >&2\n echo \"[Hook] Checking GitHub Actions status...\" >&2\n repo=$(echo \"$pr_url\" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\\1|')\n pr_num=$(echo \"$pr_url\" | sed -E 's|.*/pull/([0-9]+)|\\1|')\n echo \"[Hook] To review PR: gh pr review $pr_num --repo $repo\" >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\""
|
|
389
395
|
}
|
|
390
396
|
],
|
|
391
397
|
"description": "Log PR URL and provide review command after PR creation"
|
|
@@ -632,6 +638,16 @@
|
|
|
632
638
|
}
|
|
633
639
|
],
|
|
634
640
|
"description": "FAIL-axis instrumentation — append tool failures to the error ledger (JSONL). Never blocks; feeds fail-axis-cause-advisor.sh (#1561)"
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"matcher": "tool == \"Task\" || tool == \"Agent\"",
|
|
644
|
+
"hooks": [
|
|
645
|
+
{
|
|
646
|
+
"type": "command",
|
|
647
|
+
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/subagent-failure-advisor.sh\""
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
"description": "Advise on failed Agent/Task spawns via additionalContext (e.g. unknown subagent_type). Advisory only (additionalContext); the script's only explicit exit is exit 0 (#1775)"
|
|
635
651
|
}
|
|
636
652
|
]
|
|
637
653
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# Consecutive-Completion Guard (file name kept for hook wiring)
|
|
5
5
|
# Trigger: SubagentStop
|
|
6
|
-
# Purpose: Count consecutive subagent completions and
|
|
6
|
+
# Purpose: Count consecutive subagent completions and emit an advisory from the 4th consecutive completion on
|
|
7
7
|
# Protocol: stdin JSON -> count check -> stdout pass-through, the only explicit exit is exit 0 (R021); under set -u a corrupt counter file can exit 1 (non-blocking for SubagentStop)
|
|
8
8
|
|
|
9
9
|
input=$(cat)
|
|
10
10
|
|
|
11
11
|
count_file="/tmp/.claude-loop-count-${PPID}"
|
|
12
12
|
|
|
13
|
-
# Reset counter if
|
|
13
|
+
# Reset counter if >60s idle since the LAST update (window chains per completion)
|
|
14
14
|
if [ -f "$count_file" ]; then
|
|
15
15
|
last_mod=$(stat -c%Y "$count_file" 2>/dev/null || stat -f%m "$count_file" 2>/dev/null || echo 0)
|
|
16
16
|
now=$(date +%s)
|
|
@@ -24,9 +24,9 @@ count=$(cat "$count_file" 2>/dev/null || echo 0)
|
|
|
24
24
|
count=$((count + 1))
|
|
25
25
|
echo "$count" > "$count_file"
|
|
26
26
|
|
|
27
|
-
#
|
|
27
|
+
# Advisory from the 4th consecutive completion (each within 60s of the previous)
|
|
28
28
|
if [ "$count" -ge 4 ]; then
|
|
29
|
-
echo '[
|
|
29
|
+
echo '[ConsecutiveCompletions] ADVISORY: 4+ consecutive subagent completions, each within 60s of the previous (advisory only; nothing is paused).' >&2
|
|
30
30
|
fi
|
|
31
31
|
|
|
32
32
|
printf '%s\n' "$input"
|
|
@@ -24,7 +24,18 @@ LOG_FILE="/tmp/auto-dev-spend-${PPID}.json"
|
|
|
24
24
|
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // "unknown"' 2>/dev/null || echo "unknown")
|
|
25
25
|
description=$(printf '%s\n' "$input" | jq -r '.tool_input.description // ""' 2>/dev/null || echo "")
|
|
26
26
|
prompt_text=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt // ""' 2>/dev/null || echo "")
|
|
27
|
-
|
|
27
|
+
# PostToolUse carries "tool_response", not "tool_output" (#1775; probe: CC 2.1.310,
|
|
28
|
+
# "claude -p", one run, haiku). Measured async Agent launch shape: object {agentId,
|
|
29
|
+
# canContinueAgent, canReadOutputFile, description, isAsync, outputFile, prompt,
|
|
30
|
+
# resolvedModel, status:"async_launched"} - it has NO subagent result text, so the
|
|
31
|
+
# tokens_out estimate is 0 for async launches. A synchronous Agent result shape was
|
|
32
|
+
# NOT measured [가설]; a string tool_response or an object .stdout/.output string is
|
|
33
|
+
# used when present, then the legacy .tool_output fallback.
|
|
34
|
+
output_text=$(printf '%s\n' "$input" | jq -r '
|
|
35
|
+
(.tool_response) as $r
|
|
36
|
+
| if ($r | type) == "string" then $r
|
|
37
|
+
elif ($r | type) == "object" and (($r.stdout // $r.output | type) == "string") then ($r.stdout // $r.output)
|
|
38
|
+
else (.tool_output.output // .tool_output // "") end' 2>/dev/null || echo "")
|
|
28
39
|
|
|
29
40
|
# Derive phase from pipeline state (current_phase) or fallback to description prefix
|
|
30
41
|
phase=$(jq -r '.current_phase // .phase // "unknown"' "$PIPELINE_STATE" 2>/dev/null || echo "unknown")
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bash-cd-advisor.sh — PreToolUse(Bash): 서브셸 밖 최상위 `cd` 감지 (advisory)
|
|
3
|
+
#
|
|
4
|
+
# 배경 (#1834 / #1816 H2, R005 "Bash `cd`는 세션 cwd로 남습니다"):
|
|
5
|
+
# Bash 도구에서 괄호 밖에 놓인 `cd`는 세션 cwd로 남아(CwdChanged 발화로 확인) 이후 명령의
|
|
6
|
+
# 상대 경로를 어긋나게 한다. `cd X && ( … )`도 `cd`가 서브셸 밖이므로 지속 `cd`다.
|
|
7
|
+
# 안전한 형태는 절대 경로 또는 `( cd <dir> && … )`.
|
|
8
|
+
#
|
|
9
|
+
# 역할:
|
|
10
|
+
# .tool_input.command 에서 괄호 깊이 0, 따옴표/`$( … )`/백틱/히어독 본문 밖의 `cd`/`pushd`/`popd`
|
|
11
|
+
# 명령어 단어를 찾으면 한국어 advisory 한 단락을 전달한다. 그 외에는 아무것도 출력하지 않는다.
|
|
12
|
+
# 명령 시작 위치 = 문장 처음, `&&` `||` `;` 개행 `|` `&` 뒤, 그리고 접두 키워드(`then` `do` `else`
|
|
13
|
+
# `elif` `if` `while` `until` `{` `!` `time` `builtin` `command`)와 선행 `VAR=value` 할당 뒤.
|
|
14
|
+
# 파이프의 마지막이 아닌 구간(`cd x | b`)과 백그라운드(`cd x &`)의 cd 는 서브셸에서 실행되어 cwd가
|
|
15
|
+
# 남지 않으므로 감지하지 않는다. 파이프의 마지막 구간(`a | cd x`)은 감지한다: 이 규칙은 zsh 의미론을
|
|
16
|
+
# 따른다 — Bash 도구의 셸이 zsh(측정: $ZSH_VERSION=5.9)이고 zsh 는 마지막 파이프 요소를 현재 셸에서
|
|
17
|
+
# 실행한다(`zsh -fc 'cd /; echo x | cd /tmp; pwd'` → /tmp; bash 는 `/` 라 남지 않는다).
|
|
18
|
+
#
|
|
19
|
+
# 구현: 스캐너는 awk 한 번의 선형 패스(LC_ALL=C, 바이트 단위)다. 측정(macOS, awk 20200816): 30K자 0.03s.
|
|
20
|
+
# `cd`/`pushd`/`popd` 부분문자열이 없는 명령은 스캐너를 건너뛴다.
|
|
21
|
+
#
|
|
22
|
+
# 전달 경로 (Measured):
|
|
23
|
+
# CC 2.1.310, `claude -p`, haiku(claude-haiku-5-5), 1회 실행, PreToolUse(Bash) 훅.
|
|
24
|
+
# - exit 0 + stderr : 모델에 도달하지 않음 (마커 미관측)
|
|
25
|
+
# - exit 0 + stdout JSON hookSpecificOutput.additionalContext : 모델에 도달
|
|
26
|
+
# ("PreToolUse:Bash hook additional context: <text>" 형태로 모델이 인용)
|
|
27
|
+
# 따라서 additionalContext 로만 전달한다. 미측정: 대화형 세션, 다른 모델.
|
|
28
|
+
#
|
|
29
|
+
# 차단하지 않는다 (R021 advisory-first):
|
|
30
|
+
# 항상 exit 0. permissionDecision/decision 필드를 출력하지 않고 tool input도 수정하지 않는다.
|
|
31
|
+
# fail-open: stdin이 객체가 아니거나 command 가 없거나 jq 가 없으면 조용히 exit 0.
|
|
32
|
+
#
|
|
33
|
+
# 환경변수 override:
|
|
34
|
+
# OMCUSTOM_CD_ADVISOR=off — advisory 비활성화
|
|
35
|
+
#
|
|
36
|
+
# 알려진 한계 (documents … 테스트로 고정):
|
|
37
|
+
# - `eval "cd x"`, `bash -c 'cd x'` 등 문자열 안의 cd 는 따옴표 안이라 감지하지 않는다.
|
|
38
|
+
# - 셸 함수 본문 `f() { cd x; }` 의 cd 는 `)` 뒤라 명령 시작이 아니어서 감지하지 않는다
|
|
39
|
+
# (`{ cd x; }` 그룹 자체는 감지한다).
|
|
40
|
+
# - `case $x in a) cd x ;; esac` 의 case arm 안 cd 는 감지하지 않는다(`)` 뒤).
|
|
41
|
+
# - 히어독 구분자 줄은 `<<[-]WORD` 의 WORD(따옴표 제거)와 일치하는 줄로 판정한다. 히어독 본문은
|
|
42
|
+
# 건너뛰지만, 큰따옴표 문자열 안의 `$( … <<EOF … )` 형태는 큰따옴표 구간을 통째로 불투명하게 취급한다.
|
|
43
|
+
# - bash 로 실행되는 환경에서는 `ls | cd x` 가 cwd 를 바꾸지 않으나 zsh 기준으로 감지한다(위 참조).
|
|
44
|
+
# - `command -p cd x` 처럼 옵션이 낀 접두 형태는 감지하지 않는다.
|
|
45
|
+
|
|
46
|
+
set -uo pipefail
|
|
47
|
+
|
|
48
|
+
[ "${OMCUSTOM_CD_ADVISOR:-}" = "off" ] && exit 0
|
|
49
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
50
|
+
|
|
51
|
+
input=$(cat) || exit 0
|
|
52
|
+
cmd=$(printf '%s' "$input" | jq -r 'if type == "object" then (.tool_input.command // "" | if type == "string" then . else "" end) else "" end' 2>/dev/null) || exit 0
|
|
53
|
+
[ -n "$cmd" ] || exit 0
|
|
54
|
+
|
|
55
|
+
# 사전 필터: cwd 변경 명령어 부분문자열이 없으면 스캔하지 않는다.
|
|
56
|
+
case "$cmd" in
|
|
57
|
+
*cd*|*pushd*|*popd*) ;;
|
|
58
|
+
*) exit 0 ;;
|
|
59
|
+
esac
|
|
60
|
+
|
|
61
|
+
# 선형 awk 스캐너: 따옴표 / 괄호 깊이(`(`, `$(` 포함) / 백틱 / 히어독 / 명령 시작 위치 / 구간 종결자 추적.
|
|
62
|
+
# 종료코드 0 = 지속 cd 발견. 입력은 stdin 단일 레코드(RS=\001)로 받는다.
|
|
63
|
+
# shellcheck disable=SC2016
|
|
64
|
+
AWK_PROG='
|
|
65
|
+
BEGIN { RS = "\001" }
|
|
66
|
+
{ s = s $0 }
|
|
67
|
+
function isdelim(c) { return (c == " " || c == "\t" || c == "\n" || c == ";" || c == "&" || c == "|" || c == "(" || c == ")" || c == "<" || c == ">" || c == "\"" || c == "\047" || c == "\\" || c == "`") }
|
|
68
|
+
function scan( n, ch, i, c, nx, pv, q, depth, bt, start, assign, cand, arith, akind, sep, adv, j, w, hn, hd, hdash, k, line, dq, d, dash) {
|
|
69
|
+
n = split(s, ch, "")
|
|
70
|
+
q = ""; depth = 0; bt = 0; start = 1; assign = 0; cand = 0; arith = 0; hn = 0
|
|
71
|
+
i = 1
|
|
72
|
+
while (i <= n) {
|
|
73
|
+
c = ch[i]
|
|
74
|
+
if (q == "\047") { if (c == "\047") q = ""; i++; continue }
|
|
75
|
+
if (q == "\"") {
|
|
76
|
+
if (c == "\\") { i += 2; continue }
|
|
77
|
+
if (c == "\"") q = ""
|
|
78
|
+
i++; continue
|
|
79
|
+
}
|
|
80
|
+
if (c == "\\") { i += 2; start = 0; continue }
|
|
81
|
+
if (c == "\047") { q = "\047"; start = 0; i++; continue }
|
|
82
|
+
if (c == "\"") { q = "\""; start = 0; i++; continue }
|
|
83
|
+
if (c == "`") { bt = 1 - bt; start = bt; assign = 0; i++; continue }
|
|
84
|
+
if (c == "(") { depth++; if (i > 1 && ch[i - 1] == "(") { akind[depth] = 1; arith++ } start = 1; assign = 0; i++; continue }
|
|
85
|
+
if (c == ")") { if (akind[depth]) { akind[depth] = 0; arith-- } if (depth > 0) depth--; start = 0; assign = 0; i++; continue }
|
|
86
|
+
if (c == " " || c == "\t") { if (assign) { start = 1; assign = 0 } i++; continue }
|
|
87
|
+
if (c == "<") {
|
|
88
|
+
nx = ch[i + 1]
|
|
89
|
+
if (nx == "<") {
|
|
90
|
+
if (ch[i + 2] == "<") { i += 3; start = 0; continue }
|
|
91
|
+
if (arith > 0) { i += 2; start = 0; continue }
|
|
92
|
+
i += 2; dash = 0
|
|
93
|
+
if (ch[i] == "-") { dash = 1; i++ }
|
|
94
|
+
while (i <= n && (ch[i] == " " || ch[i] == "\t")) i++
|
|
95
|
+
dq = ""
|
|
96
|
+
while (i <= n) {
|
|
97
|
+
d = ch[i]
|
|
98
|
+
if (d == "\047" || d == "\"" || d == "\\") { i++; continue }
|
|
99
|
+
if (d == " " || d == "\t" || d == "\n" || d == ";" || d == "&" || d == "|" || d == "(" || d == ")" || d == "<" || d == ">") break
|
|
100
|
+
dq = dq d; i++
|
|
101
|
+
}
|
|
102
|
+
hn++; hd[hn] = dq; hdash[hn] = dash
|
|
103
|
+
start = 0; continue
|
|
104
|
+
}
|
|
105
|
+
start = 0; i++; continue
|
|
106
|
+
}
|
|
107
|
+
sep = ""; adv = 1
|
|
108
|
+
if (c == ";" || c == "\n") sep = "term"
|
|
109
|
+
else if (c == "|") {
|
|
110
|
+
nx = ch[i + 1]
|
|
111
|
+
if (nx == "|") { sep = "term"; adv = 2 }
|
|
112
|
+
else if (nx == "&") { sep = "pipe"; adv = 2 }
|
|
113
|
+
else sep = "pipe"
|
|
114
|
+
} else if (c == "&") {
|
|
115
|
+
nx = ch[i + 1]; pv = (i > 1) ? ch[i - 1] : ""
|
|
116
|
+
if (nx == "&") { sep = "term"; adv = 2 }
|
|
117
|
+
else if (pv == ">" || pv == "<" || nx == ">") { start = 0; i++; continue }
|
|
118
|
+
else sep = "bg"
|
|
119
|
+
}
|
|
120
|
+
if (sep != "") {
|
|
121
|
+
if (cand) { if (sep == "term") return 1; cand = 0 }
|
|
122
|
+
start = 1; assign = 0
|
|
123
|
+
i += adv
|
|
124
|
+
if (c == "\n" && hn > 0) {
|
|
125
|
+
for (k = 1; k <= hn; k++) {
|
|
126
|
+
while (i <= n) {
|
|
127
|
+
line = ""
|
|
128
|
+
while (i <= n && ch[i] != "\n") { line = line ch[i]; i++ }
|
|
129
|
+
i++
|
|
130
|
+
if (hdash[k]) sub(/^\t+/, "", line)
|
|
131
|
+
if (line == hd[k]) break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
hn = 0
|
|
135
|
+
}
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (c == "#" && start == 1) {
|
|
139
|
+
while (i <= n && ch[i] != "\n") i++
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (start == 1) {
|
|
143
|
+
j = i; w = ""
|
|
144
|
+
while (j <= n && !isdelim(ch[j])) { w = w ch[j]; j++ }
|
|
145
|
+
if (j == i) { start = 0; i++; continue }
|
|
146
|
+
if ((w == "cd" || w == "pushd" || w == "popd") && depth == 0 && bt == 0 && !cand) {
|
|
147
|
+
cand = 1; start = 0; i = j; continue
|
|
148
|
+
}
|
|
149
|
+
if (w == "then" || w == "do" || w == "else" || w == "elif" || w == "if" || w == "while" || w == "until" || w == "time" || w == "builtin" || w == "command" || w == "!" || w == "{") {
|
|
150
|
+
i = j; continue
|
|
151
|
+
}
|
|
152
|
+
if (w ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { assign = 1; start = 0; i = j; continue }
|
|
153
|
+
start = 0; i = j; continue
|
|
154
|
+
}
|
|
155
|
+
i++
|
|
156
|
+
}
|
|
157
|
+
return cand ? 1 : 0
|
|
158
|
+
}
|
|
159
|
+
END { exit (scan() ? 0 : 1) }
|
|
160
|
+
'
|
|
161
|
+
|
|
162
|
+
has_toplevel_cd() {
|
|
163
|
+
printf '%s' "$1" | LC_ALL=C awk "$AWK_PROG" 2>/dev/null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if has_toplevel_cd "$cmd"; then
|
|
167
|
+
advisory_text='[CD Advisory] 이 Bash 명령에는 서브셸 밖에 놓인 최상위 `cd`(또는 `pushd`/`popd`)가 있습니다. Bash 도구의 `cd`는 세션 cwd로 남아 이후 명령의 상대 경로를 어긋나게 합니다(`cd X && ( … )` 형태도 `cd`가 괄호 밖이므로 동일합니다). 절대 경로를 쓰거나 `( cd <dir> && … )`처럼 서브셸 안에서만 이동하십시오(R005). 이미 의도한 지속 이동이라면 이후 명령의 경로가 이를 전제하는지 확인하십시오.'
|
|
168
|
+
jq -cn --arg ctx "$advisory_text" \
|
|
169
|
+
'{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $ctx}}'
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
exit 0
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
#
|
|
10
10
|
# 역할:
|
|
11
11
|
# (1) 짧은 진행 지시이고 (2) 원인 언급이 없으며 (3) 이 세션에 기록된 도구 실패가 있을 때,
|
|
12
|
-
# Claude에게 "
|
|
12
|
+
# Claude에게 "재시도 전에 원인 가설 한 줄을 명시하고, 사용자가 묻지 말라고 지시하지 않았을 때만 확인하라"는 advisory를 전달한다 (#1834: 훅은 세션 지시를 볼 수 없다).
|
|
13
13
|
#
|
|
14
14
|
# 왜 사용자가 아니라 Claude에게 전달하는가:
|
|
15
15
|
# hookSpecificOutput.additionalContext는 모델 컨텍스트로만 들어간다(사용자에게 표시되지 않음).
|
|
@@ -121,7 +121,7 @@ fail_tools=$(tail -n 300 "$LEDGER" 2>/dev/null \
|
|
|
121
121
|
| sort | uniq -c | sort -rn | head -3 \
|
|
122
122
|
| awk '{printf "%s(%s) ", $2, $1}') || fail_tools=""
|
|
123
123
|
|
|
124
|
-
advisory_text=$(printf '[FAIL Advisory] 이 세션에 도구 실패 %s건이 기록되어 있습니다 (%s). 방금 입력은 원인 언급이 없는 진행 지시입니다. 곧바로 재시도하지 말고, 먼저 직전 실패의 원인 가설을 한 줄로
|
|
124
|
+
advisory_text=$(printf '[FAIL Advisory] 이 세션에 도구 실패 %s건이 기록되어 있습니다 (%s). 방금 입력은 원인 언급이 없는 진행 지시입니다. 곧바로 재시도하지 말고, 먼저 직전 실패의 원인 가설을 한 줄로 명시하십시오. 사용자가 묻지 말고 진행하라고 지시하지 않은 경우에만 "이 진단이 맞는지" 짧게 한 번 확인하고, 그런 지시가 있었다면 확인 없이 가설만 기록하고 진행하십시오.' \
|
|
125
125
|
"$fail_count" "${fail_tools:-unknown}")
|
|
126
126
|
|
|
127
127
|
# 사람이 보는 감사 추적용 (exit 0에서 stderr는 모델에 전달되지 않음)
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
# - 어떤 실패에도 exit 0 — 원장 기록 실패가 본 작업을 막아서는 안 된다 (R021 advisory-first).
|
|
19
19
|
# - 명령/에러 문자열은 절단하여 기록한다 (원장 비대화 방지).
|
|
20
20
|
#
|
|
21
|
+
# exit_code 필드 (#1834 / #1814 R2):
|
|
22
|
+
# PostToolUseFailure에는 숫자 exit code 필드가 없고 `error` 문자열("Exit code 3")만 온다
|
|
23
|
+
# (실측: CC 2.1.310, `claude -p`, haiku, 1회, `sh -c 'exit 3'`). 그래서 `^Exit code ([0-9]+)`
|
|
24
|
+
# 일 때만 정수를 파싱해 exit_code로 기록하고, 아니면 null이다.
|
|
25
|
+
# 기대 exit 화이트리스트는 두지 않는다 — 동기였던 `git grep` no-match(exit 1)는 같은
|
|
26
|
+
# 조건에서 PostToolUseFailure를 발생시키지 않고, returnCodeInterpretation "No matches found"를
|
|
27
|
+
# 가진 PostToolUse로만 도달했다(실측). `false`, 일반 `grep` 등 다른 exit-1 명령, 대화형 세션,
|
|
28
|
+
# 다른 모델/버전은 미측정이다.
|
|
29
|
+
#
|
|
21
30
|
# 환경변수 override:
|
|
22
31
|
# OMCUSTOM_FAILURE_LEDGER=off — 기록 완전 비활성화
|
|
23
32
|
# OMCUSTOM_ERROR_LEDGER=<path> — 원장 경로 override (기본: ~/.claude/error-ledger.jsonl)
|
|
@@ -36,6 +45,14 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
36
45
|
exit 0
|
|
37
46
|
fi
|
|
38
47
|
|
|
48
|
+
# HOME guard: with no override, skip when HOME is unset, empty or not absolute
|
|
49
|
+
# (otherwise `set -u` exits 1 on an unset HOME, or the ledger lands in a relative path).
|
|
50
|
+
if [ -z "${OMCUSTOM_ERROR_LEDGER:-}" ]; then
|
|
51
|
+
case "${HOME:-}" in
|
|
52
|
+
/*) ;;
|
|
53
|
+
*) exit 0 ;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
39
56
|
LEDGER="${OMCUSTOM_ERROR_LEDGER:-${HOME}/.claude/error-ledger.jsonl}"
|
|
40
57
|
|
|
41
58
|
if ! mkdir -p "$(dirname "$LEDGER")" 2>/dev/null; then
|
|
@@ -68,6 +85,7 @@ printf '%s' "$input" \
|
|
|
68
85
|
then (.tool_input.command // .tool_input.file_path // "")
|
|
69
86
|
else "" end) | tostring | .[0:160]),
|
|
70
87
|
interrupt: (.is_interrupt == true),
|
|
88
|
+
exit_code: ((.error | if type == "string" then (capture("^Exit code (?<c>[0-9]+)") | .c | tonumber) else null end) // null),
|
|
71
89
|
err: ((.error
|
|
72
90
|
// .tool_error
|
|
73
91
|
// (if (.tool_response | type) == "object"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
input=$(cat)
|
|
6
6
|
file_path=$(printf '%s\n' "$input" | jq -r '.file_path // ""' 2>/dev/null)
|
|
7
|
-
change_type=$(printf '%s\n' "$input" | jq -r '.change_type // "modified"' 2>/dev/null)
|
|
7
|
+
change_type=$(printf '%s\n' "$input" | jq -r '.event // .change_type // "modified"' 2>/dev/null)
|
|
8
8
|
|
|
9
9
|
if [ -z "$file_path" ]; then
|
|
10
10
|
printf '%s\n' "$input"
|
|
@@ -14,11 +14,14 @@ fi
|
|
|
14
14
|
echo "[Hook] External file change detected: ${change_type} ${file_path}" >&2
|
|
15
15
|
|
|
16
16
|
# Warn about important files
|
|
17
|
+
# Arms mirror the hooks.json FileChanged matcher (literal working-directory filenames).
|
|
18
|
+
# Nested paths such as .claude/hooks/hooks.json or .claude/settings*.json cannot be matched
|
|
19
|
+
# (the matcher only registers literal filenames in the working directory), so no arm for them.
|
|
17
20
|
case "$file_path" in
|
|
18
|
-
|
|
21
|
+
CLAUDE.md|*/CLAUDE.md)
|
|
19
22
|
echo "[Hook] WARNING: Configuration file changed externally — re-read recommended" >&2
|
|
20
23
|
;;
|
|
21
|
-
|
|
24
|
+
package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|*.lock)
|
|
22
25
|
echo "[Hook] Lock file changed — dependency state may have shifted" >&2
|
|
23
26
|
;;
|
|
24
27
|
esac
|