claude-sprint-gate 1.2.0 → 1.4.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/bin/ccsg.js CHANGED
@@ -90,18 +90,38 @@ if (uninstall) {
90
90
  process.exit(0);
91
91
  }
92
92
 
93
+ // ── Plugin files to download ───────────────────────────────────────
94
+ const PLUGIN_FILES = [
95
+ { remote: "plugin/hooks/ccsg.sh", local: "hooks/ccsg.sh" },
96
+ { remote: "plugin/hooks/hooks.json", local: "hooks/hooks.json" },
97
+ { remote: "plugin/.claude-plugin/plugin.json", local: ".claude-plugin/plugin.json" },
98
+ { remote: "plugin/commands/sprint.md", local: "commands/sprint.md" },
99
+ { remote: "plugin/commands/sprint-status.md", local: "commands/sprint-status.md" },
100
+ { remote: "plugin/commands/sprint-cancel.md", local: "commands/sprint-cancel.md" },
101
+ { remote: "plugin/commands/help.md", local: "commands/help.md" },
102
+ ];
103
+
93
104
  // ── Install ────────────────────────────────────────────────────────
94
105
  async function install() {
95
106
  console.log("\n\x1b[1mCCSG — Claude Code Sprint Gate\x1b[0m\n");
96
107
 
97
- // 1. Download and install hook
98
- info("Downloading hook script...");
108
+ // 1. Install as plugin
109
+ const pluginDir = path.join(claudeDir, "plugins", "ccsg");
110
+ info("Installing plugin...");
111
+
112
+ for (const file of PLUGIN_FILES) {
113
+ const dest = path.join(pluginDir, file.local);
114
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
115
+ const content = await download(file.remote);
116
+ fs.writeFileSync(dest, content, { mode: 0o755 });
117
+ }
118
+ info(`Plugin installed to ${pluginDir}`);
119
+
120
+ // 2. Also install standalone hook as backup (settings.json)
99
121
  fs.mkdirSync(hooksDir, { recursive: true });
100
122
  const hookContent = await download("ccsg.sh");
101
123
  fs.writeFileSync(hookScript, hookContent, { mode: 0o755 });
102
- info(`Installed ${hookScript}`);
103
124
 
104
- // 2. Settings
105
125
  const hookCommand = isGlobal
106
126
  ? `bash ${hookScript}`
107
127
  : "bash .claude/hooks/ccsg.sh";
@@ -137,7 +157,7 @@ async function install() {
137
157
  }
138
158
  }
139
159
 
140
- // 3. Goal directories (project mode)
160
+ // 3. Goal directories + mission (project mode)
141
161
  if (!isGlobal) {
142
162
  fs.mkdirSync(goalsOpen, { recursive: true });
143
163
  fs.mkdirSync(goalsCompleted, { recursive: true });
@@ -159,7 +179,6 @@ First sprint. Define what needs to be built and verify it works.
159
179
  info("Created starter goal: goals-open/goal-sprint-v1.md");
160
180
  }
161
181
 
162
- // Mission template (always included)
163
182
  const missionDest = path.join(root, "mission.md");
164
183
  if (!fs.existsSync(missionDest)) {
165
184
  const missionContent = await download("mission-template.md");
@@ -173,16 +192,19 @@ First sprint. Define what needs to be built and verify it works.
173
192
  // 4. Summary
174
193
  console.log(`\n\x1b[1m\x1b[32m[CCSG]\x1b[0m Installed.\n`);
175
194
  if (isGlobal) {
195
+ console.log(` Plugin: ${pluginDir}`);
176
196
  console.log(` Hook: ${hookScript}`);
177
197
  console.log(` Settings: ${settingsFile}`);
178
198
  } else {
199
+ console.log(" Plugin: .claude/plugins/ccsg/");
179
200
  console.log(" Hook: .claude/hooks/ccsg.sh");
180
201
  console.log(" Settings: .claude/settings.json");
181
- console.log(" Goals: goals-open/ goals-completed/");
202
+ console.log(" Goals: goals-open/ \u2192 goals-completed/");
182
203
  console.log(" Mission: mission.md (edit to set your product vision)");
183
204
  }
205
+ console.log("\n Commands: /sprint, /sprint-status, /sprint-cancel");
184
206
  console.log(
185
- "\n Edit goals-open/goal-sprint-v1.md, then start Claude Code.\n"
207
+ "\n Edit goals-open/goal-sprint-v1.md, then: claude\n Type /sprint to begin.\n"
186
208
  );
187
209
  }
188
210
 
package/ccsg.sh CHANGED
@@ -41,6 +41,20 @@ GOALS_COMPLETED="$ROOT/goals-completed"
41
41
  # Ensure directories exist
42
42
  mkdir -p "$GOALS_OPEN" "$GOALS_COMPLETED"
43
43
 
44
+ # ── Helper: safe JSON encoding ──────────────────────────────────────
45
+ json_encode() {
46
+ local input="$1"
47
+ if command -v python3 &>/dev/null; then
48
+ printf '%s' "$input" | python3 -c "import sys,json; sys.stdout.write(json.dumps(sys.stdin.read()))"
49
+ elif command -v python &>/dev/null; then
50
+ printf '%s' "$input" | python -c "import sys,json; sys.stdout.write(json.dumps(sys.stdin.read()))"
51
+ elif command -v node &>/dev/null; then
52
+ printf '%s' "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))"
53
+ else
54
+ printf '"%s"' "$(printf '%s' "$input" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ')"
55
+ fi
56
+ }
57
+
44
58
  # ── Mission support ─────────────────────────────────────────────────
45
59
  MISSION_FILE="$ROOT/mission.md"
46
60
 
