@arthai/agents 1.0.4 → 1.0.6

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 (131) hide show
  1. package/README.md +55 -3
  2. package/VERSION +1 -1
  3. package/agents/troubleshooter.md +132 -0
  4. package/bin/cli.js +366 -0
  5. package/bundles/canvas.json +1 -1
  6. package/bundles/compass.json +1 -1
  7. package/bundles/counsel.json +1 -0
  8. package/bundles/cruise.json +1 -1
  9. package/bundles/forge.json +12 -1
  10. package/bundles/prism.json +1 -0
  11. package/bundles/scalpel.json +5 -2
  12. package/bundles/sentinel.json +8 -2
  13. package/bundles/shield.json +1 -0
  14. package/bundles/spark.json +1 -0
  15. package/compiler.sh +14 -0
  16. package/dist/plugins/canvas/.claude-plugin/plugin.json +1 -1
  17. package/dist/plugins/canvas/VERSION +1 -0
  18. package/dist/plugins/canvas/commands/planning.md +100 -11
  19. package/dist/plugins/canvas/hooks/hooks.json +16 -0
  20. package/dist/plugins/canvas/hooks/project-setup.sh +109 -0
  21. package/dist/plugins/canvas/templates/CLAUDE.md.managed-block +123 -0
  22. package/dist/plugins/canvas/templates/CLAUDE.md.template +111 -0
  23. package/dist/plugins/compass/.claude-plugin/plugin.json +1 -1
  24. package/dist/plugins/compass/VERSION +1 -0
  25. package/dist/plugins/compass/commands/planning.md +100 -11
  26. package/dist/plugins/compass/hooks/hooks.json +16 -0
  27. package/dist/plugins/compass/hooks/project-setup.sh +109 -0
  28. package/dist/plugins/compass/templates/CLAUDE.md.managed-block +123 -0
  29. package/dist/plugins/compass/templates/CLAUDE.md.template +111 -0
  30. package/dist/plugins/counsel/.claude-plugin/plugin.json +1 -1
  31. package/dist/plugins/counsel/VERSION +1 -0
  32. package/dist/plugins/counsel/hooks/hooks.json +10 -0
  33. package/dist/plugins/counsel/hooks/project-setup.sh +109 -0
  34. package/dist/plugins/counsel/templates/CLAUDE.md.managed-block +123 -0
  35. package/dist/plugins/counsel/templates/CLAUDE.md.template +111 -0
  36. package/dist/plugins/cruise/.claude-plugin/plugin.json +1 -1
  37. package/dist/plugins/cruise/VERSION +1 -0
  38. package/dist/plugins/cruise/hooks/hooks.json +16 -0
  39. package/dist/plugins/cruise/hooks/project-setup.sh +109 -0
  40. package/dist/plugins/cruise/templates/CLAUDE.md.managed-block +123 -0
  41. package/dist/plugins/cruise/templates/CLAUDE.md.template +111 -0
  42. package/dist/plugins/forge/.claude-plugin/plugin.json +1 -1
  43. package/dist/plugins/forge/VERSION +1 -0
  44. package/dist/plugins/forge/agents/troubleshooter.md +132 -0
  45. package/dist/plugins/forge/commands/implement.md +99 -1
  46. package/dist/plugins/forge/commands/planning.md +100 -11
  47. package/dist/plugins/forge/hooks/escalation-guard.sh +177 -0
  48. package/dist/plugins/forge/hooks/hooks.json +22 -0
  49. package/dist/plugins/forge/hooks/project-setup.sh +109 -0
  50. package/dist/plugins/forge/templates/CLAUDE.md.managed-block +123 -0
  51. package/dist/plugins/forge/templates/CLAUDE.md.template +111 -0
  52. package/dist/plugins/prime/.claude-plugin/plugin.json +1 -1
  53. package/dist/plugins/prime/VERSION +1 -0
  54. package/dist/plugins/prime/agents/troubleshooter.md +132 -0
  55. package/dist/plugins/prime/commands/calibrate.md +20 -0
  56. package/dist/plugins/prime/commands/ci-fix.md +36 -0
  57. package/dist/plugins/prime/commands/fix.md +23 -0
  58. package/dist/plugins/prime/commands/implement.md +99 -1
  59. package/dist/plugins/prime/commands/planning.md +100 -11
  60. package/dist/plugins/prime/commands/qa-incident.md +54 -0
  61. package/dist/plugins/prime/commands/restart.md +186 -30
  62. package/dist/plugins/prime/hooks/escalation-guard.sh +177 -0
  63. package/dist/plugins/prime/hooks/hooks.json +60 -0
  64. package/dist/plugins/prime/hooks/post-config-change-restart-reminder.sh +86 -0
  65. package/dist/plugins/prime/hooks/post-server-crash-watch.sh +120 -0
  66. package/dist/plugins/prime/hooks/pre-server-port-guard.sh +110 -0
  67. package/dist/plugins/prime/hooks/project-setup.sh +109 -0
  68. package/dist/plugins/prime/hooks/sync-agents.sh +99 -12
  69. package/dist/plugins/prime/templates/CLAUDE.md.managed-block +123 -0
  70. package/dist/plugins/prime/templates/CLAUDE.md.template +111 -0
  71. package/dist/plugins/prism/.claude-plugin/plugin.json +1 -1
  72. package/dist/plugins/prism/VERSION +1 -0
  73. package/dist/plugins/prism/commands/qa-incident.md +54 -0
  74. package/dist/plugins/prism/hooks/hooks.json +12 -0
  75. package/dist/plugins/prism/hooks/project-setup.sh +109 -0
  76. package/dist/plugins/prism/templates/CLAUDE.md.managed-block +123 -0
  77. package/dist/plugins/prism/templates/CLAUDE.md.template +111 -0
  78. package/dist/plugins/scalpel/.claude-plugin/plugin.json +1 -1
  79. package/dist/plugins/scalpel/VERSION +1 -0
  80. package/dist/plugins/scalpel/agents/troubleshooter.md +132 -0
  81. package/dist/plugins/scalpel/commands/ci-fix.md +36 -0
  82. package/dist/plugins/scalpel/commands/fix.md +23 -0
  83. package/dist/plugins/scalpel/hooks/escalation-guard.sh +177 -0
  84. package/dist/plugins/scalpel/hooks/hooks.json +24 -0
  85. package/dist/plugins/scalpel/hooks/project-setup.sh +109 -0
  86. package/dist/plugins/scalpel/templates/CLAUDE.md.managed-block +123 -0
  87. package/dist/plugins/scalpel/templates/CLAUDE.md.template +111 -0
  88. package/dist/plugins/sentinel/.claude-plugin/plugin.json +1 -1
  89. package/dist/plugins/sentinel/VERSION +1 -0
  90. package/dist/plugins/sentinel/agents/troubleshooter.md +132 -0
  91. package/dist/plugins/sentinel/commands/restart.md +186 -30
  92. package/dist/plugins/sentinel/hooks/escalation-guard.sh +177 -0
  93. package/dist/plugins/sentinel/hooks/hooks.json +64 -0
  94. package/dist/plugins/sentinel/hooks/post-config-change-restart-reminder.sh +86 -0
  95. package/dist/plugins/sentinel/hooks/post-server-crash-watch.sh +120 -0
  96. package/dist/plugins/sentinel/hooks/pre-server-port-guard.sh +110 -0
  97. package/dist/plugins/sentinel/hooks/project-setup.sh +109 -0
  98. package/dist/plugins/sentinel/templates/CLAUDE.md.managed-block +123 -0
  99. package/dist/plugins/sentinel/templates/CLAUDE.md.template +111 -0
  100. package/dist/plugins/shield/.claude-plugin/plugin.json +1 -1
  101. package/dist/plugins/shield/VERSION +1 -0
  102. package/dist/plugins/shield/hooks/hooks.json +22 -12
  103. package/dist/plugins/shield/hooks/project-setup.sh +109 -0
  104. package/dist/plugins/shield/templates/CLAUDE.md.managed-block +123 -0
  105. package/dist/plugins/shield/templates/CLAUDE.md.template +111 -0
  106. package/dist/plugins/spark/.claude-plugin/plugin.json +1 -1
  107. package/dist/plugins/spark/VERSION +1 -0
  108. package/dist/plugins/spark/commands/calibrate.md +20 -0
  109. package/dist/plugins/spark/hooks/hooks.json +10 -0
  110. package/dist/plugins/spark/hooks/project-setup.sh +109 -0
  111. package/dist/plugins/spark/templates/CLAUDE.md.managed-block +123 -0
  112. package/dist/plugins/spark/templates/CLAUDE.md.template +111 -0
  113. package/hook-defs.json +31 -0
  114. package/hooks/escalation-guard.sh +177 -0
  115. package/hooks/post-config-change-restart-reminder.sh +86 -0
  116. package/hooks/post-server-crash-watch.sh +120 -0
  117. package/hooks/pre-server-port-guard.sh +110 -0
  118. package/hooks/project-setup.sh +109 -0
  119. package/hooks/sync-agents.sh +99 -12
  120. package/install.sh +2 -2
  121. package/package.json +1 -1
  122. package/portable.manifest +7 -1
  123. package/skills/calibrate/SKILL.md +20 -0
  124. package/skills/ci-fix/SKILL.md +36 -0
  125. package/skills/fix/SKILL.md +23 -0
  126. package/skills/implement/SKILL.md +99 -1
  127. package/skills/license/SKILL.md +159 -0
  128. package/skills/planning/SKILL.md +100 -11
  129. package/skills/publish/SKILL.md +3 -0
  130. package/skills/qa-incident/SKILL.md +54 -0
  131. package/skills/restart/SKILL.md +187 -31
