all-for-claudecode 2.0.0 → 2.2.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 (67) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/MIGRATION.md +10 -7
  4. package/README.md +68 -119
  5. package/agents/afc-architect.md +16 -0
  6. package/agents/afc-impl-worker.md +40 -0
  7. package/agents/afc-security.md +11 -0
  8. package/bin/cli.mjs +1 -1
  9. package/commands/analyze.md +6 -7
  10. package/commands/architect.md +5 -7
  11. package/commands/auto.md +355 -102
  12. package/commands/checkpoint.md +3 -4
  13. package/commands/clarify.md +8 -1
  14. package/commands/debug.md +40 -3
  15. package/commands/doctor.md +12 -13
  16. package/commands/ideate.md +191 -0
  17. package/commands/implement.md +211 -66
  18. package/commands/init.md +76 -61
  19. package/commands/launch.md +181 -0
  20. package/commands/plan.md +86 -22
  21. package/commands/principles.md +6 -2
  22. package/commands/resume.md +1 -2
  23. package/commands/review.md +68 -18
  24. package/commands/security.md +10 -13
  25. package/commands/spec.md +60 -3
  26. package/commands/tasks.md +19 -4
  27. package/commands/test.md +24 -6
  28. package/docs/phase-gate-protocol.md +6 -6
  29. package/hooks/hooks.json +29 -3
  30. package/package.json +19 -11
  31. package/schemas/hooks.schema.json +75 -0
  32. package/schemas/marketplace.schema.json +52 -0
  33. package/schemas/plugin.schema.json +53 -0
  34. package/scripts/afc-bash-guard.sh +6 -6
  35. package/scripts/afc-blast-radius.sh +418 -0
  36. package/scripts/afc-config-change.sh +6 -4
  37. package/scripts/afc-consistency-check.sh +261 -0
  38. package/scripts/afc-dag-validate.mjs +94 -0
  39. package/scripts/afc-dag-validate.sh +142 -0
  40. package/scripts/afc-failure-hint.sh +6 -4
  41. package/scripts/afc-parallel-validate.mjs +81 -0
  42. package/scripts/afc-parallel-validate.sh +33 -45
  43. package/scripts/afc-permission-request.sh +56 -11
  44. package/scripts/afc-pipeline-manage.sh +46 -46
  45. package/scripts/afc-preflight-check.sh +6 -3
  46. package/scripts/afc-schema-validate.sh +225 -0
  47. package/scripts/afc-session-end.sh +5 -5
  48. package/scripts/afc-state.sh +256 -0
  49. package/scripts/afc-stop-gate.sh +32 -24
  50. package/scripts/afc-subagent-context.sh +15 -6
  51. package/scripts/afc-subagent-stop.sh +4 -2
  52. package/scripts/afc-task-completed-gate.sh +19 -25
  53. package/scripts/afc-teammate-idle.sh +9 -14
  54. package/scripts/afc-test-pre-gen.sh +141 -0
  55. package/scripts/afc-timeline-log.sh +9 -6
  56. package/scripts/afc-user-prompt-submit.sh +8 -10
  57. package/scripts/afc-worktree-create.sh +56 -0
  58. package/scripts/afc-worktree-remove.sh +47 -0
  59. package/scripts/install-shellspec.sh +38 -0
  60. package/scripts/pre-compact-checkpoint.sh +6 -4
  61. package/scripts/session-start-context.sh +9 -8
  62. package/scripts/track-afc-changes.sh +6 -9
  63. package/templates/afc.config.template.md +12 -76
  64. package/templates/afc.config.express-api.md +0 -99
  65. package/templates/afc.config.monorepo.md +0 -98
  66. package/templates/afc.config.nextjs-fsd.md +0 -107
  67. package/templates/afc.config.react-spa.md +0 -96
