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,261 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# afc-consistency-check.sh — Cross-reference validation for project consistency
|
|
5
|
+
# Checks: config placeholders, agent names, hook scripts, test coverage
|
|
6
|
+
# Run as part of: npm run lint
|
|
7
|
+
|
|
8
|
+
# shellcheck disable=SC2329
|
|
9
|
+
cleanup() {
|
|
10
|
+
:
|
|
11
|
+
}
|
|
12
|
+
trap cleanup EXIT
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
PROJECT_DIR="${1:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
16
|
+
ERRORS=0
|
|
17
|
+
WARNINGS=0
|
|
18
|
+
|
|
19
|
+
# --- Helpers ---
|
|
20
|
+
|
|
21
|
+
fail() {
|
|
22
|
+
printf "[afc:consistency] FAIL: %s\n" "$1" >&2
|
|
23
|
+
ERRORS=$((ERRORS + 1))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
warn() {
|
|
27
|
+
printf "[afc:consistency] WARN: %s\n" "$1" >&2
|
|
28
|
+
WARNINGS=$((WARNINGS + 1))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ok() {
|
|
32
|
+
printf "[afc:consistency] ✓ %s\n" "$1"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# --- Check 1: Config Placeholder Validation ---
|
|
36
|
+
# Verify all {config.*} references in commands/ and docs/ map to known config keys
|
|
37
|
+
|
|
38
|
+
check_config_placeholders() {
|
|
39
|
+
local template="$PROJECT_DIR/templates/afc.config.template.md"
|
|
40
|
+
if [ ! -f "$template" ]; then
|
|
41
|
+
warn "Config template not found: $template"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Extract valid config keys from template
|
|
46
|
+
# YAML keys: ci, gate, test
|
|
47
|
+
local yaml_keys
|
|
48
|
+
yaml_keys=$(grep -oE '^\s*[a-z_]+:' "$template" 2>/dev/null | sed 's/[[:space:]]*//;s/://' | sort -u || true)
|
|
49
|
+
# Section headers → lowercase with underscores: Architecture → architecture, Code Style → code_style, Project Context → project_context
|
|
50
|
+
local section_keys
|
|
51
|
+
section_keys=$(grep -oE '^## [A-Za-z ]+' "$template" 2>/dev/null \
|
|
52
|
+
| sed 's/^## //' \
|
|
53
|
+
| tr '[:upper:]' '[:lower:]' \
|
|
54
|
+
| sed 's/ /_/g' \
|
|
55
|
+
| sort -u || true)
|
|
56
|
+
|
|
57
|
+
local valid_keys
|
|
58
|
+
valid_keys=$(printf '%s\n%s\n' "$yaml_keys" "$section_keys" | sort -u)
|
|
59
|
+
|
|
60
|
+
# Extract all {config.*} references from commands and docs
|
|
61
|
+
local refs
|
|
62
|
+
refs=$(grep -rohE '\{config\.[a-z_]+\}' "$PROJECT_DIR/commands/" "$PROJECT_DIR/docs/" 2>/dev/null \
|
|
63
|
+
| sed 's/{config\.//;s/}//' \
|
|
64
|
+
| sort -u || true)
|
|
65
|
+
|
|
66
|
+
local count=0
|
|
67
|
+
local invalid=0
|
|
68
|
+
for ref in $refs; do
|
|
69
|
+
count=$((count + 1))
|
|
70
|
+
if ! printf '%s\n' "$valid_keys" | grep -qxF "$ref"; then
|
|
71
|
+
fail "{config.$ref} referenced but not defined in config template"
|
|
72
|
+
invalid=$((invalid + 1))
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
if [ "$invalid" -eq 0 ]; then
|
|
77
|
+
ok "Config placeholders: $count references, all valid"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- Check 2: Agent Name Consistency ---
|
|
82
|
+
# Verify subagent_type references in commands match agent definitions
|
|
83
|
+
|
|
84
|
+
check_agent_names() {
|
|
85
|
+
local agents_dir="$PROJECT_DIR/agents"
|
|
86
|
+
if [ ! -d "$agents_dir" ]; then
|
|
87
|
+
warn "Agents directory not found"
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Extract agent names from agent files (name: field in frontmatter)
|
|
92
|
+
local defined_agents
|
|
93
|
+
defined_agents=$(grep -h '^name:' "$agents_dir"/*.md 2>/dev/null \
|
|
94
|
+
| sed 's/^name:[[:space:]]*//' \
|
|
95
|
+
| tr -d '"' \
|
|
96
|
+
| sort -u || true)
|
|
97
|
+
|
|
98
|
+
# Extract subagent_type references from commands (afc:agent-name pattern)
|
|
99
|
+
local referenced_agents
|
|
100
|
+
referenced_agents=$(grep -rohE 'subagent_type:[[:space:]]*"afc:[^"]*"' "$PROJECT_DIR/commands/" 2>/dev/null \
|
|
101
|
+
| sed 's/.*"afc://;s/"//' \
|
|
102
|
+
| sort -u || true)
|
|
103
|
+
|
|
104
|
+
local count=0
|
|
105
|
+
local invalid=0
|
|
106
|
+
for ref in $referenced_agents; do
|
|
107
|
+
count=$((count + 1))
|
|
108
|
+
if ! printf '%s\n' "$defined_agents" | grep -qxF "$ref"; then
|
|
109
|
+
fail "subagent_type 'afc:$ref' referenced but no agents/$ref.md found"
|
|
110
|
+
invalid=$((invalid + 1))
|
|
111
|
+
fi
|
|
112
|
+
done
|
|
113
|
+
|
|
114
|
+
# Check for unprefixed subagent_type that should have afc: prefix
|
|
115
|
+
local unprefixed
|
|
116
|
+
unprefixed=$(grep -rohE 'subagent_type:[[:space:]]*"afc-[^"]*"' "$PROJECT_DIR/commands/" 2>/dev/null \
|
|
117
|
+
| sed 's/.*subagent_type:[[:space:]]*"//;s/".*//' \
|
|
118
|
+
| sort -u || true)
|
|
119
|
+
for ref in $unprefixed; do
|
|
120
|
+
if printf '%s\n' "$defined_agents" | grep -qxF "$ref"; then
|
|
121
|
+
fail "subagent_type '$ref' should use 'afc:$ref' prefix (found in agents/)"
|
|
122
|
+
invalid=$((invalid + 1))
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
if [ "$invalid" -eq 0 ]; then
|
|
127
|
+
ok "Agent names: $count references, all consistent"
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# --- Check 3: Hook Script Existence ---
|
|
132
|
+
# Verify all scripts referenced in hooks.json actually exist
|
|
133
|
+
|
|
134
|
+
check_hook_scripts() {
|
|
135
|
+
local hooks_file="$PROJECT_DIR/hooks/hooks.json"
|
|
136
|
+
if [ ! -f "$hooks_file" ]; then
|
|
137
|
+
warn "hooks.json not found"
|
|
138
|
+
return
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
local scripts
|
|
142
|
+
scripts=$(grep -oE 'scripts/[^"]+\.sh' "$hooks_file" 2>/dev/null | sort -u || true)
|
|
143
|
+
|
|
144
|
+
local count=0
|
|
145
|
+
local missing=0
|
|
146
|
+
for script in $scripts; do
|
|
147
|
+
count=$((count + 1))
|
|
148
|
+
if [ ! -f "$PROJECT_DIR/$script" ]; then
|
|
149
|
+
fail "hooks.json references '$script' but file not found"
|
|
150
|
+
missing=$((missing + 1))
|
|
151
|
+
fi
|
|
152
|
+
done
|
|
153
|
+
|
|
154
|
+
if [ "$missing" -eq 0 ]; then
|
|
155
|
+
ok "Hook scripts: $count references, all exist"
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# --- Check 4: Test Coverage ---
|
|
160
|
+
# Verify each afc-*.sh script (except afc-state.sh library) has a spec file
|
|
161
|
+
|
|
162
|
+
check_test_coverage() {
|
|
163
|
+
local count=0
|
|
164
|
+
local missing=0
|
|
165
|
+
for script in "$PROJECT_DIR"/scripts/afc-*.sh; do
|
|
166
|
+
local scriptname
|
|
167
|
+
scriptname=$(basename "$script" .sh)
|
|
168
|
+
# Skip shared library and self (validation script)
|
|
169
|
+
if [ "$scriptname" = "afc-state" ] || [ "$scriptname" = "afc-consistency-check" ]; then
|
|
170
|
+
continue
|
|
171
|
+
fi
|
|
172
|
+
count=$((count + 1))
|
|
173
|
+
if [ ! -f "$PROJECT_DIR/spec/${scriptname}_spec.sh" ]; then
|
|
174
|
+
fail "scripts/$scriptname.sh has no spec/${scriptname}_spec.sh"
|
|
175
|
+
missing=$((missing + 1))
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
|
|
179
|
+
if [ "$missing" -eq 0 ]; then
|
|
180
|
+
ok "Test coverage: $count scripts, all have specs"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# --- Check 5: Version Sync ---
|
|
185
|
+
# Verify version numbers match across package.json, plugin.json, marketplace.json
|
|
186
|
+
|
|
187
|
+
check_version_sync() {
|
|
188
|
+
local pkg="$PROJECT_DIR/package.json"
|
|
189
|
+
local plugin="$PROJECT_DIR/.claude-plugin/plugin.json"
|
|
190
|
+
local market="$PROJECT_DIR/.claude-plugin/marketplace.json"
|
|
191
|
+
|
|
192
|
+
if [ ! -f "$pkg" ] || [ ! -f "$plugin" ] || [ ! -f "$market" ]; then
|
|
193
|
+
warn "One or more version files missing"
|
|
194
|
+
return
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
local pkg_ver plugin_ver market_meta market_plugin
|
|
198
|
+
if command -v jq >/dev/null 2>&1; then
|
|
199
|
+
pkg_ver=$(jq -r '.version' "$pkg")
|
|
200
|
+
plugin_ver=$(jq -r '.version' "$plugin")
|
|
201
|
+
market_meta=$(jq -r '.metadata.version' "$market")
|
|
202
|
+
market_plugin=$(jq -r '.plugins[0].version' "$market")
|
|
203
|
+
else
|
|
204
|
+
pkg_ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$pkg" | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//;s/"//')
|
|
205
|
+
plugin_ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$plugin" | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//;s/"//')
|
|
206
|
+
market_meta=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$market" | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//;s/"//')
|
|
207
|
+
market_plugin=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$market" | sed -n '2p' | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//;s/"//')
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
if [ "$pkg_ver" = "$plugin_ver" ] && [ "$plugin_ver" = "$market_meta" ] && [ "$market_meta" = "$market_plugin" ]; then
|
|
211
|
+
ok "Version sync: $pkg_ver (all 4 match)"
|
|
212
|
+
else
|
|
213
|
+
fail "Version mismatch: package.json=$pkg_ver plugin.json=$plugin_ver marketplace.meta=$market_meta marketplace.plugin=$market_plugin"
|
|
214
|
+
fi
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# --- Check 6: SSOT Phase Constants ---
|
|
218
|
+
# Verify SSOT phase list in afc-state.sh is not duplicated in other scripts
|
|
219
|
+
|
|
220
|
+
check_phase_ssot() {
|
|
221
|
+
# shellcheck source=afc-state.sh
|
|
222
|
+
. "$SCRIPT_DIR/afc-state.sh"
|
|
223
|
+
|
|
224
|
+
local dupes=0
|
|
225
|
+
# Look for hardcoded phase lists (pipe-separated patterns with 3+ known phases)
|
|
226
|
+
for script in "$PROJECT_DIR"/scripts/afc-*.sh; do
|
|
227
|
+
local scriptname
|
|
228
|
+
scriptname=$(basename "$script")
|
|
229
|
+
# Skip the SSOT source itself and this validation script
|
|
230
|
+
if [ "$scriptname" = "afc-state.sh" ] || [ "$scriptname" = "afc-consistency-check.sh" ]; then
|
|
231
|
+
continue
|
|
232
|
+
fi
|
|
233
|
+
# Check for hardcoded phase case patterns (spec|plan|...|clean style)
|
|
234
|
+
if grep -qE 'spec\|plan\|.*\|clean' "$script" 2>/dev/null; then
|
|
235
|
+
fail "$scriptname contains hardcoded phase list — use SSOT helpers from afc-state.sh"
|
|
236
|
+
dupes=$((dupes + 1))
|
|
237
|
+
fi
|
|
238
|
+
done
|
|
239
|
+
|
|
240
|
+
if [ "$dupes" -eq 0 ]; then
|
|
241
|
+
ok "Phase SSOT: no hardcoded phase lists found in scripts"
|
|
242
|
+
fi
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# --- Run All Checks ---
|
|
246
|
+
|
|
247
|
+
printf "[afc:consistency] Running cross-reference validation...\n"
|
|
248
|
+
|
|
249
|
+
check_config_placeholders
|
|
250
|
+
check_agent_names
|
|
251
|
+
check_hook_scripts
|
|
252
|
+
check_test_coverage
|
|
253
|
+
check_version_sync
|
|
254
|
+
check_phase_ssot
|
|
255
|
+
|
|
256
|
+
printf "\n[afc:consistency] Done: %d errors, %d warnings\n" "$ERRORS" "$WARNINGS"
|
|
257
|
+
|
|
258
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
259
|
+
exit 1
|
|
260
|
+
fi
|
|
261
|
+
exit 0
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// DAG Validator: Check task dependency graph for circular references
|
|
4
|
+
// Parses tasks.md and validates that depends: declarations form a valid DAG.
|
|
5
|
+
//
|
|
6
|
+
// Usage: afc-dag-validate.mjs <tasks_file_path>
|
|
7
|
+
// Exit 0: valid DAG (no cycles)
|
|
8
|
+
// Exit 1: cycle detected — prints cycle path
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
const tasksFile = process.argv[2];
|
|
13
|
+
if (!tasksFile) {
|
|
14
|
+
process.stderr.write(`Usage: ${process.argv[1]} <tasks_file_path>\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let content;
|
|
19
|
+
try {
|
|
20
|
+
content = readFileSync(tasksFile, 'utf8');
|
|
21
|
+
} catch {
|
|
22
|
+
process.stderr.write(`Error: file not found: ${tasksFile}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Parse tasks and dependencies
|
|
27
|
+
const taskPattern = /^\s*-\s*\[[ xX]\]\s+(T\d+)/;
|
|
28
|
+
const depsPattern = /depends:\s*\[([^\]]*)\]/;
|
|
29
|
+
|
|
30
|
+
const nodes = new Set();
|
|
31
|
+
const edges = new Map(); // from -> [to, ...]
|
|
32
|
+
|
|
33
|
+
for (const line of content.split(/\r?\n/)) {
|
|
34
|
+
const taskMatch = line.match(taskPattern);
|
|
35
|
+
if (!taskMatch) continue;
|
|
36
|
+
|
|
37
|
+
const taskId = taskMatch[1];
|
|
38
|
+
nodes.add(taskId);
|
|
39
|
+
if (!edges.has(taskId)) edges.set(taskId, []);
|
|
40
|
+
|
|
41
|
+
const depsMatch = line.match(depsPattern);
|
|
42
|
+
if (depsMatch) {
|
|
43
|
+
const deps = depsMatch[1].match(/T\d+/g) || [];
|
|
44
|
+
for (const dep of deps) {
|
|
45
|
+
// Edge: dep → taskId (taskId depends on dep)
|
|
46
|
+
if (!edges.has(dep)) edges.set(dep, []);
|
|
47
|
+
edges.get(dep).push(taskId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (nodes.size === 0) {
|
|
53
|
+
process.stdout.write('Valid: no tasks found, nothing to validate\n');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// DFS cycle detection with full cycle path
|
|
58
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
59
|
+
const color = new Map();
|
|
60
|
+
const parent = new Map();
|
|
61
|
+
|
|
62
|
+
for (const node of nodes) color.set(node, WHITE);
|
|
63
|
+
|
|
64
|
+
function dfs(node) {
|
|
65
|
+
color.set(node, GRAY);
|
|
66
|
+
for (const neighbor of (edges.get(node) || [])) {
|
|
67
|
+
if (!color.has(neighbor)) continue;
|
|
68
|
+
if (color.get(neighbor) === GRAY) {
|
|
69
|
+
// Cycle found — reconstruct path
|
|
70
|
+
const cycle = [neighbor, node];
|
|
71
|
+
let cur = node;
|
|
72
|
+
while (cur !== neighbor && parent.has(cur)) {
|
|
73
|
+
cur = parent.get(cur);
|
|
74
|
+
cycle.push(cur);
|
|
75
|
+
}
|
|
76
|
+
cycle.reverse();
|
|
77
|
+
process.stdout.write(`CYCLE: ${cycle.join(' → ')}\n`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
if (color.get(neighbor) === WHITE) {
|
|
81
|
+
parent.set(neighbor, node);
|
|
82
|
+
dfs(neighbor);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
color.set(node, BLACK);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
if (color.get(node) === WHITE) {
|
|
90
|
+
dfs(node);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.stdout.write(`Valid: ${nodes.size} tasks, no circular dependencies\n`);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# DAG Validator: Check task dependency graph for circular references
|
|
5
|
+
# Calls Node.js ESM version if available, falls back to bash implementation.
|
|
6
|
+
#
|
|
7
|
+
# Usage: afc-dag-validate.sh <tasks_file_path>
|
|
8
|
+
# Exit 0: valid DAG (no cycles)
|
|
9
|
+
# Exit 1: cycle detected — prints cycle path
|
|
10
|
+
|
|
11
|
+
# shellcheck disable=SC2329
|
|
12
|
+
cleanup() {
|
|
13
|
+
:
|
|
14
|
+
}
|
|
15
|
+
trap cleanup EXIT
|
|
16
|
+
|
|
17
|
+
TASKS_FILE="${1:-}"
|
|
18
|
+
if [ -z "$TASKS_FILE" ]; then
|
|
19
|
+
printf 'Usage: %s <tasks_file_path>\n' "$0" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [ ! -f "$TASKS_FILE" ]; then
|
|
24
|
+
printf 'Error: file not found: %s\n' "$TASKS_FILE" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# --- Node.js fast path ---
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
30
|
+
if command -v node >/dev/null 2>&1; then
|
|
31
|
+
node "$SCRIPT_DIR/afc-dag-validate.mjs" "$TASKS_FILE"
|
|
32
|
+
exit $?
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# --- Bash fallback ---
|
|
36
|
+
|
|
37
|
+
# Parse tasks and dependencies
|
|
38
|
+
TMPDIR_WORK="$(mktemp -d)"
|
|
39
|
+
# shellcheck disable=SC2064
|
|
40
|
+
trap "rm -rf '$TMPDIR_WORK'; :" EXIT
|
|
41
|
+
|
|
42
|
+
NODES_FILE="$TMPDIR_WORK/nodes.txt"
|
|
43
|
+
EDGES_FILE="$TMPDIR_WORK/edges.txt"
|
|
44
|
+
: > "$NODES_FILE"
|
|
45
|
+
: > "$EDGES_FILE"
|
|
46
|
+
|
|
47
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
48
|
+
if ! printf '%s\n' "$line" | grep -qE '^\s*-\s*\[[ xX]\]\s+T[0-9]+'; then
|
|
49
|
+
continue
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
task_id="$(printf '%s\n' "$line" | grep -oE 'T[0-9]+' | head -1)"
|
|
53
|
+
[ -z "$task_id" ] && continue
|
|
54
|
+
|
|
55
|
+
printf '%s\n' "$task_id" >> "$NODES_FILE"
|
|
56
|
+
|
|
57
|
+
deps_raw="$(printf '%s\n' "$line" | grep -oE 'depends:\s*\[([^]]*)\]' | sed 's/depends:[[:space:]]*\[//;s/\]//' || true)"
|
|
58
|
+
if [ -n "$deps_raw" ]; then
|
|
59
|
+
printf '%s\n' "$deps_raw" | tr ',' '\n' | while IFS= read -r dep; do
|
|
60
|
+
dep_id="$(printf '%s\n' "$dep" | grep -oE 'T[0-9]+' || true)"
|
|
61
|
+
if [ -n "$dep_id" ]; then
|
|
62
|
+
printf '%s\t%s\n' "$dep_id" "$task_id" >> "$EDGES_FILE"
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
fi
|
|
66
|
+
done < "$TASKS_FILE"
|
|
67
|
+
|
|
68
|
+
TOTAL_TASKS=$(wc -l < "$NODES_FILE" | tr -d ' ')
|
|
69
|
+
|
|
70
|
+
if [ "$TOTAL_TASKS" -eq 0 ]; then
|
|
71
|
+
printf 'Valid: no tasks found, nothing to validate\n'
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# DFS cycle detection using color marking
|
|
76
|
+
COLOR_DIR="$TMPDIR_WORK/colors"
|
|
77
|
+
mkdir -p "$COLOR_DIR"
|
|
78
|
+
|
|
79
|
+
while IFS= read -r node; do
|
|
80
|
+
printf '0' > "$COLOR_DIR/$node"
|
|
81
|
+
done < "$NODES_FILE"
|
|
82
|
+
|
|
83
|
+
CYCLE_FOUND=0
|
|
84
|
+
CYCLE_PATH=""
|
|
85
|
+
|
|
86
|
+
dfs_check() {
|
|
87
|
+
local start="$1"
|
|
88
|
+
local stack_file="$TMPDIR_WORK/stack.txt"
|
|
89
|
+
printf '%s\n' "$start" > "$stack_file"
|
|
90
|
+
|
|
91
|
+
while [ -s "$stack_file" ]; do
|
|
92
|
+
current="$(tail -1 "$stack_file")"
|
|
93
|
+
|
|
94
|
+
color_file="$COLOR_DIR/$current"
|
|
95
|
+
[ ! -f "$color_file" ] && { sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"; continue; }
|
|
96
|
+
|
|
97
|
+
color="$(cat "$color_file")"
|
|
98
|
+
|
|
99
|
+
if [ "$color" = "0" ]; then
|
|
100
|
+
printf '1' > "$color_file"
|
|
101
|
+
|
|
102
|
+
neighbors="$(grep -E "^${current}\t" "$EDGES_FILE" | cut -f2 || true)"
|
|
103
|
+
if [ -n "$neighbors" ]; then
|
|
104
|
+
while IFS= read -r neighbor; do
|
|
105
|
+
[ -z "$neighbor" ] && continue
|
|
106
|
+
nb_color_file="$COLOR_DIR/$neighbor"
|
|
107
|
+
[ ! -f "$nb_color_file" ] && continue
|
|
108
|
+
|
|
109
|
+
nb_color="$(cat "$nb_color_file")"
|
|
110
|
+
if [ "$nb_color" = "1" ]; then
|
|
111
|
+
CYCLE_FOUND=1
|
|
112
|
+
CYCLE_PATH="CYCLE: $neighbor → $current → $neighbor"
|
|
113
|
+
return
|
|
114
|
+
elif [ "$nb_color" = "0" ]; then
|
|
115
|
+
printf '%s\n' "$neighbor" >> "$stack_file"
|
|
116
|
+
fi
|
|
117
|
+
done <<EOF_NEIGHBORS
|
|
118
|
+
$neighbors
|
|
119
|
+
EOF_NEIGHBORS
|
|
120
|
+
fi
|
|
121
|
+
elif [ "$color" = "1" ]; then
|
|
122
|
+
printf '2' > "$color_file"
|
|
123
|
+
sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"
|
|
124
|
+
else
|
|
125
|
+
sed -i '' '$d' "$stack_file" 2>/dev/null || sed -i '$d' "$stack_file"
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
while IFS= read -r node; do
|
|
131
|
+
color="$(cat "$COLOR_DIR/$node" 2>/dev/null || printf '0')"
|
|
132
|
+
if [ "$color" = "0" ]; then
|
|
133
|
+
dfs_check "$node"
|
|
134
|
+
if [ "$CYCLE_FOUND" -eq 1 ]; then
|
|
135
|
+
printf '%s\n' "$CYCLE_PATH"
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
done < "$NODES_FILE"
|
|
140
|
+
|
|
141
|
+
printf 'Valid: %d tasks, no circular dependencies\n' "$TOTAL_TASKS"
|
|
142
|
+
exit 0
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
# PostToolUseFailure Hook: Output hints matching error patterns on tool failure
|
|
4
4
|
|
|
5
|
+
# shellcheck source=afc-state.sh
|
|
6
|
+
. "$(dirname "$0")/afc-state.sh"
|
|
7
|
+
|
|
5
8
|
# shellcheck disable=SC2329
|
|
6
9
|
cleanup() {
|
|
7
10
|
# Placeholder for temporary resource cleanup if needed
|
|
@@ -10,7 +13,6 @@ cleanup() {
|
|
|
10
13
|
trap cleanup EXIT
|
|
11
14
|
|
|
12
15
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
13
|
-
PIPELINE_FLAG="$PROJECT_DIR/.claude/.afc-active"
|
|
14
16
|
FAILURES_LOG="$PROJECT_DIR/.claude/.afc-failures.log"
|
|
15
17
|
|
|
16
18
|
# Parse input from stdin
|
|
@@ -29,7 +31,7 @@ TOOL_NAME="${TOOL_NAME:-unknown}"
|
|
|
29
31
|
ERROR="${ERROR:-}"
|
|
30
32
|
|
|
31
33
|
# If pipeline is active, log failure (normalize error message to single line)
|
|
32
|
-
if
|
|
34
|
+
if afc_state_is_active && [ -n "$ERROR" ]; then
|
|
33
35
|
ERROR_ONELINE=$(printf '%s\n' "$ERROR" | head -1 | cut -c1-200)
|
|
34
36
|
printf '%s\n' "$(date +%s) $TOOL_NAME: $ERROR_ONELINE" >> "$FAILURES_LOG"
|
|
35
37
|
fi
|
|
@@ -64,14 +66,14 @@ esac
|
|
|
64
66
|
if [ -n "$HINT" ]; then
|
|
65
67
|
# Generate safe JSON with jq if available, otherwise strip special chars and use printf
|
|
66
68
|
if command -v jq &> /dev/null; then
|
|
67
|
-
jq -n --arg ctx "[
|
|
69
|
+
jq -n --arg ctx "[afc:hint] $HINT (tool: $TOOL_NAME)" \
|
|
68
70
|
'{"hookSpecificOutput":{"hookEventName":"PostToolUseFailure","additionalContext":$ctx}}' 2>/dev/null || true
|
|
69
71
|
else
|
|
70
72
|
# shellcheck disable=SC1003
|
|
71
73
|
SAFE_HINT=$(printf '%s' "$HINT" | tr -d '"' | tr -d '\\')
|
|
72
74
|
# shellcheck disable=SC1003
|
|
73
75
|
SAFE_TOOL=$(printf '%s' "$TOOL_NAME" | tr -d '"' | tr -d '\\')
|
|
74
|
-
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUseFailure","additionalContext":"[
|
|
76
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUseFailure","additionalContext":"[afc:hint] %s (tool: %s)"}}\n' "$SAFE_HINT" "$SAFE_TOOL"
|
|
75
77
|
fi
|
|
76
78
|
fi
|
|
77
79
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Parallel Task Validator: Check for file path conflicts among [P] tasks
|
|
4
|
+
// within the same phase.
|
|
5
|
+
//
|
|
6
|
+
// Usage: afc-parallel-validate.mjs <tasks_file_path>
|
|
7
|
+
// Exit 0: valid (no overlaps, or no [P] tasks)
|
|
8
|
+
// Exit 1: overlaps detected — prints conflict details
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
const tasksFile = process.argv[2];
|
|
13
|
+
if (!tasksFile) {
|
|
14
|
+
process.stderr.write(`Usage: ${process.argv[1]} <tasks_file_path>\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let content;
|
|
19
|
+
try {
|
|
20
|
+
content = readFileSync(tasksFile, 'utf8');
|
|
21
|
+
} catch {
|
|
22
|
+
process.stderr.write(`Error: file not found: ${tasksFile}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const phasePattern = /^## Phase (\d+)/;
|
|
27
|
+
const taskPPattern = /^\s*-\s*\[[ xX]\]\s+(T\d+)\s+\[P\]/;
|
|
28
|
+
const backtickPattern = /`([^`]+)`/g;
|
|
29
|
+
|
|
30
|
+
let currentPhase = '';
|
|
31
|
+
let totalPTasks = 0;
|
|
32
|
+
const conflicts = [];
|
|
33
|
+
const phasesWithP = new Set();
|
|
34
|
+
|
|
35
|
+
// Single-pass parsing
|
|
36
|
+
let phaseFileMap = new Map(); // file_path -> task_id
|
|
37
|
+
|
|
38
|
+
for (const line of content.split(/\r?\n/)) {
|
|
39
|
+
const phaseMatch = line.match(phasePattern);
|
|
40
|
+
if (phaseMatch) {
|
|
41
|
+
currentPhase = phaseMatch[1];
|
|
42
|
+
phaseFileMap = new Map();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!currentPhase) continue;
|
|
47
|
+
|
|
48
|
+
const taskMatch = line.match(taskPPattern);
|
|
49
|
+
if (!taskMatch) continue;
|
|
50
|
+
|
|
51
|
+
const taskId = taskMatch[1];
|
|
52
|
+
totalPTasks++;
|
|
53
|
+
phasesWithP.add(currentPhase);
|
|
54
|
+
|
|
55
|
+
// Extract backtick-wrapped file paths (containing / or .)
|
|
56
|
+
let match;
|
|
57
|
+
backtickPattern.lastIndex = 0;
|
|
58
|
+
while ((match = backtickPattern.exec(line)) !== null) {
|
|
59
|
+
const path = match[1];
|
|
60
|
+
if (!/[/.]/.test(path)) continue;
|
|
61
|
+
|
|
62
|
+
const existing = phaseFileMap.get(path);
|
|
63
|
+
if (existing) {
|
|
64
|
+
conflicts.push(`CONFLICT: Phase ${currentPhase} — ${existing} and ${taskId} both target ${path}`);
|
|
65
|
+
} else {
|
|
66
|
+
phaseFileMap.set(path, taskId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (totalPTasks === 0) {
|
|
72
|
+
process.stdout.write('Valid: no [P] tasks found, nothing to validate\n');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (conflicts.length > 0) {
|
|
77
|
+
process.stdout.write(conflicts.join('\n') + '\n');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
process.stdout.write(`Valid: ${totalPTasks} [P] tasks across ${phasesWithP.size} phases, no file overlaps\n`);
|