oh-my-customcode 2.0.7 → 2.0.9

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 (45) hide show
  1. package/README.md +3 -2
  2. package/dist/cli/index.js +28 -7
  3. package/dist/index.js +28 -7
  4. package/package.json +1 -1
  5. package/templates/.claude/hooks/hooks.json +29 -13
  6. package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +47 -22
  7. package/templates/.claude/hooks/scripts/auto-continue-guard.sh +5 -5
  8. package/templates/.claude/hooks/scripts/auto-dev-token-tracker.sh +12 -1
  9. package/templates/.claude/hooks/scripts/bash-cd-advisor.sh +172 -0
  10. package/templates/.claude/hooks/scripts/content-hash-validator.sh +65 -48
  11. package/templates/.claude/hooks/scripts/destructive-git-guard.sh +32 -17
  12. package/templates/.claude/hooks/scripts/fail-axis-cause-advisor.sh +2 -2
  13. package/templates/.claude/hooks/scripts/failure-ledger.sh +18 -0
  14. package/templates/.claude/hooks/scripts/file-change-validator.sh +6 -3
  15. package/templates/.claude/hooks/scripts/git-delegation-guard.sh +178 -27
  16. package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +43 -20
  17. package/templates/.claude/hooks/scripts/omcustom-auto-update.sh +19 -5
  18. package/templates/.claude/hooks/scripts/rule-deletion-guard.sh +118 -1
  19. package/templates/.claude/hooks/scripts/schema-validator.sh +8 -11
  20. package/templates/.claude/hooks/scripts/session-env-check.sh +20 -5
  21. package/templates/.claude/hooks/scripts/stall-detection-advisor.sh +9 -5
  22. package/templates/.claude/hooks/scripts/stuck-detector.sh +21 -4
  23. package/templates/.claude/hooks/scripts/subagent-failure-advisor.sh +48 -29
  24. package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +15 -8
  25. package/templates/.claude/ontology/agents.yaml +71 -7
  26. package/templates/.claude/ontology/graphs/agent-skill.json +64 -5
  27. package/templates/.claude/ontology/graphs/full-graph.json +236 -3
  28. package/templates/.claude/ontology/skills.yaml +65 -1
  29. package/templates/.claude/rules/MUST-agent-design.md +1 -11
  30. package/templates/.claude/rules/MUST-enforcement-policy.md +7 -7
  31. package/templates/.claude/rules/MUST-orchestrator-coordination.md +1 -1
  32. package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
  33. package/templates/.claude/settings.json +29 -13
  34. package/templates/.claude/skills/fsd/SKILL.md +10 -9
  35. package/templates/.claude/skills/goal/SKILL.md +5 -5
  36. package/templates/.claude/skills/model-escalation/SKILL.md +16 -12
  37. package/templates/.claude/skills/omcustom-loop/SKILL.md +4 -4
  38. package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +13 -0
  39. package/templates/guides/claude-code/04-agent-skills.md +1 -1
  40. package/templates/guides/claude-code/15-version-compatibility.md +47 -47
  41. package/templates/guides/git-safety/README.md +4 -1
  42. package/templates/guides/middleware-patterns/README.md +4 -4
  43. package/templates/guides/multi-model-routing/README.md +1 -7
  44. package/templates/manifest.json +1 -1
  45. 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-java21 |
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 | 3 | arch-documenter, arch-speckit, agora-runner |
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.7",
338
+ version: "2.0.9",
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: "Block dev servers outside tmux - ensures you can access logs",
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: 'inline script unconditionally `exit 1`-blocks on ANY match once invoked; it never re-parses tool_input.command, so widening the matcher to bare "Bash" would block every Bash call'
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: "Block creation of random .md files - keeps docs consolidated",
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 [[ =~ ]] checks before blocking — the matcher condition is fully redundant with the script body"
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: "Block dev servers outside tmux - ensures you can access logs",
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: 'inline script unconditionally `exit 1`-blocks on ANY match once invoked; it never re-parses tool_input.command, so widening the matcher to bare "Bash" would block every Bash call'
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: "Block creation of random .md files - keeps docs consolidated",
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 [[ =~ ]] checks before blocking — the matcher condition is fully redundant with the script body"
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.7",
2930
+ version: "2.0.9",
2910
2931
  description: "Batteries-included agent harness for Claude Code",
2911
2932
  type: "module",
2912
2933
  bin: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "2.0.7",