@@ -0,0 +1,418 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Blast Radius Analyzer: Traces source/dependency fan-out for planned changes
5
+ # Detects circular source chains and generates an impact report.
6
+ #
7
+ # Usage: afc-blast-radius.sh <plan_file_or_dir> [project_root]
8
+ # - plan file: parses File Change Map table to extract planned file changes
9
+ # - directory: scans all .sh files for dependency analysis
10
+ #
11
+ # Exit 0: analysis complete (no cycles)
12
+ # Exit 1: cycle detected or error
13
+
14
+ TMPDIR_WORK=""
15
+
16
+ # shellcheck disable=SC2329
17
+ cleanup() {
18
+ if [ -n "$TMPDIR_WORK" ] && [ -d "$TMPDIR_WORK" ]; then
19
+ rm -rf "$TMPDIR_WORK"
20
+ fi
21
+ }
22
+ trap cleanup EXIT
23
+
24
+ # ── Args ──────────────────────────────────────────────────
25
+
26
+ INPUT_PATH="${1:-}"
27
+ PROJECT_ROOT="${2:-${CLAUDE_PROJECT_DIR:-$(pwd)}}"
28
+
29
+ if [ -z "$INPUT_PATH" ]; then
30
+ printf '[afc:blast-radius] Usage: %s <plan_file_or_dir> [project_root]\n' "$0" >&2
31
+ exit 1
32
+ fi
33
+
34
+ if [ ! -e "$INPUT_PATH" ]; then
35
+ printf '[afc:blast-radius] Error: path not found: %s\n' "$INPUT_PATH" >&2
36
+ exit 1
37
+ fi
38
+
39
+ TMPDIR_WORK="$(mktemp -d)"
40
+
41
+ PLANNED_FILES="$TMPDIR_WORK/planned.txt"
42
+ ALL_DEPS="$TMPDIR_WORK/deps.txt"
43
+ DIRECT_DEPENDENTS="$TMPDIR_WORK/dependents.txt"
44
+ HOOKS_REFS="$TMPDIR_WORK/hooks_refs.txt"
45
+ FAN_OUT="$TMPDIR_WORK/fan_out.txt"
46
+ CYCLE_RESULT="$TMPDIR_WORK/cycle.txt"
47
+ : > "$PLANNED_FILES"
48
+ : > "$ALL_DEPS"
49
+ : > "$DIRECT_DEPENDENTS"
50
+ : > "$HOOKS_REFS"
51
+ : > "$FAN_OUT"
52
+ : > "$CYCLE_RESULT"
53
+
54
+ # ── Parse planned files ──────────────────────────────────
55
+
56
+ if [ -f "$INPUT_PATH" ]; then
57
+ # Parse plan.md: extract file paths from File Change Map table
58
+ # Format: | `path/to/file` | Action | description | ~N |
59
+ while IFS= read -r line || [ -n "$line" ]; do
60
+ # Match lines with backtick-delimited paths in table cells
61
+ # shellcheck disable=SC2016
62
+ if printf '%s\n' "$line" | grep -qE '^\|.*`[^`]+`.*\|'; then
63
+ file_path=""
64
+ # shellcheck disable=SC2016
65
+ file_path=$(printf '%s\n' "$line" | sed -n 's/^|[[:space:]]*`\([^`]*\)`.*/\1/p')
66
+ if [ -n "$file_path" ]; then
67
+ printf '%s\n' "$file_path" >> "$PLANNED_FILES"
68
+ fi
69
+ fi
70
+ done < "$INPUT_PATH"
71
+ elif [ -d "$INPUT_PATH" ]; then
72
+ # Directory mode: scan all .sh files
73
+ find "$INPUT_PATH" -name '*.sh' -type f 2>/dev/null | while IFS= read -r f; do
74
+ # Make path relative to project root if possible
75
+ rel_path="${f#"$PROJECT_ROOT"/}"
76
+ printf '%s\n' "$rel_path" >> "$PLANNED_FILES"
77
+ done
78
+ fi
79
+
80
+ PLANNED_COUNT=$(wc -l < "$PLANNED_FILES" | tr -d ' ')
81
+
82
+ if [ "$PLANNED_COUNT" -eq 0 ]; then
83
+ printf 'Impact Analysis:\n'
84
+ printf ' Planned changes: 0 files\n'
85
+ printf ' Direct dependents: 0 files\n'
86
+ printf ' High fan-out (>5 dependents): none\n'
87
+ printf ' Cross-references: none\n'
88
+ printf ' Circular dependencies: none\n'
89
+ printf ' Total blast radius: 0 files\n'
90
+ exit 0
91
+ fi
92
+
93
+ # ── Build source dependency map ──────────────────────────
94
+ # Scan all .sh files in the project for `source` and `. ` directives
95
+
96
+ SCRIPTS_DIR="$PROJECT_ROOT/scripts"
97
+ ALL_SH_FILES="$TMPDIR_WORK/all_sh.txt"
98
+ : > "$ALL_SH_FILES"
99
+
100
+ # Collect all shell scripts in the project
101
+ if [ -d "$SCRIPTS_DIR" ]; then
102
+ find "$SCRIPTS_DIR" -name '*.sh' -type f 2>/dev/null >> "$ALL_SH_FILES"
103
+ fi
104
+ # Also check spec/ directory for test files that may source scripts
105
+ if [ -d "$PROJECT_ROOT/spec" ]; then
106
+ find "$PROJECT_ROOT/spec" -name '*.sh' -type f 2>/dev/null >> "$ALL_SH_FILES"
107
+ fi
108
+
109
+ # For each shell file, extract what it sources
110
+ # Format: sourcer<TAB>sourced_basename
111
+ while IFS= read -r sh_file || [ -n "$sh_file" ]; do
112
+ [ -z "$sh_file" ] && continue
113
+ [ ! -f "$sh_file" ] && continue
114
+
115
+ sourcer_rel="${sh_file#"$PROJECT_ROOT"/}"
116
+
117
+ # Match: source "path" or . "path" (with various quoting)
118
+ # Lines like: . "$(dirname "$0")/afc-state.sh" have nested quotes,
119
+ # so we extract the .sh basename directly from the line.
120
+ while IFS= read -r src_line || [ -n "$src_line" ]; do
121
+ [ -z "$src_line" ] && continue
122
+
123
+ # Extract the last .sh filename from the source line
124
+ sourced_base=""
125
+ sourced_base=$(printf '%s\n' "$src_line" | grep -oE '[a-zA-Z0-9_.-]+\.sh' | tail -1 || true)
126
+ [ -z "$sourced_base" ] && continue
127
+
128
+ # Find the actual file path that matches this basename
129
+ sourced_rel=""
130
+ if [ -d "$SCRIPTS_DIR" ]; then
131
+ sourced_match=$(find "$SCRIPTS_DIR" -name "$sourced_base" -type f 2>/dev/null | head -1 || true)
132
+ if [ -n "$sourced_match" ]; then
133
+ sourced_rel="${sourced_match#"$PROJECT_ROOT"/}"
134
+ fi
135
+ fi
136
+ if [ -z "$sourced_rel" ]; then
137
+ # Try spec/ or other dirs
138
+ sourced_match=$(find "$PROJECT_ROOT" -name "$sourced_base" -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/vendor/*" 2>/dev/null | head -1 || true)
139
+ if [ -n "$sourced_match" ]; then
140
+ sourced_rel="${sourced_match#"$PROJECT_ROOT"/}"
141
+ else
142
+ sourced_rel="scripts/$sourced_base"
143
+ fi
144
+ fi
145
+
146
+ # Record: sourcer sources sourced_rel
147
+ printf '%s\t%s\n' "$sourcer_rel" "$sourced_rel" >> "$ALL_DEPS"
148
+ done < <(grep -nE '^\s*(\.|source)\s+' "$sh_file" 2>/dev/null || true)
149
+ done < "$ALL_SH_FILES"
150
+
151
+ # ── Find direct dependents of planned files ──────────────
152
+
153
+ while IFS= read -r planned || [ -n "$planned" ]; do
154
+ [ -z "$planned" ] && continue
155
+ planned_base="$(basename "$planned")"
156
+
157
+ # Find scripts that source this planned file
158
+ # shellcheck disable=SC2002
159
+ while IFS=$'\t' read -r sourcer sourced || [ -n "$sourcer" ]; do
160
+ sourced_base="$(basename "$sourced" 2>/dev/null || true)"
161
+ if [ "$sourced_base" = "$planned_base" ] || [ "$sourced" = "$planned" ]; then
162
+ printf '%s\n' "$sourcer" >> "$DIRECT_DEPENDENTS"
163
+ fi
164
+ done < "$ALL_DEPS"
165
+ done < "$PLANNED_FILES"
166
+
167
+ # Deduplicate dependents, exclude files already in planned list
168
+ if [ -s "$DIRECT_DEPENDENTS" ]; then
169
+ sort -u "$DIRECT_DEPENDENTS" > "$TMPDIR_WORK/dependents_unique.txt"
170
+ # Remove planned files from dependents (they are not "additional" impact)
171
+ comm -23 "$TMPDIR_WORK/dependents_unique.txt" <(sort "$PLANNED_FILES") > "$DIRECT_DEPENDENTS" 2>/dev/null || \
172
+ mv "$TMPDIR_WORK/dependents_unique.txt" "$DIRECT_DEPENDENTS"
173
+ fi
174
+
175
+ DEPENDENT_COUNT=$(wc -l < "$DIRECT_DEPENDENTS" | tr -d ' ')
176
+
177
+ # ── Compute fan-out per planned file ─────────────────────
178
+
179
+ while IFS= read -r planned || [ -n "$planned" ]; do
180
+ [ -z "$planned" ] && continue
181
+ planned_base="$(basename "$planned")"
182
+
183
+ count=0
184
+ while IFS=$'\t' read -r _sourcer sourced || [ -n "$_sourcer" ]; do
185
+ sourced_base="$(basename "$sourced" 2>/dev/null || true)"
186
+ if [ "$sourced_base" = "$planned_base" ] || [ "$sourced" = "$planned" ]; then
187
+ count=$((count + 1))
188
+ fi
189
+ done < "$ALL_DEPS"
190
+
191
+ if [ "$count" -gt 0 ]; then
192
+ printf '%d\t%s\n' "$count" "$planned" >> "$FAN_OUT"
193
+ fi
194
+ done < "$PLANNED_FILES"
195
+
196
+ # ── Check hooks.json cross-references ────────────────────
197
+
198
+ HOOKS_FILE="$PROJECT_ROOT/hooks/hooks.json"
199
+ if [ -f "$HOOKS_FILE" ]; then
200
+ while IFS= read -r planned || [ -n "$planned" ]; do
201
+ [ -z "$planned" ] && continue
202
+ planned_base="$(basename "$planned")"
203
+
204
+ if grep -q "$planned_base" "$HOOKS_FILE" 2>/dev/null; then
205
+ printf '%s\n' "$planned_base" >> "$HOOKS_REFS"
206
+ fi
207
+ done < "$PLANNED_FILES"
208
+ fi
209
+
210
+ # Deduplicate hooks refs
211
+ if [ -s "$HOOKS_REFS" ]; then
212
+ sort -u "$HOOKS_REFS" > "$TMPDIR_WORK/hooks_unique.txt"
213
+ mv "$TMPDIR_WORK/hooks_unique.txt" "$HOOKS_REFS"
214
+ fi
215
+
216
+ # ── Cycle detection (DFS) ────────────────────────────────
217
+ # Build adjacency: sourced -> sourcer (if sourced changes, sourcer is affected)
218
+ # But for cycles we care about: A sources B, B sources A
219
+ # So we detect cycles in the "sources" graph: edge from sourcer to sourced
220
+
221
+ CYCLE_FOUND=0
222
+ NODES_FILE="$TMPDIR_WORK/nodes.txt"
223
+ : > "$NODES_FILE"
224
+
225
+ # Collect all unique nodes from deps
226
+ if [ -s "$ALL_DEPS" ]; then
227
+ cut -f1 "$ALL_DEPS" >> "$NODES_FILE"
228
+ cut -f2 "$ALL_DEPS" >> "$NODES_FILE"
229
+ sort -u "$NODES_FILE" > "$TMPDIR_WORK/nodes_unique.txt"
230
+ mv "$TMPDIR_WORK/nodes_unique.txt" "$NODES_FILE"
231
+ fi
232
+
233
+ NODE_COUNT=$(wc -l < "$NODES_FILE" | tr -d ' ')
234
+
235
+ if [ "$NODE_COUNT" -gt 0 ]; then
236
+ COLOR_DIR="$TMPDIR_WORK/colors"
237
+ mkdir -p "$COLOR_DIR"
238
+
239
+ # Initialize all nodes as WHITE (0)
240
+ while IFS= read -r node || [ -n "$node" ]; do
241
+ [ -z "$node" ] && continue
242
+ # Use md5/hash for safe filenames (paths contain /)
243
+ node_hash=$(printf '%s' "$node" | md5sum 2>/dev/null | cut -d' ' -f1 || printf '%s' "$node" | md5 2>/dev/null || printf '%s' "$node" | tr '/' '_')
244
+ printf '0' > "$COLOR_DIR/$node_hash"
245
+ # Map hash back to name
246
+ printf '%s\t%s\n' "$node_hash" "$node" >> "$TMPDIR_WORK/hash_map.txt"
247
+ done < "$NODES_FILE"
248
+
249
+ # DFS from each white node
250
+ dfs_visit() {
251
+ local start_hash="$1"
252
+ local stack_file="$TMPDIR_WORK/dfs_stack.txt"
253
+ local path_file="$TMPDIR_WORK/dfs_path.txt"
254
+ printf '%s\n' "$start_hash" > "$stack_file"
255
+ : > "$path_file"
256
+
257
+ while [ -s "$stack_file" ]; do
258
+ current_hash="$(tail -1 "$stack_file")"
259
+
260
+ color_file="$COLOR_DIR/$current_hash"
261
+ if [ ! -f "$color_file" ]; then
262
+ # Remove from stack
263
+ sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"
264
+ continue
265
+ fi
266
+
267
+ color="$(cat "$color_file")"
268
+
269
+ if [ "$color" = "0" ]; then
270
+ # Mark GRAY (in progress)
271
+ printf '1' > "$color_file"
272
+ printf '%s\n' "$current_hash" >> "$path_file"
273
+
274
+ # Get current node name
275
+ current_name=$(grep -E "^${current_hash}\t" "$TMPDIR_WORK/hash_map.txt" 2>/dev/null | cut -f2 | head -1 || true)
276
+ [ -z "$current_name" ] && { sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"; continue; }
277
+
278
+ # Find neighbors (files this script sources)
279
+ has_neighbor=0
280
+ while IFS=$'\t' read -r src dst || [ -n "$src" ]; do
281
+ if [ "$src" = "$current_name" ]; then
282
+ dst_hash=$(printf '%s' "$dst" | md5sum 2>/dev/null | cut -d' ' -f1 || printf '%s' "$dst" | md5 2>/dev/null || printf '%s' "$dst" | tr '/' '_')
283
+ dst_color_file="$COLOR_DIR/$dst_hash"
284
+ [ ! -f "$dst_color_file" ] && continue
285
+
286
+ dst_color="$(cat "$dst_color_file")"
287
+ if [ "$dst_color" = "1" ]; then
288
+ # Back edge found -> cycle
289
+ CYCLE_FOUND=1
290
+ # Reconstruct cycle path
291
+ dst_name=$(grep -E "^${dst_hash}\t" "$TMPDIR_WORK/hash_map.txt" 2>/dev/null | cut -f2 | head -1 || true)
292
+ cycle_str="CYCLE:"
293
+ in_cycle=0
294
+ while IFS= read -r ph || [ -n "$ph" ]; do
295
+ if [ "$ph" = "$dst_hash" ]; then
296
+ in_cycle=1
297
+ fi
298
+ if [ "$in_cycle" -eq 1 ]; then
299
+ pname=$(grep -E "^${ph}\t" "$TMPDIR_WORK/hash_map.txt" 2>/dev/null | cut -f2 | head -1 || true)
300
+ if [ -n "$pname" ]; then
301
+ cycle_str="${cycle_str} ${pname} ->"
302
+ fi
303
+ fi
304
+ done < "$path_file"
305
+ cycle_str="${cycle_str} ${dst_name}"
306
+ printf '%s\n' "$cycle_str" > "$CYCLE_RESULT"
307
+ return
308
+ elif [ "$dst_color" = "0" ]; then
309
+ printf '%s\n' "$dst_hash" >> "$stack_file"
310
+ has_neighbor=1
311
+ fi
312
+ fi
313
+ done < "$ALL_DEPS"
314
+
315
+ if [ "$has_neighbor" -eq 0 ]; then
316
+ # Leaf node, mark BLACK
317
+ printf '2' > "$color_file"
318
+ sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"
319
+ sed -i '' '$d' "$path_file" 2>/dev/null || sed -i '$d' "$path_file"
320
+ fi
321
+ elif [ "$color" = "1" ]; then
322
+ # Returning from recursion, mark BLACK
323
+ printf '2' > "$color_file"
324
+ sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"
325
+ sed -i '' '$d' "$path_file" 2>/dev/null || sed -i '$d' "$path_file"
326
+ else
327
+ # Already BLACK, skip
328
+ sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"
329
+ fi
330
+ done
331
+ }
332
+
333
+ while IFS= read -r node || [ -n "$node" ]; do
334
+ [ -z "$node" ] && continue
335
+ node_hash=$(printf '%s' "$node" | md5sum 2>/dev/null | cut -d' ' -f1 || printf '%s' "$node" | md5 2>/dev/null || printf '%s' "$node" | tr '/' '_')
336
+ color_file="$COLOR_DIR/$node_hash"
337
+ [ ! -f "$color_file" ] && continue
338
+ color="$(cat "$color_file")"
339
+ if [ "$color" = "0" ]; then
340
+ dfs_visit "$node_hash"
341
+ if [ "$CYCLE_FOUND" -eq 1 ]; then
342
+ break
343
+ fi
344
+ fi
345
+ done < "$NODES_FILE"
346
+ fi
347
+
348
+ # ── Generate Report ──────────────────────────────────────
349
+
350
+ # Collect all impacted files (planned + dependents + hooks-referenced scripts)
351
+ ALL_IMPACTED="$TMPDIR_WORK/all_impacted.txt"
352
+ cat "$PLANNED_FILES" > "$ALL_IMPACTED"
353
+ if [ -s "$DIRECT_DEPENDENTS" ]; then
354
+ cat "$DIRECT_DEPENDENTS" >> "$ALL_IMPACTED"
355
+ fi
356
+ # hooks.json itself is impacted if any planned script is referenced
357
+ if [ -s "$HOOKS_REFS" ]; then
358
+ printf 'hooks/hooks.json\n' >> "$ALL_IMPACTED"
359
+ fi
360
+ sort -u "$ALL_IMPACTED" > "$TMPDIR_WORK/all_impacted_unique.txt"
361
+ mv "$TMPDIR_WORK/all_impacted_unique.txt" "$ALL_IMPACTED"
362
+
363
+ TOTAL_RADIUS=$(wc -l < "$ALL_IMPACTED" | tr -d ' ')
364
+
365
+ printf 'Impact Analysis:\n'
366
+ printf ' Planned changes: %d files\n' "$PLANNED_COUNT"
367
+ printf ' Direct dependents: %d files\n' "$DEPENDENT_COUNT"
368
+
369
+ # High fan-out section
370
+ HIGH_FANOUT_PRINTED=0
371
+ if [ -s "$FAN_OUT" ]; then
372
+ while IFS=$'\t' read -r count file || [ -n "$count" ]; do
373
+ [ -z "$count" ] && continue
374
+ if [ "$count" -gt 5 ]; then
375
+ if [ "$HIGH_FANOUT_PRINTED" -eq 0 ]; then
376
+ printf ' High fan-out (>5 dependents):\n'
377
+ HIGH_FANOUT_PRINTED=1
378
+ fi
379
+ printf ' - %s (sourced by %d scripts)\n' "$file" "$count"
380
+ fi
381
+ done < <(sort -rn "$FAN_OUT")
382
+ fi
383
+ if [ "$HIGH_FANOUT_PRINTED" -eq 0 ]; then
384
+ printf ' High fan-out (>5 dependents): none\n'
385
+ fi
386
+
387
+ # Cross-references section
388
+ if [ -s "$HOOKS_REFS" ]; then
389
+ refs_list=""
390
+ while IFS= read -r ref || [ -n "$ref" ]; do
391
+ if [ -z "$refs_list" ]; then
392
+ refs_list="$ref"
393
+ else
394
+ refs_list="$refs_list, $ref"
395
+ fi
396
+ done < "$HOOKS_REFS"
397
+ printf ' Cross-references:\n'
398
+ printf ' - hooks.json references: %s\n' "$refs_list"
399
+ else
400
+ printf ' Cross-references: none\n'
401
+ fi
402
+
403
+ # Circular dependencies section
404
+ if [ "$CYCLE_FOUND" -eq 1 ] && [ -s "$CYCLE_RESULT" ]; then
405
+ cycle_path=$(cat "$CYCLE_RESULT")
406
+ printf ' Circular dependencies: %s\n' "$cycle_path"
407
+ else
408
+ printf ' Circular dependencies: none\n'
409
+ fi
410
+
411
+ printf ' Total blast radius: %d files\n' "$TOTAL_RADIUS"
412
+
413
+ # Exit code: 1 if cycles found
414
+ if [ "$CYCLE_FOUND" -eq 1 ]; then
415
+ exit 1
416
+ fi
417
+
418
+ exit 0
@@ -3,26 +3,28 @@ set -euo pipefail
3
3
  # ConfigChange Hook: Audit and block config changes while pipeline is active
4
4
  # policy_settings changes are logged only; other changes are blocked (exit 2)
5
5
 
6
+ # shellcheck source=afc-state.sh
7
+ . "$(dirname "$0")/afc-state.sh"
8
+
6
9
  # trap: Preserve exit code on abnormal termination + stderr message
7
10
  # shellcheck disable=SC2329
8
11
  cleanup() {
9
12
  local exit_code=$?
10
13
  if [ "$exit_code" -ne 0 ] && [ "$exit_code" -ne 2 ]; then
11
- echo "AFC CONFIG: Abnormal exit (exit code: $exit_code)" >&2
14
+ echo "[afc:config] Abnormal exit (code: $exit_code)" >&2
12
15
  fi
13
16
  exit "$exit_code"
14
17
  }
15
18
  trap cleanup EXIT
16
19
 
17
20
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
18
- PIPELINE_FLAG="${PROJECT_DIR}/.claude/.afc-active"
19
21
  AUDIT_LOG="${PROJECT_DIR}/.claude/.afc-config-audit.log"
20
22
 
21
23
  # Read hook data from stdin
22
24
  INPUT=$(cat)
23
25
 
24
26
  # Exit silently if pipeline is inactive
25
- if [ ! -f "$PIPELINE_FLAG" ]; then
27
+ if ! afc_state_is_active; then
26
28
  exit 0
27
29
  fi
28
30
 
@@ -54,5 +56,5 @@ fi
54
56
 
55
57
  # Other changes: Write audit log + block
56
58
  printf '[%s] source=%s path=%s\n' "$TIMESTAMP" "$SOURCE" "$FILE_PATH" >> "$AUDIT_LOG"
57
- echo "AFC CONFIG: Config change detected while pipeline is active. source=${SOURCE} path=${FILE_PATH}" >&2
59
+ echo "[afc:config] Config change detected while pipeline active. source=${SOURCE} path=${FILE_PATH}" >&2
58
60
  exit 2