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,225 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# afc-schema-validate.sh — Validate JSON files against JSON Schema definitions
|
|
5
|
+
# Usage: afc-schema-validate.sh [--json-file FILE --schema FILE | --all]
|
|
6
|
+
# Exit: 0 = valid, 1 = validation error
|
|
7
|
+
|
|
8
|
+
# shellcheck disable=SC2329
|
|
9
|
+
cleanup() { :; }
|
|
10
|
+
trap cleanup EXIT
|
|
11
|
+
|
|
12
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
13
|
+
SCHEMAS_DIR="${PLUGIN_ROOT}/schemas"
|
|
14
|
+
|
|
15
|
+
# --- Node.js embedded validator ---
|
|
16
|
+
validate_with_node() {
|
|
17
|
+
local json_file="$1" schema_file="$2"
|
|
18
|
+
# shellcheck disable=SC2016
|
|
19
|
+
node -e '
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const jsonFile = process.argv[1];
|
|
22
|
+
const schemaFile = process.argv[2];
|
|
23
|
+
|
|
24
|
+
let data, schema;
|
|
25
|
+
try { data = JSON.parse(fs.readFileSync(jsonFile, "utf8")); }
|
|
26
|
+
catch (e) { console.error("[afc:schema] JSON parse error in " + jsonFile + ": " + e.message); process.exit(1); }
|
|
27
|
+
try { schema = JSON.parse(fs.readFileSync(schemaFile, "utf8")); }
|
|
28
|
+
catch (e) { console.error("[afc:schema] Schema parse error in " + schemaFile + ": " + e.message); process.exit(1); }
|
|
29
|
+
|
|
30
|
+
const errors = [];
|
|
31
|
+
|
|
32
|
+
function validate(value, sch, path) {
|
|
33
|
+
if (!sch || typeof sch !== "object") return;
|
|
34
|
+
|
|
35
|
+
// type check
|
|
36
|
+
if (sch.type) {
|
|
37
|
+
const t = sch.type;
|
|
38
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
39
|
+
if (actual === "number" && t === "integer") {
|
|
40
|
+
if (!Number.isInteger(value)) errors.push(path + ": expected integer, got float");
|
|
41
|
+
} else if (t !== actual) {
|
|
42
|
+
errors.push(path + ": expected " + t + ", got " + actual);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// enum check
|
|
48
|
+
if (sch.enum && !sch.enum.includes(value)) {
|
|
49
|
+
errors.push(path + ": value \"" + value + "\" not in enum [" + sch.enum.join(", ") + "]");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// pattern check
|
|
53
|
+
if (sch.pattern && typeof value === "string") {
|
|
54
|
+
if (!new RegExp(sch.pattern).test(value)) {
|
|
55
|
+
errors.push(path + ": \"" + value + "\" does not match pattern " + sch.pattern);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// minLength
|
|
60
|
+
if (sch.minLength !== undefined && typeof value === "string" && value.length < sch.minLength) {
|
|
61
|
+
errors.push(path + ": string length " + value.length + " < minLength " + sch.minLength);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// minimum (integer/number)
|
|
65
|
+
if (sch.minimum !== undefined && typeof value === "number" && value < sch.minimum) {
|
|
66
|
+
errors.push(path + ": value " + value + " < minimum " + sch.minimum);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// minItems (array)
|
|
70
|
+
if (sch.minItems !== undefined && Array.isArray(value) && value.length < sch.minItems) {
|
|
71
|
+
errors.push(path + ": array length " + value.length + " < minItems " + sch.minItems);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// object validations
|
|
75
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
76
|
+
// required
|
|
77
|
+
if (sch.required) {
|
|
78
|
+
for (const r of sch.required) {
|
|
79
|
+
if (!(r in value)) errors.push(path + "." + r + ": required field missing");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// properties
|
|
84
|
+
if (sch.properties) {
|
|
85
|
+
for (const [k, v] of Object.entries(value)) {
|
|
86
|
+
if (sch.properties[k]) {
|
|
87
|
+
validate(v, sch.properties[k], path + "." + k);
|
|
88
|
+
} else if (sch.patternProperties) {
|
|
89
|
+
let matched = false;
|
|
90
|
+
for (const [pat, patSch] of Object.entries(sch.patternProperties)) {
|
|
91
|
+
if (new RegExp(pat).test(k)) { validate(v, patSch, path + "." + k); matched = true; break; }
|
|
92
|
+
}
|
|
93
|
+
if (!matched && sch.additionalProperties === false) {
|
|
94
|
+
errors.push(path + "." + k + ": unexpected property");
|
|
95
|
+
}
|
|
96
|
+
} else if (sch.additionalProperties === false) {
|
|
97
|
+
errors.push(path + "." + k + ": unexpected property");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (sch.patternProperties) {
|
|
101
|
+
for (const [k, v] of Object.entries(value)) {
|
|
102
|
+
let matched = false;
|
|
103
|
+
for (const [pat, patSch] of Object.entries(sch.patternProperties)) {
|
|
104
|
+
if (new RegExp(pat).test(k)) { validate(v, patSch, path + "." + k); matched = true; break; }
|
|
105
|
+
}
|
|
106
|
+
if (!matched && sch.additionalProperties === false) {
|
|
107
|
+
errors.push(path + "." + k + ": unexpected property");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// array validations
|
|
114
|
+
if (Array.isArray(value) && sch.items) {
|
|
115
|
+
for (let i = 0; i < value.length; i++) {
|
|
116
|
+
validate(value[i], sch.items, path + "[" + i + "]");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// $ref resolution (local definitions only)
|
|
121
|
+
if (sch["$ref"]) {
|
|
122
|
+
const refPath = sch["$ref"].replace("#/definitions/", "");
|
|
123
|
+
if (schema.definitions && schema.definitions[refPath]) {
|
|
124
|
+
validate(value, schema.definitions[refPath], path);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
validate(data, schema, "$");
|
|
130
|
+
|
|
131
|
+
if (errors.length > 0) {
|
|
132
|
+
console.error("[afc:schema] " + jsonFile + " validation failed:");
|
|
133
|
+
errors.forEach(e => console.error(" " + e));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
} else {
|
|
136
|
+
console.log("[afc:schema] " + jsonFile + " — valid");
|
|
137
|
+
}
|
|
138
|
+
' "$json_file" "$schema_file"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# --- jq fallback (basic structure only) ---
|
|
142
|
+
validate_with_jq() {
|
|
143
|
+
local json_file="$1" schema_file="$2"
|
|
144
|
+
|
|
145
|
+
# Step 1: valid JSON?
|
|
146
|
+
if ! jq empty "$json_file" 2>/dev/null; then
|
|
147
|
+
printf '%s\n' "[afc:schema] JSON parse error in ${json_file}" >&2
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Step 2: check required top-level keys from schema
|
|
152
|
+
local required_keys
|
|
153
|
+
required_keys=$(jq -r '.required[]? // empty' "$schema_file" 2>/dev/null || true)
|
|
154
|
+
if [ -n "$required_keys" ]; then
|
|
155
|
+
local missing=0
|
|
156
|
+
while IFS= read -r key; do
|
|
157
|
+
if ! jq -e ".[\"${key}\"]" "$json_file" >/dev/null 2>&1; then
|
|
158
|
+
printf '%s\n' "[afc:schema] ${json_file}: required field missing: ${key}" >&2
|
|
159
|
+
missing=1
|
|
160
|
+
fi
|
|
161
|
+
done <<< "$required_keys"
|
|
162
|
+
if [ "$missing" -eq 1 ]; then
|
|
163
|
+
return 1
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
printf '%s\n' "[afc:schema] ${json_file} — valid (jq basic)"
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# --- Main ---
|
|
172
|
+
validate_file() {
|
|
173
|
+
local json_file="$1" schema_file="$2"
|
|
174
|
+
|
|
175
|
+
if [ ! -f "$json_file" ]; then
|
|
176
|
+
printf '%s\n' "[afc:schema] File not found: ${json_file}" >&2
|
|
177
|
+
return 1
|
|
178
|
+
fi
|
|
179
|
+
if [ ! -f "$schema_file" ]; then
|
|
180
|
+
printf '%s\n' "[afc:schema] Schema not found: ${schema_file}" >&2
|
|
181
|
+
return 1
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
if command -v node >/dev/null 2>&1; then
|
|
185
|
+
validate_with_node "$json_file" "$schema_file"
|
|
186
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
187
|
+
printf '%s\n' "[afc:schema] WARNING: node not found, using jq basic validation" >&2
|
|
188
|
+
validate_with_jq "$json_file" "$schema_file"
|
|
189
|
+
else
|
|
190
|
+
printf '%s\n' "[afc:schema] WARNING: neither node nor jq found, skipping validation" >&2
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Parse arguments
|
|
196
|
+
MODE="all"
|
|
197
|
+
JSON_FILE=""
|
|
198
|
+
SCHEMA_FILE=""
|
|
199
|
+
|
|
200
|
+
while [ $# -gt 0 ]; do
|
|
201
|
+
case "$1" in
|
|
202
|
+
--json-file) JSON_FILE="$2"; MODE="single"; shift 2 ;;
|
|
203
|
+
--schema) SCHEMA_FILE="$2"; shift 2 ;;
|
|
204
|
+
--all) MODE="all"; shift ;;
|
|
205
|
+
*) printf '%s\n' "[afc] Usage: afc-schema-validate.sh [--json-file FILE --schema FILE | --all]" >&2; exit 1 ;;
|
|
206
|
+
esac
|
|
207
|
+
done
|
|
208
|
+
|
|
209
|
+
if [ "$MODE" = "single" ]; then
|
|
210
|
+
if [ -z "$JSON_FILE" ] || [ -z "$SCHEMA_FILE" ]; then
|
|
211
|
+
printf '%s\n' "[afc] Usage: afc-schema-validate.sh --json-file FILE --schema FILE" >&2
|
|
212
|
+
exit 1
|
|
213
|
+
fi
|
|
214
|
+
validate_file "$JSON_FILE" "$SCHEMA_FILE"
|
|
215
|
+
else
|
|
216
|
+
ERRORS=0
|
|
217
|
+
validate_file "${PLUGIN_ROOT}/hooks/hooks.json" "${SCHEMAS_DIR}/hooks.schema.json" || ERRORS=$((ERRORS + 1))
|
|
218
|
+
validate_file "${PLUGIN_ROOT}/.claude-plugin/plugin.json" "${SCHEMAS_DIR}/plugin.schema.json" || ERRORS=$((ERRORS + 1))
|
|
219
|
+
validate_file "${PLUGIN_ROOT}/.claude-plugin/marketplace.json" "${SCHEMAS_DIR}/marketplace.schema.json" || ERRORS=$((ERRORS + 1))
|
|
220
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
221
|
+
printf '%s\n' "[afc:schema] ${ERRORS} file(s) failed validation" >&2
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|
|
224
|
+
printf '%s\n' "[afc:schema] All 3 files valid"
|
|
225
|
+
fi
|
|
@@ -5,6 +5,9 @@ set -euo pipefail
|
|
|
5
5
|
#
|
|
6
6
|
# Gap fix: Ensures resumability via /afc:resume even after session ends
|
|
7
7
|
|
|
8
|
+
# shellcheck source=afc-state.sh
|
|
9
|
+
. "$(dirname "$0")/afc-state.sh"
|
|
10
|
+
|
|
8
11
|
# shellcheck disable=SC2329
|
|
9
12
|
cleanup() {
|
|
10
13
|
# Extend here if temporary file cleanup is needed
|
|
@@ -12,18 +15,15 @@ cleanup() {
|
|
|
12
15
|
}
|
|
13
16
|
trap cleanup EXIT
|
|
14
17
|
|
|
15
|
-
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
16
|
-
PIPELINE_FLAG="${PROJECT_DIR}/.claude/.afc-active"
|
|
17
|
-
|
|
18
18
|
# Consume stdin early (required -- pipe breaks if not consumed)
|
|
19
19
|
INPUT=$(cat)
|
|
20
20
|
|
|
21
21
|
# If pipeline is not active -> exit silently
|
|
22
|
-
if
|
|
22
|
+
if ! afc_state_is_active; then
|
|
23
23
|
exit 0
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
FEATURE=$(
|
|
26
|
+
FEATURE=$(afc_state_read feature || echo '')
|
|
27
27
|
|
|
28
28
|
# Parse reason: jq preferred, grep/sed fallback
|
|
29
29
|
REASON=""
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# afc-state.sh — Shared state library for pipeline state management
|
|
3
|
+
# Source this file: . "$(dirname "$0")/afc-state.sh"
|
|
4
|
+
# Replaces 4 flag files (.afc-active, .afc-phase, .afc-ci-passed, .afc-changes.log)
|
|
5
|
+
# with a single .afc-state.json file.
|
|
6
|
+
|
|
7
|
+
# State file path
|
|
8
|
+
_AFC_STATE_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}/.claude"
|
|
9
|
+
_AFC_STATE_FILE="${_AFC_STATE_DIR}/.afc-state.json"
|
|
10
|
+
|
|
11
|
+
# --- Phase Constants (SSOT) ---
|
|
12
|
+
# All valid pipeline phases. Update HERE when adding a new phase.
|
|
13
|
+
AFC_VALID_PHASES="spec|plan|tasks|implement|review|clean|clarify|test-pre-gen|blast-radius|fast-path"
|
|
14
|
+
# Phases that do NOT require CI gate to pass (preparatory phases)
|
|
15
|
+
AFC_CI_EXEMPT_PHASES="spec|plan|tasks|clarify|test-pre-gen|blast-radius"
|
|
16
|
+
|
|
17
|
+
# Check if a phase name is valid
|
|
18
|
+
# Usage: afc_is_valid_phase <phase>
|
|
19
|
+
# Returns: 0 if valid, 1 if not
|
|
20
|
+
afc_is_valid_phase() {
|
|
21
|
+
printf '%s\n' "$AFC_VALID_PHASES" | tr '|' '\n' | grep -qxF "$1"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Check if a phase is exempt from CI gate
|
|
25
|
+
# Usage: afc_is_ci_exempt <phase>
|
|
26
|
+
# Returns: 0 if exempt, 1 if CI required
|
|
27
|
+
afc_is_ci_exempt() {
|
|
28
|
+
printf '%s\n' "$AFC_CI_EXEMPT_PHASES" | tr '|' '\n' | grep -qxF "$1"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# --- Public API ---
|
|
32
|
+
|
|
33
|
+
# Check if pipeline is active (state file exists and has feature)
|
|
34
|
+
# Returns: 0 if active, 1 if not
|
|
35
|
+
afc_state_is_active() {
|
|
36
|
+
[ -f "$_AFC_STATE_FILE" ] && [ -s "$_AFC_STATE_FILE" ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Read a field from state file
|
|
40
|
+
# Usage: afc_state_read <field>
|
|
41
|
+
# Fields: feature, phase, ciPassedAt, startedAt
|
|
42
|
+
# Returns: field value on stdout, exit 1 if not found
|
|
43
|
+
afc_state_read() {
|
|
44
|
+
local field="$1"
|
|
45
|
+
if [ ! -f "$_AFC_STATE_FILE" ]; then
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
if command -v jq >/dev/null 2>&1; then
|
|
49
|
+
local val
|
|
50
|
+
val=$(jq -r --arg f "$field" '.[$f] // empty' "$_AFC_STATE_FILE" 2>/dev/null) || return 1
|
|
51
|
+
[ -n "$val" ] && printf '%s\n' "$val" && return 0
|
|
52
|
+
return 1
|
|
53
|
+
else
|
|
54
|
+
# grep/sed fallback for simple string/number fields
|
|
55
|
+
local val
|
|
56
|
+
val=$(grep -o "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$_AFC_STATE_FILE" 2>/dev/null \
|
|
57
|
+
| head -1 | sed 's/.*:[[:space:]]*"\(.*\)"/\1/')
|
|
58
|
+
if [ -z "$val" ]; then
|
|
59
|
+
# Try numeric value
|
|
60
|
+
val=$(grep -o "\"${field}\"[[:space:]]*:[[:space:]]*[0-9]*" "$_AFC_STATE_FILE" 2>/dev/null \
|
|
61
|
+
| head -1 | sed 's/.*:[[:space:]]*//')
|
|
62
|
+
fi
|
|
63
|
+
[ -n "$val" ] && printf '%s\n' "$val" && return 0
|
|
64
|
+
return 1
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Write/update a field in state file
|
|
69
|
+
# Usage: afc_state_write <field> <value>
|
|
70
|
+
afc_state_write() {
|
|
71
|
+
local field="$1" value="$2"
|
|
72
|
+
mkdir -p "$_AFC_STATE_DIR"
|
|
73
|
+
if [ ! -f "$_AFC_STATE_FILE" ]; then
|
|
74
|
+
printf '{}' > "$_AFC_STATE_FILE"
|
|
75
|
+
fi
|
|
76
|
+
if command -v jq >/dev/null 2>&1; then
|
|
77
|
+
local tmp
|
|
78
|
+
tmp=$(mktemp)
|
|
79
|
+
local jq_ok=0
|
|
80
|
+
if printf '%s' "$value" | grep -qE '^[0-9]+$'; then
|
|
81
|
+
jq --arg f "$field" --argjson v "$value" '.[$f] = $v' "$_AFC_STATE_FILE" > "$tmp" 2>/dev/null && jq_ok=1
|
|
82
|
+
else
|
|
83
|
+
jq --arg f "$field" --arg v "$value" '.[$f] = $v' "$_AFC_STATE_FILE" > "$tmp" 2>/dev/null && jq_ok=1
|
|
84
|
+
fi
|
|
85
|
+
if [ "$jq_ok" -eq 1 ]; then
|
|
86
|
+
mv "$tmp" "$_AFC_STATE_FILE"
|
|
87
|
+
else
|
|
88
|
+
rm -f "$tmp"
|
|
89
|
+
fi
|
|
90
|
+
else
|
|
91
|
+
# sed fallback: replace or append field
|
|
92
|
+
# Escape sed-special chars in value: \ first, then &
|
|
93
|
+
local safe_val="$value"
|
|
94
|
+
safe_val="${safe_val//\\/\\\\}"
|
|
95
|
+
safe_val="${safe_val//&/\\&}"
|
|
96
|
+
if grep -q "\"${field}\"" "$_AFC_STATE_FILE" 2>/dev/null; then
|
|
97
|
+
local tmp
|
|
98
|
+
tmp=$(mktemp)
|
|
99
|
+
if printf '%s' "$value" | grep -qE '^[0-9]+$'; then
|
|
100
|
+
sed "s/\"${field}\"[[:space:]]*:[[:space:]]*[^,}]*/\"${field}\": ${value}/" "$_AFC_STATE_FILE" > "$tmp"
|
|
101
|
+
else
|
|
102
|
+
sed "s/\"${field}\"[[:space:]]*:[[:space:]]*[^,}]*/\"${field}\": \"${safe_val}\"/" "$_AFC_STATE_FILE" > "$tmp"
|
|
103
|
+
fi
|
|
104
|
+
mv "$tmp" "$_AFC_STATE_FILE"
|
|
105
|
+
else
|
|
106
|
+
# Append before closing brace
|
|
107
|
+
local tmp
|
|
108
|
+
tmp=$(mktemp)
|
|
109
|
+
if printf '%s' "$value" | grep -qE '^[0-9]+$'; then
|
|
110
|
+
sed "s/}$/,\"${field}\": ${value}}/" "$_AFC_STATE_FILE" > "$tmp"
|
|
111
|
+
else
|
|
112
|
+
sed "s/}$/,\"${field}\": \"${safe_val}\"}/" "$_AFC_STATE_FILE" > "$tmp"
|
|
113
|
+
fi
|
|
114
|
+
# Fix leading comma on empty object
|
|
115
|
+
sed 's/{,/{/' "$tmp" > "$_AFC_STATE_FILE"
|
|
116
|
+
rm -f "$tmp"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Remove a field from state file
|
|
122
|
+
# Usage: afc_state_remove <field>
|
|
123
|
+
afc_state_remove() {
|
|
124
|
+
local field="$1"
|
|
125
|
+
if [ ! -f "$_AFC_STATE_FILE" ]; then
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
if command -v jq >/dev/null 2>&1; then
|
|
129
|
+
local tmp
|
|
130
|
+
tmp=$(mktemp)
|
|
131
|
+
if jq --arg f "$field" 'del(.[$f])' "$_AFC_STATE_FILE" > "$tmp" 2>/dev/null; then
|
|
132
|
+
mv "$tmp" "$_AFC_STATE_FILE"
|
|
133
|
+
else
|
|
134
|
+
rm -f "$tmp"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
# sed fallback: skip removal (non-critical)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Initialize state for a new pipeline
|
|
141
|
+
# Usage: afc_state_init <feature>
|
|
142
|
+
afc_state_init() {
|
|
143
|
+
local feature="$1"
|
|
144
|
+
local now
|
|
145
|
+
now=$(date +%s)
|
|
146
|
+
mkdir -p "$_AFC_STATE_DIR"
|
|
147
|
+
if command -v jq >/dev/null 2>&1; then
|
|
148
|
+
jq -n --arg f "$feature" --argjson t "$now" \
|
|
149
|
+
'{feature: $f, phase: "spec", startedAt: $t}' > "$_AFC_STATE_FILE"
|
|
150
|
+
else
|
|
151
|
+
local safe_feature="${feature//\\/\\\\}"
|
|
152
|
+
safe_feature="${safe_feature//\"/\\\"}"
|
|
153
|
+
printf '{"feature": "%s", "phase": "spec", "startedAt": %s}\n' "$safe_feature" "$now" > "$_AFC_STATE_FILE"
|
|
154
|
+
fi
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Delete the state file (pipeline ended)
|
|
158
|
+
afc_state_delete() {
|
|
159
|
+
rm -f "$_AFC_STATE_FILE"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Append a file path to the changes array
|
|
163
|
+
# Usage: afc_state_append_change <file_path>
|
|
164
|
+
afc_state_append_change() {
|
|
165
|
+
local file_path="$1"
|
|
166
|
+
if [ ! -f "$_AFC_STATE_FILE" ]; then
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
if command -v jq >/dev/null 2>&1; then
|
|
170
|
+
local tmp
|
|
171
|
+
tmp=$(mktemp)
|
|
172
|
+
if jq --arg p "$file_path" '.changes = ((.changes // []) + [$p] | unique)' "$_AFC_STATE_FILE" > "$tmp" 2>/dev/null; then
|
|
173
|
+
mv "$tmp" "$_AFC_STATE_FILE"
|
|
174
|
+
else
|
|
175
|
+
rm -f "$tmp"
|
|
176
|
+
fi
|
|
177
|
+
else
|
|
178
|
+
# Fallback: use a sidecar changes file
|
|
179
|
+
printf '%s\n' "$file_path" >> "${_AFC_STATE_FILE%.json}.changes.log"
|
|
180
|
+
sort -u -o "${_AFC_STATE_FILE%.json}.changes.log" "${_AFC_STATE_FILE%.json}.changes.log"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Read all changes as newline-separated list
|
|
185
|
+
# Usage: afc_state_read_changes
|
|
186
|
+
afc_state_read_changes() {
|
|
187
|
+
if [ ! -f "$_AFC_STATE_FILE" ]; then
|
|
188
|
+
return 1
|
|
189
|
+
fi
|
|
190
|
+
if command -v jq >/dev/null 2>&1; then
|
|
191
|
+
jq -r '.changes[]? // empty' "$_AFC_STATE_FILE" 2>/dev/null
|
|
192
|
+
else
|
|
193
|
+
# Fallback: read sidecar file
|
|
194
|
+
if [ -f "${_AFC_STATE_FILE%.json}.changes.log" ]; then
|
|
195
|
+
cat "${_AFC_STATE_FILE%.json}.changes.log"
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Invalidate CI (remove ciPassedAt)
|
|
201
|
+
afc_state_invalidate_ci() {
|
|
202
|
+
afc_state_remove "ciPassedAt"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Record CI pass timestamp
|
|
206
|
+
afc_state_ci_pass() {
|
|
207
|
+
local now
|
|
208
|
+
now=$(date +%s)
|
|
209
|
+
afc_state_write "ciPassedAt" "$now"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Record a phase checkpoint with git SHA
|
|
213
|
+
# Usage: afc_state_checkpoint <phase>
|
|
214
|
+
afc_state_checkpoint() {
|
|
215
|
+
local phase="$1"
|
|
216
|
+
if [ ! -f "$_AFC_STATE_FILE" ]; then
|
|
217
|
+
return 1
|
|
218
|
+
fi
|
|
219
|
+
local git_sha=""
|
|
220
|
+
if cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" 2>/dev/null; then
|
|
221
|
+
git_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "")
|
|
222
|
+
fi
|
|
223
|
+
local now
|
|
224
|
+
now=$(date +%s)
|
|
225
|
+
if command -v jq >/dev/null 2>&1; then
|
|
226
|
+
local tmp
|
|
227
|
+
tmp=$(mktemp)
|
|
228
|
+
if jq --arg p "$phase" --arg s "$git_sha" --argjson t "$now" \
|
|
229
|
+
'.phaseCheckpoints = ((.phaseCheckpoints // []) + [{"phase": $p, "gitSha": $s, "timestamp": $t}])' \
|
|
230
|
+
"$_AFC_STATE_FILE" > "$tmp" 2>/dev/null; then
|
|
231
|
+
mv "$tmp" "$_AFC_STATE_FILE"
|
|
232
|
+
else
|
|
233
|
+
rm -f "$tmp"
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
# No sed fallback — phaseCheckpoints is array-typed, too complex for sed
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Legacy fallback: check if old flag files exist
|
|
240
|
+
# Returns: 0 if legacy state found, 1 if not
|
|
241
|
+
# Side effect: sets FEATURE and PHASE variables
|
|
242
|
+
_afc_state_legacy_check() {
|
|
243
|
+
local dir="${_AFC_STATE_DIR}"
|
|
244
|
+
if [ -f "$dir/.afc-active" ]; then
|
|
245
|
+
# shellcheck disable=SC2034
|
|
246
|
+
FEATURE=$(head -1 "$dir/.afc-active" 2>/dev/null | tr -d '\n\r')
|
|
247
|
+
# shellcheck disable=SC2034
|
|
248
|
+
PHASE=""
|
|
249
|
+
if [ -f "$dir/.afc-phase" ]; then
|
|
250
|
+
# shellcheck disable=SC2034
|
|
251
|
+
PHASE=$(head -1 "$dir/.afc-phase" 2>/dev/null | tr -d '\n\r')
|
|
252
|
+
fi
|
|
253
|
+
return 0
|
|
254
|
+
fi
|
|
255
|
+
return 1
|
|
256
|
+
}
|
package/scripts/afc-stop-gate.sh
CHANGED
|
@@ -5,27 +5,25 @@ set -euo pipefail
|
|
|
5
5
|
#
|
|
6
6
|
# Gap fix: "Prompts are not enforcement" -> Physical enforcement via exit 2
|
|
7
7
|
|
|
8
|
+
# shellcheck source=afc-state.sh
|
|
9
|
+
. "$(dirname "$0")/afc-state.sh"
|
|
10
|
+
|
|
8
11
|
# trap: Preserve exit code on abnormal termination + stderr message
|
|
9
12
|
# shellcheck disable=SC2329
|
|
10
13
|
cleanup() {
|
|
11
14
|
local exit_code=$?
|
|
12
15
|
if [ "$exit_code" -ne 0 ] && [ "$exit_code" -ne 2 ]; then
|
|
13
|
-
echo "
|
|
16
|
+
echo "[afc:gate] Abnormal exit (code: $exit_code)" >&2
|
|
14
17
|
fi
|
|
15
18
|
exit "$exit_code"
|
|
16
19
|
}
|
|
17
20
|
trap cleanup EXIT
|
|
18
21
|
|
|
19
|
-
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
20
|
-
PIPELINE_FLAG="${PROJECT_DIR}/.claude/.afc-active"
|
|
21
|
-
CI_FLAG="${PROJECT_DIR}/.claude/.afc-ci-passed"
|
|
22
|
-
PHASE_FLAG="${PROJECT_DIR}/.claude/.afc-phase"
|
|
23
|
-
|
|
24
22
|
# Consume stdin (required -- pipe breaks if not consumed)
|
|
25
23
|
INPUT=$(cat)
|
|
26
24
|
|
|
27
25
|
# If pipeline is not active -> pass through
|
|
28
|
-
if
|
|
26
|
+
if ! afc_state_is_active; then
|
|
29
27
|
exit 0
|
|
30
28
|
fi
|
|
31
29
|
|
|
@@ -42,35 +40,45 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
|
|
42
40
|
exit 0
|
|
43
41
|
fi
|
|
44
42
|
|
|
45
|
-
FEATURE="$(
|
|
43
|
+
FEATURE="$(afc_state_read feature || echo '')"
|
|
46
44
|
|
|
47
|
-
# Check current Phase
|
|
48
|
-
CURRENT_PHASE=""
|
|
49
|
-
if [ -f "$PHASE_FLAG" ]; then
|
|
50
|
-
CURRENT_PHASE="$(head -1 "$PHASE_FLAG" | tr -d '\n\r')"
|
|
51
|
-
fi
|
|
45
|
+
# Check current Phase
|
|
46
|
+
CURRENT_PHASE="$(afc_state_read phase || echo '')"
|
|
52
47
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
;;
|
|
58
|
-
esac
|
|
48
|
+
# Preparatory phases do not require CI -> pass through
|
|
49
|
+
if afc_is_ci_exempt "${CURRENT_PHASE:-}"; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
59
52
|
|
|
60
53
|
# Implement/Review/Clean Phase (4-6) require CI to pass
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
CI_TIME="$(afc_state_read ciPassedAt 2>/dev/null || echo '')"
|
|
55
|
+
CI_TIME="$(printf '%s' "$CI_TIME" | tr -dc '0-9')"
|
|
56
|
+
CI_TIME="${CI_TIME:-0}"
|
|
57
|
+
|
|
58
|
+
if [ "$CI_TIME" -eq 0 ]; then
|
|
59
|
+
# Check last_assistant_message for premature completion claims
|
|
60
|
+
LAST_MSG=""
|
|
61
|
+
if command -v jq &>/dev/null; then
|
|
62
|
+
LAST_MSG=$(printf '%s\n' "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null || true)
|
|
63
|
+
else
|
|
64
|
+
LAST_MSG=$(printf '%s\n' "$INPUT" | grep -o '"last_assistant_message"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"//;s/"$//' 2>/dev/null || true)
|
|
65
|
+
fi
|
|
66
|
+
LAST_MSG=$(printf '%s\n' "$LAST_MSG" | head -1 | cut -c1-500)
|
|
67
|
+
|
|
68
|
+
if printf '%s\n' "$LAST_MSG" | grep -qiE '(done|complete[^s]|finished|implemented|all tasks)' 2>/dev/null; then
|
|
69
|
+
printf "[afc:gate] CI has not passed. Pipeline '%s' Phase '%s' requires CI gate.\n → Run your CI command and verify it passes\n" "${FEATURE:-unknown}" "${CURRENT_PHASE:-unknown}" >&2
|
|
70
|
+
else
|
|
71
|
+
printf "[afc:gate] CI has not been run. Pipeline '%s' Phase '%s' requires CI gate.\n → Run your CI command to pass the gate\n" "${FEATURE:-unknown}" "${CURRENT_PHASE:-unknown}" >&2
|
|
72
|
+
fi
|
|
63
73
|
exit 2
|
|
64
74
|
fi
|
|
65
75
|
|
|
66
76
|
# Verify CI passed within the last 10 minutes (prevent stale results)
|
|
67
|
-
CI_TIME="$(cat "$CI_FLAG" 2>/dev/null | head -1 | tr -dc '0-9' || true)"
|
|
68
|
-
CI_TIME="${CI_TIME:-0}"
|
|
69
77
|
NOW="$(date +%s)"
|
|
70
78
|
if [ "$CI_TIME" -gt 0 ]; then
|
|
71
79
|
DIFF=$(( NOW - CI_TIME ))
|
|
72
80
|
if [ "$DIFF" -gt 600 ]; then
|
|
73
|
-
|
|
81
|
+
printf "[afc:gate] CI results are stale (%ss ago).\n → Run your CI command again\n" "$DIFF" >&2
|
|
74
82
|
exit 2
|
|
75
83
|
fi
|
|
76
84
|
fi
|
|
@@ -6,6 +6,9 @@ set -euo pipefail
|
|
|
6
6
|
#
|
|
7
7
|
# Gap fix: Subagents do not inherit parent context, so explicit injection is required
|
|
8
8
|
|
|
9
|
+
# shellcheck source=afc-state.sh
|
|
10
|
+
. "$(dirname "$0")/afc-state.sh"
|
|
11
|
+
|
|
9
12
|
# shellcheck disable=SC2329
|
|
10
13
|
cleanup() {
|
|
11
14
|
:
|
|
@@ -13,21 +16,20 @@ cleanup() {
|
|
|
13
16
|
trap cleanup EXIT
|
|
14
17
|
|
|
15
18
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
16
|
-
PIPELINE_FLAG="$PROJECT_DIR/.claude/.afc-active"
|
|
17
19
|
|
|
18
20
|
# Consume stdin (required -- pipe breaks if not consumed)
|
|
19
21
|
cat > /dev/null
|
|
20
22
|
|
|
21
23
|
# Exit silently if pipeline is inactive
|
|
22
|
-
if
|
|
24
|
+
if ! afc_state_is_active; then
|
|
23
25
|
exit 0
|
|
24
26
|
fi
|
|
25
27
|
|
|
26
28
|
# 1. Read feature name
|
|
27
|
-
FEATURE=$(
|
|
29
|
+
FEATURE=$(afc_state_read feature || echo "unknown")
|
|
28
30
|
|
|
29
31
|
# 2. Read current phase
|
|
30
|
-
PHASE=$(
|
|
32
|
+
PHASE=$(afc_state_read phase || echo "unknown")
|
|
31
33
|
|
|
32
34
|
# 3. Build context string
|
|
33
35
|
CONTEXT="[AFC PIPELINE] Feature: $FEATURE | Phase: $PHASE"
|
|
@@ -38,17 +40,24 @@ CONFIG_FILE="$PROJECT_DIR/.claude/afc.config.md"
|
|
|
38
40
|
if [ -f "$CONFIG_FILE" ]; then
|
|
39
41
|
# Extract Architecture section (## Architecture to next ##)
|
|
40
42
|
# shellcheck disable=SC2001
|
|
41
|
-
ARCH=$(sed -n '/^## Architecture/,/^## /p' "$CONFIG_FILE" 2>/dev/null | sed '1d;/^## /d;/^$/d' | head -
|
|
43
|
+
ARCH=$(sed -n '/^## Architecture/,/^## /p' "$CONFIG_FILE" 2>/dev/null | sed '1d;/^## /d;/^$/d' | head -15 | tr '\n' ' ' | sed 's/ */ /g;s/^ *//;s/ *$//')
|
|
42
44
|
if [ -n "$ARCH" ]; then
|
|
43
45
|
CONTEXT="$CONTEXT | Architecture: $ARCH"
|
|
44
46
|
fi
|
|
45
47
|
|
|
46
48
|
# Extract Code Style section (## Code Style to next ##)
|
|
47
49
|
# shellcheck disable=SC2001
|
|
48
|
-
STYLE=$(sed -n '/^## Code Style/,/^## /p' "$CONFIG_FILE" 2>/dev/null | sed '1d;/^## /d;/^$/d' | head -
|
|
50
|
+
STYLE=$(sed -n '/^## Code Style/,/^## /p' "$CONFIG_FILE" 2>/dev/null | sed '1d;/^## /d;/^$/d' | head -15 | tr '\n' ' ' | sed 's/ */ /g;s/^ *//;s/ *$//')
|
|
49
51
|
if [ -n "$STYLE" ]; then
|
|
50
52
|
CONTEXT="$CONTEXT | Code Style: $STYLE"
|
|
51
53
|
fi
|
|
54
|
+
|
|
55
|
+
# Extract Project Context section (## Project Context to next ## or EOF)
|
|
56
|
+
# shellcheck disable=SC2001
|
|
57
|
+
PROJ_CTX=$(sed -n '/^## Project Context/,/^## /p' "$CONFIG_FILE" 2>/dev/null | sed '1d;/^## /d;/^$/d' | head -15 | tr '\n' ' ' | sed 's/ */ /g;s/^ *//;s/ *$//')
|
|
58
|
+
if [ -n "$PROJ_CTX" ]; then
|
|
59
|
+
CONTEXT="$CONTEXT | Project Context: $PROJ_CTX"
|
|
60
|
+
fi
|
|
52
61
|
fi
|
|
53
62
|
|
|
54
63
|
# 5. Output as hookSpecificOutput JSON (required for SubagentStart context injection)
|