@tw93/waza 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +35 -0
- package/rules/anti-patterns.md +38 -0
- package/rules/chinese.md +18 -0
- package/rules/durable-context.md +27 -0
- package/rules/english.md +14 -0
- package/scripts/build_metadata.py +360 -0
- package/scripts/check_routing_drift.py +82 -0
- package/scripts/dispatcher-template.md +43 -0
- package/scripts/dispatcher.md +53 -0
- package/scripts/package-skill.sh +71 -0
- package/scripts/packaging_filter.py +55 -0
- package/scripts/setup-rule.sh +109 -0
- package/scripts/setup-statusline.sh +127 -0
- package/scripts/skill_checks.py +483 -0
- package/scripts/skill_frontmatter.py +110 -0
- package/scripts/statusline.sh +321 -0
- package/scripts/validate_package.py +66 -0
- package/scripts/verify_skills.py +100 -0
- package/skills/RESOLVER.md +91 -0
- package/skills/check/SKILL.md +338 -0
- package/skills/check/agents/reviewer-architecture.md +39 -0
- package/skills/check/agents/reviewer-security.md +39 -0
- package/skills/check/references/persona-catalog.md +56 -0
- package/skills/check/references/project-context.md +107 -0
- package/skills/check/references/public-reply.md +14 -0
- package/skills/check/scripts/audit_signals.py +485 -0
- package/skills/check/scripts/run-tests.sh +19 -0
- package/skills/design/SKILL.md +134 -0
- package/skills/design/references/design-aesthetic-quality.md +67 -0
- package/skills/design/references/design-data-viz.md +34 -0
- package/skills/design/references/design-reference.md +278 -0
- package/skills/design/references/design-tokens.md +53 -0
- package/skills/design/references/design-traps.md +43 -0
- package/skills/health/SKILL.md +231 -0
- package/skills/health/agents/inspector-context.md +119 -0
- package/skills/health/agents/inspector-control.md +84 -0
- package/skills/health/agents/inspector-maintainability.md +55 -0
- package/skills/health/scripts/check-agent-context.sh +5 -0
- package/skills/health/scripts/check-doc-refs.sh +8 -0
- package/skills/health/scripts/check-maintainability.sh +8 -0
- package/skills/health/scripts/check-verifier-output.sh +5 -0
- package/skills/health/scripts/check_agent_context.py +407 -0
- package/skills/health/scripts/check_doc_refs.py +110 -0
- package/skills/health/scripts/check_maintainability.py +629 -0
- package/skills/health/scripts/check_verifier_output.py +116 -0
- package/skills/health/scripts/collect-data.sh +760 -0
- package/skills/hunt/SKILL.md +197 -0
- package/skills/hunt/references/failure-patterns.md +75 -0
- package/skills/hunt/references/ime-unicode.md +58 -0
- package/skills/hunt/references/logging-techniques.md +72 -0
- package/skills/hunt/references/rendering-debug.md +34 -0
- package/skills/learn/SKILL.md +128 -0
- package/skills/read/SKILL.md +108 -0
- package/skills/read/references/read-methods.md +110 -0
- package/skills/read/references/save-paths.md +33 -0
- package/skills/read/scripts/fetch.sh +105 -0
- package/skills/read/scripts/fetch_feishu.py +246 -0
- package/skills/read/scripts/fetch_local.py +218 -0
- package/skills/read/scripts/fetch_weixin.py +107 -0
- package/skills/think/SKILL.md +155 -0
- package/skills/write/SKILL.md +129 -0
- package/skills/write/references/write-en.md +197 -0
- package/skills/write/references/write-zh-bilingual.md +60 -0
- package/skills/write/references/write-zh-prose.md +48 -0
- package/skills/write/references/write-zh-release-notes.md +38 -0
- package/skills/write/references/write-zh.md +645 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Collect agent configuration data for health audit.
|
|
3
|
+
# Outputs labeled sections for each data source.
|
|
4
|
+
# Run from any directory; uses pwd as the project root.
|
|
5
|
+
#
|
|
6
|
+
# Known failure modes (for interpreting (unavailable) output):
|
|
7
|
+
# jq not installed -> conversation extract and signals print "(unavailable)"; treat as [INSUFFICIENT DATA]
|
|
8
|
+
# python3 not on PATH -> MCP/hooks/allowedTools sections print "(unavailable)"; do not flag those areas
|
|
9
|
+
# settings.local.json absent -> hooks, MCP, allowedTools all show "(unavailable)"; normal for global-settings-only projects
|
|
10
|
+
# MEMORY.md path -> built via sed on pwd; unusual chars produce wrong project key; verify manually if (none) seems wrong
|
|
11
|
+
# Conversation scope -> only 2 most recent .jsonl files sampled; fewer than 2 = [LOW CONFIDENCE]
|
|
12
|
+
# MCP token estimate -> assumes ~25 tools/server, ~200 tokens/tool; treat as directional, not precise
|
|
13
|
+
# Tier misclassification -> .next/, __pycache__, .turbo/ can inflate file count; recheck manually if tier feels wrong
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
P=$(pwd)
|
|
17
|
+
SETTINGS="$P/.claude/settings.local.json"
|
|
18
|
+
TIER="${1:-auto}"
|
|
19
|
+
MODE="${2:-${WAZA_HEALTH_MODE:-summary}}"
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
if [ "${WAZA_HEALTH_DEEP:-0}" = "1" ]; then
|
|
22
|
+
MODE="deep"
|
|
23
|
+
fi
|
|
24
|
+
case "$MODE" in
|
|
25
|
+
summary|deep) ;;
|
|
26
|
+
*) MODE="summary" ;;
|
|
27
|
+
esac
|
|
28
|
+
PROJECT_KEY=$(printf '%s' "$P" | sed 's|[/_]|-|g; s|^-||')
|
|
29
|
+
CONVO_DIR="$HOME/.claude/projects/-${PROJECT_KEY}"
|
|
30
|
+
|
|
31
|
+
resolve_health_helper() {
|
|
32
|
+
local name="$1"
|
|
33
|
+
local installed_path=""
|
|
34
|
+
local candidate=""
|
|
35
|
+
|
|
36
|
+
for candidate in "$SCRIPT_DIR/$name" "./skills/health/scripts/$name"; do
|
|
37
|
+
if [ -f "$candidate" ]; then
|
|
38
|
+
printf '%s\n' "$candidate"
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
installed_path="$(npx skills path tw93/Waza 2>/dev/null || true)"
|
|
44
|
+
if [ -n "$installed_path" ] && [ -f "$installed_path/skills/health/scripts/$name" ]; then
|
|
45
|
+
printf '%s\n' "$installed_path/skills/health/scripts/$name"
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
return 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
count_project_files() {
|
|
53
|
+
local count
|
|
54
|
+
count=$(git -C "$P" ls-files 2>/dev/null | wc -l | tr -d ' ' || true)
|
|
55
|
+
if [ -z "$count" ] || [ "$count" = "0" ]; then
|
|
56
|
+
count=$(find "$P" -type f \
|
|
57
|
+
-not -path "*/.git/*" \
|
|
58
|
+
-not -path "*/node_modules/*" \
|
|
59
|
+
-not -path "*/dist/*" \
|
|
60
|
+
-not -path "*/build/*" \
|
|
61
|
+
2>/dev/null | wc -l | tr -d ' ')
|
|
62
|
+
fi
|
|
63
|
+
printf '%s\n' "${count:-0}"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
count_contributors() {
|
|
67
|
+
local count
|
|
68
|
+
count=$(git -C "$P" log -n 500 --format='%ae' 2>/dev/null | sort -u | wc -l | tr -d ' ' || true)
|
|
69
|
+
printf '%s\n' "${count:-0}"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
count_ci_workflows() {
|
|
73
|
+
local count=0
|
|
74
|
+
if [ -d "$P/.github/workflows" ]; then
|
|
75
|
+
count=$(find "$P/.github/workflows" -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | wc -l | tr -d ' ')
|
|
76
|
+
fi
|
|
77
|
+
printf '%s\n' "${count:-0}"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
count_local_skills() {
|
|
81
|
+
local count=0
|
|
82
|
+
if [ -d "$P/.claude/skills" ]; then
|
|
83
|
+
count=$(find -L "$P/.claude/skills" -maxdepth 4 -name "SKILL.md" 2>/dev/null | while IFS= read -r f; do
|
|
84
|
+
grep -q '^name: health$' "$f" 2>/dev/null && continue
|
|
85
|
+
echo "$f"
|
|
86
|
+
done | wc -l | tr -d ' ')
|
|
87
|
+
fi
|
|
88
|
+
printf '%s\n' "${count:-0}"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resolve_symlink() {
|
|
92
|
+
readlink -f "$1" 2>/dev/null && return
|
|
93
|
+
# macOS fallback: resolve symlink chain manually
|
|
94
|
+
local target="$1"
|
|
95
|
+
local depth=0
|
|
96
|
+
while [ -L "$target" ] && [ "$depth" -lt 32 ]; do
|
|
97
|
+
local dir
|
|
98
|
+
dir=$(cd "$(dirname "$target")" && pwd -P)
|
|
99
|
+
target=$(readlink "$target")
|
|
100
|
+
case "$target" in /*) ;; *) target="$dir/$target" ;; esac
|
|
101
|
+
depth=$((depth + 1))
|
|
102
|
+
done
|
|
103
|
+
printf '%s\n' "$target"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
count_file_lines() {
|
|
107
|
+
local file="$1"
|
|
108
|
+
if [ -f "$file" ]; then
|
|
109
|
+
wc -l < "$file" | tr -d ' '
|
|
110
|
+
else
|
|
111
|
+
echo 0
|
|
112
|
+
fi
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
count_file_words() {
|
|
116
|
+
local file="$1"
|
|
117
|
+
if [ -f "$file" ]; then
|
|
118
|
+
wc -w < "$file" | tr -d ' '
|
|
119
|
+
else
|
|
120
|
+
echo 0
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
list_rule_files() {
|
|
125
|
+
if [ -d "$P/.claude/rules" ]; then
|
|
126
|
+
find "$P/.claude/rules" -type f -name "*.md" 2>/dev/null | sort || true
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
print_rule_files() {
|
|
131
|
+
local found=0
|
|
132
|
+
while IFS= read -r f; do
|
|
133
|
+
[ -n "$f" ] || continue
|
|
134
|
+
found=1
|
|
135
|
+
echo "--- $f ---"
|
|
136
|
+
cat "$f"
|
|
137
|
+
done < <(list_rule_files)
|
|
138
|
+
[ "$found" -eq 1 ] || echo "(none)"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
print_file_summary() {
|
|
142
|
+
local label="$1"
|
|
143
|
+
local file="$2"
|
|
144
|
+
|
|
145
|
+
if [ ! -f "$file" ]; then
|
|
146
|
+
echo "${label}_present: no"
|
|
147
|
+
return
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
echo "${label}_present: yes"
|
|
151
|
+
echo "${label}_path: $file"
|
|
152
|
+
echo "${label}_lines: $(count_file_lines "$file")"
|
|
153
|
+
echo "${label}_words: $(count_file_words "$file")"
|
|
154
|
+
|
|
155
|
+
local headings
|
|
156
|
+
headings=$(grep -nE '^[[:space:]]*#{1,3}[[:space:]]+' "$file" 2>/dev/null | head -8 || true)
|
|
157
|
+
if [ -n "$headings" ]; then
|
|
158
|
+
echo "${label}_headings:"
|
|
159
|
+
printf '%s\n' "$headings" | sed 's/^/ /'
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
print_settings_summary() {
|
|
164
|
+
local file="$1"
|
|
165
|
+
if [ ! -f "$file" ]; then
|
|
166
|
+
echo "settings_local_json: no"
|
|
167
|
+
return
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
echo "settings_local_json: yes"
|
|
171
|
+
echo "settings_local_json_path: $file"
|
|
172
|
+
echo "settings_local_json_lines: $(count_file_lines "$file")"
|
|
173
|
+
echo "settings_local_json_bytes: $(wc -c < "$file" | tr -d ' ')"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
print_rule_file_summary() {
|
|
177
|
+
local files count=0
|
|
178
|
+
files=$(list_rule_files)
|
|
179
|
+
if [ -z "$files" ]; then
|
|
180
|
+
echo "rule_files: 0"
|
|
181
|
+
return
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
count=$(printf '%s\n' "$files" | wc -l | tr -d ' ')
|
|
185
|
+
echo "rule_files: $count"
|
|
186
|
+
while IFS= read -r f; do
|
|
187
|
+
[ -n "$f" ] || continue
|
|
188
|
+
echo "path=$f lines=$(count_file_lines "$f") words=$(count_file_words "$f")"
|
|
189
|
+
done <<EOF
|
|
190
|
+
$files
|
|
191
|
+
EOF
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
rules_word_count() {
|
|
195
|
+
local words=0
|
|
196
|
+
if [ -d "$P/.claude/rules" ]; then
|
|
197
|
+
words=$(while IFS= read -r f; do
|
|
198
|
+
[ -n "$f" ] || continue
|
|
199
|
+
cat "$f"
|
|
200
|
+
done < <(list_rule_files) | wc -w | tr -d ' ')
|
|
201
|
+
fi
|
|
202
|
+
printf '%s\n' "${words:-0}"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
collect_skill_descriptions_raw() {
|
|
206
|
+
if [ -d "$P/.claude/skills" ]; then
|
|
207
|
+
grep -r "^description:" "$P/.claude/skills" 2>/dev/null || true
|
|
208
|
+
fi
|
|
209
|
+
if [ -d "$HOME/.claude/skills" ]; then
|
|
210
|
+
grep -r "^description:" "$HOME/.claude/skills" 2>/dev/null || true
|
|
211
|
+
fi
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
print_skill_descriptions() {
|
|
215
|
+
local out
|
|
216
|
+
out=$(collect_skill_descriptions_raw | sort -u)
|
|
217
|
+
if [ -n "$out" ]; then
|
|
218
|
+
printf '%s\n' "$out"
|
|
219
|
+
else
|
|
220
|
+
echo "(none)"
|
|
221
|
+
fi
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
print_skill_description_summary() {
|
|
225
|
+
local out count
|
|
226
|
+
out=$(collect_skill_descriptions_raw | sort -u)
|
|
227
|
+
if [ -z "$out" ]; then
|
|
228
|
+
echo "skill_descriptions: 0"
|
|
229
|
+
return
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
count=$(printf '%s\n' "$out" | wc -l | tr -d ' ')
|
|
233
|
+
echo "skill_descriptions: $count"
|
|
234
|
+
printf '%s\n' "$out" | head -20 | awk -F: '{
|
|
235
|
+
path=$1
|
|
236
|
+
line=$0
|
|
237
|
+
sub(/^[^:]*:/, "", line)
|
|
238
|
+
printf "path=%s description_chars=%d\n", path, length(line)
|
|
239
|
+
}'
|
|
240
|
+
if [ "$count" -gt 20 ]; then
|
|
241
|
+
echo "skill_descriptions_truncated: yes"
|
|
242
|
+
fi
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
skill_description_word_count() {
|
|
246
|
+
local words
|
|
247
|
+
words=$(collect_skill_descriptions_raw | wc -w | tr -d ' ')
|
|
248
|
+
printf '%s\n' "${words:-0}"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
list_skill_files() {
|
|
252
|
+
local dir="$1"
|
|
253
|
+
[ -d "$dir" ] || return 0
|
|
254
|
+
find -L "$dir" -maxdepth 4 -name "SKILL.md" 2>/dev/null | sort || true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
is_health_skill() {
|
|
258
|
+
grep -q '^name: health$' "$1" 2>/dev/null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
list_conversation_files() {
|
|
262
|
+
[ -d "$CONVO_DIR" ] || return 0
|
|
263
|
+
ls -1t "$CONVO_DIR"/*.jsonl 2>/dev/null || true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
print_conversation_file_listing() {
|
|
267
|
+
local out
|
|
268
|
+
out=$(ls -lhS "$CONVO_DIR"/*.jsonl 2>/dev/null || true)
|
|
269
|
+
if [ -n "$out" ]; then
|
|
270
|
+
printf '%s\n' "$out" | head -10
|
|
271
|
+
else
|
|
272
|
+
echo "(no conversation files)"
|
|
273
|
+
fi
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
previous_conversation_files() {
|
|
277
|
+
list_conversation_files | tail -n +2 | head -2
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
sample_jsonl_prefix() {
|
|
281
|
+
local file="$1"
|
|
282
|
+
local limit="${2:-512000}"
|
|
283
|
+
LC_ALL=C awk -v limit="$limit" '
|
|
284
|
+
{
|
|
285
|
+
line = $0 ORS
|
|
286
|
+
next_bytes = bytes + length(line)
|
|
287
|
+
if (next_bytes > limit) {
|
|
288
|
+
exit
|
|
289
|
+
}
|
|
290
|
+
printf "%s", line
|
|
291
|
+
bytes = next_bytes
|
|
292
|
+
}
|
|
293
|
+
' "$file"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
extract_messages_from_file() {
|
|
297
|
+
local file="$1"
|
|
298
|
+
sample_jsonl_prefix "$file" | jq -r '
|
|
299
|
+
def flatten:
|
|
300
|
+
if (.isMeta // false) or (.toolUseResult? != null) then
|
|
301
|
+
empty
|
|
302
|
+
else
|
|
303
|
+
(.message.content // .content // .text // "")
|
|
304
|
+
| if type == "array" then
|
|
305
|
+
[ .[] | if type == "object" and .type == "text" then .text elif type == "string" then . else empty end ] | join(" ")
|
|
306
|
+
elif type == "string" then .
|
|
307
|
+
else empty
|
|
308
|
+
end
|
|
309
|
+
| gsub("[\\r\\n]+"; " ")
|
|
310
|
+
| gsub(" +"; " ")
|
|
311
|
+
| sub("^ "; "")
|
|
312
|
+
| sub(" $"; "")
|
|
313
|
+
end;
|
|
314
|
+
(.type // .role // "") as $kind
|
|
315
|
+
| (flatten) as $text
|
|
316
|
+
| if ($text | length) == 0 then
|
|
317
|
+
empty
|
|
318
|
+
elif $kind == "user" then
|
|
319
|
+
"USER: " + $text
|
|
320
|
+
elif $kind == "assistant" then
|
|
321
|
+
"ASSISTANT: " + $text
|
|
322
|
+
elif $kind == "system" then
|
|
323
|
+
"SYSTEM: " + $text
|
|
324
|
+
else
|
|
325
|
+
empty
|
|
326
|
+
end
|
|
327
|
+
' 2>/dev/null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
extract_signals_from_file() {
|
|
331
|
+
local file="$1"
|
|
332
|
+
sample_jsonl_prefix "$file" | jq -r '
|
|
333
|
+
def flatten:
|
|
334
|
+
if (.isMeta // false) or (.toolUseResult? != null) then
|
|
335
|
+
empty
|
|
336
|
+
else
|
|
337
|
+
(.message.content // .content // .text // "")
|
|
338
|
+
| if type == "array" then
|
|
339
|
+
[ .[] | if type == "object" and .type == "text" then .text elif type == "string" then . else empty end ] | join(" ")
|
|
340
|
+
elif type == "string" then .
|
|
341
|
+
else empty
|
|
342
|
+
end
|
|
343
|
+
| gsub("[\\r\\n]+"; " ")
|
|
344
|
+
| gsub(" +"; " ")
|
|
345
|
+
| sub("^ "; "")
|
|
346
|
+
| sub(" $"; "")
|
|
347
|
+
end;
|
|
348
|
+
def is_correction:
|
|
349
|
+
test("(?i)(\\bdon'\''t\\b|\\bdo not\\b|\\bplease don'\''t\\b|\\binstead\\b|\\bnext time\\b|\\bremember\\b|\\buse\\b.*\\binstead\\b|\\bnot\\b.*\\bbut\\b)")
|
|
350
|
+
or test("(不要再|请不要|不要|别再|下次|记得|改成|改为|而不是|别用|去掉|统一成)");
|
|
351
|
+
(.type // .role // "") as $kind
|
|
352
|
+
| (flatten) as $text
|
|
353
|
+
| if ($text | length) == 0 then
|
|
354
|
+
empty
|
|
355
|
+
elif ($text | test("(?i)(conversation was compressed|context limit|context window|truncat|/compact|context management|token limit|window is full|compaction)")) then
|
|
356
|
+
"CONTEXT SIGNAL: " + $text
|
|
357
|
+
# Keep this conservative: false positives pollute enforcement-gap analysis.
|
|
358
|
+
elif $kind == "user" and ($text | is_correction) then
|
|
359
|
+
"USER CORRECTION: " + $text
|
|
360
|
+
else
|
|
361
|
+
empty
|
|
362
|
+
end
|
|
363
|
+
' 2>/dev/null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
print_conversation_signals() {
|
|
367
|
+
local files file chunk found=0
|
|
368
|
+
files=$(previous_conversation_files)
|
|
369
|
+
if [ -z "$files" ]; then
|
|
370
|
+
echo "(no conversation files)"
|
|
371
|
+
return
|
|
372
|
+
fi
|
|
373
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
374
|
+
echo "(unavailable: jq not installed or parse error)"
|
|
375
|
+
return
|
|
376
|
+
fi
|
|
377
|
+
while IFS= read -r file; do
|
|
378
|
+
[ -f "$file" ] || continue
|
|
379
|
+
if ! chunk=$(extract_signals_from_file "$file"); then
|
|
380
|
+
echo "(unavailable: jq not installed or parse error)"
|
|
381
|
+
return
|
|
382
|
+
fi
|
|
383
|
+
chunk=$(printf '%s\n' "$chunk" | head -20 || true)
|
|
384
|
+
if [ -n "$chunk" ]; then
|
|
385
|
+
found=1
|
|
386
|
+
echo "--- file: $file ---"
|
|
387
|
+
printf '%s\n' "$chunk"
|
|
388
|
+
fi
|
|
389
|
+
done <<EOF
|
|
390
|
+
$files
|
|
391
|
+
EOF
|
|
392
|
+
[ "$found" -eq 1 ] || echo "(no conversation signals detected)"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
print_conversation_extract() {
|
|
396
|
+
local files file chunk found=0
|
|
397
|
+
files=$(previous_conversation_files)
|
|
398
|
+
if [ -z "$files" ]; then
|
|
399
|
+
echo "(no conversation files)"
|
|
400
|
+
return
|
|
401
|
+
fi
|
|
402
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
403
|
+
echo "(unavailable: jq not installed or parse error)"
|
|
404
|
+
return
|
|
405
|
+
fi
|
|
406
|
+
while IFS= read -r file; do
|
|
407
|
+
[ -f "$file" ] || continue
|
|
408
|
+
found=1
|
|
409
|
+
echo "--- file: $file ---"
|
|
410
|
+
if ! chunk=$(extract_messages_from_file "$file"); then
|
|
411
|
+
echo "(unavailable: jq not installed or parse error)"
|
|
412
|
+
return
|
|
413
|
+
fi
|
|
414
|
+
chunk=$(printf '%s\n' "$chunk" | grep -v '^ASSISTANT: $' | head -150 || true)
|
|
415
|
+
if [ -n "$chunk" ]; then
|
|
416
|
+
printf '%s\n' "$chunk"
|
|
417
|
+
else
|
|
418
|
+
echo "(no extractable conversation messages)"
|
|
419
|
+
fi
|
|
420
|
+
done <<EOF
|
|
421
|
+
$files
|
|
422
|
+
EOF
|
|
423
|
+
[ "$found" -eq 1 ] || echo "(no conversation files)"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
print_mcp_access_denials() {
|
|
427
|
+
local files file chunk found=0
|
|
428
|
+
files=$(list_conversation_files | head -5)
|
|
429
|
+
if [ -z "$files" ]; then
|
|
430
|
+
echo "(no conversation files)"
|
|
431
|
+
return
|
|
432
|
+
fi
|
|
433
|
+
while IFS= read -r file; do
|
|
434
|
+
[ -f "$file" ] || continue
|
|
435
|
+
chunk=$(head -c 1048576 "$file" | grep -Em 2 'Access denied - path outside allowed directories|tool-results/.+ not in ' 2>/dev/null || true)
|
|
436
|
+
if [ -n "$chunk" ]; then
|
|
437
|
+
found=1
|
|
438
|
+
printf '%s\n' "$chunk"
|
|
439
|
+
fi
|
|
440
|
+
done <<EOF
|
|
441
|
+
$files
|
|
442
|
+
EOF
|
|
443
|
+
[ "$found" -eq 1 ] || echo "(none found)"
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
PROJECT_FILES=$(count_project_files)
|
|
447
|
+
CONTRIBUTORS=$(count_contributors)
|
|
448
|
+
CI_WORKFLOWS=$(count_ci_workflows)
|
|
449
|
+
|
|
450
|
+
echo "[1/12] Tier metrics..."
|
|
451
|
+
echo "=== TIER METRICS ==="
|
|
452
|
+
echo "project_files: $PROJECT_FILES"
|
|
453
|
+
echo "contributors: $CONTRIBUTORS"
|
|
454
|
+
echo "ci_workflows: $CI_WORKFLOWS"
|
|
455
|
+
echo "skills: $(count_local_skills)"
|
|
456
|
+
echo "claude_md_lines: $(count_file_lines "$P/CLAUDE.md")"
|
|
457
|
+
echo "collection_mode: $MODE"
|
|
458
|
+
|
|
459
|
+
# Auto-detect tier if not passed as argument.
|
|
460
|
+
# Matches SKILL.md definition: Simple = <500 files AND <=1 contributor AND no CI.
|
|
461
|
+
if [ "$TIER" = "auto" ]; then
|
|
462
|
+
if [ "${PROJECT_FILES:-0}" -lt 500 ] && [ "${CONTRIBUTORS:-0}" -le 1 ] && [ "${CI_WORKFLOWS:-0}" -eq 0 ]; then
|
|
463
|
+
TIER="simple"
|
|
464
|
+
elif [ "${PROJECT_FILES:-0}" -lt 5000 ]; then
|
|
465
|
+
TIER="standard"
|
|
466
|
+
else
|
|
467
|
+
TIER="complex"
|
|
468
|
+
fi
|
|
469
|
+
fi
|
|
470
|
+
echo "detected_tier: $TIER"
|
|
471
|
+
|
|
472
|
+
echo "[2/12] CLAUDE.md (global + local)..."
|
|
473
|
+
echo "=== CLAUDE.md (global) ==="
|
|
474
|
+
if [ "$MODE" = "deep" ]; then
|
|
475
|
+
cat ~/.claude/CLAUDE.md 2>/dev/null || echo "(none)"
|
|
476
|
+
else
|
|
477
|
+
print_file_summary "global_claude_md" "$HOME/.claude/CLAUDE.md"
|
|
478
|
+
fi
|
|
479
|
+
echo "=== CLAUDE.md (local) ==="
|
|
480
|
+
if [ "$MODE" = "deep" ]; then
|
|
481
|
+
cat "$P/CLAUDE.md" 2>/dev/null || echo "(none)"
|
|
482
|
+
else
|
|
483
|
+
print_file_summary "local_claude_md" "$P/CLAUDE.md"
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
echo "[3/12] Settings, hooks, MCP..."
|
|
487
|
+
echo "=== settings.local.json ==="
|
|
488
|
+
if [ "$MODE" = "deep" ]; then
|
|
489
|
+
cat "$SETTINGS" 2>/dev/null || echo "(none)"
|
|
490
|
+
else
|
|
491
|
+
print_settings_summary "$SETTINGS"
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
echo "[4/12] Rules + skill descriptions..."
|
|
495
|
+
echo "=== rules/ ==="
|
|
496
|
+
if [ "$MODE" = "deep" ]; then
|
|
497
|
+
print_rule_files
|
|
498
|
+
else
|
|
499
|
+
print_rule_file_summary
|
|
500
|
+
fi
|
|
501
|
+
echo "=== skill descriptions ==="
|
|
502
|
+
if [ "$MODE" = "deep" ]; then
|
|
503
|
+
print_skill_descriptions
|
|
504
|
+
else
|
|
505
|
+
print_skill_description_summary
|
|
506
|
+
fi
|
|
507
|
+
|
|
508
|
+
echo "[5/12] Context budget estimate..."
|
|
509
|
+
echo "=== STARTUP CONTEXT ESTIMATE ==="
|
|
510
|
+
echo "global_claude_words: $(count_file_words "$HOME/.claude/CLAUDE.md")"
|
|
511
|
+
echo "local_claude_words: $(count_file_words "$P/CLAUDE.md")"
|
|
512
|
+
echo "rules_words: $(rules_word_count)"
|
|
513
|
+
echo "skill_desc_words: $(skill_description_word_count)"
|
|
514
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
515
|
+
python3 - "$SETTINGS" "$MODE" <<'PYEOF' 2>/dev/null || echo "(unavailable)"
|
|
516
|
+
import json
|
|
517
|
+
import sys
|
|
518
|
+
|
|
519
|
+
path = sys.argv[1]
|
|
520
|
+
mode = sys.argv[2]
|
|
521
|
+
try:
|
|
522
|
+
with open(path) as fh:
|
|
523
|
+
d = json.load(fh)
|
|
524
|
+
except Exception:
|
|
525
|
+
msg = '(unavailable: settings.local.json missing or malformed)'
|
|
526
|
+
print('=== hooks ===')
|
|
527
|
+
print(msg)
|
|
528
|
+
print('=== MCP ===')
|
|
529
|
+
print(msg)
|
|
530
|
+
print('=== MCP FILESYSTEM ===')
|
|
531
|
+
print(msg)
|
|
532
|
+
print('=== allowedTools count ===')
|
|
533
|
+
print(msg)
|
|
534
|
+
sys.exit(0)
|
|
535
|
+
|
|
536
|
+
print('=== hooks ===')
|
|
537
|
+
hooks = d.get('hooks', {})
|
|
538
|
+
if mode == 'deep':
|
|
539
|
+
print(json.dumps(hooks, indent=2))
|
|
540
|
+
elif isinstance(hooks, dict):
|
|
541
|
+
names = sorted(hooks.keys())
|
|
542
|
+
print(f'hook_events: {len(names)}')
|
|
543
|
+
print('hook_event_names:', ', '.join(names) if names else '(none)')
|
|
544
|
+
else:
|
|
545
|
+
print('hook_events: (unknown format)')
|
|
546
|
+
|
|
547
|
+
print('=== MCP ===')
|
|
548
|
+
servers = d.get('mcpServers', d.get('enabledMcpjsonServers', {}))
|
|
549
|
+
names = list(servers.keys()) if isinstance(servers, dict) else list(servers)
|
|
550
|
+
count = len(names)
|
|
551
|
+
print(f'servers({count}):', ', '.join(names))
|
|
552
|
+
est = count * 25 * 200
|
|
553
|
+
print(f'est_tokens: ~{est} ({round(est/2000)}% of 200K)')
|
|
554
|
+
|
|
555
|
+
print('=== MCP FILESYSTEM ===')
|
|
556
|
+
if isinstance(servers, list):
|
|
557
|
+
print('filesystem_present: (array format -- check .mcp.json)')
|
|
558
|
+
print('allowedDirectories: (not detectable)')
|
|
559
|
+
else:
|
|
560
|
+
filesystem = servers.get('filesystem') if isinstance(servers, dict) else None
|
|
561
|
+
allowed = []
|
|
562
|
+
if isinstance(filesystem, dict):
|
|
563
|
+
allowed = filesystem.get('allowedDirectories') or (
|
|
564
|
+
filesystem.get('config', {}).get('allowedDirectories')
|
|
565
|
+
if isinstance(filesystem.get('config'), dict)
|
|
566
|
+
else []
|
|
567
|
+
)
|
|
568
|
+
if not allowed and isinstance(filesystem.get('args'), list):
|
|
569
|
+
args = filesystem['args']
|
|
570
|
+
for index, value in enumerate(args):
|
|
571
|
+
if value in ('--allowed-directories', '--allowedDirectories') and index + 1 < len(args):
|
|
572
|
+
allowed = [args[index + 1]]
|
|
573
|
+
break
|
|
574
|
+
if not allowed:
|
|
575
|
+
allowed = [value for value in args if value.startswith('/') or (value.startswith('~') and len(value) > 1)]
|
|
576
|
+
print('filesystem_present:', 'yes' if filesystem else 'no')
|
|
577
|
+
if mode == 'deep':
|
|
578
|
+
print('allowedDirectories:', allowed or '(missing or not detected)')
|
|
579
|
+
else:
|
|
580
|
+
print('allowedDirectories_count:', len(allowed))
|
|
581
|
+
|
|
582
|
+
print('=== allowedTools count ===')
|
|
583
|
+
print(len(d.get('permissions', {}).get('allow', [])))
|
|
584
|
+
PYEOF
|
|
585
|
+
else
|
|
586
|
+
echo "=== hooks ==="
|
|
587
|
+
echo "(unavailable)"
|
|
588
|
+
echo "=== MCP ==="
|
|
589
|
+
echo "(unavailable)"
|
|
590
|
+
echo "=== MCP FILESYSTEM ==="
|
|
591
|
+
echo "(unavailable)"
|
|
592
|
+
echo "=== allowedTools count ==="
|
|
593
|
+
echo "(unavailable)"
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
echo "[6/12] Nested CLAUDE.md + gitignore..."
|
|
597
|
+
echo "=== NESTED CLAUDE.md ==="
|
|
598
|
+
_NESTED_CLAUDE=$(find "$P" -maxdepth 4 -name "CLAUDE.md" -not -path "$P/CLAUDE.md" -not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null || true)
|
|
599
|
+
if [ -n "$_NESTED_CLAUDE" ]; then
|
|
600
|
+
printf '%s\n' "$_NESTED_CLAUDE"
|
|
601
|
+
else
|
|
602
|
+
echo "(none)"
|
|
603
|
+
fi
|
|
604
|
+
echo "=== GITIGNORE ==="
|
|
605
|
+
_GITIGNORE_HIT=$(git -C "$P" check-ignore -v .claude/settings.local.json 2>/dev/null || true)
|
|
606
|
+
if [ -n "$_GITIGNORE_HIT" ]; then
|
|
607
|
+
_GITIGNORE_SOURCE=${_GITIGNORE_HIT%%:*}
|
|
608
|
+
case "$_GITIGNORE_SOURCE" in
|
|
609
|
+
.gitignore|.claude/.gitignore)
|
|
610
|
+
echo "settings.local.json: gitignored"
|
|
611
|
+
;;
|
|
612
|
+
*)
|
|
613
|
+
echo "settings.local.json: ignored only by non-project rule ($_GITIGNORE_SOURCE) -- add a repo-local ignore rule"
|
|
614
|
+
;;
|
|
615
|
+
esac
|
|
616
|
+
else
|
|
617
|
+
echo "settings.local.json: NOT gitignored -- risk of committing tokens/credentials"
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
echo "[7/12] HANDOFF.md + MEMORY.md..."
|
|
621
|
+
echo "=== HANDOFF.md ===" ; cat "$P/HANDOFF.md" 2>/dev/null || echo "(none)"
|
|
622
|
+
echo "=== MEMORY.md ==="
|
|
623
|
+
if [ -f "$HOME/.claude/projects/-${PROJECT_KEY}/memory/MEMORY.md" ]; then
|
|
624
|
+
head -50 "$HOME/.claude/projects/-${PROJECT_KEY}/memory/MEMORY.md"
|
|
625
|
+
else
|
|
626
|
+
echo "(none)"
|
|
627
|
+
fi
|
|
628
|
+
|
|
629
|
+
echo "[8/12] Conversation signals + extract..."
|
|
630
|
+
echo "=== CONVERSATION FILES ==="
|
|
631
|
+
print_conversation_file_listing
|
|
632
|
+
|
|
633
|
+
echo "=== CONVERSATION SIGNALS ==="
|
|
634
|
+
print_conversation_signals
|
|
635
|
+
|
|
636
|
+
if [ "$TIER" != "simple" ] && [ "$MODE" = "deep" ]; then
|
|
637
|
+
echo "=== CONVERSATION EXTRACT ==="
|
|
638
|
+
print_conversation_extract
|
|
639
|
+
echo "=== MCP ACCESS DENIALS ==="
|
|
640
|
+
print_mcp_access_denials
|
|
641
|
+
else
|
|
642
|
+
echo "=== CONVERSATION EXTRACT ===" ; echo "(skipped: summary mode; ask for a deep health audit or run collect-data.sh auto deep for full conversation extracts)"
|
|
643
|
+
echo "=== MCP ACCESS DENIALS ===" ; echo "(skipped: summary mode; ask for a deep health audit or run collect-data.sh auto deep for access-denial scan)"
|
|
644
|
+
fi
|
|
645
|
+
|
|
646
|
+
echo "[9/12] Agent config..."
|
|
647
|
+
if [ "$MODE" = "deep" ]; then
|
|
648
|
+
echo "=== AGENT CONFIG DETAIL ==="
|
|
649
|
+
else
|
|
650
|
+
echo "=== AGENT CONFIG SUMMARY ==="
|
|
651
|
+
fi
|
|
652
|
+
AGENT_CONTEXT_SCRIPT="$(resolve_health_helper check-agent-context.sh || true)"
|
|
653
|
+
if [ -n "$AGENT_CONTEXT_SCRIPT" ]; then
|
|
654
|
+
if ! bash "$AGENT_CONTEXT_SCRIPT" "$P" "$MODE"; then
|
|
655
|
+
echo "(unavailable: check-agent-context.sh failed)"
|
|
656
|
+
fi
|
|
657
|
+
else
|
|
658
|
+
echo "(unavailable: check-agent-context.sh not found)"
|
|
659
|
+
fi
|
|
660
|
+
|
|
661
|
+
echo "[10/12] AI maintainability..."
|
|
662
|
+
if [ "$MODE" = "deep" ]; then
|
|
663
|
+
echo "=== AI MAINTAINABILITY DETAIL ==="
|
|
664
|
+
else
|
|
665
|
+
echo "=== AI MAINTAINABILITY SUMMARY ==="
|
|
666
|
+
fi
|
|
667
|
+
MAINTAINABILITY_SCRIPT="$(resolve_health_helper check-maintainability.sh || true)"
|
|
668
|
+
if [ -n "$MAINTAINABILITY_SCRIPT" ]; then
|
|
669
|
+
if ! bash "$MAINTAINABILITY_SCRIPT" "$P" "$MODE"; then
|
|
670
|
+
echo "(unavailable: check-maintainability.sh failed)"
|
|
671
|
+
fi
|
|
672
|
+
else
|
|
673
|
+
echo "(unavailable: check-maintainability.sh not found)"
|
|
674
|
+
fi
|
|
675
|
+
|
|
676
|
+
echo "[11/12] Skill inventory + frontmatter + provenance..."
|
|
677
|
+
echo "=== SKILL INVENTORY ==="
|
|
678
|
+
_SKILL_FOUND=0
|
|
679
|
+
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
|
|
680
|
+
[ -d "$DIR" ] || continue
|
|
681
|
+
while IFS= read -r f; do
|
|
682
|
+
[ -n "$f" ] || continue
|
|
683
|
+
is_health_skill "$f" && continue
|
|
684
|
+
_SKILL_FOUND=1
|
|
685
|
+
WORDS=$(wc -w < "$f" | tr -d ' ')
|
|
686
|
+
IS_LINK="no"; LINK_TARGET=""
|
|
687
|
+
SKILL_DIR=$(dirname "$f")
|
|
688
|
+
if [ -L "$SKILL_DIR" ]; then
|
|
689
|
+
IS_LINK="yes"; LINK_TARGET=$(resolve_symlink "$SKILL_DIR")
|
|
690
|
+
fi
|
|
691
|
+
echo "path=$f words=$WORDS symlink=$IS_LINK target=$LINK_TARGET"
|
|
692
|
+
done < <(list_skill_files "$DIR")
|
|
693
|
+
done
|
|
694
|
+
[ "$_SKILL_FOUND" -eq 1 ] || echo "(none)"
|
|
695
|
+
|
|
696
|
+
echo "=== SKILL FRONTMATTER ==="
|
|
697
|
+
if [ "$MODE" = "deep" ]; then
|
|
698
|
+
_FRONTMATTER_FOUND=0
|
|
699
|
+
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
|
|
700
|
+
[ -d "$DIR" ] || continue
|
|
701
|
+
while IFS= read -r f; do
|
|
702
|
+
[ -n "$f" ] || continue
|
|
703
|
+
is_health_skill "$f" && continue
|
|
704
|
+
_FRONTMATTER_FOUND=1
|
|
705
|
+
if head -1 "$f" | grep -q '^---'; then
|
|
706
|
+
echo "frontmatter=yes path=$f"
|
|
707
|
+
sed -n '2,/^---$/p' "$f" | head -10
|
|
708
|
+
else
|
|
709
|
+
echo "frontmatter=MISSING path=$f"
|
|
710
|
+
fi
|
|
711
|
+
done < <(list_skill_files "$DIR")
|
|
712
|
+
done
|
|
713
|
+
[ "$_FRONTMATTER_FOUND" -eq 1 ] || echo "(none)"
|
|
714
|
+
else
|
|
715
|
+
echo "(skipped: summary mode; use collect-data.sh auto deep to print skill frontmatter samples)"
|
|
716
|
+
fi
|
|
717
|
+
|
|
718
|
+
echo "=== SKILL SYMLINK PROVENANCE ==="
|
|
719
|
+
_PROVENANCE_FOUND=0
|
|
720
|
+
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
|
|
721
|
+
[ -d "$DIR" ] || continue
|
|
722
|
+
find "$DIR" -maxdepth 1 -type l 2>/dev/null | while IFS= read -r link; do
|
|
723
|
+
_PROVENANCE_FOUND=1
|
|
724
|
+
TARGET=$(resolve_symlink "$link")
|
|
725
|
+
echo "link=$(basename "$link") target=$TARGET"
|
|
726
|
+
GIT_ROOT=$(git -C "$TARGET" rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
727
|
+
if [ -n "$GIT_ROOT" ]; then
|
|
728
|
+
REMOTE=$(git -C "$GIT_ROOT" remote get-url origin 2>/dev/null || echo "unknown")
|
|
729
|
+
COMMIT=$(git -C "$GIT_ROOT" rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
730
|
+
echo " git_remote=$REMOTE commit=$COMMIT"
|
|
731
|
+
fi
|
|
732
|
+
done
|
|
733
|
+
done
|
|
734
|
+
if ! { for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
|
|
735
|
+
[ -d "$DIR" ] || continue
|
|
736
|
+
find "$DIR" -maxdepth 1 -type l 2>/dev/null
|
|
737
|
+
done | grep -q .; }; then
|
|
738
|
+
echo "(none)"
|
|
739
|
+
fi
|
|
740
|
+
|
|
741
|
+
echo "[12/12] Skill content sample + security scan..."
|
|
742
|
+
if [ "$TIER" != "simple" ] && [ "$MODE" = "deep" ]; then
|
|
743
|
+
echo "=== SKILL FULL CONTENT ==="
|
|
744
|
+
_CONTENT_COUNT=0
|
|
745
|
+
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
|
|
746
|
+
[ -d "$DIR" ] || continue
|
|
747
|
+
while IFS= read -r f; do
|
|
748
|
+
[ -n "$f" ] || continue
|
|
749
|
+
is_health_skill "$f" && continue
|
|
750
|
+
_CONTENT_COUNT=$((_CONTENT_COUNT + 1))
|
|
751
|
+
[ "$_CONTENT_COUNT" -le 3 ] || break
|
|
752
|
+
echo "--- FULL: $f ---"
|
|
753
|
+
head -60 "$f"
|
|
754
|
+
done < <(list_skill_files "$DIR")
|
|
755
|
+
[ "$_CONTENT_COUNT" -ge 3 ] && break
|
|
756
|
+
done
|
|
757
|
+
[ "$_CONTENT_COUNT" -gt 0 ] || echo "(none)"
|
|
758
|
+
else
|
|
759
|
+
echo "=== SKILL FULL CONTENT ===" ; echo "(skipped: summary mode; ask for a deep health audit or run collect-data.sh auto deep to sample skill bodies)"
|
|
760
|
+
fi
|