@@ -0,0 +1,177 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook (Bash): Circuit breaker for consecutive failures.
3
+ # Tracks failed commands by error signature. After 3 failures with the same
4
+ # signature, forces KB consultation and outputs a structured stuck report.
5
+ #
6
+ # Uses .claude/.escalation-state.json to persist state across tool calls.
7
+ # stdout is injected as context.
8
+
9
+ set -euo pipefail
10
+
11
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
12
+ STATE_FILE="$PROJECT_DIR/.claude/.escalation-state.json"
13
+ CIRCUIT_BREAKER_THRESHOLD=3
14
+
15
+ # Extract the command and exit code
16
+ COMMAND="${CLAUDE_TOOL_INPUT_COMMAND:-}"
17
+ if [ -z "$COMMAND" ] && [ -n "${CLAUDE_TOOL_INPUT:-}" ]; then
18
+ COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('command',''))" 2>/dev/null) || true
19
+ fi
20
+
21
+ EXIT_CODE="${CLAUDE_TOOL_RESULT_EXIT_CODE:-0}"
22
+
23
+ [ -z "$COMMAND" ] && exit 0
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Skip tracking for non-failing commands and read-only commands
27
+ # ---------------------------------------------------------------------------
28
+
29
+ # Success — reset state if we had failures for this signature
30
+ if [ "$EXIT_CODE" = "0" ]; then
31
+ if [ -f "$STATE_FILE" ]; then
32
+ # Clear state on success — the issue is resolved
33
+ python3 -c "
34
+ import json, sys
35
+ try:
36
+ with open('$STATE_FILE', 'r') as f:
37
+ state = json.load(f)
38
+ # Reset consecutive failures
39
+ state['consecutive_failures'] = 0
40
+ state['last_error_signature'] = ''
41
+ state['attempts'] = []
42
+ with open('$STATE_FILE', 'w') as f:
43
+ json.dump(state, f, indent=2)
44
+ except:
45
+ pass
46
+ " 2>/dev/null || true
47
+ fi
48
+ exit 0
49
+ fi
50
+
51
+ # Skip tracking for read-only commands (ls, cat, grep, git status, etc.)
52
+ if echo "$COMMAND" | grep -qE '^\s*(ls|cat|head|tail|grep|rg|git\s+(status|log|diff|show|branch)|echo|pwd|which|type|file|wc)\b'; then
53
+ exit 0
54
+ fi
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Compute error signature from command + exit code
58
+ # ---------------------------------------------------------------------------
59
+
60
+ # Get the last few lines of error output (from CLAUDE_TOOL_RESULT_STDERR or infer)
61
+ ERROR_OUTPUT="${CLAUDE_TOOL_RESULT_STDERR:-}"
62
+ if [ -z "$ERROR_OUTPUT" ] && [ -n "${CLAUDE_TOOL_RESULT_STDOUT:-}" ]; then
63
+ # Sometimes errors go to stdout (e.g., npm, python)
64
+ ERROR_OUTPUT=$(echo "${CLAUDE_TOOL_RESULT_STDOUT:-}" | tail -5)
65
+ fi
66
+
67
+ # Compute signature: hash of (command_base + error_pattern)
68
+ # Strip variable parts (paths, timestamps, PIDs) for stable signatures
69
+ COMMAND_BASE=$(echo "$COMMAND" | awk '{print $1, $2}')
70
+ ERROR_PATTERN=$(echo "$ERROR_OUTPUT" | sed 's/[0-9]\{4,\}//g; s|/[^ ]*||g' | head -3)
71
+ SIGNATURE=$(echo "${COMMAND_BASE}:${ERROR_PATTERN}" | shasum -a 256 | cut -c1-16)
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Update state and check circuit breaker
75
+ # ---------------------------------------------------------------------------
76
+
77
+ RESULT=$(python3 -c "
78
+ import json, sys, time, os
79
+
80
+ state_file = '$STATE_FILE'
81
+ signature = '$SIGNATURE'
82
+ command = '''$COMMAND'''[:200]
83
+ error = '''$ERROR_OUTPUT'''[:500]
84
+ threshold = $CIRCUIT_BREAKER_THRESHOLD
85
+
86
+ # Load or create state
87
+ state = {'consecutive_failures': 0, 'last_error_signature': '', 'attempts': [], 'total_circuits_tripped': 0}
88
+ try:
89
+ if os.path.exists(state_file):
90
+ with open(state_file, 'r') as f:
91
+ state = json.load(f)
92
+ except:
93
+ pass
94
+
95
+ # Check if same signature as last failure
96
+ if state.get('last_error_signature') == signature:
97
+ state['consecutive_failures'] = state.get('consecutive_failures', 0) + 1
98
+ else:
99
+ # New error signature — reset counter
100
+ state['consecutive_failures'] = 1
101
+ state['attempts'] = []
102
+
103
+ state['last_error_signature'] = signature
104
+ state['attempts'] = (state.get('attempts', []) + [{'command': command, 'error': error[:200], 'time': time.time()}])[-5:]
105
+
106
+ # Check threshold
107
+ tripped = state['consecutive_failures'] >= threshold
108
+
109
+ if tripped:
110
+ state['total_circuits_tripped'] = state.get('total_circuits_tripped', 0) + 1
111
+
112
+ # Save state
113
+ os.makedirs(os.path.dirname(state_file), exist_ok=True)
114
+ with open(state_file, 'w') as f:
115
+ json.dump(state, f, indent=2)
116
+
117
+ # Output: tripped|count|attempts_summary
118
+ attempts_summary = ' / '.join([a.get('command', '')[:60] for a in state.get('attempts', [])])
119
+ print(f\"{'TRIPPED' if tripped else 'OK'}|{state['consecutive_failures']}|{attempts_summary}\")
120
+ " 2>/dev/null) || exit 0
121
+
122
+ STATUS=$(echo "$RESULT" | cut -d'|' -f1)
123
+ COUNT=$(echo "$RESULT" | cut -d'|' -f2)
124
+ ATTEMPTS=$(echo "$RESULT" | cut -d'|' -f3-)
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # If not tripped, show warning at count 2
128
+ # ---------------------------------------------------------------------------
129
+
130
+ if [ "$STATUS" = "OK" ] && [ "$COUNT" = "2" ]; then
131
+ echo "ESCALATION WARNING: 2 consecutive failures with same error pattern."
132
+ echo "One more failure triggers the circuit breaker."
133
+ echo "Before retrying: check .claude/knowledge/ for known solutions."
134
+ exit 0
135
+ fi
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Circuit breaker tripped — inject structured stuck report
139
+ # ---------------------------------------------------------------------------
140
+
141
+ if [ "$STATUS" = "TRIPPED" ]; then
142
+ echo "CIRCUIT BREAKER TRIPPED — $COUNT consecutive failures with same error signature."
143
+ echo ""
144
+ echo "STOP. Do not attempt another fix without completing these steps:"
145
+ echo ""
146
+ echo "1. CONSULT KNOWLEDGE BASE:"
147
+ echo " - Read .claude/knowledge/shared/conventions.md"
148
+ echo " - Read .claude/knowledge/qa-knowledge/ (search for error keywords)"
149
+ echo " - Run: git log --all --grep='fix:' --oneline -10"
150
+ echo ""
151
+ echo "2. GATHER EVIDENCE (if not already done):"
152
+ echo " - Read full error output (not just the last line)"
153
+ echo " - Check environment: env vars, ports (lsof), processes (ps), disk (df)"
154
+ echo " - Check dependencies: node_modules, venv, Docker containers"
155
+ echo ""
156
+ echo "3. IF STILL STUCK — use this template to ask for help:"
157
+ echo " ┌─────────────────────────────────────────────────────────┐"
158
+ echo " │ STUCK REPORT │"
159
+ echo " │ │"
160
+ echo " │ Error: [exact error message] │"
161
+ echo " │ Context: [what I was doing] │"
162
+ echo " │ Attempts: │"
163
+ echo " │ 1. [what I tried] -> [result] │"
164
+ echo " │ 2. [what I tried] -> [result] │"
165
+ echo " │ 3. [what I tried] -> [result] │"
166
+ echo " │ Evidence: [logs, state, KB search results] │"
167
+ echo " │ What I need: [access/data/decision] │"
168
+ echo " │ My recommendation: [option A because X] │"
169
+ echo " └─────────────────────────────────────────────────────────┘"
170
+ echo ""
171
+ echo "4. IF ON A TEAM — escalate to troubleshooter agent or ask a teammate."
172
+ echo " If solo — present the stuck report to the user with options."
173
+ echo ""
174
+ echo "Previous attempts: $ATTEMPTS"
175
+ fi
176
+
177
+ exit 0
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/project-setup.sh",
10
+ "timeout": 10
11
+ }
12
+ ]
13
+ },
4
14
  {
5
15
  "matcher": "",
6
16
  "hooks": [
@@ -102,6 +112,46 @@
102
112
  "timeout": 5
103
113
  }
104
114
  ]
