autoworkflow 3.6.1 → 3.8.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.
@@ -1,10 +1,14 @@
1
1
  #!/bin/bash
2
- # AutoWorkflow Pre-Edit Hook
2
+ # AutoWorkflow Pre-Edit Hook (v3.7.0 - Fail-Closed Design)
3
3
  # Runs on: PreToolUse for Write/Edit tools
4
- # Purpose: Enforce plan approval before implementation
4
+ # Purpose: BLOCK edits by default unless workflow state is properly set
5
5
  #
6
- # This hook implements the plan_approval_gate enforcement
7
- # It BLOCKS Write/Edit operations if plan hasn't been approved
6
+ # DESIGN PRINCIPLE: Fail-closed
7
+ # - If state is not initialized BLOCK (auto-init as feature)
8
+ # - If plan not approved → BLOCK
9
+ # - If suggestions not shown (feature tasks) → BLOCK
10
+ # - If multiple edits per turn → BLOCK
11
+ # - Only allow edit if ALL checks pass
8
12
 
9
13
  # Colors
10
14
  RED='\033[0;31m'
@@ -24,14 +28,43 @@ TASK_TYPE_FILE="$STATE_DIR/task-type"
24
28
  PLAN_APPROVED_FILE="$STATE_DIR/plan-approved"
25
29
  SUGGESTIONS_SHOWN_FILE="$STATE_DIR/suggestions-shown"
26
30
  CURRENT_TURN_EDITS_FILE="$STATE_DIR/current-turn-edits"
27
- SELECTED_ITEMS_FILE="$STATE_DIR/selected-items"
31
+ WORKFLOW_INITIALIZED_FILE="$STATE_DIR/workflow-initialized"
32
+
33
+ # Auto-initialize workflow state with safe defaults
34
+ auto_init_state() {
35
+ mkdir -p "$STATE_DIR"
36
+
37
+ # Set safe defaults - assume feature task (strictest)
38
+ echo "feature" > "$TASK_TYPE_FILE"
39
+ echo "IMPLEMENT" > "$PHASE_FILE"
40
+ echo "false" > "$PLAN_APPROVED_FILE"
41
+ echo "false" > "$SUGGESTIONS_SHOWN_FILE"
42
+ echo "0" > "$CURRENT_TURN_EDITS_FILE"
43
+ echo "true" > "$WORKFLOW_INITIALIZED_FILE"
44
+
45
+ echo ""
46
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
47
+ echo -e "${CYAN}${BOLD}📋 AUTOWORKFLOW: STATE INITIALIZED${NC}"
48
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
49
+ echo ""
50
+ echo "Workflow state auto-initialized with safe defaults:"
51
+ echo " Task type: feature (strictest)"
52
+ echo " Plan approved: false"
53
+ echo " Suggestions shown: false"
54
+ echo ""
55
+ }
56
+
57
+ # Check if workflow is initialized
58
+ is_workflow_initialized() {
59
+ [ -d "$STATE_DIR" ] && [ -f "$WORKFLOW_INITIALIZED_FILE" ]
60
+ }
28
61
 
29
62
  # Get current phase
30
63
  get_phase() {
31
64
  if [ -f "$PHASE_FILE" ]; then
32
65
  cat "$PHASE_FILE"
33
66
  else
34
- echo "IDLE"
67
+ echo "UNKNOWN"
35
68
  fi
36
69
  }
37
70
 
@@ -40,7 +73,7 @@ get_task_type() {
40
73
  if [ -f "$TASK_TYPE_FILE" ]; then
41
74
  cat "$TASK_TYPE_FILE"
42
75
  else
43
- echo "unknown"
76
+ echo "feature" # Default to strictest
44
77
  fi
45
78
  }
46
79
 
@@ -53,7 +86,7 @@ is_plan_approved() {
53
86
  return 1
54
87
  }
55
88
 
56
- # Check if suggestions were shown (for feature tasks)
89
+ # Check if suggestions were shown
57
90
  suggestions_shown() {
58
91
  if [ -f "$SUGGESTIONS_SHOWN_FILE" ]; then
59
92
  local status=$(cat "$SUGGESTIONS_SHOWN_FILE")
@@ -79,142 +112,182 @@ increment_turn_edits() {
79
112
  echo "$next"
80
113
  }
81
114
 
82
- # Check if user has selected items to implement
83
- has_selected_items() {
84
- if [ -f "$SELECTED_ITEMS_FILE" ]; then
85
- local content=$(cat "$SELECTED_ITEMS_FILE")
86
- [ -n "$content" ] && return 0
87
- fi
88
- return 1
89
- }
90
-
91
- # Check if task type requires approval
92
- requires_approval() {
115
+ # Check if task type requires strict workflow
116
+ requires_strict_workflow() {
93
117
  local task_type="$1"
94
118
  case "$task_type" in
95
- # These task types require plan approval
119
+ # Strict workflow required
96
120
  feature|fix|refactor|perf|security|test)
97
121
  return 0
98
122
  ;;
99
- # These can proceed without explicit approval
100
- docs|style|config|query)
123
+ # Relaxed workflow
124
+ docs|style|config)
101
125
  return 1
102
126
  ;;
103
- # Unknown defaults to requiring approval
127
+ # Unknown = strict
104
128
  *)
105
129
  return 0
106
130
  ;;
107
131
  esac
108
132
  }
109
133
 