@@ -101,23 +115,39 @@ build_mission_response() {
101
115
  [ -f "$f" ] && completed_count=$((completed_count + 1))
102
116
  done
103
117
 
104
- # Read mission content for re-prompt
105
118
  local mission_content=""
106
- if [ -f "$MISSION_FILE" ]; then
107
- mission_content=$(cat "$MISSION_FILE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')
108
- fi
119
+ [ -f "$MISSION_FILE" ] && mission_content=$(cat "$MISSION_FILE")
109
120
 
110
121
  log "MISSION — next sprint: ${prefix}-v${next_ver} ($completed_count archived)"
111
122
 
123
+ local reason_text="YOU CANNOT STOP. Sprint verified and complete. Mission is active.
124
+
125
+ YOUR MISSION:
126
+ ---
127
+ ${mission_content}
128
+ ---
129
+
130
+ Your next action: create ${next_file}
131
+
132
+ 1. Read goals-completed/ to see what was delivered in prior sprints (${completed_count} archived files)
133
+ 2. Define the next sprint goals that advance the mission above
134
+ 3. Write them as markdown checkboxes in ${next_file}
135
+ 4. Start executing immediately
136
+
137
+ Do not stop. Do not ask for confirmation. Create the sprint file and start working."
138
+
139
+ local system_msg="CCSG | Mission active | Next: ${next_file} | ${completed_count} sprints archived"
140
+
141
+ local reason_json
142
+ reason_json=$(json_encode "$reason_text")
143
+ local system_json
144
+ system_json=$(json_encode "$system_msg")
145
+
112
146
  cat <<ENDJSON
113
147
  {
114
- "continue": false,
115
- "decision": "next_sprint",
116
- "reason": "YOU CANNOT STOP. Sprint verified and complete. Mission is active.\\n\\nYOUR MISSION:\\n---\\n${mission_content}---\\n\\nYour next action: create ${next_file}\\n\\n1. Read goals-completed/ to see what was delivered in prior sprints (${completed_count} archived files)\\n2. Define the next sprint goals that advance the mission above\\n3. Write them as markdown checkboxes in ${next_file}\\n4. Start executing immediately\\n\\nDo not stop. Do not ask for confirmation. Create the sprint file and start working.",
117
- "next_sprint_file": "${next_file}",
118
- "next_version": ${next_ver},
119
- "mission_file": "mission.md",
120
- "completed_sprint_count": ${completed_count}
148
+ "decision": "block",
149
+ "reason": ${reason_json},
150
+ "systemMessage": ${system_json}
121
151
  }
122
152
  ENDJSON
123
153
  }
@@ -152,65 +182,44 @@ TOTAL=$((UNCHECKED + CHECKED))
152
182
 
153
183
  log "checked=$CHECKED unchecked=$UNCHECKED total=$TOTAL"
154
184
 
155
- # ── Helper: build JSON for unmet items ───────────────────────────────
185
+ # ── Helper: build block response (Ralph Wiggum pattern) ─────────────
156
186
  build_block_response() {
157
187
  local source_file="$1"
158
- local extra_msg="$2"
159
-
160
- local unmet_lines
161
- unmet_lines=$(grep '^\- \[ \]' "$source_file" | sed 's/^\- \[ \] //')
162
-
163
- # JSON array of unmet items
164
- local unmet_json="["
165
- local first=true
166
- while IFS= read -r line; do
167
- local escaped
168
- escaped=$(printf '%s' "$line" | sed 's/\\/\\\\/g; s/"/\\"/g')
169
- if [ "$first" = true ]; then
170
- unmet_json="${unmet_json}\"${escaped}\""
171
- first=false
172
- else
173
- unmet_json="${unmet_json},\"${escaped}\""
174
- fi
175
- done <<< "$unmet_lines"
176
- unmet_json="${unmet_json}]"
177
-
178
- # Human-readable reason
179
- local summary="${UNCHECKED} of ${TOTAL} criteria in $(basename "$source_file") are not met:"
180
- while IFS= read -r line; do
181
- local escaped_line
182
- escaped_line=$(printf '%s' "$line" | sed 's/\\/\\\\/g; s/"/\\"/g')
183
- summary="${summary}\\n - ${escaped_line}"
184
- done <<< "$unmet_lines"
185
-
186
- if [ -n "$extra_msg" ]; then
187
- local escaped_extra
188
- escaped_extra=$(printf '%s' "$extra_msg" | sed 's/\\/\\\\/g; s/"/\\"/g')
189
- summary="${summary}\\n\\n${escaped_extra}"
190
- fi
191
-
192
- summary="${summary}\\nKeep working."
188
+ local reason_text="$2"
189
+ local system_msg="CCSG | ${CHECKED} of ${TOTAL} done | Goal: $(basename "$source_file")"
193
190
 
194
191
  log "BLOCKING — ${UNCHECKED} of ${TOTAL} not met in $(basename "$source_file")"
195
192
 
193
+ local reason_json
194
+ reason_json=$(json_encode "$reason_text")
195
+
196
+ local system_json
197
+ system_json=$(json_encode "$system_msg")
198
+
196
199
  cat <<ENDJSON
197
200
  {
198
- "continue": false,
199
201
  "decision": "block",
200
- "reason": "${summary}",
201
- "unmet_criteria": ${unmet_json},
202
- "progress": "${CHECKED} of ${TOTAL} done",
203
- "goal_file": "$(basename "$source_file")"
202
+ "reason": ${reason_json},
203
+ "systemMessage": ${system_json}
204
204
  }
205
205
  ENDJSON
206
206
  }
207
207
 
208
208
  # ── BLOCK: unchecked items remain ────────────────────────────────────
209
209
  if [ "$UNCHECKED" -gt 0 ]; then
210
- # Re-prompt: inject the full goal file so the agent gets the complete task back
211
- GOAL_CONTENT=$(cat "$GOAL_FILE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')
210
+ GOAL_CONTENT=$(cat "$GOAL_FILE")
211
+
212
+ REPROMPT="YOU CANNOT STOP. ${UNCHECKED} of ${TOTAL} items in $(basename "$GOAL_FILE") are not done.
212
213
 
213
- REPROMPT="YOU CANNOT STOP. ${UNCHECKED} of ${TOTAL} items in $(basename "$GOAL_FILE") are not done.\\n\\nRe-read the goal file and continue executing. Here is your active task:\\n\\n---\\n${GOAL_CONTENT}---\\n\\nWork through each unchecked item (- [ ]) in order. Check each one off (- [x]) as you complete it. Do not stop until every box is checked.\\n\\nWORK ETHIC: Compilation is not verification. Test each thing the way its end user would use it. A UI — open it in a browser, click every button, fill every form. An API — call it with real requests, check the actual responses. A CLI — run the commands, read the output. A pipeline — feed it real data, verify what comes out. Whatever it is, use it the way a customer would, not the way the developer who wrote it imagines it works. If you cannot show real output proving it works end-to-end, it is not done. Never trade correctness for speed."
214
+ Re-read the goal file and continue executing. Here is your active task:
215
+
216
+ ---
217
+ ${GOAL_CONTENT}
218
+ ---
219
+
220
+ Work through each unchecked item (- [ ]) in order. Check each one off (- [x]) as you complete it. Do not stop until every box is checked.
221
+
222
+ WORK ETHIC: Compilation is not verification. Test each thing the way its end user would use it. A UI — open it in a browser, click every button, fill every form. An API — call it with real requests, check the actual responses. A CLI — run the commands, read the output. A pipeline — feed it real data, verify what comes out. Whatever it is, use it the way a customer would, not the way the developer who wrote it imagines it works. If you cannot show real output proving it works end-to-end, it is not done. Never trade correctness for speed."
214
223
 
215
224
  build_block_response "$GOAL_FILE" "$REPROMPT"
216
225
  exit 0
@@ -346,7 +355,13 @@ TOTAL=$UNCHECKED
346
355
  # Block with the new verification sprint
347
356
  GOAL_FILE="$VERIFY_FILE"
348
357
  # Re-prompt with full verification sprint content
349
- VERIFY_CONTENT=$(cat "$VERIFY_FILE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')
350
- VERIFY_REPROMPT="You just finished a sprint. But finishing code is not finishing the product. A verification sprint has been created. YOUR TASK NOW:\\n\\n---\\n${VERIFY_CONTENT}---\\n\\nWork through each unchecked item. Do not stop until every box is checked."
358
+ VERIFY_CONTENT=$(cat "$VERIFY_FILE")
359
+ VERIFY_REPROMPT="You just finished a sprint. But finishing code is not finishing the product. A verification sprint has been created. YOUR TASK NOW:
360
+
361
+ ---
362
+ ${VERIFY_CONTENT}
363
+ ---
364
+
365
+ Work through each unchecked item. Do not stop until every box is checked."
351
366
  build_block_response "$VERIFY_FILE" "$VERIFY_REPROMPT"
352
367
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-sprint-gate",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Claude Code Sprint Gate — sprint lifecycle manager with verification gates",
5
5
  "bin": {
6
6
  "ccsg": "bin/ccsg.js"
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "ccsg",
3
+ "version": "1.3.0",
4
+ "description": "Claude Code Sprint Gate — sprint lifecycle manager. Prevents stop until goals are met, auto-generates verification sprints, supports multi-sprint missions.",
5
+ "author": {
6
+ "name": "panbergco",
7
+ "url": "https://github.com/panbergco/claude-code-sprint-gate"
8
+ }
9
+ }
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: "Explain CCSG and available commands"
3
+ ---
4
+
5
+ # CCSG — Claude Code Sprint Gate
6
+
7
+ Please explain the following to the user:
8
+
9
+ ## What is CCSG?
10
+
11
+ CCSG is a sprint lifecycle manager for Claude Code. It prevents the agent from stopping until all acceptance criteria in a goal file are met. When a sprint is complete, it auto-generates a verification sprint that forces end-to-end testing before the work is considered done.
12
+
13
+ ## How it works
14
+
15
+ 1. Goal files in `goals-open/` define sprint items as checkboxes (`- [ ]`)
16
+ 2. The stop hook fires every time the agent tries to stop
17
+ 3. If unchecked items remain, the hook blocks the stop and re-prompts with the full goal
18
+ 4. When all items are checked, a verification sprint is auto-generated
19
+ 5. The agent must pass verification before the sprint is archived
20
+ 6. If `mission.md` exists, a new sprint is prompted after verification
21
+
22
+ ## Available Commands
23
+
24
+ ### /sprint
25
+ Start working on the current goal file. The agent reads the goal, executes each item, and cannot stop until all are checked off.
26
+
27
+ ### /sprint-status
28
+ Show current progress — active goal, items done, mission state, archive count.
29
+
30
+ ### /sprint-cancel
31
+ Emergency stop — archives the current goal so the agent can exit. Work is preserved, not deleted.
32
+
33
+ ## Key Files
34
+
35
+ - `goals-open/goal-*.md` — Active sprint with checkbox items
36
+ - `goals-completed/` — Archive of finished sprints
37
+ - `mission.md` — Optional. Drives multi-sprint campaigns with a product vision.
38
+ - `stop-condition.md` — Simple mode fallback (single file, no verification)
39
+
40
+ ## Mission Mode
41
+
42
+ When `mission.md` exists, completing a verification sprint does not allow stop. Instead, CCSG prompts the agent to create the next sprint based on the mission objectives. Add `Sprints: N` to cap the campaign.
43
+
44
+ ## Learn More
45
+
46
+ Repository: https://github.com/panbergco/claude-code-sprint-gate
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: "Emergency stop — remove all goal files to allow exit"
3
+ allowed-tools: ["Bash", "Read", "Glob"]
4
+ ---
5
+
6
+ # Sprint Cancel
7
+
8
+ Emergency break — allows the agent to stop by clearing the active sprint.
9
+
10
+ 1. Check if there's an active goal in `goals-open/`
11
+ 2. If yes, show the file name and current progress (checked/unchecked count)
12
+ 3. Move it to `goals-completed/` (archive, don't delete)
13
+ 4. Report: "Sprint cancelled. Goal archived to goals-completed/. You can now stop."
14
+
15
+ Do NOT delete goal files — always move to goals-completed/ so work is preserved.
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: "Show current sprint progress"
3
+ allowed-tools: ["Bash", "Read", "Glob"]
4
+ ---
5
+
6
+ # Sprint Status
7
+
8
+ Check the current sprint state and report:
9
+
10
+ 1. Run: `ls goals-open/goal-*.md 2>/dev/null` to find the active goal
11
+ 2. If a goal file exists, read it and count:
12
+ - Checked items: `grep -c '^\- \[x\]' <file>`
13
+ - Unchecked items: `grep -c '^\- \[ \]' <file>`
14
+ 3. Check if `mission.md` exists
15
+ 4. Count files in `goals-completed/`
16
+
17
+ Report a concise summary:
18
+ - Active sprint file name and title
19
+ - Progress: N of M items done
20
+ - Mission: active (with sprint cap if set) or inactive
21
+ - Completed sprints in archive
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: "Start working on the current sprint"
3
+ allowed-tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"]
4
+ hide-from-slash-command-tool: "true"
5
+ ---
6
+
7
+ # Sprint — Start Working
8
+
9
+ You are now in sprint mode. The stop hook will prevent you from stopping until all items are done.
10
+
11
+ ## Your task
12
+
13
+ 1. Check `goals-open/` for the active goal file (the most recent `goal-*.md`)
14
+ 2. If a `mission.md` exists, read it — that is your persona and objective
15
+ 3. Read the goal file completely
16
+ 4. Work through every unchecked item (`- [ ]`) in order
17
+ 5. As you complete each item, check it off (`- [x]`) in the goal file
18
+ 6. When all items are checked, the stop hook will handle what comes next:
19
+ - If it's a regular sprint: a verification sprint is auto-generated
20
+ - If it's a verification sprint: the sprint is archived
21
+ - If a mission is active: the next sprint is prompted
22
+
23
+ ## Rules
24
+
25
+ - Do NOT stop or pause to ask for confirmation. Execute continuously.
26
+ - Do NOT skip items. Work through them in order.
27
+ - Test each deliverable the way its end user would use it — compilation is not verification.
28
+ - If you get stuck on an item, try a different approach. Do not abandon it.
29
+ - Check items off (`- [x]`) only AFTER you have completed and verified them.
30
+ - When you try to stop, the hook will re-prompt you if work remains. This is by design.
31
+
32
+ ## Begin
33
+
34
+ Read the active goal file now and start executing.
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # CCSG — Claude Code Sprint Gate
4
+ #
5
+ # Sprint lifecycle manager for Claude Code. Prevents the agent from
6
+ # stopping until all acceptance criteria are met. When a sprint is
7
+ # complete, auto-generates a verification sprint that forces end-to-end
8
+ # testing of the full deployment before allowing stop.
9
+ #
10
+ # Directory structure:
11
+ # goals-open/ — Active sprint (one goal file at a time)
12
+ # goals-completed/ — Archive of finished sprints
13
+ #
14
+ # Flow:
15
+ # 1. Agent works through items in the active goal file
16
+ # 2. Tries to stop → CCSG checks for unchecked items → blocks if any remain
17
+ # 3. All items checked on a regular sprint →
18
+ # CCSG creates a verification sprint, moves original to completed, blocks
19
+ # 4. All items checked on a verification sprint →
20
+ # CCSG moves it to completed, allows stop
21
+ #
22
+ # Mission mode (mission.md):
23
+ # When mission.md exists, completing a verification sprint does NOT allow
24
+ # stop. Instead, CCSG blocks and instructs the agent to create the next
25
+ # sprint — reading mission.md for persona/objectives and goals-completed/
26
+ # for history. The cycle repeats until the sprint cap is reached or
27
+ # mission.md is removed.
28
+ #
29
+ # Fallback: if no goal files exist, checks stop-condition.md (backward compat)
30
+ #
31
+
32
+ LOG_FILE="/tmp/ccsg.log"
33
+ log() { echo "$(date): $1" >> "$LOG_FILE"; }
34
+ log "stop hook fired"
35
+
36
+ # Find project root
37
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
38
+ GOALS_OPEN="$ROOT/goals-open"
39
+ GOALS_COMPLETED="$ROOT/goals-completed"
40
+
41
+ # Ensure directories exist
42
+ mkdir -p "$GOALS_OPEN" "$GOALS_COMPLETED"
43
+
44
+ # ── Helper: safe JSON encoding ──────────────────────────────────────
45
+ json_encode() {
46
+ local input="$1"
47
+ if command -v python3 &>/dev/null; then
48
+ printf '%s' "$input" | python3 -c "import sys,json; sys.stdout.write(json.dumps(sys.stdin.read()))"
49
+ elif command -v python &>/dev/null; then
50
+ printf '%s' "$input" | python -c "import sys,json; sys.stdout.write(json.dumps(sys.stdin.read()))"
51
+ elif command -v node &>/dev/null; then
52
+ printf '%s' "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))"
53
+ else
54
+ printf '"%s"' "$(printf '%s' "$input" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ')"
55
+ fi
56
+ }
57
+
58
+ # ── Mission support ─────────────────────────────────────────────────
59
+ MISSION_FILE="$ROOT/mission.md"
60
+
61
+ mission_active() {
62
+ [ ! -f "$MISSION_FILE" ] && return 1
63
+
64
+ # Check sprint cap if specified (line like "Sprints: 20")
65
+ local max_sprints
66
+ max_sprints=$(grep -iE '^Sprints:\s*[0-9]+' "$MISSION_FILE" 2>/dev/null | head -1 | grep -o '[0-9]*')
67
+ if [ -n "$max_sprints" ]; then
68
+ local completed=0
69
+ for f in "$GOALS_COMPLETED"/goal-*.md; do
70
+ [ -f "$f" ] || continue
71
+ [[ "$(basename "$f")" == *"-verify"* ]] && continue
72
+ completed=$((completed + 1))
73
+ done
74
+ if [ "$completed" -ge "$max_sprints" ]; then
75
+ log "MISSION — sprint cap reached ($completed/$max_sprints)"
76
+ return 1
77
+ fi
78
+ fi
79
+
80
+ return 0
81
+ }
82
+
83
+ next_sprint_info() {
84
+ # Returns "prefix|next_version" — inferred from completed sprints
85
+ local highest=0
86
+ local prefix=""
87
+
88
+ for f in "$GOALS_COMPLETED"/goal-*.md; do
89
+ [ -f "$f" ] || continue
90
+ local bn
91
+ bn=$(basename "$f" .md)
92
+ [[ "$bn" == *"-verify" ]] && continue
93
+
94
+ local ver
95
+ ver=$(echo "$bn" | grep -oE 'v[0-9]+' | tail -1 | tr -d 'v')
96
+ if [ -n "$ver" ] && [ "$ver" -gt "$highest" ]; then
97
+ highest=$ver
98
+ prefix=$(echo "$bn" | sed "s/-v${ver}$//")
99
+ fi
100
+ done
101
+
102
+ [ -z "$prefix" ] && prefix="goal-sprint"
103
+ echo "${prefix}|$((highest + 1))"
104
+ }
105
+
106
+ build_mission_response() {
107
+ local info
108
+ info=$(next_sprint_info)
109
+ local prefix="${info%%|*}"
110
+ local next_ver="${info##*|}"
111
+ local next_file="goals-open/${prefix}-v${next_ver}.md"
112
+
113
+ local completed_count=0
114
+ for f in "$GOALS_COMPLETED"/goal-*.md; do
115
+ [ -f "$f" ] && completed_count=$((completed_count + 1))
116
+ done
117
+
118
+ local mission_content=""
119
+ [ -f "$MISSION_FILE" ] && mission_content=$(cat "$MISSION_FILE")
120
+
121
+ log "MISSION — next sprint: ${prefix}-v${next_ver} ($completed_count archived)"
122
+
123
+ local reason_text="YOU CANNOT STOP. Sprint verified and complete. Mission is active.
124
+
125
+ YOUR MISSION:
126
+ ---
127
+ ${mission_content}
128
+ ---
129
+
130
+ Your next action: create ${next_file}
131
+
132
+ 1. Read goals-completed/ to see what was delivered in prior sprints (${completed_count} archived files)
133
+ 2. Define the next sprint goals that advance the mission above
134
+ 3. Write them as markdown checkboxes in ${next_file}
135
+ 4. Start executing immediately
136
+
137
+ Do not stop. Do not ask for confirmation. Create the sprint file and start working."
138
+
139
+ local system_msg="CCSG | Mission active | Next: ${next_file} | ${completed_count} sprints archived"
140
+
141
+ local reason_json
142
+ reason_json=$(json_encode "$reason_text")
143
+ local system_json
144
+ system_json=$(json_encode "$system_msg")
145
+
146
+ cat <<ENDJSON
147
+ {
148
+ "decision": "block",
149
+ "reason": ${reason_json},
150
+ "systemMessage": ${system_json}
151
+ }
152
+ ENDJSON
153
+ }
154
+
155
+ # ── Locate the active goal file ──────────────────────────────────────
156
+ GOAL_FILE=$(ls -t "$GOALS_OPEN"/goal-*.md 2>/dev/null | head -1)
157
+ IS_GOALS_SYSTEM=false
158
+
159
+ if [ -n "$GOAL_FILE" ]; then
160
+ IS_GOALS_SYSTEM=true
161
+ log "active goal: $(basename "$GOAL_FILE")"
162
+ else
163
+ # Fallback to stop-condition.md
164
+ GOAL_FILE="$ROOT/stop-condition.md"
165
+ if [ ! -f "$GOAL_FILE" ]; then
166
+ # Mission mode: if mission.md exists, prompt next sprint even with empty goals-open/
167
+ if mission_active; then
168
+ build_mission_response
169
+ exit 0
170
+ fi
171
+ log "no active goal or stop-condition.md found, allowing stop"
172
+ echo '{"continue": true}'
173
+ exit 0
174
+ fi
175
+ log "fallback to stop-condition.md"
176
+ fi
177
+
178
+ # ── Count checked / unchecked ────────────────────────────────────────
179
+ UNCHECKED=$(grep -c '^\- \[ \]' "$GOAL_FILE" 2>/dev/null) || UNCHECKED=0
180
+ CHECKED=$(grep -c '^\- \[x\]' "$GOAL_FILE" 2>/dev/null) || CHECKED=0
181
+ TOTAL=$((UNCHECKED + CHECKED))
182
+
183
+ log "checked=$CHECKED unchecked=$UNCHECKED total=$TOTAL"
184
+
185
+ # ── Helper: build block response (Ralph Wiggum pattern) ─────────────
186
+ build_block_response() {
187
+ local source_file="$1"
188
+ local reason_text="$2"
189
+ local system_msg="CCSG | ${CHECKED} of ${TOTAL} done | Goal: $(basename "$source_file")"
190
+
191
+ log "BLOCKING — ${UNCHECKED} of ${TOTAL} not met in $(basename "$source_file")"
192
+
193
+ local reason_json
194
+ reason_json=$(json_encode "$reason_text")
195
+
196
+ local system_json
197
+ system_json=$(json_encode "$system_msg")
198
+
199
+ cat <<ENDJSON
200
+ {
201
+ "decision": "block",
202
+ "reason": ${reason_json},
203
+ "systemMessage": ${system_json}
204
+ }
205
+ ENDJSON
206
+ }
207
+
208
+ # ── BLOCK: unchecked items remain ────────────────────────────────────
209
+ if [ "$UNCHECKED" -gt 0 ]; then
210
+ GOAL_CONTENT=$(cat "$GOAL_FILE")
211
+
212
+ REPROMPT="YOU CANNOT STOP. ${UNCHECKED} of ${TOTAL} items in $(basename "$GOAL_FILE") are not done.
213
+
214
+ Re-read the goal file and continue executing. Here is your active task:
215
+
216
+ ---
217
+ ${GOAL_CONTENT}
218
+ ---
219
+
220
+ Work through each unchecked item (- [ ]) in order. Check each one off (- [x]) as you complete it. Do not stop until every box is checked.
221
+
222
+ WORK ETHIC: Compilation is not verification. Test each thing the way its end user would use it. A UI — open it in a browser, click every button, fill every form. An API — call it with real requests, check the actual responses. A CLI — run the commands, read the output. A pipeline — feed it real data, verify what comes out. Whatever it is, use it the way a customer would, not the way the developer who wrote it imagines it works. If you cannot show real output proving it works end-to-end, it is not done. Never trade correctness for speed."
223
+
224
+ build_block_response "$GOAL_FILE" "$REPROMPT"
225
+ exit 0
226
+ fi
227
+
228
+ # ── ALL ITEMS CHECKED (or no checkboxes at all) ─────────────────────
229
+
230
+ # If no checkboxes exist, there's nothing to enforce
231
+ if [ "$TOTAL" -eq 0 ]; then
232
+ log "no checkboxes found in $(basename "$GOAL_FILE"), allowing stop"
233
+ echo '{"continue": true}'
234
+ exit 0
235
+ fi
236
+
237
+ # If using standalone stop-condition.md (no goals system), allow stop
238
+ if [ "$IS_GOALS_SYSTEM" = false ]; then
239
+ log "all criteria in stop-condition.md met, allowing stop"
240
+ echo '{"continue": true}'
241
+ exit 0
242
+ fi
243
+
244
+ GOAL_BASENAME=$(basename "$GOAL_FILE")
245
+
246
+ # ── Verification sprint complete → finish ────────────────────────────
247
+ if [[ "$GOAL_BASENAME" == *"-verify"* ]]; then
248
+ mv "$GOAL_FILE" "$GOALS_COMPLETED/"
249
+ log "DONE — verification sprint $GOAL_BASENAME complete, moved to goals-completed/"
250
+
251
+ # Mission mode: don't stop — prompt the next sprint
252
+ if mission_active; then
253
+ build_mission_response
254
+ exit 0
255
+ fi
256
+
257
+ echo '{"continue": true}'
258
+ exit 0
259
+ fi
260
+
261
+ # ── Regular sprint complete → generate verification sprint ───────────
262
+ log "sprint $GOAL_BASENAME complete — generating verification sprint"
263
+
264
+ # Extract sprint info
265
+ SPRINT_TITLE=$(grep -m1 '^# ' "$GOAL_FILE" | sed 's/^# //')
266
+ SPRINT_NAME="${GOAL_BASENAME%.md}"
267
+ VERIFY_FILE="$GOALS_OPEN/${SPRINT_NAME}-verify.md"
268
+
269
+ # Collect what was delivered (checked items)
270
+ DELIVERED=$(grep '^\- \[x\]' "$GOAL_FILE" | sed 's/^\- \[x\] //')
271
+
272
+ # Build per-item verification lines
273
+ VERIFY_ITEMS=""
274
+ while IFS= read -r item; do
275
+ [ -n "$item" ] && VERIFY_ITEMS="${VERIFY_ITEMS}
276
+ - [ ] VERIFY: ${item}"
277
+ done <<< "$DELIVERED"
278
+
279
+ # Write the verification sprint
280
+ cat > "$VERIFY_FILE" <<SPRINT
281
+ # Verification — ${SPRINT_TITLE}
282
+
283
+ Sprint \`${SPRINT_NAME}\` is complete. Every checkbox was checked.
284
+ But checked boxes are claims, not proof. This verification sprint
285
+ exists because "it should work" is not the same as "it works."
286
+
287
+ Your job now is to prove — through actual testing — that everything
288
+ delivered in the previous sprint is real, working, and ready for a
289
+ real user to depend on.
290
+
291
+ ## Work Ethic
292
+
293
+ Compilation is not verification. Test each thing the way its end user would use it:
294
+
295
+ - **A UI** — open it in a browser, click every button, fill every form, walk every path
296
+ - **An API** — call every endpoint with real requests, check the actual responses
297
+ - **A CLI** — run the commands with real arguments, read the real output
298
+ - **A pipeline** — feed it real data, verify what comes out the other end
299
+
300
+ Whatever it is, use it the way a customer would — not the way the developer who
301
+ wrote it imagines it works. If you cannot show real output proving it works
302
+ end-to-end, it is not done. Never trade correctness for speed. One properly
303
+ verified feature is worth more than ten that "should work."
304
+
305
+ ## Build Verification
306
+
307
+ - [ ] Delete all cached/compiled artifacts and rebuild from a clean state
308
+ - [ ] All dependencies resolve cleanly — no warnings, no version conflicts
309
+ - [ ] Build completes without warnings that could indicate runtime issues
310
+
311
+ ## Full Test Suite
312
+
313
+ - [ ] Run the COMPLETE test suite — every test file, not just the new ones
314
+ - [ ] Zero failures, zero skipped tests without documented justification
315
+ - [ ] If there are integration tests, run those too — they catch what unit tests miss
316
+
317
+ ## End-to-End Feature Verification
318
+
319
+ Walk through every feature delivered in the sprint as a real user would.
320
+ Do not just call functions — use the actual UI/CLI/API entry points.
321
+ ${VERIFY_ITEMS}
322
+
323
+ ## Regression Testing
324
+
325
+ - [ ] Features from prior sprints still work — spot check at least 3
326
+ - [ ] No existing functionality broken by the new changes
327
+ - [ ] Trigger error cases intentionally and verify they are handled gracefully
328
+ - [ ] Check edge cases: empty inputs, missing data, concurrent access, large payloads
329
+
330
+ ## Deployment Readiness
331
+
332
+ - [ ] No hardcoded development/debug values in production code
333
+ - [ ] No temporary code, TODO hacks, or commented-out blocks left behind
334
+ - [ ] No debug logging (console.log, print, debugger) left in source
335
+ - [ ] Environment configuration is correct and complete
336
+ - [ ] Dependencies are locked — no floating versions that could break tomorrow
337
+ - [ ] No new security vulnerabilities introduced (review dependency changes)
338
+
339
+ ## Final Verdict
340
+
341
+ - [ ] The product works. Not "should work." Works. You tested it yourself and saw it work.
342
+ SPRINT
343
+
344
+ log "created verification sprint: $(basename "$VERIFY_FILE")"
345
+
346
+ # Move the completed sprint to archive
347
+ mv "$GOAL_FILE" "$GOALS_COMPLETED/"
348
+ log "moved $GOAL_BASENAME to goals-completed/"
349
+
350
+ # Re-count for the new verification sprint
351
+ UNCHECKED=$(grep -c '^\- \[ \]' "$VERIFY_FILE" 2>/dev/null) || UNCHECKED=0
352
+ CHECKED=0
353
+ TOTAL=$UNCHECKED
354
+
355
+ # Block with the new verification sprint
356
+ GOAL_FILE="$VERIFY_FILE"
357
+ # Re-prompt with full verification sprint content
358
+ VERIFY_CONTENT=$(cat "$VERIFY_FILE")
359
+ VERIFY_REPROMPT="You just finished a sprint. But finishing code is not finishing the product. A verification sprint has been created. YOUR TASK NOW:
360
+
361
+ ---
362
+ ${VERIFY_CONTENT}
363
+ ---
364
+
365
+ Work through each unchecked item. Do not stop until every box is checked."
366
+ build_block_response "$VERIFY_FILE" "$VERIFY_REPROMPT"
367
+ exit 0
@@ -0,0 +1,15 @@
1
+ {
2
+ "description": "CCSG stop hook — sprint gate with re-prompting",
3
+ "hooks": {
4
+ "Stop": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/ccsg.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
package/test-ccsg.sh CHANGED
@@ -177,9 +177,9 @@ EOF
177
177
 
178
178
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
179
179
 
180
- assert_contains "blocks stop" '"continue": false' "$OUTPUT"
181
- assert_contains "lists unmet items" '"First task"' "$OUTPUT"
182
- assert_contains "has progress" '"progress"' "$OUTPUT"
180
+ assert_contains "blocks stop" '"decision": "block"' "$OUTPUT"
181
+ assert_contains "lists unmet items" 'First task' "$OUTPUT"
182
+ assert_contains "has systemMessage" '"systemMessage"' "$OUTPUT"
183
183
  assert_contains "includes work ethic" 'way its end user would use it' "$OUTPUT"
184
184
  assert_json_valid "valid JSON" "$OUTPUT"
185
185
 
@@ -220,12 +220,12 @@ EOF
220
220
 
221
221
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
222
222
 
223
- assert_contains "blocks stop" '"continue": false' "$OUTPUT"
223
+ assert_contains "blocks stop" '"decision": "block"' "$OUTPUT"
224
224
  assert_contains "reports count" '2 of 3' "$OUTPUT"
225
- assert_contains "lists unmet item" '"Not done yet"' "$OUTPUT"
226
- assert_contains "lists other unmet item" '"Also not done"' "$OUTPUT"
227
- assert_contains "shows progress" '1 of 3 done' "$OUTPUT"
228
- assert_contains "shows goal file name" '"goal_file"' "$OUTPUT"
225
+ assert_contains "lists unmet item" 'Not done yet' "$OUTPUT"
226
+ assert_contains "lists other unmet item" 'Also not done' "$OUTPUT"
227
+ assert_contains "shows progress in systemMessage" '1 of 3 done' "$OUTPUT"
228
+ assert_contains "has systemMessage with goal name" 'goal-test-v1' "$OUTPUT"
229
229
  assert_contains "work ethic reminder" 'WORK ETHIC' "$OUTPUT"
230
230
  assert_json_valid "valid JSON" "$OUTPUT"
231
231
 
@@ -242,7 +242,7 @@ EOF
242
242
 
243
243
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
244
244
 
245
- assert_contains "blocks stop" '"continue": false' "$OUTPUT"
245
+ assert_contains "blocks stop" '"decision": "block"' "$OUTPUT"
246
246
  assert_json_valid "valid JSON despite special chars" "$OUTPUT"
247
247
 
248
248
  # ═══════════════════════════════════════════════════════════════════════
@@ -268,7 +268,7 @@ EOF
268
268
 
269
269
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
270
270
 
271
- assert_contains "blocks stop (verification needed)" '"continue": false' "$OUTPUT"
271
+ assert_contains "blocks stop (verification needed)" '"decision": "block"' "$OUTPUT"
272
272
  assert_file_exists "verification sprint created" "$GOALS_OPEN/goal-feature-v5-verify.md"
273
273
  assert_file_not_exists "original moved out of goals-open" "$GOALS_OPEN/goal-feature-v5.md"
274
274
  assert_file_exists "original moved to goals-completed" "$GOALS_COMPLETED/goal-feature-v5.md"
@@ -325,7 +325,7 @@ echo "=== Test 9: Verification sprint blocks when incomplete ==="
325
325
  # The verification sprint from test 6 should still be in goals-open
326
326
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
327
327
 
328
- assert_contains "blocks on incomplete verification" '"continue": false' "$OUTPUT"
328
+ assert_contains "blocks on incomplete verification" '"decision": "block"' "$OUTPUT"
329
329
  assert_contains "references verification file" 'verify' "$OUTPUT"
330
330
  assert_contains "includes work ethic" 'way its end user would use it' "$OUTPUT"
331
331
 
@@ -373,14 +373,14 @@ EOF
373
373
 
374
374
  # Step 2: Agent tries to stop → should block
375
375
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
376
- assert_contains "step 1: blocks on incomplete sprint" '"continue": false' "$OUTPUT"
376
+ assert_contains "step 1: blocks on incomplete sprint" '"decision": "block"' "$OUTPUT"
377
377
 
378
378
  # Step 3: Agent completes the work
379
379
  sed -i 's/^\- \[ \]/- [x]/' "$GOALS_OPEN/goal-widget-v10.md"
380
380
 
381
381
  # Step 4: Agent tries to stop → should block (creates verification)
382
382
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
383
- assert_contains "step 2: blocks for verification" '"continue": false' "$OUTPUT"
383
+ assert_contains "step 2: blocks for verification" '"decision": "block"' "$OUTPUT"
384
384
  assert_file_exists "step 2: verification sprint exists" "$GOALS_OPEN/goal-widget-v10-verify.md"
385
385
  assert_file_exists "step 2: original archived" "$GOALS_COMPLETED/goal-widget-v10.md"
386
386
 
@@ -443,7 +443,7 @@ EOF
443
443
 
444
444
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
445
445
 
446
- assert_contains "blocks on goal file, ignoring stop-condition.md" '"continue": false' "$OUTPUT"
446
+ assert_contains "blocks on goal file, ignoring stop-condition.md" '"decision": "block"' "$OUTPUT"
447
447
  assert_contains "references goal file" 'goal-priority-v1' "$OUTPUT"
448
448
 
449
449
  # ---------- Test 15: Log file records lifecycle events ----------
@@ -496,10 +496,10 @@ EOF
496
496
 
497
497
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
498
498
 
499
- assert_contains "blocks stop (mission active)" '"continue": false' "$OUTPUT"
500
- assert_contains "decision is next_sprint" '"next_sprint"' "$OUTPUT"
499
+ assert_contains "blocks stop (mission active)" '"decision": "block"' "$OUTPUT"
500
+ assert_contains "blocks for next sprint" '"decision": "block"' "$OUTPUT"
501
501
  assert_contains "suggests v2" 'v2' "$OUTPUT"
502
- assert_contains "references mission.md" 'mission.md' "$OUTPUT"
502
+ assert_contains "references mission" 'Mission active' "$OUTPUT"
503
503
  assert_contains "tells agent to keep working" 'Do not stop' "$OUTPUT"
504
504
  assert_file_exists "verify moved to completed" "$GOALS_COMPLETED/goal-product-v1-verify.md"
505
505
  assert_file_not_exists "verify removed from open" "$GOALS_OPEN/goal-product-v1-verify.md"
@@ -520,8 +520,8 @@ cp /dev/null "$GOALS_COMPLETED/goal-widget-v3.md"
520
520
 
521
521
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
522
522
 
523
- assert_contains "blocks stop" '"continue": false' "$OUTPUT"
524
- assert_contains "decision is next_sprint" '"next_sprint"' "$OUTPUT"
523
+ assert_contains "blocks stop" '"decision": "block"' "$OUTPUT"
524
+ assert_contains "blocks for next sprint" '"decision": "block"' "$OUTPUT"
525
525
  assert_contains "increments to v4" 'v4' "$OUTPUT"
526
526
  assert_json_valid "valid JSON" "$OUTPUT"
527
527
 
@@ -589,13 +589,13 @@ EOF
589
589
 
590
590
  # Complete sprint → creates verification
591
591
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
592
- assert_contains "lc step 1: blocks for verification" '"continue": false' "$OUTPUT"
592
+ assert_contains "lc step 1: blocks for verification" '"decision": "block"' "$OUTPUT"
593
593
  assert_file_exists "lc step 1: verification created" "$GOALS_OPEN/goal-lifecycle-v1-verify.md"
594
594
 
595
595
  # Complete verification → mission prompts sprint 2
596
596
  sed -i 's/^\- \[ \]/- [x]/' "$GOALS_OPEN/goal-lifecycle-v1-verify.md"
597
597
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
598
- assert_contains "lc step 2: mission prompts next sprint" '"next_sprint"' "$OUTPUT"
598
+ assert_contains "lc step 2: mission blocks for next sprint" 'Mission active' "$OUTPUT"
599
599
  assert_contains "lc step 2: suggests v2" 'v2' "$OUTPUT"
600
600
 
601
601
  # Sprint 2: create and complete
@@ -605,7 +605,7 @@ cat > "$GOALS_OPEN/goal-lifecycle-v2.md" <<'EOF'
605
605
  EOF
606
606
 
607
607
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
608
- assert_contains "lc step 3: blocks for verification" '"continue": false' "$OUTPUT"
608
+ assert_contains "lc step 3: blocks for verification" '"decision": "block"' "$OUTPUT"
609
609
  assert_file_exists "lc step 3: verification created" "$GOALS_OPEN/goal-lifecycle-v2-verify.md"
610
610
 
611
611
  # Complete verification → cap reached, allows stop
@@ -631,7 +631,7 @@ done
631
631
 
632
632
  OUTPUT=$(bash "$HOOK" 2>/dev/null)
633
633
 
634
- assert_contains "still blocks at sprint 51" '"continue": false' "$OUTPUT"
634
+ assert_contains "still blocks at sprint 51" '"decision": "block"' "$OUTPUT"
635
635
  assert_contains "suggests v51" 'v51' "$OUTPUT"
636
636
  assert_json_valid "valid JSON" "$OUTPUT"
637
637