6
+ "version": "2.0.9",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -17,10 +17,10 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "#!/bin/bash\ninput=$(cat)\ncmd=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.command // \"\"')\n\n# Block dev servers that aren't run in tmux\necho '[Hook] BLOCKED: Dev server must run in tmux for log access' >&2\necho '[Hook] Use this command instead:' >&2\necho \"[Hook] tmux new-session -d -s dev 'npm run dev'\" >&2\necho '[Hook] Then: tmux attach -t dev' >&2\nexit 1"
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": "Block dev servers outside tmux - ensures you can access logs"
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/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [[ \"$file_path\" =~ \\.(md|txt)$ ]] && [[ ! \"$file_path\" =~ (README|CLAUDE|AGENT|SKILL)\\.md$ ]]; then\n echo \"[Hook] BLOCKED: Unnecessary documentation file creation\" >&2\n echo \"[Hook] File: $file_path\" >&2\n echo \"[Hook] Use README.md or AGENT.md for documentation instead\" >&2\n exit 1\nfi\n\nprintf '%s\\n' \"$input\""
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": "Block creation of random .md files - keeps docs consolidated"
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 + 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)"
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": "Validate externally modified files — advisory staleness warning (v2.1.83+)"
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"
@@ -499,7 +505,7 @@
499
505
  "description": "Advisory cost cap monitoring — warn when session cost approaches configurable limit"
500
506
  },
