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.
- package/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +3 -4
- package/MIGRATION.md +10 -7
- package/README.md +68 -119
- package/agents/afc-architect.md +16 -0
- package/agents/afc-impl-worker.md +40 -0
- package/agents/afc-security.md +11 -0
- package/bin/cli.mjs +1 -1
- package/commands/analyze.md +6 -7
- package/commands/architect.md +5 -7
- package/commands/auto.md +355 -102
- package/commands/checkpoint.md +3 -4
- package/commands/clarify.md +8 -1
- package/commands/debug.md +40 -3
- package/commands/doctor.md +12 -13
- package/commands/ideate.md +191 -0
- package/commands/implement.md +211 -66
- package/commands/init.md +76 -61
- package/commands/launch.md +181 -0
- package/commands/plan.md +86 -22
- package/commands/principles.md +6 -2
- package/commands/resume.md +1 -2
- package/commands/review.md +68 -18
- package/commands/security.md +10 -13
- package/commands/spec.md +60 -3
- package/commands/tasks.md +19 -4
- package/commands/test.md +24 -6
- package/docs/phase-gate-protocol.md +6 -6
- package/hooks/hooks.json +29 -3
- package/package.json +19 -11
- package/schemas/hooks.schema.json +75 -0
- package/schemas/marketplace.schema.json +52 -0
- package/schemas/plugin.schema.json +53 -0
- package/scripts/afc-bash-guard.sh +6 -6
- package/scripts/afc-blast-radius.sh +418 -0
- package/scripts/afc-config-change.sh +6 -4
- package/scripts/afc-consistency-check.sh +261 -0
- package/scripts/afc-dag-validate.mjs +94 -0
- package/scripts/afc-dag-validate.sh +142 -0
- package/scripts/afc-failure-hint.sh +6 -4
- package/scripts/afc-parallel-validate.mjs +81 -0
- package/scripts/afc-parallel-validate.sh +33 -45
- package/scripts/afc-permission-request.sh +56 -11
- package/scripts/afc-pipeline-manage.sh +46 -46
- package/scripts/afc-preflight-check.sh +6 -3
- package/scripts/afc-schema-validate.sh +225 -0
- package/scripts/afc-session-end.sh +5 -5
- package/scripts/afc-state.sh +256 -0
- package/scripts/afc-stop-gate.sh +32 -24
- package/scripts/afc-subagent-context.sh +15 -6
- package/scripts/afc-subagent-stop.sh +4 -2
- package/scripts/afc-task-completed-gate.sh +19 -25
- package/scripts/afc-teammate-idle.sh +9 -14
- package/scripts/afc-test-pre-gen.sh +141 -0
- package/scripts/afc-timeline-log.sh +9 -6
- package/scripts/afc-user-prompt-submit.sh +8 -10
- package/scripts/afc-worktree-create.sh +56 -0
- package/scripts/afc-worktree-remove.sh +47 -0
- package/scripts/install-shellspec.sh +38 -0
- package/scripts/pre-compact-checkpoint.sh +6 -4
- package/scripts/session-start-context.sh +9 -8
- package/scripts/track-afc-changes.sh +6 -9
- package/templates/afc.config.template.md +12 -76
- package/templates/afc.config.express-api.md +0 -99
- package/templates/afc.config.monorepo.md +0 -98
- package/templates/afc.config.nextjs-fsd.md +0 -107
- 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 "
|
|
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
|
|
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 "
|
|
59
|
+
echo "[afc:config] Config change detected while pipeline active. source=${SOURCE} path=${FILE_PATH}" >&2
|
|
58
60
|
exit 2
|