115
+ },
116
+ {
117
+ "matcher": "Bash",
118
+ "hooks": [
119
+ {
120
+ "type": "command",
121
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/escalation-guard.sh",
122
+ "timeout": 5
123
+ }
124
+ ]
125
+ },
126
+ {
127
+ "matcher": "Edit",
128
+ "hooks": [
129
+ {
130
+ "type": "command",
131
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-config-change-restart-reminder.sh",
132
+ "timeout": 5
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ "matcher": "Write",
138
+ "hooks": [
139
+ {
140
+ "type": "command",
141
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-config-change-restart-reminder.sh",
142
+ "timeout": 5
143
+ }
144
+ ]
145
+ },
146
+ {
147
+ "matcher": "Bash",
148
+ "hooks": [
149
+ {
150
+ "type": "command",
151
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-server-crash-watch.sh",
152
+ "timeout": 15
153
+ }
154
+ ]
105
155
  }
106
156
  ],
107
157
  "SessionEnd": [
@@ -178,6 +228,16 @@
178
228
  "timeout": 5
179
229
  }
180
230
  ]
231
+ },
232
+ {
233
+ "matcher": "Bash",
234
+ "hooks": [
235
+ {
236
+ "type": "command",
237
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-server-port-guard.sh",
238
+ "timeout": 5
239
+ }
240
+ ]
181
241
  }