501
507
  {
502
- "matcher": "tool == \"Read\"",
508
+ "matcher": "tool == \"Read\" || tool == \"Edit\" || tool == \"Write\"",
503
509
  "hooks": [
504
510
  {
505
511
  "type": "command",
@@ -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
  }
@@ -7,7 +7,11 @@ command -v jq >/dev/null 2>&1 || exit 0
7
7
  # Agent Teams Advisor Hook
8
8
  # Trigger: PreToolUse, tool == "Task" || tool == "Agent"
9
9
  # Purpose: Track Agent/Task tool usage count per session and warn when Agent Teams may be more appropriate
10
- # Protocol: stdin JSON -> process -> stdout pass-through, exit 0 always (advisory only)
10
+ # Protocol: stdin JSON -> process -> exit 0 always (advisory only)
11
+ # Once per session (#1885): each distinct advisory key is shown at most once, claimed atomically via a
12
+ # per-key marker dir under /tmp/.claude-teams-advisory-shown-${PPID}.d/ (parallel-hook safe).
13
+ # Channel (#1885): exit-0 stderr never reaches the model (CC 2.1.310 probe, #1775 comment 6405197553);
14
+ # the advisory is delivered as stdout JSON hookSpecificOutput.additionalContext. No stdin passthrough.
11
15
 
12
16
  input=$(cat)
13
17
 
@@ -25,7 +29,6 @@ ENV_STATUS="/tmp/.claude-env-status-${PPID}"
25
29
  if [ -f "$ENV_STATUS" ]; then
26
30
  teams_status=$(grep "agent_teams=" "$ENV_STATUS" 2>/dev/null | cut -d= -f2 || echo "unknown")
27
31
  if [ "$teams_status" != "enabled" ]; then
28
- printf '%s\n' "$input"
29
32
  exit 0
30
33
  fi
31
34
  fi
@@ -36,6 +39,9 @@ BATCH_ISSUES=0
36
39
  WORKFLOW_FILE=$(ls /tmp/.claude-workflow-*-"${PPID}".json 2>/dev/null | head -1 || true)
37
40
  if [ -n "$WORKFLOW_FILE" ] && [ -f "$WORKFLOW_FILE" ]; then
38
41
  BATCH_ISSUES=$(jq -r '.issue_count // 0' "$WORKFLOW_FILE" 2>/dev/null || echo 0)
42
+ # Sanitize before arithmetic: non-numeric/over-long -> 0; 10# avoids base-8 errors on leading zeros.
43
+ case "$BATCH_ISSUES" in ''|*[!0-9]*|??????????*) BATCH_ISSUES=0 ;; esac
44
+ BATCH_ISSUES=$((10#$BATCH_ISSUES))
39
45
  fi
40
46
 
41
47
  # Also check release-plan context (existence only — if file exists, treat as batch)
@@ -48,38 +54,57 @@ fi
48
54
 
49
55
  # Extract task info from input
50
56
  agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // "unknown"')
51
- prompt_preview=$(printf '%s\n' "$input" | jq -r '.tool_input.description? // ""' | head -c 60)
57
+ prompt_preview=$(printf '%s\n' "$input" | jq -r '(.tool_input.description? // "") | tostring | .[0:60]')
52
58
 
53
59
  # Session-scoped counter using parent PID as session identifier
54
60
  COUNTER_FILE="/tmp/.claude-task-count-${PPID}"
55
61
 
56
62
  # Read and increment counter
57
63
  if [ -f "$COUNTER_FILE" ]; then
58
- COUNT=$(cat "$COUNTER_FILE")
64
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
65
+ case "$COUNT" in ''|*[!0-9]*|??????????*) COUNT=0 ;; esac
66
+ COUNT=$((10#$COUNT))
59
67
  COUNT=$((COUNT + 1))
60
68
  else
61
69
  COUNT=1
62
70
  fi
63
- echo "$COUNT" > "$COUNTER_FILE"
71
+ { echo "$COUNT" > "$COUNTER_FILE"; } 2>/dev/null || true
72
+
73
+ SHOWN_DIR="/tmp/.claude-teams-advisory-shown-${PPID}.d"
74
+ # fresh KEY: atomic claim via mkdir of a per-key marker dir (hash of KEY; no shared file, so no
75
+ # check-then-append race and no newline pollution). Success => emit. State failures are silent;
76
+ # if the marker dir itself is unusable we fail open (emit) rather than lose the advisory.
77
+ fresh() {
78
+ { mkdir -p "$SHOWN_DIR"; } 2>/dev/null || true
79
+ [ -d "$SHOWN_DIR" ] || return 0
80
+ local h
81
+ h=$(printf '%s' "$1" | cksum | cut -d' ' -f1) || h=""
82
+ [ -n "$h" ] || return 0
83
+ if { mkdir "$SHOWN_DIR/$h"; } 2>/dev/null; then return 0; fi
84
+ return 1
85
+ }
64
86
 
65
87
  # Warn when batch context detected (even on first call) or from 2nd call onward
66
- if [ "$BATCH_ISSUES" -ge 3 ] && [ "$COUNT" -eq 1 ]; then
67
- echo "" >&2
68
- echo "--- [R018 Advisor] Batch context detected (${BATCH_ISSUES} issues) ---" >&2
69
- echo " RECOMMENDATION: Use Agent Teams (TeamCreate) for this batch." >&2
70
- echo " Current: Agent(${agent_type}) -- ${prompt_preview}" >&2
71
- echo "-----------------------------------------------------------" >&2
72
- elif [ "$COUNT" -ge 2 ]; then
73
- echo "" >&2
74
- echo "--- [R018 Advisor] Agent/Task tool call #${COUNT} in this session ---" >&2
75
- echo " WARNING: Multiple Task calls detected. Consider Agent Teams if:" >&2
76
- echo " * 3+ agents needed for this work" >&2
77
- echo " * Review -> fix -> re-review cycle exists" >&2
78
- echo " * Agents need shared state or coordination" >&2
79
- echo " Current: Agent(${agent_type}) -- ${prompt_preview}" >&2
80
- echo "-----------------------------------------------------------" >&2
88
+ ctx=""
89
+ if [ "$BATCH_ISSUES" -ge 3 ] && [ "$COUNT" -eq 1 ] && fresh "batch:${BATCH_ISSUES}"; then
90
+ ctx="
91
+ --- [R018 Advisor] Batch context detected (${BATCH_ISSUES} issues) ---
92
+ RECOMMENDATION: Use Agent Teams (TeamCreate) for this batch.
93
+ Current: Agent(${agent_type}) -- ${prompt_preview}
94
+ -----------------------------------------------------------"
95
+ elif [ "$COUNT" -ge 2 ] && fresh "repeat"; then
96
+ ctx="
97
+ --- [R018 Advisor] Agent/Task tool call #${COUNT} in this session ---
98
+ WARNING: Multiple Task calls detected. Consider Agent Teams if:
99
+ * 3+ agents needed for this work
100
+ * Review -> fix -> re-review cycle exists
101
+ * Agents need shared state or coordination
102
+ Current: Agent(${agent_type}) -- ${prompt_preview}
103
+ -----------------------------------------------------------"
81
104
  fi
82
105
 
83
- # Always pass through -- advisory only, never blocks
84
- printf '%s\n' "$input"
106
+ # Advisory only, never blocks: emit additionalContext when there is something to say
107
+ if [ -n "$ctx" ]; then
108
+ jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
109
+ fi
85
110
  exit 0
@@ -1,16 +1,16 @@
1
1
  #!/bin/bash
2
2
  set -euo pipefail
3
3
 
4
- # Auto-Continue Guard
4
+ # Consecutive-Completion Guard (file name kept for hook wiring)
5
5
  # Trigger: SubagentStop
6
- # Purpose: Count consecutive subagent completions and warn when auto-continue limit reached
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 stale (>60s since last update)
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
- # Warn if limit reached
27
+ # Advisory from the 4th consecutive completion (each within 60s of the previous)
28
28
  if [ "$count" -ge 4 ]; then
29
- echo '[AutoContinue] SAFETY: auto-continue limit (3) reached. Pausing.' >&2
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
- output_text=$(printf '%s\n' "$input" | jq -r '.tool_output.output // .tool_output // ""' 2>/dev/null || echo "")
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")