110
- # Main check
111
- main() {
112
- local phase=$(get_phase)
113
- local task_type=$(get_task_type)
134
+ # GATE 1: Plan Approval Check
135
+ check_plan_approval() {
136
+ local task_type="$1"
114
137
 
115
- # Skip check if no task is active
116
- if [ "$phase" = "IDLE" ]; then
117
- exit 0
138
+ if ! requires_strict_workflow "$task_type"; then
139
+ return 0 # Pass for relaxed task types
118
140
  fi
119
141
 
120
- # Skip check for task types that don't require approval
121
- if ! requires_approval "$task_type"; then
122
- exit 0
142
+ if ! is_plan_approved; then
143
+ echo ""
144
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
145
+ echo -e "${RED}${BOLD}⛔ GATE 1: PLAN APPROVAL REQUIRED${NC}"
146
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
147
+ echo ""
148
+ echo -e "${CYAN}Task Type:${NC} $task_type"
149
+ echo -e "${CYAN}Plan Approved:${NC} NO"
150
+ echo ""
151
+ echo "You MUST get user approval before making any edits."
152
+ echo ""
153
+ echo -e "${BOLD}Required steps:${NC}"
154
+ echo " 1. Analyze the codebase"
155
+ echo " 2. Present your plan to the user"
156
+ echo " 3. Wait for explicit approval (yes/proceed/approved)"
157
+ echo " 4. THEN implement"
158
+ echo ""
159
+ echo -e "${DIM}To mark approved: echo 'true' > $PLAN_APPROVED_FILE${NC}"
160
+ echo ""
161
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
162
+ echo ""
163
+ return 1
123
164
  fi
165
+ return 0
166
+ }
124
167
 
125
- # Skip check if already in IMPLEMENT or later phases
126
- case "$phase" in
127
- IMPLEMENT|VERIFY|FIX|AUDIT|PRE_COMMIT|COMMIT|UPDATE)
128
- # Already past approval, allow edits
129
- exit 0
130
- ;;
131
- esac
168
+ # GATE 2: Suggestions Check (for feature tasks)
169
+ check_suggestions() {
170
+ local task_type="$1"
171
+
172
+ # Only enforce for feature tasks
173
+ if [ "$task_type" != "feature" ]; then
174
+ return 0
175
+ fi
176
+
177
+ if ! suggestions_shown; then
178
+ echo ""
179
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
180
+ echo -e "${RED}${BOLD}⛔ GATE 2: SUGGESTIONS REQUIRED${NC}"
181
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
182
+ echo ""
183
+ echo -e "${CYAN}Task Type:${NC} feature"
184
+ echo ""
185
+ echo "For feature tasks, you MUST show 3-tier suggestions FIRST:"
186
+ echo ""
187
+ echo " 🔴 Required - Must implement"
188
+ echo " 🟡 Recommended - Should implement"
189
+ echo " 🟢 Optional - Nice to have"
190
+ echo ""
191
+ echo "Show suggestions, let user select, THEN implement ONE at a time."
192
+ echo ""
193
+ echo -e "${DIM}To mark shown: echo 'true' > $SUGGESTIONS_SHOWN_FILE${NC}"
194
+ echo ""
195
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
196
+ echo ""
197
+ return 1
198
+ fi
199
+ return 0
200
+ }
201
+
202
+ # GATE 3: One Edit Per Turn Check (applies to ALL implementation)
203
+ check_one_edit_per_turn() {
204
+ local task_type="$1"
132
205
 
133
- # Check if we're in a pre-approval phase
134
- if [ "$phase" = "ANALYZE" ] || [ "$phase" = "PLAN" ]; then
135
- if ! is_plan_approved; then
136
- echo ""
137
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
138
- echo -e "${RED}${BOLD}⛔ AUTOWORKFLOW: PLAN APPROVAL REQUIRED${NC}"
139
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
140
- echo ""
141
- echo -e "${CYAN}Current Phase:${NC} $phase"
142
- echo -e "${CYAN}Task Type:${NC} $task_type"
143
- echo -e "${CYAN}Plan Approved:${NC} NO"
144
- echo ""
145
- echo "Cannot edit files before plan approval."
146
- echo ""
147
- echo -e "${BOLD}Required workflow:${NC}"
148
- echo " 1. Complete ANALYZE phase (read relevant files)"
149
- echo " 2. Present PLAN with suggestions"
150
- echo " 3. Wait for user approval"
151
- echo " 4. THEN implement"
152
- echo ""
153
- echo -e "${DIM}To approve, user must say: yes, proceed, approved, go ahead${NC}"
154
- echo ""
155
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
156
- echo ""
157
-
158
- # Exit with error to BLOCK the edit
159
- exit 1
160
- fi
206
+ if ! requires_strict_workflow "$task_type"; then
207
+ return 0 # Pass for relaxed task types
161
208
  fi
162
209
 
163
- # CHECK: Suggestions must be shown for feature tasks before implementing
164
- if [ "$task_type" = "feature" ] && [ "$phase" = "IMPLEMENT" ]; then
165
- if ! suggestions_shown; then
166
- echo ""
167
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
168
- echo -e "${RED}${BOLD}⛔ AUTOWORKFLOW: SUGGESTIONS REQUIRED${NC}"
169
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
170
- echo ""
171
- echo -e "${CYAN}Task Type:${NC} feature"
172
- echo ""
173
- echo "For feature tasks, you MUST show 3-tier suggestions first:"
174
- echo ""
175
- echo " 🔴 Required - Must implement"
176
- echo " 🟡 Recommended - Should implement"
177
- echo " 🟢 Optional - Nice to have"
178
- echo ""
179
- echo "Show suggestions, let user select items, THEN implement."
180
- echo ""
181
- echo -e "${DIM}To mark suggestions shown: echo 'true' > $SUGGESTIONS_SHOWN_FILE${NC}"
182
- echo ""
183
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
184
- echo ""
185
- exit 1
186
- fi
210
+ local edit_count=$(get_turn_edit_count)
211
+
212
+ if [ "$edit_count" -ge 1 ]; then
213
+ echo ""
214
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
215
+ echo -e "${YELLOW}${BOLD}⛔ GATE 3: ONE EDIT PER TURN${NC}"
216
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
217
+ echo ""
218
+ echo -e "${CYAN}Edits this turn:${NC} $edit_count"
219
+ echo ""
220
+ echo "You can only make ONE edit per user turn."
221
+ echo ""
222
+ echo "This ensures:"
223
+ echo " 1. Easier to track changes"
224
+ echo " 2. Errors caught early"
225
+ echo " 3. User can review incrementally"
226
+ echo ""
227
+ echo "Wait for user's next message before making another edit."
228
+ echo ""
229
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
230
+ echo ""
231
+ return 1
187
232
  fi
188
233
 
189
- # CHECK: One fix at a time (max 1 edit per turn in FIX phase)
190
- if [ "$phase" = "FIX" ]; then
191
- local edit_count=$(get_turn_edit_count)
192
- if [ "$edit_count" -ge 1 ]; then
193
- echo ""
194
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
195
- echo -e "${YELLOW}${BOLD}⚠ AUTOWORKFLOW: ONE FIX AT A TIME${NC}"
196
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
197
- echo ""
198
- echo -e "${CYAN}Edits this turn:${NC} $edit_count"
199
- echo ""
200
- echo "Fix ONE issue at a time, then verify."
201
- echo ""
202
- echo "This ensures:"
203
- echo " 1. Easier to track what changed"
204
- echo " 2. Errors caught early"
205
- echo " 3. User can review incrementally"
206
- echo ""
207
- echo "Wait for verification to complete before next fix."
208
- echo ""
209
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
210
- echo ""
211
- exit 1
212
- fi
213
- # Track this edit
214
- increment_turn_edits > /dev/null
234
+ return 0
235
+ }
236
+
237
+ # Main execution
238
+ main() {
239
+ # STEP 1: Check if workflow is initialized
240
+ if ! is_workflow_initialized; then
241
+ # Auto-initialize with safe defaults
242
+ auto_init_state
243
+
244
+ # After init, BLOCK this edit attempt
245
+ echo ""
246
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
247
+ echo -e "${RED}${BOLD}⛔ AUTOWORKFLOW: EDIT BLOCKED${NC}"
248
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
249
+ echo ""
250
+ echo "Workflow just initialized. Cannot edit yet."
251
+ echo ""
252
+ echo "You must first:"
253
+ echo " 1. Show your analysis/plan to the user"
254
+ echo " 2. Get user approval"
255
+ echo " 3. For features: Show 3-tier suggestions"
256
+ echo " 4. THEN implement one fix at a time"
257
+ echo ""
258
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
259
+ echo ""
260
+ exit 1
215
261
  fi
216
262
 
217
- # All checks passed, allow edit
263
+ # Get current state
264
+ local task_type=$(get_task_type)
265
+ local phase=$(get_phase)
266
+
267
+ # STEP 2: Run all gates
268
+
269
+ # GATE 1: Plan Approval
270
+ if ! check_plan_approval "$task_type"; then
271
+ exit 1
272
+ fi
273
+
274
+ # GATE 2: Suggestions (feature tasks only)
275
+ if ! check_suggestions "$task_type"; then
276
+ exit 1
277
+ fi
278
+
279
+ # GATE 3: One Edit Per Turn
280
+ if ! check_one_edit_per_turn "$task_type"; then
281
+ exit 1
282
+ fi
283
+
284
+ # ALL GATES PASSED - Allow edit and track it
285
+ increment_turn_edits > /dev/null
286
+
287
+ echo ""
288
+ echo -e "${GREEN}✓${NC} Edit allowed (turn edit #$(get_turn_edit_count))"
289
+ echo ""
290
+
218
291
  exit 0
219
292
  }
