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
@@ -3,6 +3,7 @@ set -euo pipefail
3
3
 
4
4
  # Parallel Task Validator: Parse tasks.md and check for file path conflicts
5
5
  # among [P]-marked (parallel) tasks within the same phase.
6
+ # Calls Node.js ESM version if available, falls back to bash implementation.
6
7
  #
7
8
  # Usage: afc-parallel-validate.sh <tasks_file_path>
8
9
  # Exit 0: valid (no overlaps, or no [P] tasks found)
@@ -14,7 +15,6 @@ cleanup() {
14
15
  }
15
16
  trap cleanup EXIT
16
17
 
17
- # PROJECT_DIR kept for convention consistency with other afc scripts
18
18
  # shellcheck disable=SC2034
19
19
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
20
20
 
@@ -29,92 +29,85 @@ if [ ! -f "$TASKS_FILE" ]; then
29
29
  exit 1
30
30
  fi
31
31
 
32
- # ------------------------------------------------------------------
33
- # Parse phases and [P] tasks
34
- # Phase headers match: ## Phase N: ...
35
- # Task lines match: - [ ] T{NNN} [P] {desc} `{path}` ...
36
- # ------------------------------------------------------------------
32
+ # --- Node.js fast path ---
33
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
34
+ if command -v node >/dev/null 2>&1; then
35
+ node "$SCRIPT_DIR/afc-parallel-validate.mjs" "$TASKS_FILE"
36
+ exit $?
37
+ fi
38
+
39
+ # --- Bash fallback ---
37
40
 
38
41
  current_phase=""
39
42
  total_p_tasks=0
40
43
  conflict_found=0
41
44
  conflict_messages=""
42
45
 
43
- # We process line by line using a while loop.
44
- # Two associative arrays are used per phase to track:
45
- # phase_files[file_path]="task_id" — first task that claimed the file
46
- # phase_tasks[file_path]="task_id" — same map for conflict lookup
47
- # Because bash 3 (macOS default) lacks associative arrays we use temp files.
48
-
49
46
  TMPDIR_WORK="$(mktemp -d)"
50
47
  # shellcheck disable=SC2064
51
48
  trap "rm -rf '$TMPDIR_WORK'; :" EXIT
52
49
 
53
- # File that accumulates seen paths for the current phase:
54
- # format: <file_path><TAB><task_id>
55
50
  phase_index="$TMPDIR_WORK/phase_index.tsv"
56
51
 
57
52
  flush_phase() {
58
- # Reset the per-phase index for a new phase
59
53
  : > "$phase_index"
60
54
  }
61
55
 
62
56
  flush_phase
63
57
 
64
58
  while IFS= read -r line || [ -n "$line" ]; do
65
- # Detect phase header: ## Phase N: ...
66
59
  if printf '%s\n' "$line" | grep -qE '^## Phase [0-9]+'; then
67
- # Extract phase number
68
60
  current_phase="$(printf '%s\n' "$line" | sed 's/^## Phase \([0-9]*\).*/\1/')"
69
61
  flush_phase
70
62
  continue
71
63
  fi
72
64
 
73
- # Only process [P]-marked task lines when inside a phase
74
65
  [ -z "$current_phase" ] && continue
75
66
 
76
- # Match task lines containing [P] marker
77
67
  if ! printf '%s\n' "$line" | grep -qE '^\s*-\s*\[[ xX]\]\s+T[0-9]+\s+\[P\]'; then
78
68
  continue
79
69
  fi
80
70
 
81
- # Extract task ID: first T{NNN} token
82
71
  task_id="$(printf '%s\n' "$line" | grep -oE 'T[0-9]+' | head -1)"
83
72
  [ -z "$task_id" ] && continue
84
73
 
