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 +30 -8
- package/ccsg.sh +72 -57
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/commands/help.md +46 -0
- package/plugin/commands/sprint-cancel.md +15 -0
- package/plugin/commands/sprint-status.md +21 -0
- package/plugin/commands/sprint.md +34 -0
- package/plugin/hooks/ccsg.sh +367 -0
- package/plugin/hooks/hooks.json +15 -0
- package/test-ccsg.sh +23 -23
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.
|
|
98
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
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
|
|
185
|
+
# ── Helper: build block response (Ralph Wiggum pattern) ─────────────
|
|
156
186
|
build_block_response() {
|
|
157
187
|
local source_file="$1"
|
|
158
|
-
local
|
|
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":
|
|
201
|
-
"
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
@@ -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
|
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" '"
|
|
181
|
-
assert_contains "lists unmet items" '
|
|
182
|
-
assert_contains "has
|
|
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" '"
|
|
223
|
+
assert_contains "blocks stop" '"decision": "block"' "$OUTPUT"
|
|
224
224
|
assert_contains "reports count" '2 of 3' "$OUTPUT"
|
|
225
|
-
assert_contains "lists unmet item" '
|
|
226
|
-
assert_contains "lists other unmet item" '
|
|
227
|
-
assert_contains "shows progress" '1 of 3 done' "$OUTPUT"
|
|
228
|
-
assert_contains "
|
|
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" '"
|
|
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)" '"
|
|
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" '"
|
|
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" '"
|
|
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" '"
|
|
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" '"
|
|
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)" '"
|
|
500
|
-
assert_contains "
|
|
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
|
|
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" '"
|
|
524
|
-
assert_contains "
|
|
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" '"
|
|
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
|
|
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" '"
|
|
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" '"
|
|
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
|
|