220
293
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Automated workflow enforcement for Claude Code via hooks and system prompts.
4
4
 
5
- **v3.6.0** - Multi-language support, one-fix-at-a-time enforcement, suggestions gate.
5
+ **v3.8.0** - Smart CLI updates: version tracking + user file preservation.
6
6
 
7
7
  When you use Claude Code with AutoWorkflow, hooks automatically enforce workflow phases, block unauthorized edits, and guide Claude through a structured process for all coding tasks.
8
8
 
@@ -14,10 +14,61 @@ When you use Claude Code with AutoWorkflow, hooks automatically enforce workflow
14
14
  npx autoworkflow init
15
15
  ```
16
16
 
17
- Options:
18
- - `npx autoworkflow init` - Required + recommended files
19
- - `npx autoworkflow init --all` - Include .vscode and config templates
20
- - `npx autoworkflow init --minimal` - Required files only
17
+ ### CLI Commands
18
+
19
+ | Command | Description |
20
+ |---------|-------------|
21
+ | `npx autoworkflow init` | Fresh install (required + recommended files) |
22
+ | `npx autoworkflow init --all` | Include .vscode and config templates |
23
+ | `npx autoworkflow init --minimal` | Required files only |
24
+ | `npx autoworkflow init --force` | Full reinstall (backup + overwrite all) |
25
+ | `npx autoworkflow update` | Smart update - core files only, preserve user files |
26
+ | `npx autoworkflow status` | Show installed version and components |
27
+
28
+ ### Smart Updates (v3.8.0)
29
+
30
+ The CLI now tracks installed versions and preserves user customizations:
31
+
32
+ ```
33
+ npx autoworkflow status # Check version and what would change
34
+ npx autoworkflow update # Update core files, preserve BLUEPRINT.md
35
+ ```
36
+
37
+ | File Category | On `init` | On `update` |
38
+ |---------------|-----------|-------------|
39
+ | **Core** (hooks, system, CLAUDE.md) | Installed | Updated |
40
+ | **User** (BLUEPRINT.md, AI_RULES.md) | Created if missing | **Preserved** |
41
+ | **Commands/Skills** | Installed | Updated |
42
+ | **Optional** (.vscode, configs) | Only with `--all` | Skipped |
43
+
44
+ ---
45
+
46
+ ## What's New in v3.7.0
47
+
48
+ ### Fail-Closed Enforcement Design
49
+ Previous versions "failed open" - if state wasn't set, edits were allowed. v3.7.0 "fails closed":
50
+
51
+ | Scenario | Before (v3.6) | After (v3.7) |
52
+ |----------|---------------|--------------|
53
+ | No workflow state | ✅ Allowed | ⛔ BLOCKED |
54
+ | Plan not approved | ⛔ Blocked (in PLAN phase only) | ⛔ BLOCKED (always) |
55
+ | No suggestions shown | ⛔ Blocked (in IMPLEMENT only) | ⛔ BLOCKED (always for features) |
56
+ | Multiple edits/turn | ⛔ Blocked (in FIX only) | ⛔ BLOCKED (always) |
57
+
58
+ ### Auto-Initialization with Safe Defaults
59
+ When Claude attempts the first edit without workflow state:
60
+ 1. Auto-creates state with `task-type: feature` (strictest)
61
+ 2. Sets `plan-approved: false`
62
+ 3. Sets `suggestions-shown: false`
63
+ 4. **BLOCKS the edit** immediately
64
+ 5. Claude must show plan/suggestions first, get approval, then try again
65
+
66
+ ### Three Gates (All Must Pass)
67
+ ```
68
+ GATE 1: Plan Approval → Must have user approval
69
+ GATE 2: Suggestions → Must show 3-tier suggestions (features)
70
+ GATE 3: One Edit/Turn → Max 1 edit per user message
71
+ ```
21
72
 
22
73
  ---
23
74
 
@@ -149,16 +200,18 @@ ANALYZE → PLAN → CONFIRM → IMPLEMENT → VERIFY → AUDIT → COMMIT → U
149
200
 
150
201
  ---
151
202
 
152
- ## Blocking Gates
203
+ ## Blocking Gates (Fail-Closed)
204
+
205
+ | Gate | Blocks If | Applies To |
206
+ |------|-----------|------------|
207
+ | **State Init** | No workflow state exists | ALL tasks |
208
+ | **Plan Approval** | User hasn't approved | feature, fix, refactor, perf, security, test |
209
+ | **Suggestions** | 3-tier suggestions not shown | feature tasks |
210
+ | **One Edit/Turn** | Already edited this turn | feature, fix, refactor, perf, security, test |
211
+ | **Verify** | Language-specific errors | ALL (post-edit) |
212
+ | **Pre-Commit** | TODO/FIXME, console.log | ALL commits |
153
213
 
154
- | Gate | Blocks If | Enforced By |
155
- |------|-----------|-------------|
156
- | Plan Approval | User hasn't approved the plan | `pre-edit.sh` (exit 1) |
157
- | Suggestions | Feature task without 3-tier suggestions shown | `pre-edit.sh` (exit 1) |
158
- | One-Fix-At-A-Time | Multiple edits in FIX phase per turn | `pre-edit.sh` (exit 1) |
159
- | Verify | TypeScript/ESLint/language-specific errors | `post-edit.sh` loop |
160
- | Audit | Orphan features or circular dependencies | Required before commit |
161
- | Pre-Commit | TODO/FIXME, console.log, bad format | `pre-commit-check.sh` (exit 1) |
214
+ **Design:** All gates are checked on EVERY edit. Unknown state = strictest defaults.
162
215
 
163
216
  ---
164
217
 
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, cpSync, mkdirSync, chmodSync, renameSync, unlinkSync } from 'fs';
3
+ import { existsSync, cpSync, mkdirSync, chmodSync, renameSync, unlinkSync, readFileSync, writeFileSync } from 'fs';
4
4
  import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
 
@@ -11,6 +11,10 @@ const packageRoot = join(__dirname, '..');
11
11
  const args = process.argv.slice(2);
12
12
  const command = args[0];
13
13
 
14
+ // Get package version
15
+ const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
16
+ const PACKAGE_VERSION = packageJson.version;
17
+
14
18
  const colors = {
15
19
  green: (text) => `\x1b[32m${text}\x1b[0m`,
16
20
  yellow: (text) => `\x1b[33m${text}\x1b[0m`,
@@ -20,50 +24,147 @@ const colors = {
20
24
  dim: (text) => `\x1b[2m${text}\x1b[0m`,
21
25
  };
22
26
 
27
+ // File categories for smart updates
28
+ const FILE_CATEGORIES = {
29
+ // Core files - always updated (system functionality)
30
+ core: [
31
+ { src: '.claude/hooks', dest: '.claude/hooks', name: '.claude/hooks/' },
32
+ { src: '.claude/settings.json', dest: '.claude/settings.json', name: '.claude/settings.json' },
33
+ { src: 'system', dest: 'system', name: 'system/' },
34
+ { src: 'CLAUDE.md', dest: 'CLAUDE.md', name: 'CLAUDE.md' },
35
+ ],
36
+ // User-customizable files - preserved on update, created on init
37
+ user: [
38
+ { src: 'instructions/AI_RULES.md', dest: 'instructions/AI_RULES.md', name: 'instructions/AI_RULES.md' },
39
+ // BLUEPRINT.md is special - never overwrite if user has content
40
+ ],
41
+ // Commands and skills - updated (but could have user additions)
42
+ commands: [
43
+ { src: '.claude/commands', dest: '.claude/commands', name: '.claude/commands/' },
44
+ { src: '.claude/skills', dest: '.claude/skills', name: '.claude/skills/' },
45
+ ],
46
+ // Scripts and git hooks
47
+ scripts: [
48
+ { src: 'scripts', dest: 'scripts', name: 'scripts/' },
49
+ { src: 'hooks', dest: 'hooks', name: 'hooks/' },
50
+ ],
51
+ // Optional config templates - skip if exists
52
+ optional: [
53
+ { src: '.vscode', dest: '.vscode', name: '.vscode/' },
54
+ { src: '.prettierrc', dest: '.prettierrc', name: '.prettierrc' },
55
+ { src: 'eslint.config.example.js', dest: 'eslint.config.js', name: 'eslint.config.js' },
56
+ { src: 'tsconfig.example.json', dest: 'tsconfig.json', name: 'tsconfig.json' },
57
+ ],
58
+ };
59
+
23
60
  function printHelp() {
24
61
  console.log(`
25
- ${colors.bold('AutoWorkflow CLI')}
62
+ ${colors.bold('AutoWorkflow CLI')} ${colors.dim(`v${PACKAGE_VERSION}`)}
26
63
 
27
64
  Usage: npx autoworkflow <command> [options]
28
65
 
29
66
  Commands:
30
- init Set up AutoWorkflow in current directory
31
- init --all Include optional files (.vscode, configs)
32
- init --minimal Required files only
33
- help Show this help message
67
+ ${colors.cyan('init')} Set up AutoWorkflow in current directory
68
+ ${colors.cyan('init --all')} Include optional files (.vscode, configs)
69
+ ${colors.cyan('init --minimal')} Required files only
70
+ ${colors.cyan('init --force')} Full reinstall (backup + overwrite all)
71
+
72
+ ${colors.cyan('update')} Smart update - core files only, preserve user files
73
+ ${colors.cyan('status')} Show installed version and what would change
74
+
75
+ ${colors.cyan('help')} Show this help message
34
76
 
35
77
  Options:
36
- --no-backup Overwrite existing files without backup
78
+ --no-backup Overwrite existing files without backup
79
+ --force Force overwrite all files (with backup)
37
80
 
38
81
  Examples:
39
- npx autoworkflow init
40
- npx autoworkflow init --all
82
+ npx autoworkflow init # Fresh install
83
+ npx autoworkflow update # Update core files only
84
+ npx autoworkflow status # Check what would change
85
+ npx autoworkflow init --force # Full reinstall
41
86
  `);
42
87
  }
43
88
 
89
+ // Version tracking
90
+ function getInstalledVersion(cwd) {
91
+ const versionFile = join(cwd, '.claude', '.autoworkflow', 'version');
92
+ if (existsSync(versionFile)) {
93
+ try {
94
+ return readFileSync(versionFile, 'utf8').trim();
95
+ } catch (e) {
96
+ return null;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function setInstalledVersion(cwd, version) {
103
+ const stateDir = join(cwd, '.claude', '.autoworkflow');
104
+ mkdirSync(stateDir, { recursive: true });
105
+ writeFileSync(join(stateDir, 'version'), version);
106
+ }
107
+
108
+ // Check if BLUEPRINT.md has user content (not just template)
109
+ function hasUserBlueprint(cwd) {
110
+ const blueprintPath = join(cwd, 'instructions', 'BLUEPRINT.md');
111
+ if (!existsSync(blueprintPath)) {
112
+ return false;
113
+ }
114
+
115
+ try {
116
+ const content = readFileSync(blueprintPath, 'utf8');
117
+ // Check if it's just a template or has real content
118
+ const templateMarkers = [
119
+ '<!-- TEMPLATE',
120
+ '<!-- AUTO-GENERATED TEMPLATE',
121
+ '## Features\n\n(To be documented)',
122
+ '## Features\n\n_None documented yet_',
123
+ ];
124
+
125
+ // If it has any template markers, it's not user content
126
+ for (const marker of templateMarkers) {
127
+ if (content.includes(marker)) {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // If it has substantial content (more than 200 chars after stripping headers), it's user content
133
+ const strippedContent = content.replace(/^#.*$/gm, '').replace(/\s+/g, ' ').trim();
134
+ return strippedContent.length > 200;
135
+ } catch (e) {
136
+ return false;
137
+ }
138
+ }
139
+
44
140
  function backupFile(dest, name) {
45
141
  if (existsSync(dest)) {
46
142
  const backupPath = `${dest}.backup`;
47
- // If backup already exists, add timestamp
48
143
  const finalBackupPath = existsSync(backupPath)
49
144
  ? `${dest}.backup.${Date.now()}`
50
145
  : backupPath;
51
146
  try {
52
147
  renameSync(dest, finalBackupPath);
53
- console.log(` ${colors.yellow('↪')} ${name} ${colors.dim(`→ backed up to ${finalBackupPath.split('/').pop()}`)}`);
148
+ console.log(` ${colors.yellow('↪')} ${name} ${colors.dim(`→ backed up`)}`);
54
149
  return true;
55
150
  } catch (err) {
56
151
  console.log(` ${colors.red('✗')} Failed to backup ${name}: ${err.message}`);
57
152
  return false;
58
153
  }
59
154
  }
60
- return true; // No backup needed
155
+ return true;
61
156
  }
62
157
 
63
158
  function copyFile(src, dest, name, options = {}) {
64
- const noBackup = args.includes('--no-backup');
159
+ const { noBackup = false, skipIfExists = false } = options;
65
160
 
66
161
  if (existsSync(src)) {
162
+ // Skip if exists and skipIfExists is true
163
+ if (skipIfExists && existsSync(dest)) {
164
+ console.log(` ${colors.dim('○')} ${name} ${colors.dim('(exists, skipped)')}`);
165
+ return true;
166
+ }
167
+
67
168
  // Backup existing file/folder if it exists
68
169
  if (!noBackup && existsSync(dest)) {
69
170
  const backed = backupFile(dest, name);
@@ -84,49 +185,19 @@ function copyFile(src, dest, name, options = {}) {
84
185
  }
85
186
  }
86
187
 
87
- function init(options = {}) {
88
- const cwd = process.cwd();
89
- const all = options.all || args.includes('--all');
90
- const minimal = options.minimal || args.includes('--minimal');
91
- const noBackup = args.includes('--no-backup');
92
-
93
- console.log(`\n${colors.bold('AutoWorkflow Setup')}\n`);
94
- console.log(`Installing to: ${colors.cyan(cwd)}`);
95
- if (!noBackup) {
96
- console.log(`${colors.dim('Existing files will be backed up with .backup suffix')}\n`);
97
- } else {
98
- console.log(`${colors.yellow('⚠')} ${colors.dim('--no-backup: Existing files will be overwritten')}\n`);
99
- }
100
-
101
- // Required files
102
- console.log(colors.bold('Required files:'));
103
- copyFile(join(packageRoot, 'CLAUDE.md'), join(cwd, 'CLAUDE.md'), 'CLAUDE.md');
104
- copyFile(join(packageRoot, 'system'), join(cwd, 'system'), 'system/');
105
- copyFile(join(packageRoot, '.claude'), join(cwd, '.claude'), '.claude/');
106
-
107
- // Copy instructions folder but remove BLUEPRINT.md (hook will auto-generate it)
108
- copyFile(join(packageRoot, 'instructions'), join(cwd, 'instructions'), 'instructions/');
109
- const blueprintPath = join(cwd, 'instructions', 'BLUEPRINT.md');
110
- if (existsSync(blueprintPath)) {
111
- try {
112
- unlinkSync(blueprintPath);
113
- console.log(` ${colors.cyan('ℹ')} BLUEPRINT.md removed (will be auto-generated on first run)`);
114
- } catch (e) {
115
- // Ignore
116
- }
117
- }
118
-
119
- // Make Claude hooks executable
188
+ function makeHooksExecutable(cwd) {
120
189
  if (process.platform !== 'win32' && existsSync(join(cwd, '.claude', 'hooks'))) {
121
190
  try {
122
191
  const hookFiles = [
123
192
  'session-check.sh',
124
193
  'post-edit.sh',
194
+ 'pre-edit.sh',
125
195
  'pre-commit-check.sh',
126
196
  'pre-tool-router.sh',
127
197
  'phase-transition.sh',
128
198
  'audit-runner.sh',
129
- 'blueprint-generator.sh'
199
+ 'blueprint-generator.sh',
200
+ 'post-commit.sh',
130
201
  ];
131
202
  hookFiles.forEach(hook => {
132
203
  const hookPath = join(cwd, '.claude', 'hooks', hook);
@@ -134,36 +205,15 @@ function init(options = {}) {
134
205
  chmodSync(hookPath, 0o755);
135
206
  }
136
207
  });
137
- console.log(` ${colors.green('✓')} Claude hooks made executable`);
208
+ console.log(` ${colors.green('✓')} Hooks made executable`);
138
209
  } catch (e) {
139
210
  // Ignore chmod errors
140
211
  }
141
212
  }
213
+ }
142
214
 
143
- if (!minimal) {
144
- // Recommended files
145
- console.log(`\n${colors.bold('Recommended files:')}`);
146
- copyFile(join(packageRoot, 'scripts'), join(cwd, 'scripts'), 'scripts/');
147
- copyFile(join(packageRoot, 'hooks'), join(cwd, 'hooks'), 'hooks/');
148
- }
149
-
150
- if (all) {
151
- // Optional files
152
- console.log(`\n${colors.bold('Optional files:')}`);
153
- copyFile(join(packageRoot, '.vscode'), join(cwd, '.vscode'), '.vscode/');
154
- copyFile(join(packageRoot, '.prettierrc'), join(cwd, '.prettierrc'), '.prettierrc');
155
-
156
- // Copy example configs with proper names
157
- if (existsSync(join(packageRoot, 'eslint.config.example.js'))) {
158
- copyFile(join(packageRoot, 'eslint.config.example.js'), join(cwd, 'eslint.config.js'), 'eslint.config.js');
159
- }
160
- if (existsSync(join(packageRoot, 'tsconfig.example.json'))) {
161
- copyFile(join(packageRoot, 'tsconfig.example.json'), join(cwd, 'tsconfig.json'), 'tsconfig.json');
162
- }
163
- }
164
-
165
- // Setup git hooks if hooks were copied
166
- if (!minimal && existsSync(join(cwd, 'hooks')) && existsSync(join(cwd, '.git'))) {
215
+ function setupGitHooks(cwd) {
216
+ if (existsSync(join(cwd, 'hooks')) && existsSync(join(cwd, '.git'))) {
167
217
  console.log(`\n${colors.bold('Git hooks:')}`);
168
218
  const hooksDir = join(cwd, '.git', 'hooks');
169
219
  mkdirSync(hooksDir, { recursive: true });
@@ -171,7 +221,6 @@ function init(options = {}) {
171
221
  const hooksCopied = copyFile(join(cwd, 'hooks', 'pre-commit'), join(hooksDir, 'pre-commit'), 'pre-commit');
172
222
  copyFile(join(cwd, 'hooks', 'commit-msg'), join(hooksDir, 'commit-msg'), 'commit-msg');
173
223
 
174
- // Make hooks executable (Unix only)
175
224
  if (hooksCopied && process.platform !== 'win32') {
176
225
  try {
177
226
  chmodSync(join(hooksDir, 'pre-commit'), 0o755);
@@ -181,6 +230,217 @@ function init(options = {}) {
181
230
  }
182
231
  }
183
232
  }
233
+ }
234
+
235
+ // STATUS command
236
+ function status() {
237
+ const cwd = process.cwd();
238
+ const installedVersion = getInstalledVersion(cwd);
239
+
240
+ console.log(`\n${colors.bold('AutoWorkflow Status')}\n`);
241
+ console.log(`Package version: ${colors.cyan(PACKAGE_VERSION)}`);
242
+ console.log(`Installed version: ${installedVersion ? colors.cyan(installedVersion) : colors.dim('not installed')}`);
243
+
244
+ if (installedVersion && installedVersion === PACKAGE_VERSION) {
245
+ console.log(`\n${colors.green('✓')} You're on the latest version.\n`);
246
+ } else if (installedVersion) {
247
+ console.log(`\n${colors.yellow('⚠')} Update available: ${installedVersion} → ${PACKAGE_VERSION}`);
248
+ console.log(` Run: ${colors.cyan('npx autoworkflow update')}\n`);
249
+ } else {
250
+ console.log(`\n${colors.yellow('⚠')} AutoWorkflow not initialized in this directory.`);
251
+ console.log(` Run: ${colors.cyan('npx autoworkflow init')}\n`);
252
+ return;
253
+ }
254
+
255
+ // Check what exists
256
+ console.log(`${colors.bold('Installed components:')}`);
257
+ const components = [
258
+ { path: 'CLAUDE.md', name: 'CLAUDE.md' },
259
+ { path: '.claude/hooks', name: 'Hooks' },
260
+ { path: '.claude/settings.json', name: 'Settings' },
261
+ { path: '.claude/commands', name: 'Commands' },
262
+ { path: '.claude/skills', name: 'Skills' },
263
+ { path: 'system', name: 'System docs' },
264
+ { path: 'instructions/AI_RULES.md', name: 'AI Rules' },
265
+ { path: 'instructions/BLUEPRINT.md', name: 'Blueprint' },
266
+ { path: 'scripts', name: 'Scripts' },
267
+ { path: 'hooks', name: 'Git hooks' },
268
+ ];
269
+
270
+ components.forEach(({ path, name }) => {
271
+ const exists = existsSync(join(cwd, path));
272
+ const status = exists ? colors.green('✓') : colors.dim('○');
273
+ console.log(` ${status} ${name}`);
274
+ });
275
+
276
+ // Check for user customizations
277
+ const hasBlueprint = hasUserBlueprint(cwd);
278
+ if (hasBlueprint) {
279
+ console.log(`\n${colors.cyan('ℹ')} User BLUEPRINT.md detected - will be preserved on update.`);
280
+ }
281
+
282
+ console.log('');
283
+ }
284
+
285
+ // UPDATE command - smart update, preserve user files
286
+ function update() {
287
+ const cwd = process.cwd();
288
+ const installedVersion = getInstalledVersion(cwd);
289
+ const noBackup = args.includes('--no-backup');
290
+
291
+ console.log(`\n${colors.bold('AutoWorkflow Update')}\n`);
292
+
293
+ if (!installedVersion) {
294
+ console.log(`${colors.yellow('⚠')} AutoWorkflow not initialized. Run ${colors.cyan('npx autoworkflow init')} instead.\n`);
295
+ return;
296
+ }
297
+
298
+ console.log(`Updating: ${colors.dim(installedVersion)} → ${colors.cyan(PACKAGE_VERSION)}`);
299
+ console.log(`${colors.dim('User files (BLUEPRINT.md, AI_RULES.md) will be preserved')}\n`);
300
+
301
+ // Update core files
302
+ console.log(colors.bold('Updating core files:'));
303
+ FILE_CATEGORIES.core.forEach(({ src, dest, name }) => {
304
+ copyFile(join(packageRoot, src), join(cwd, dest), name, { noBackup });
305
+ });
306
+
307
+ // Update commands and skills
308
+ console.log(`\n${colors.bold('Updating commands/skills:')}`);
309
+ FILE_CATEGORIES.commands.forEach(({ src, dest, name }) => {
310
+ copyFile(join(packageRoot, src), join(cwd, dest), name, { noBackup });
311
+ });
312
+
313
+ // Make hooks executable
314
+ makeHooksExecutable(cwd);
315
+
316
+ // Update version
317
+ setInstalledVersion(cwd, PACKAGE_VERSION);
318
+
319
+ console.log(`\n${colors.green('✓')} ${colors.bold('Updated to v' + PACKAGE_VERSION)}`);
320
+ console.log(`\n${colors.dim('User files preserved. Run')} ${colors.cyan('npx autoworkflow init --force')} ${colors.dim('for full reinstall.')}\n`);
321
+ }
322
+
323
+ // INIT command
324
+ function init(options = {}) {
325
+ const cwd = process.cwd();
326
+ const all = options.all || args.includes('--all');
327
+ const minimal = options.minimal || args.includes('--minimal');
328
+ const force = options.force || args.includes('--force');
329
+ const noBackup = args.includes('--no-backup');
330
+
331
+ const installedVersion = getInstalledVersion(cwd);
332
+
333
+ // IMPORTANT: Check for user BLUEPRINT BEFORE any copying happens
334
+ const userHasBlueprint = hasUserBlueprint(cwd);
335
+ const blueprintPath = join(cwd, 'instructions', 'BLUEPRINT.md');
336
+ let savedBlueprintContent = null;
337
+
338
+ // Save user's BLUEPRINT.md content before any operations
339
+ if (userHasBlueprint) {
340
+ try {
341
+ savedBlueprintContent = readFileSync(blueprintPath, 'utf8');
342
+ } catch (e) {
343
+ // Ignore read errors
344
+ }
345
+ }
346
+
347
+ console.log(`\n${colors.bold('AutoWorkflow Setup')} ${colors.dim(`v${PACKAGE_VERSION}`)}\n`);
348
+ console.log(`Installing to: ${colors.cyan(cwd)}`);
349
+
350
+ if (installedVersion && !force) {
351
+ console.log(`\n${colors.yellow('⚠')} AutoWorkflow v${installedVersion} already installed.`);
352
+ console.log(` Use ${colors.cyan('npx autoworkflow update')} to update core files`);
353
+ console.log(` Use ${colors.cyan('npx autoworkflow init --force')} to reinstall everything\n`);
354
+ return;
355
+ }
356
+
357
+ if (!noBackup) {
358
+ console.log(`${colors.dim('Existing files will be backed up with .backup suffix')}\n`);
359
+ } else {
360
+ console.log(`${colors.yellow('⚠')} ${colors.dim('--no-backup: Existing files will be overwritten')}\n`);
361
+ }
362
+
363
+ // Required files
364
+ console.log(colors.bold('Core files:'));
365
+ copyFile(join(packageRoot, 'CLAUDE.md'), join(cwd, 'CLAUDE.md'), 'CLAUDE.md', { noBackup });
366
+ copyFile(join(packageRoot, 'system'), join(cwd, 'system'), 'system/', { noBackup });
367
+ copyFile(join(packageRoot, '.claude'), join(cwd, '.claude'), '.claude/', { noBackup });
368
+
369
+ // Handle instructions folder - preserve user BLUEPRINT.md
370
+ console.log(`\n${colors.bold('Instructions:')}`);
371
+ const instructionsDir = join(cwd, 'instructions');
372
+ mkdirSync(instructionsDir, { recursive: true });
373
+
374
+ // Copy AI_RULES.md (skip if exists and not forcing)
375
+ const aiRulesPath = join(cwd, 'instructions', 'AI_RULES.md');
376
+ if (!existsSync(aiRulesPath) || force) {
377
+ copyFile(
378
+ join(packageRoot, 'instructions', 'AI_RULES.md'),
379
+ aiRulesPath,
380
+ 'instructions/AI_RULES.md',
381
+ { noBackup }
382
+ );
383
+ } else {
384
+ console.log(` ${colors.dim('○')} instructions/AI_RULES.md ${colors.dim('(exists, preserved)')}`);
385
+ }
386
+
387
+ // BLUEPRINT.md - Restore user content if we saved it
388
+ if (savedBlueprintContent) {
389
+ // Restore the user's BLUEPRINT.md content
390
+ try {
391
+ writeFileSync(blueprintPath, savedBlueprintContent);
392
+ console.log(` ${colors.cyan('★')} instructions/BLUEPRINT.md ${colors.dim('(user content preserved)')}`);
393
+ } catch (e) {
394
+ console.log(` ${colors.red('✗')} Failed to restore BLUEPRINT.md: ${e.message}`);
395
+ }
396
+ } else if (existsSync(blueprintPath)) {
397
+ // Check if current file is a template (package just copied it)
398
+ const currentContent = readFileSync(blueprintPath, 'utf8');
399
+ const isTemplate = currentContent.includes('<!-- TEMPLATE') ||
400
+ currentContent.includes('(To be documented)') ||
401
+ currentContent.includes('_None documented yet_') ||
402
+ currentContent.replace(/^#.*$/gm, '').replace(/\s+/g, ' ').trim().length < 200;
403
+
404
+ if (isTemplate) {
405
+ // Remove template so hook can regenerate project-specific one
406
+ try {
407
+ unlinkSync(blueprintPath);
408
+ console.log(` ${colors.cyan('ℹ')} BLUEPRINT.md template removed (will be auto-generated)`);
409
+ } catch (e) {
410
+ // Ignore
411
+ }
412
+ } else {
413
+ console.log(` ${colors.cyan('★')} instructions/BLUEPRINT.md ${colors.dim('(content preserved)')}`);
414
+ }
415
+ } else {
416
+ console.log(` ${colors.cyan('ℹ')} BLUEPRINT.md will be auto-generated on first run`);
417
+ }
418
+
419
+ // Make Claude hooks executable
420
+ makeHooksExecutable(cwd);
421
+
422
+ if (!minimal) {
423
+ // Recommended files
424
+ console.log(`\n${colors.bold('Scripts:')}`);
425
+ copyFile(join(packageRoot, 'scripts'), join(cwd, 'scripts'), 'scripts/', { noBackup });
426
+ copyFile(join(packageRoot, 'hooks'), join(cwd, 'hooks'), 'hooks/', { noBackup });
427
+ }
428
+
429
+ if (all) {
430
+ // Optional files - skip if exists
431
+ console.log(`\n${colors.bold('Optional files:')}`);
432
+ FILE_CATEGORIES.optional.forEach(({ src, dest, name }) => {
433
+ copyFile(join(packageRoot, src), join(cwd, dest), name, { noBackup, skipIfExists: !force });
434
+ });
435
+ }
436
+
437
+ // Setup git hooks
438
+ if (!minimal) {
439
+ setupGitHooks(cwd);
440
+ }
441
+
442
+ // Save installed version
443
+ setInstalledVersion(cwd, PACKAGE_VERSION);
184
444
 
185
445
  console.log(`\n${colors.green('✓')} ${colors.bold('AutoWorkflow initialized!')}\n`);
186
446
 
@@ -198,12 +458,22 @@ switch (command) {
198
458
  case 'init':
199
459
  init();
200
460
  break;
461
+ case 'update':
462
+ update();
463
+ break;
464
+ case 'status':
465
+ status();
466
+ break;
201
467
  case 'help':
202
468
  case '--help':
203
469
  case '-h':
204
470
  case undefined:
205
471
  printHelp();
206
472
  break;
473
+ case '--version':
474
+ case '-v':
475
+ console.log(PACKAGE_VERSION);
476
+ break;
207
477
  default:
208
478
  console.log(`${colors.red('Unknown command:')} ${command}`);
209
479
  printHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoworkflow",
3
- "version": "3.6.1",
3
+ "version": "3.8.0",
4
4
  "description": "Automated workflow enforcement for Claude Code via hooks and system prompts",
5
5
  "type": "module",
6
6
  "bin": {