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,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 [ ! -f "$PIPELINE_FLAG" ]; then
22
+ if ! afc_state_is_active; then
23
23
  exit 0
24
24
  fi
25
25
 
26
- FEATURE=$(head -1 "$PIPELINE_FLAG" | tr -d '\n\r')
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
+ }
@@ -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 "AFC GATE: Abnormal exit (exit code: $exit_code)" >&2
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 [ ! -f "$PIPELINE_FLAG" ]; then
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="$(head -1 "$PIPELINE_FLAG" | tr -d '\n\r')"
43
+ FEATURE="$(afc_state_read feature || echo '')"
46
44
 
47
- # Check current Phase if phase file exists
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
- # Spec/Plan/Tasks Phase (1-3) do not require CI -> pass through
54
- case "${CURRENT_PHASE:-}" in
55
- spec|plan|tasks)
56
- exit 0
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
- if [ ! -f "$CI_FLAG" ]; then
62
- echo "AFC GATE: CI has not been run. Pipeline '${FEATURE:-unknown}' Phase '${CURRENT_PHASE:-unknown}' requires passing the CI gate. Run your CI command and record the timestamp in .claude/.afc-ci-passed." >&2
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
- echo "AFC GATE: CI results are stale (${DIFF} seconds ago). Please run your CI command again." >&2
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 [ ! -f "$PIPELINE_FLAG" ]; then
24
+ if ! afc_state_is_active; then
23
25
  exit 0
24
26
  fi
25
27
 
26
28
  # 1. Read feature name
27
- FEATURE=$(head -1 "$PIPELINE_FLAG" 2>/dev/null | tr -d '\n\r' || echo "unknown")
29
+ FEATURE=$(afc_state_read feature || echo "unknown")
28
30
 
29
31
  # 2. Read current phase
30
- PHASE=$(head -1 "$PROJECT_DIR/.claude/.afc-phase" 2>/dev/null | tr -d '\n\r' || echo "unknown")
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 -5 | tr '\n' ' ' | sed 's/ */ /g;s/^ *//;s/ *$//')
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 -5 | tr '\n' ' ' | sed 's/ */ /g;s/^ *//;s/ *$//')
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)