@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/package.json +35 -0
  4. package/rules/anti-patterns.md +38 -0
  5. package/rules/chinese.md +18 -0
  6. package/rules/durable-context.md +27 -0
  7. package/rules/english.md +14 -0
  8. package/scripts/build_metadata.py +360 -0
  9. package/scripts/check_routing_drift.py +82 -0
  10. package/scripts/dispatcher-template.md +43 -0
  11. package/scripts/dispatcher.md +53 -0
  12. package/scripts/package-skill.sh +71 -0
  13. package/scripts/packaging_filter.py +55 -0
  14. package/scripts/setup-rule.sh +109 -0
  15. package/scripts/setup-statusline.sh +127 -0
  16. package/scripts/skill_checks.py +483 -0
  17. package/scripts/skill_frontmatter.py +110 -0
  18. package/scripts/statusline.sh +321 -0
  19. package/scripts/validate_package.py +66 -0
  20. package/scripts/verify_skills.py +100 -0
  21. package/skills/RESOLVER.md +91 -0
  22. package/skills/check/SKILL.md +338 -0
  23. package/skills/check/agents/reviewer-architecture.md +39 -0
  24. package/skills/check/agents/reviewer-security.md +39 -0
  25. package/skills/check/references/persona-catalog.md +56 -0
  26. package/skills/check/references/project-context.md +107 -0
  27. package/skills/check/references/public-reply.md +14 -0
  28. package/skills/check/scripts/audit_signals.py +485 -0
  29. package/skills/check/scripts/run-tests.sh +19 -0
  30. package/skills/design/SKILL.md +134 -0
  31. package/skills/design/references/design-aesthetic-quality.md +67 -0
  32. package/skills/design/references/design-data-viz.md +34 -0
  33. package/skills/design/references/design-reference.md +278 -0
  34. package/skills/design/references/design-tokens.md +53 -0
  35. package/skills/design/references/design-traps.md +43 -0
  36. package/skills/health/SKILL.md +231 -0
  37. package/skills/health/agents/inspector-context.md +119 -0
  38. package/skills/health/agents/inspector-control.md +84 -0
  39. package/skills/health/agents/inspector-maintainability.md +55 -0
  40. package/skills/health/scripts/check-agent-context.sh +5 -0
  41. package/skills/health/scripts/check-doc-refs.sh +8 -0
  42. package/skills/health/scripts/check-maintainability.sh +8 -0
  43. package/skills/health/scripts/check-verifier-output.sh +5 -0
  44. package/skills/health/scripts/check_agent_context.py +407 -0
  45. package/skills/health/scripts/check_doc_refs.py +110 -0
  46. package/skills/health/scripts/check_maintainability.py +629 -0
  47. package/skills/health/scripts/check_verifier_output.py +116 -0
  48. package/skills/health/scripts/collect-data.sh +760 -0
  49. package/skills/hunt/SKILL.md +197 -0
  50. package/skills/hunt/references/failure-patterns.md +75 -0
  51. package/skills/hunt/references/ime-unicode.md +58 -0
  52. package/skills/hunt/references/logging-techniques.md +72 -0
  53. package/skills/hunt/references/rendering-debug.md +34 -0
  54. package/skills/learn/SKILL.md +128 -0
  55. package/skills/read/SKILL.md +108 -0
  56. package/skills/read/references/read-methods.md +110 -0
  57. package/skills/read/references/save-paths.md +33 -0
  58. package/skills/read/scripts/fetch.sh +105 -0
  59. package/skills/read/scripts/fetch_feishu.py +246 -0
  60. package/skills/read/scripts/fetch_local.py +218 -0
  61. package/skills/read/scripts/fetch_weixin.py +107 -0
  62. package/skills/think/SKILL.md +155 -0
  63. package/skills/write/SKILL.md +129 -0
  64. package/skills/write/references/write-en.md +197 -0
  65. package/skills/write/references/write-zh-bilingual.md +60 -0
  66. package/skills/write/references/write-zh-prose.md +48 -0
  67. package/skills/write/references/write-zh-release-notes.md +38 -0
  68. 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