182
242
  ]
183
243
  }
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook (Edit/Write): Remind to restart after config file changes.
3
+ # Detects modifications to docker-compose, .env, Dockerfile, nginx, and port
4
+ # configs, then reminds the user that a /restart may be needed.
5
+ #
6
+ # stdout is injected as context.
7
+
8
+ set -euo pipefail
9
+
10
+ # Extract the file path from tool input
11
+ FILE_PATH="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"
12
+ if [ -z "$FILE_PATH" ] && [ -n "${CLAUDE_TOOL_INPUT:-}" ]; then
13
+ FILE_PATH=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('file_path',''))" 2>/dev/null) || true
14
+ fi
15
+
16
+ [ -z "$FILE_PATH" ] && exit 0
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Check if the modified file is a config that affects running services
20
+ # ---------------------------------------------------------------------------
21
+
22
+ BASENAME=$(basename "$FILE_PATH")
23
+ NEEDS_RESTART=false
24
+ REASON=""
25
+
26
+ case "$BASENAME" in
27
+ docker-compose.yml|docker-compose.yaml|docker-compose.*.yml|docker-compose.*.yaml|compose.yml|compose.yaml)
28
+ NEEDS_RESTART=true
29
+ REASON="Docker Compose config changed — containers may need rebuilding"
30
+ ;;
31
+ .env|.env.local|.env.development|.env.production)
32
+ NEEDS_RESTART=true
33
+ REASON="Environment variables changed — servers need restart to pick up new values"
34
+ ;;
35
+ Dockerfile|Dockerfile.*)
36
+ NEEDS_RESTART=true
37
+ REASON="Dockerfile changed — container needs rebuilding"
38
+ ;;
39
+ nginx.conf|nginx.*.conf)
40
+ NEEDS_RESTART=true
41
+ REASON="Nginx config changed — needs reload"
42
+ ;;
43
+ next.config.js|next.config.mjs|next.config.ts)
44
+ NEEDS_RESTART=true
45
+ REASON="Next.js config changed — dev server needs restart"
46
+ ;;
47
+ vite.config.js|vite.config.ts|vite.config.mjs)
48
+ NEEDS_RESTART=true
49
+ REASON="Vite config changed — dev server needs restart"
50
+ ;;
51
+ tsconfig.json|tsconfig.*.json)
52
+ NEEDS_RESTART=true
53
+ REASON="TypeScript config changed — dev server may need restart"
54
+ ;;
55
+ webpack.config.js|webpack.config.ts)
56
+ NEEDS_RESTART=true
57
+ REASON="Webpack config changed — dev server needs restart"
58
+ ;;
59
+ package.json)
60
+ NEEDS_RESTART=true
61
+ REASON="package.json changed — may need npm install + restart"
62
+ ;;
63
+ pyproject.toml|setup.py|setup.cfg|requirements.txt|requirements.*.txt)
64
+ NEEDS_RESTART=true
65
+ REASON="Python dependencies changed — may need pip install + restart"
66
+ ;;
67
+ Procfile|Procfile.*)
68
+ NEEDS_RESTART=true
69
+ REASON="Procfile changed — process definitions updated"
70
+ ;;
71
+ esac
72
+
73
+ # Also check for port-related changes in any file
74
+ if [ "$NEEDS_RESTART" = "false" ]; then
75
+ # Check if the file path contains common config directories
76
+ if echo "$FILE_PATH" | grep -qE '(config/|conf/|\.config)'; then
77
+ NEEDS_RESTART=true
78
+ REASON="Config file modified — services may need restart"
79
+ fi
80
+ fi
81
+
82
+ $NEEDS_RESTART || exit 0
83
+
84
+ echo "CONFIG CHANGED — $REASON. Run /restart to apply."
85
+
86
+ exit 0
@@ -0,0 +1,120 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook (Bash): Detect server start commands and check for crash loops.
3
+ # After a server start is detected, waits briefly then verifies the process is
4
+ # still running and the port is responding. Catches services that start then die.
5
+ #
6
+ # stdout is injected as context.
7
+
8
+ set -euo pipefail
9
+
10
+ COMMAND="${CLAUDE_TOOL_INPUT_COMMAND:-}"
11
+ if [ -z "$COMMAND" ] && [ -n "${CLAUDE_TOOL_INPUT:-}" ]; then
12
+ COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('command',''))" 2>/dev/null) || true
13
+ fi
14
+
15
+ [ -z "$COMMAND" ] && exit 0
16
+
17
+ # Only check commands that ran successfully
18
+ EXIT_CODE="${CLAUDE_TOOL_RESULT_EXIT_CODE:-0}"
19
+ [ "$EXIT_CODE" != "0" ] && exit 0
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Detect server start commands and extract ports
23
+ # ---------------------------------------------------------------------------
24
+
25
+ PORT=""
26
+ SERVICE=""
27
+
28
+ # npm/pnpm/yarn dev/start
29
+ if echo "$COMMAND" | grep -qE '\b(npm|pnpm|yarn)\s+run\s+(dev|start|serve)\b'; then
30
+ PORT=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
31
+ PORT="${PORT:-3000}"
32
+ SERVICE="Node.js dev server"
33
+ fi
34
+
35
+ # next dev
36
+ if echo "$COMMAND" | grep -qE '\bnext\s+dev\b'; then
37
+ PORT=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
38
+ PORT="${PORT:-3000}"
39
+ SERVICE="Next.js dev server"
40
+ fi
41
+
42
+ # uvicorn
43
+ if echo "$COMMAND" | grep -qE '\buvicorn\b'; then
44
+ PORT=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
45
+ PORT="${PORT:-8000}"
46
+ SERVICE="Uvicorn"
47
+ fi
48
+
49
+ # gunicorn
50
+ if echo "$COMMAND" | grep -qE '\bgunicorn\b'; then
51
+ PORT=$(echo "$COMMAND" | grep -oE '\-b\s+[^:]+:([0-9]+)' | grep -oE '[0-9]+$')
52
+ if [ -z "$PORT" ]; then
53
+ PORT=$(echo "$COMMAND" | grep -oE '\-\-bind\s+[^:]+:([0-9]+)' | grep -oE '[0-9]+$')
54
+ fi
55
+ PORT="${PORT:-8000}"
56
+ SERVICE="Gunicorn"
57
+ fi
58
+
59
+ # flask run
60
+ if echo "$COMMAND" | grep -qE '\bflask\s+run\b'; then
61
+ PORT=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
62
+ PORT="${PORT:-5000}"
63
+ SERVICE="Flask"
64
+ fi
65
+
66
+ # docker compose up
67
+ if echo "$COMMAND" | grep -qE '\bdocker\s+compose\s+up\b'; then
68
+ SERVICE="Docker Compose services"
69
+ # For docker compose, check container status instead of port
70
+ sleep 8
71
+ down_containers=$(docker compose ps --format '{{.Name}} {{.Status}}' 2>/dev/null | grep -viE 'Up|running' | head -5) || true
72
+ if [ -n "$down_containers" ]; then
73
+ echo "CRASH DETECTED — Docker containers not healthy after 8s:"
74
+ echo "$down_containers"
75
+ echo "Check logs: docker compose logs --tail=30"
76
+ fi
77
+ exit 0
78
+ fi
79
+
80
+ # rails server
81
+ if echo "$COMMAND" | grep -qE '\brails\s+s(erver)?\b'; then
82
+ PORT=$(echo "$COMMAND" | grep -oE '\-p\s+([0-9]+)' | awk '{print $2}')
83
+ PORT="${PORT:-3000}"
84
+ SERVICE="Rails server"
85
+ fi
86
+
87
+ # No server start detected
88
+ [ -z "$SERVICE" ] && exit 0
89
+ [ -z "$PORT" ] && exit 0
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Wait and check for crash loop
93
+ # ---------------------------------------------------------------------------
94
+
95
+ sleep 8
96
+
97
+ # Check if process is still listening on the port
98
+ pid=$(lsof -ti:"$PORT" 2>/dev/null | head -1) || true
99
+
100
+ if [ -z "$pid" ]; then
101
+ echo "CRASH DETECTED — $SERVICE (port $PORT) is no longer running after 8s."
102
+ echo "The process started but has since exited. Check the terminal output for errors."
103
+ echo "Common causes: missing env vars, port conflict, dependency errors, syntax errors."
104
+ exit 0
105
+ fi
106
+
107
+ # Process is alive — try a quick HTTP health check
108
+ http_status=$(timeout 5 curl -sf -o /dev/null -w "%{http_code}" "http://localhost:$PORT/" 2>/dev/null) || http_status="no_response"
109
+
110
+ if [ "$http_status" = "no_response" ]; then
111
+ # Process exists but not responding to HTTP — might be starting up or non-HTTP
112
+ echo "$SERVICE (port $PORT): process alive (PID $pid) but not responding to HTTP yet. May still be starting."
113
+ elif echo "$http_status" | grep -qE '^[2345]'; then
114
+ # Any HTTP response means the server is up
115
+ : # Silent success — don't spam on healthy starts
116
+ else
117
+ echo "$SERVICE (port $PORT): process alive but returned HTTP $http_status."
118
+ fi
119
+
120
+ exit 0
@@ -0,0 +1,110 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook (Bash): Check port availability before server start commands.
3
+ # Detects server start commands and warns if the target port is already occupied
4
+ # by an unexpected process.
5
+ #
6
+ # Exit 0 = allow the command
7
+ # Exit 2 = block the command (stdout shown as reason)
8
+ # stdout = injected as context (warnings)
9
+
10
+ set -euo pipefail
11
+
12
+ COMMAND="${CLAUDE_TOOL_INPUT_COMMAND:-}"
13
+ if [ -z "$COMMAND" ] && [ -n "${CLAUDE_TOOL_INPUT:-}" ]; then
14
+ COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('command',''))" 2>/dev/null) || true
15
+ fi
16
+
17
+ [ -z "$COMMAND" ] && exit 0
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Detect server start commands and extract ports
21
+ # ---------------------------------------------------------------------------
22
+
23
+ declare -a PORTS=()
24
+
25
+ # npm/pnpm/yarn dev/start — default port 3000 unless --port specified
26
+ if echo "$COMMAND" | grep -qE '\b(npm|pnpm|yarn)\s+run\s+(dev|start|serve)\b'; then
27
+ port=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
28
+ if [ -z "$port" ]; then
29
+ port=$(echo "$COMMAND" | grep -oE '\bp(ort)?\s*=?\s*([0-9]{4,5})' | grep -oE '[0-9]+')
30
+ fi
31
+ PORTS+=("${port:-3000}")
32
+ fi
33
+
34
+ # next dev — default port 3000
35
+ if echo "$COMMAND" | grep -qE '\bnext\s+dev\b'; then
36
+ port=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
37
+ PORTS+=("${port:-3000}")
38
+ fi
39
+
40
+ # vite — default port 5173
41
+ if echo "$COMMAND" | grep -qE '\bvite\b' && ! echo "$COMMAND" | grep -qE '\bvite\s+build\b'; then
42
+ port=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
43
+ PORTS+=("${port:-5173}")
44
+ fi
45
+
46
+ # uvicorn — default port 8000
47
+ if echo "$COMMAND" | grep -qE '\buvicorn\b'; then
48
+ port=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
49
+ PORTS+=("${port:-8000}")
50
+ fi
51
+
52
+ # gunicorn — default port 8000
53
+ if echo "$COMMAND" | grep -qE '\bgunicorn\b'; then
54
+ port=$(echo "$COMMAND" | grep -oE '\-b\s+[^:]+:([0-9]+)' | grep -oE '[0-9]+$')
55
+ if [ -z "$port" ]; then
56
+ port=$(echo "$COMMAND" | grep -oE '\-\-bind\s+[^:]+:([0-9]+)' | grep -oE '[0-9]+$')
57
+ fi
58
+ PORTS+=("${port:-8000}")
59
+ fi
60
+
61
+ # flask run — default port 5000
62
+ if echo "$COMMAND" | grep -qE '\bflask\s+run\b'; then
63
+ port=$(echo "$COMMAND" | grep -oE '\-\-port\s+([0-9]+)' | awk '{print $2}')
64
+ PORTS+=("${port:-5000}")
65
+ fi
66
+
67
+ # docker compose up — check compose file for ports
68
+ if echo "$COMMAND" | grep -qE '\bdocker\s+compose\s+up\b'; then
69
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
70
+ if [ -f "$PROJECT_DIR/docker-compose.yml" ]; then
71
+ # Extract host ports from ports: mappings (e.g., "3000:3000", "5432:5432")
72
+ compose_ports=$(grep -oE '^\s*-\s*"?([0-9]+):' "$PROJECT_DIR/docker-compose.yml" 2>/dev/null | grep -oE '[0-9]+' | head -5) || true
73
+ for p in $compose_ports; do
74
+ PORTS+=("$p")
75
+ done
76
+ fi
77
+ fi
78
+
79
+ # rails server — default port 3000
80
+ if echo "$COMMAND" | grep -qE '\brails\s+s(erver)?\b'; then
81
+ port=$(echo "$COMMAND" | grep -oE '\-p\s+([0-9]+)' | awk '{print $2}')
82
+ PORTS+=("${port:-3000}")
83
+ fi
84
+
85
+ # go run with -addr or explicit port
86
+ if echo "$COMMAND" | grep -qE '\bgo\s+run\b'; then
87
+ port=$(echo "$COMMAND" | grep -oE ':([0-9]{4,5})' | head -1 | tr -d ':')
88
+ [ -n "$port" ] && PORTS+=("$port")
89
+ fi
90
+
91
+ # No server command detected
92
+ [ ${#PORTS[@]} -eq 0 ] && exit 0
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Check each port for conflicts
96
+ # ---------------------------------------------------------------------------
97
+
98
+ for port in "${PORTS[@]}"; do
99
+ [ -z "$port" ] && continue
100
+
101
+ pid=$(lsof -ti:"$port" 2>/dev/null | head -1) || true
102
+ if [ -n "$pid" ]; then
103
+ proc_name=$(ps -p "$pid" -o comm= 2>/dev/null) || proc_name="unknown"
104
+ echo "PORT CONFLICT — port $port is already in use by $proc_name (PID $pid)."
105
+ echo "Kill it first: lsof -ti:$port | xargs kill -9"
106
+ echo "Or use a different port."
107
+ fi
108
+ done
109
+
110
+ exit 0
@@ -0,0 +1,109 @@
1
+ #!/bin/bash
2
+ # hooks/project-setup.sh — SessionStart hook: first-run project setup for marketplace installs.
3
+ #
4
+ # Runs on every SessionStart. Checks for marker file .claude/.toolkit-setup-done.
5
+ # If missing (or version is stale): creates CLAUDE.md from template, injects managed block,
6
+ # updates .gitignore with toolkit markers.
7
+ #
8
+ # Templates are read from ${CLAUDE_PLUGIN_ROOT}/templates/ (bundled with the plugin).
9
+
10
+ set -euo pipefail
11
+
12
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
13
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
14
+ MARKER_FILE="$PROJECT_DIR/.claude/.toolkit-setup-done"
15
+
16
+ # Exit silently if CLAUDE_PLUGIN_ROOT is not set (not running from a plugin context)
17
+ if [ -z "$PLUGIN_ROOT" ]; then
18
+ exit 0
19
+ fi
20
+
21
+ TEMPLATES_DIR="$PLUGIN_ROOT/templates"
22
+ TEMPLATE_CLAUDE="$TEMPLATES_DIR/CLAUDE.md.template"
23
+ TEMPLATE_BLOCK="$TEMPLATES_DIR/CLAUDE.md.managed-block"
24
+
25
+ # Exit silently if templates are not bundled
26
+ if [ ! -f "$TEMPLATE_CLAUDE" ] || [ ! -f "$TEMPLATE_BLOCK" ]; then
27
+ exit 0
28
+ fi
29
+
30
+ # Read toolkit version from VERSION file (bundled alongside plugin)
31
+ TOOLKIT_VERSION="0.0.0"
32
+ if [ -f "$PLUGIN_ROOT/VERSION" ]; then
33
+ TOOLKIT_VERSION=$(cat "$PLUGIN_ROOT/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "0.0.0")
34
+ fi
35
+
36
+ MANAGED_START="<!-- >>> claude-agents toolkit (DO NOT EDIT THIS BLOCK) >>> -->"
37
+ MANAGED_END="<!-- <<< claude-agents toolkit <<< -->"
38
+ GITIGNORE_START="# >>> claude-agents managed (DO NOT EDIT THIS BLOCK) >>>"
39
+
40
+ # Check marker file — skip if current version is already set up
41
+ if [ -f "$MARKER_FILE" ]; then
42
+ STORED_VERSION=$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]' || echo "")
43
+ if [ "$STORED_VERSION" = "$TOOLKIT_VERSION" ]; then
44
+ exit 0
45
+ fi
46
+ fi
47
+
48
+ CLAUDE_MD="$PROJECT_DIR/CLAUDE.md"
49
+ GITIGNORE="$PROJECT_DIR/.gitignore"
50
+
51
+ # 1. Create CLAUDE.md from template if it doesn't exist (never overwrite existing)
52
+ if [ ! -f "$CLAUDE_MD" ]; then
53
+ PROJECT_NAME=$(basename "$PROJECT_DIR")
54
+ sed "s/{{PROJECT_NAME}}/$PROJECT_NAME/g" "$TEMPLATE_CLAUDE" > "$CLAUDE_MD"
55
+ fi
56
+
57
+ # 2. Inject managed block into CLAUDE.md if missing, or update if version is stale
58
+ if [ -f "$CLAUDE_MD" ]; then
59
+ BLOCK_CONTENT=$(cat "$TEMPLATE_BLOCK")
60
+ NEW_BLOCK=$(printf '%s\n<!-- version: %s -->\n%s\n%s' \
61
+ "$MANAGED_START" "$TOOLKIT_VERSION" "$BLOCK_CONTENT" "$MANAGED_END")
62
+
63
+ if ! grep -qF "$MANAGED_START" "$CLAUDE_MD"; then
64
+ # Block missing — append it
65
+ printf '\n\n%s\n' "$NEW_BLOCK" >> "$CLAUDE_MD"
66
+ else
67
+ # Block exists — update if version changed
68
+ EXISTING_VERSION=$(grep -o '<!-- version: [^>]* -->' "$CLAUDE_MD" 2>/dev/null | head -1 | sed 's/<!-- version: //;s/ -->//' | tr -d '[:space:]' || echo "")
69
+ if [ "$EXISTING_VERSION" != "$TOOLKIT_VERSION" ]; then
70
+ # Replace block contents between markers
71
+ tmp=$(mktemp)
72
+ in_block=false
73
+ wrote_block=false
74
+ while IFS= read -r line; do
75
+ if echo "$line" | grep -qF "$MANAGED_START"; then
76
+ in_block=true
77
+ printf '%s\n' "$NEW_BLOCK" >> "$tmp"
78
+ wrote_block=true
79
+ continue
80
+ fi
81
+ if echo "$line" | grep -qF "$MANAGED_END"; then
82
+ in_block=false
83
+ continue
84
+ fi
85
+ if ! $in_block; then
86
+ echo "$line" >> "$tmp"
87
+ fi
88
+ done < "$CLAUDE_MD"
89
+ mv "$tmp" "$CLAUDE_MD"
90
+ fi
91
+ fi
92
+ fi
93
+
94
+ # 3. Update .gitignore with toolkit marker block if missing
95
+ if [ ! -f "$GITIGNORE" ] || ! grep -qF "$GITIGNORE_START" "$GITIGNORE" 2>/dev/null; then
96
+ GITIGNORE_BLOCK=$(printf '%s\n.claude/.toolkit-last-seen-sha\n.claude/.toolkit-setup-done\n.claude/.claude-agents.conf\n%s' \
97
+ "$GITIGNORE_START" "# <<< claude-agents managed <<<")
98
+ if [ ! -f "$GITIGNORE" ]; then
99
+ printf '%s\n' "$GITIGNORE_BLOCK" > "$GITIGNORE"
100
+ else
101
+ printf '\n\n%s\n' "$GITIGNORE_BLOCK" >> "$GITIGNORE"
102
+ fi
103
+ fi
104
+
105
+ # 4. Write marker file with current version
106
+ mkdir -p "$PROJECT_DIR/.claude"
107
+ printf '%s\n' "$TOOLKIT_VERSION" > "$MARKER_FILE"
108
+
109
+ exit 0