85
- # Extract backtick-wrapped file path (first occurrence)
86
- file_path="$(printf '%s\n' "$line" | sed "s/.*\`\([^\`]*\)\`.*/\1/" | head -1)"
74
+ # shellcheck disable=SC2016
75
+ file_paths_raw="$(printf '%s\n' "$line" | grep -oE '`[^`]+`' | sed 's/`//g' || true)"
76
+ file_paths=""
77
+ if [ -n "$file_paths_raw" ]; then
78
+ file_paths="$(printf '%s\n' "$file_paths_raw" | grep -E '[/.]' || true)"
79
+ fi
87
80
 
88
- # Skip if no file path found or extraction failed (line unchanged means no backtick)
89
- if [ -z "$file_path" ] || [ "$file_path" = "$line" ]; then
81
+ if [ -z "$file_paths" ]; then
90
82
  total_p_tasks=$((total_p_tasks + 1))
91
83
  continue
92
84
  fi
93
85
 
94
86
  total_p_tasks=$((total_p_tasks + 1))
95
87
 
96
- # Look for this file_path in current phase index
97
- existing_task="$(grep -F "${file_path} " "$phase_index" | cut -f2 | head -1 || true)"
88
+ while IFS= read -r file_path; do
89
+ [ -z "$file_path" ] && continue
98
90
 
99
- if [ -n "$existing_task" ]; then
100
- # Conflict detected
101
- conflict_found=1
102
- msg="CONFLICT: Phase ${current_phase} — ${existing_task} and ${task_id} both target ${file_path}"
103
- if [ -z "$conflict_messages" ]; then
104
- conflict_messages="$msg"
105
- else
106
- conflict_messages="${conflict_messages}
91
+ existing_task="$(grep -F "${file_path} " "$phase_index" | cut -f2 | head -1 || true)"
92
+
93
+ if [ -n "$existing_task" ]; then
94
+ conflict_found=1
95
+ msg="CONFLICT: Phase ${current_phase} ${existing_task} and ${task_id} both target ${file_path}"
96
+ if [ -z "$conflict_messages" ]; then
97
+ conflict_messages="$msg"
98
+ else
99
+ conflict_messages="${conflict_messages}
107
100
  ${msg}"
101
+ fi
102
+ else
103
+ printf '%s\t%s\n' "$file_path" "$task_id" >> "$phase_index"
108
104
  fi
109
- else
110
- # Record this file path for the current phase
111
- printf '%s\t%s\n' "$file_path" "$task_id" >> "$phase_index"
112
- fi
105
+ done <<EOF_PATHS
106
+ $file_paths
107
+ EOF_PATHS
113
108
 
114
109
  done < "$TASKS_FILE"
115
110
 
116
- # Count distinct phases that had [P] tasks by checking how many phase headers
117
- # had at least one [P] task line (reparse for count only)
118
111
  phases_with_p=0
119
112
  current_phase_count=""
120
113
  phase_had_p=0
@@ -134,15 +127,10 @@ while IFS= read -r line || [ -n "$line" ]; do
134
127
  fi
135
128
  done < "$TASKS_FILE"
136
129
 
137
- # Flush last phase
138
130
  if [ "$phase_had_p" -eq 1 ]; then
139
131
  phases_with_p=$((phases_with_p + 1))
140
132
  fi
141
133
 
142
- # ------------------------------------------------------------------
143
- # Output
144
- # ------------------------------------------------------------------
145
-
146
134
  if [ "$total_p_tasks" -eq 0 ]; then
147
135
  printf 'Valid: no [P] tasks found, nothing to validate\n'
148
136
  exit 0
@@ -4,6 +4,9 @@ set -euo pipefail
4
4
  # PermissionRequest Hook: Auto-allow CI-related Bash commands during implement/review Phase
5
5
  # Only exact whitelist matches allowed; commands with chaining (&&/;/|/$()) fall through to default behavior (user confirmation)
6
6
 
7
+ # shellcheck source=afc-state.sh
8
+ . "$(dirname "$0")/afc-state.sh"
9
+
7
10
  # shellcheck disable=SC2329
8
11
  cleanup() {
9
12
  :
@@ -11,22 +14,17 @@ cleanup() {
11
14
  trap cleanup EXIT
12
15
 
13
16
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
14
- PIPELINE_FLAG="$PROJECT_DIR/.claude/.afc-active"
15
- PHASE_FLAG="$PROJECT_DIR/.claude/.afc-phase"
16
17
 
17
18
  # Read hook data from stdin
18
19
  INPUT=$(cat)
19
20
 
20
21
  # Exit silently if pipeline is inactive
21
- if [ ! -f "$PIPELINE_FLAG" ]; then
22
+ if ! afc_state_is_active; then
22
23
  exit 0
23
24
  fi
24
25
 
25
26
  # Only active during implement/review Phase
26
- PHASE=""
27
- if [ -f "$PHASE_FLAG" ]; then
28
- PHASE="$(head -1 "$PHASE_FLAG" | tr -d '\n\r')"
29
- fi
27
+ PHASE="$(afc_state_read phase || echo '')"
30
28
  case "${PHASE:-}" in
31
29
  implement|review) ;;
32
30
  *) exit 0 ;;
@@ -54,13 +52,60 @@ case "$COMMAND" in
54
52
  *$'\n'*) exit 0 ;;
55
53
  esac
56
54
 
55
+ # Build dynamic whitelist from afc.config.md (CI/gate/test commands)
56
+ DYNAMIC_WHITELIST=""
57
+ CONFIG_FILE="$PROJECT_DIR/.claude/afc.config.md"
58
+ if [ -f "$CONFIG_FILE" ]; then
59
+ # Extract ci, gate, test values from YAML code block
60
+ # Handles both quoted and unquoted: ci: "npm run lint" or ci: npm run lint
61
+ for key in ci gate test; do
62
+ val=$(grep -E "^\s*${key}:\s*\"[^\"]*\"" "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*'"${key}"': *"\([^"]*\)".*/\1/' || true)
63
+ if [ -n "$val" ] && [ "$val" != '""' ]; then
64
+ DYNAMIC_WHITELIST="${DYNAMIC_WHITELIST:+${DYNAMIC_WHITELIST}|}${val}"
65
+ # Generate PM-agnostic variants (npm → pnpm, yarn, bun)
66
+ case "$val" in
67
+ "npm run "*)
68
+ suffix="${val#npm run }"
69
+ DYNAMIC_WHITELIST="${DYNAMIC_WHITELIST}|pnpm run ${suffix}|yarn run ${suffix}|bun run ${suffix}"
70
+ ;;
71
+ "npm test"*)
72
+ suffix="${val#npm test}"
73
+ DYNAMIC_WHITELIST="${DYNAMIC_WHITELIST}|pnpm test${suffix}|yarn test${suffix}|bun test${suffix}"
74
+ ;;
75
+ esac
76
+ fi
77
+ done
78
+ fi
79
+
57
80
  # Whitelist exact match (uses space + $ to prevent prefix matching)
58
81
  ALLOWED=false
59
- case "$COMMAND" in
60
- "npm run lint"|"npm test"|"npm run test:all")
82
+
83
+ # Check dynamic whitelist first (from afc.config.md)
84
+ if [ -n "$DYNAMIC_WHITELIST" ]; then
85
+ # Use printf + grep for safe matching (no eval)
86
+ if printf '%s\n' "$DYNAMIC_WHITELIST" | tr '|' '\n' | grep -qxF "$COMMAND"; then
61
87
  ALLOWED=true
62
- ;;
63
- esac
88
+ fi
89
+ fi
90
+
91
+ # Hardcoded fallback whitelist (always active for backward compatibility)
92
+ if [ "$ALLOWED" = "false" ]; then
93
+ case "$COMMAND" in
94
+ "npm run lint"|"npm test"|"npm run test:all")
95
+ ALLOWED=true
96
+ ;;
97
+ esac
98
+ fi
99
+
100
+ # Plugin's own scripts (auto-allow during pipeline execution)
101
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
102
+ if [ "$ALLOWED" = "false" ] && [ -n "$PLUGIN_ROOT" ]; then
103
+ case "$COMMAND" in
104
+ "\"${PLUGIN_ROOT}/scripts/"*|"${PLUGIN_ROOT}/scripts/"*)
105
+ ALLOWED=true
106
+ ;;
107
+ esac
108
+ fi
64
109
 
65
110
  # Prefix matching (allow paths after shellcheck, prettier, chmod +x)
66
111
  if [ "$ALLOWED" = "false" ]; then
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
2
  set -euo pipefail
3
3
 
4
- # Pipeline Management: Manage afc pipeline state flags
5
- # Manages flag files referenced by other hook scripts
4
+ # Pipeline Management: Manage afc pipeline state
5
+ # Uses .afc-state.json for all state (replaces legacy flag files)
6
6
  #
7
7
  # Usage:
8
8
  # afc-pipeline-manage.sh start <feature-name>
@@ -16,16 +16,17 @@ set -euo pipefail
16
16
 
17
17
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
18
18
  FLAG_DIR="$PROJECT_DIR/.claude"
19
- PIPELINE_FLAG="$FLAG_DIR/.afc-active"
20
- PHASE_FLAG="$FLAG_DIR/.afc-phase"
21
- CI_FLAG="$FLAG_DIR/.afc-ci-passed"
22
- CHANGES_LOG="$FLAG_DIR/.afc-changes.log"
19
+
20
+ # Source state library
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22
+ # shellcheck source=afc-state.sh
23
+ . "$SCRIPT_DIR/afc-state.sh"
23
24
 
24
25
  # shellcheck disable=SC2329
25
26
  cleanup() {
26
27
  local exit_code=$?
27
28
  if [ "$exit_code" -ne 0 ]; then
28
- echo "afc-pipeline-manage: exited with code $exit_code" >&2
29
+ echo "[afc:pipeline] Abnormal exit (code: $exit_code)" >&2
29
30
  fi
30
31
  exit "$exit_code"
31
32
  }
@@ -35,34 +36,32 @@ mkdir -p "$FLAG_DIR"
35
36
 
36
37
  COMMAND="${1:-}"
37
38
  if [ -z "$COMMAND" ]; then
38
- echo "Usage: $0 {start|phase|ci-pass|end|status} [args]" >&2
39
+ echo "[afc] Usage: $0 {start|phase|ci-pass|end|status} [args]" >&2
39
40
  exit 1
40
41
  fi
41
42
 
42
43
  case "$COMMAND" in
43
44
  start)
44
45
  if [ -z "${2:-}" ]; then
45
- echo "Feature name required" >&2
46
+ echo "[afc:pipeline] Feature name required" >&2
46
47
  exit 1
47
48
  fi
48
49
  # Sanitize feature name (strip newlines, path traversal, limit length)
49
- FEATURE=$(printf '%s' "$2" | tr -d '\n\r/' | cut -c1-100)
50
+ FEATURE=$(printf '%s' "$2" | tr -d '\n\r/"\\&' | cut -c1-100)
50
51
  if [ -z "$FEATURE" ]; then
51
- echo "Feature name invalid after sanitization" >&2
52
+ echo "[afc:pipeline] Feature name invalid after sanitization" >&2
52
53
  exit 1
53
54
  fi
54
55
 
55
56
  # Prevent duplicate execution
56
- if [ -f "$PIPELINE_FLAG" ]; then
57
- EXISTING=$(cat "$PIPELINE_FLAG")
58
- echo "WARNING: Pipeline already active: $EXISTING" >&2
59
- echo "Use '$0 end --force' to clear, or '$0 status' to check." >&2
57
+ if afc_state_is_active; then
58
+ EXISTING=$(afc_state_read feature || echo "unknown")
59
+ echo "[afc] WARNING: Pipeline already active: $EXISTING" >&2
60
+ echo "Use '$0 end --force' to clear, or '$0 status' to check" >&2
60
61
  exit 1
61
62
  fi
62
63
 
63
- printf '%s\n' "$FEATURE" > "$PIPELINE_FLAG"
64
- printf '%s\n' "spec" > "$PHASE_FLAG"
65
- rm -f "$CI_FLAG" "$CHANGES_LOG"
64
+ afc_state_init "$FEATURE"
66
65
 
67
66
  # Safety snapshot
68
67
  if cd "$PROJECT_DIR" 2>/dev/null; then
@@ -74,35 +73,34 @@ case "$COMMAND" in
74
73
 
75
74
  phase)
76
75
  PHASE="${2:?Phase name required}"
77
- case "$PHASE" in
78
- spec|plan|tasks|implement|review|clean)
79
- printf '%s\n' "$PHASE" > "$PHASE_FLAG"
80
- rm -f "$CI_FLAG" # Reset CI for new Phase
81
- echo "Phase: $PHASE"
82
- ;;
83
- *)
84
- echo "Invalid phase: $PHASE (valid: spec|plan|tasks|implement|review|clean)" >&2
85
- exit 1
86
- ;;
87
- esac
76
+ if afc_is_valid_phase "$PHASE"; then
77
+ afc_state_write "phase" "$PHASE"
78
+ afc_state_invalidate_ci
79
+ echo "Phase: $PHASE"
80
+ else
81
+ printf "[afc:pipeline] Invalid phase: %s\n → Valid phases: %s\n" "$PHASE" "$AFC_VALID_PHASES" >&2
82
+ exit 1
83
+ fi
88
84
  ;;
89
85
 
90
86
  ci-pass)
91
- date +%s > "$CI_FLAG"
87
+ afc_state_ci_pass
92
88
  echo "CI passed at $(date '+%H:%M:%S')"
93
89
  ;;
94
90
 
95
91
  end)
96
92
  FORCE="${2:-}"
97
93
  FEATURE=""
98
- if [ -f "$PIPELINE_FLAG" ]; then
99
- FEATURE=$(cat "$PIPELINE_FLAG")
94
+ if afc_state_is_active; then
95
+ FEATURE=$(afc_state_read feature || echo "")
100
96
  elif [ "$FORCE" != "--force" ]; then
101
- echo "No active pipeline to end." >&2
97
+ echo "[afc:pipeline] No active pipeline to end" >&2
102
98
  exit 0
103
99
  fi
104
100
 
105
- rm -f "$PIPELINE_FLAG" "$PHASE_FLAG" "$CI_FLAG" "$CHANGES_LOG"
101
+ afc_state_delete
102
+ # Clean sidecar changes file if it exists (jq-less fallback)
103
+ rm -f "$FLAG_DIR/.afc-state.changes.log"
106
104
  rm -f "$FLAG_DIR/.afc-failures.log" "$FLAG_DIR/.afc-task-results.log" "$FLAG_DIR/.afc-config-audit.log"
107
105
 
108
106
  # Clean up safety tag and phase tags (on successful completion)
@@ -117,12 +115,15 @@ case "$COMMAND" in
117
115
  ;;
118
116
 
119
117
  status)
120
- if [ -f "$PIPELINE_FLAG" ]; then
121
- echo "Active: $(cat "$PIPELINE_FLAG")"
122
- [ -f "$PHASE_FLAG" ] && echo "Phase: $(cat "$PHASE_FLAG")"
123
- [ -f "$CI_FLAG" ] && echo "CI: passed ($(cat "$CI_FLAG"))"
124
- if [ -f "$CHANGES_LOG" ]; then
125
- CHANGE_COUNT=$(wc -l < "$CHANGES_LOG" | tr -d ' ')
118
+ if afc_state_is_active; then
119
+ echo "Active: $(afc_state_read feature || echo 'unknown')"
120
+ PHASE=$(afc_state_read phase 2>/dev/null || true)
121
+ [ -n "$PHASE" ] && echo "Phase: $PHASE"
122
+ CI_TS=$(afc_state_read ciPassedAt 2>/dev/null || true)
123
+ [ -n "$CI_TS" ] && echo "CI: passed ($CI_TS)"
124
+ CHANGES=$(afc_state_read_changes 2>/dev/null || true)
125
+ if [ -n "$CHANGES" ]; then
126
+ CHANGE_COUNT=$(printf '%s\n' "$CHANGES" | wc -l | tr -d ' ')
126
127
  echo "Changes: $CHANGE_COUNT files"
127
128
  fi
128
129
  else
@@ -134,10 +135,9 @@ case "$COMMAND" in
134
135
  EVENT="${2:-}"
135
136
  MSG="${3:-}"
136
137
  if [ -z "$EVENT" ]; then
137
- echo "Usage: $0 log <event_type> <message>" >&2
138
+ echo "[afc] Usage: $0 log <event_type> <message>" >&2
138
139
  exit 1
139
140
  fi
140
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
141
141
  "$SCRIPT_DIR/afc-timeline-log.sh" "$EVENT" "$MSG"
142
142
  ;;
143
143
 
@@ -146,14 +146,14 @@ case "$COMMAND" in
146
146
  # Sanitize to digits only
147
147
  PHASE_NUM=$(printf '%s' "$PHASE_NUM" | tr -dc '0-9' | cut -c1-2)
148
148
  if [ -z "$PHASE_NUM" ]; then
149
- echo "Invalid phase number" >&2
149
+ echo "[afc:pipeline] Invalid phase number" >&2
150
150
  exit 1
151
151
  fi
152
152
  if cd "$PROJECT_DIR" 2>/dev/null; then
153
153
  git tag -f "afc/phase-${PHASE_NUM}" 2>/dev/null || true
154
154
  echo "Phase tag created: afc/phase-${PHASE_NUM}"
155
155
  else
156
- echo "Cannot create tag: not a git repo" >&2
156
+ echo "[afc:pipeline] Cannot create tag: not a git repo" >&2
157
157
  exit 1
158
158
  fi
159
159
  ;;
@@ -172,13 +172,13 @@ case "$COMMAND" in
172
172
  echo "No phase tags to remove"
173
173
  fi
174
174
  else
175
- echo "Cannot clean tags: not a git repo" >&2
175
+ echo "[afc:pipeline] Cannot clean tags: not a git repo" >&2
176
176
  exit 0
177
177
  fi
178
178
  ;;
179
179
 
180
180
  *)
181
- echo "Usage: $0 {start|phase|ci-pass|end|status|log|phase-tag|phase-tag-clean} [args]" >&2
181
+ echo "[afc] Usage: $0 {start|phase|ci-pass|end|status|log|phase-tag|phase-tag-clean} [args]" >&2
182
182
  exit 1
183
183
  ;;
184
184
  esac
@@ -1,6 +1,9 @@
1
1
  #!/bin/bash
2
2
  set -euo pipefail
3
3
 
4
+ # shellcheck source=afc-state.sh
5
+ . "$(dirname "$0")/afc-state.sh"
6
+
4
7
  # shellcheck disable=SC2329
5
8
  cleanup() {
6
9
  :
@@ -165,9 +168,9 @@ fi
165
168
 
166
169
  # ── Check 5: No active pipeline ──────────────────────────
167
170
 
168
- ACTIVE_FILE="$PROJECT_DIR/.claude/.afc-active"
169
- if [[ -f "$ACTIVE_FILE" ]]; then
170
- ACTIVE_NAME=$(head -1 "$ACTIVE_FILE" 2>/dev/null | tr -d '\n\r' | cut -c1-100 || printf 'unknown')
171
+ if afc_state_is_active; then
172
+ ACTIVE_NAME=$(afc_state_read feature 2>/dev/null || printf 'unknown')
173
+ ACTIVE_NAME=$(printf '%s' "$ACTIVE_NAME" | cut -c1-100)
171
174
  printf ' \xe2\x9c\x97 No active pipeline: pipeline already running (%s)\n' "$ACTIVE_NAME"
172
175
  FAIL_COUNT=$((FAIL_COUNT + 1))
173
176
  else