gsd-cc 1.2.1 → 1.3.1
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/install.js +103 -0
- package/hooks/gsd-boundary-guard.sh +62 -0
- package/hooks/gsd-context-monitor.sh +55 -0
- package/hooks/gsd-prompt-guard.sh +91 -0
- package/hooks/gsd-statusline.sh +73 -0
- package/hooks/gsd-workflow-guard.sh +52 -0
- package/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -155,9 +155,89 @@ function install(isGlobal) {
|
|
|
155
155
|
fs.chmodSync(autoLoop, 0o755);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// 5. Install hooks
|
|
159
|
+
const hooksSrc = path.join(__dirname, '..', 'hooks');
|
|
160
|
+
const hooksDest = path.join(skillsBase, 'gsd-cc-shared', 'hooks');
|
|
161
|
+
if (fs.existsSync(hooksSrc)) {
|
|
162
|
+
copyDir(hooksSrc, hooksDest);
|
|
163
|
+
// Make hooks executable
|
|
164
|
+
const hookFiles = fs.readdirSync(hooksDest);
|
|
165
|
+
for (const f of hookFiles) {
|
|
166
|
+
fs.chmodSync(path.join(hooksDest, f), 0o755);
|
|
167
|
+
}
|
|
168
|
+
fileCount += hookFiles.length;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 6. Configure hooks in settings.json
|
|
172
|
+
installHooks(isGlobal, hooksDest);
|
|
173
|
+
|
|
158
174
|
console.log(` ${green}✓${reset} Installed ${fileCount} files to ${label}`);
|
|
159
175
|
}
|
|
160
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Install hooks into .claude/settings.json or .claude/settings.local.json
|
|
179
|
+
*/
|
|
180
|
+
function installHooks(isGlobal, hooksDir) {
|
|
181
|
+
const settingsPath = isGlobal
|
|
182
|
+
? path.join(os.homedir(), '.claude', 'settings.json')
|
|
183
|
+
: path.join(process.cwd(), '.claude', 'settings.local.json');
|
|
184
|
+
|
|
185
|
+
let settings = {};
|
|
186
|
+
if (fs.existsSync(settingsPath)) {
|
|
187
|
+
try {
|
|
188
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
189
|
+
} catch (e) {
|
|
190
|
+
settings = {};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!settings.hooks) settings.hooks = {};
|
|
195
|
+
|
|
196
|
+
const boundaryGuard = path.join(hooksDir, 'gsd-boundary-guard.sh');
|
|
197
|
+
const promptGuard = path.join(hooksDir, 'gsd-prompt-guard.sh');
|
|
198
|
+
const contextMonitor = path.join(hooksDir, 'gsd-context-monitor.sh');
|
|
199
|
+
const workflowGuard = path.join(hooksDir, 'gsd-workflow-guard.sh');
|
|
200
|
+
const statusline = path.join(hooksDir, 'gsd-statusline.sh');
|
|
201
|
+
|
|
202
|
+
// Remove all existing GSD-CC hooks before adding (idempotent)
|
|
203
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
204
|
+
settings.hooks[event] = settings.hooks[event].filter(
|
|
205
|
+
h => !JSON.stringify(h).includes('gsd-')
|
|
206
|
+
);
|
|
207
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// PreToolUse: boundary guard + prompt injection guard on Edit/Write
|
|
211
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
212
|
+
settings.hooks.PreToolUse.push({
|
|
213
|
+
matcher: 'Edit|Write',
|
|
214
|
+
hooks: [
|
|
215
|
+
{ type: 'command', command: boundaryGuard, timeout: 5000 },
|
|
216
|
+
{ type: 'command', command: promptGuard, timeout: 5000 }
|
|
217
|
+
]
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// PostToolUse: context monitor (all tools) + workflow guard (Edit/Write)
|
|
221
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
222
|
+
settings.hooks.PostToolUse.push({
|
|
223
|
+
hooks: [{ type: 'command', command: contextMonitor, timeout: 5000 }]
|
|
224
|
+
});
|
|
225
|
+
settings.hooks.PostToolUse.push({
|
|
226
|
+
matcher: 'Edit|Write',
|
|
227
|
+
hooks: [{ type: 'command', command: workflowGuard, timeout: 5000 }]
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Notification: statusline
|
|
231
|
+
if (!settings.hooks.Notification) settings.hooks.Notification = [];
|
|
232
|
+
settings.hooks.Notification.push({
|
|
233
|
+
hooks: [{ type: 'command', command: statusline, timeout: 3000 }]
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
237
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
238
|
+
console.log(` ${green}✓${reset} Hooks configured in ${settingsPath.replace(os.homedir(), '~').replace(process.cwd(), '.')}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
161
241
|
/**
|
|
162
242
|
* Write language to CLAUDE.md
|
|
163
243
|
*/
|
|
@@ -254,9 +334,32 @@ function uninstall() {
|
|
|
254
334
|
}
|
|
255
335
|
}
|
|
256
336
|
|
|
337
|
+
// Clean up hooks from settings files
|
|
338
|
+
for (const isGlobal of [true, false]) {
|
|
339
|
+
const settingsPath = isGlobal
|
|
340
|
+
? path.join(os.homedir(), '.claude', 'settings.json')
|
|
341
|
+
: path.join(process.cwd(), '.claude', 'settings.local.json');
|
|
342
|
+
if (fs.existsSync(settingsPath)) {
|
|
343
|
+
try {
|
|
344
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
345
|
+
if (settings.hooks) {
|
|
346
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
347
|
+
settings.hooks[event] = settings.hooks[event].filter(
|
|
348
|
+
h => !JSON.stringify(h).includes('gsd-')
|
|
349
|
+
);
|
|
350
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
351
|
+
}
|
|
352
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
353
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
354
|
+
}
|
|
355
|
+
} catch (e) { /* ignore parse errors */ }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
257
359
|
if (!removed) {
|
|
258
360
|
console.log(` ${yellow}No GSD-CC installation found.${reset}`);
|
|
259
361
|
} else {
|
|
362
|
+
console.log(` ${green}✓${reset} Hooks removed from settings`);
|
|
260
363
|
console.log(`\n ${green}Done.${reset} GSD-CC has been removed.`);
|
|
261
364
|
}
|
|
262
365
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GSD-CC Boundary Guard — PreToolUse hook
|
|
3
|
+
# Blocks Write/Edit operations on files listed in .gsd/STATE.md boundaries.
|
|
4
|
+
# This is a HARD enforcement — Claude cannot bypass this regardless of prompting.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
8
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
9
|
+
|
|
10
|
+
# Only check Edit and Write operations
|
|
11
|
+
if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Get the file being edited/written
|
|
16
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
17
|
+
if [ -z "$FILE_PATH" ]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if STATE.md exists and has boundaries
|
|
22
|
+
STATE_FILE="$CWD/.gsd/STATE.md"
|
|
23
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Extract boundary files from STATE.md
|
|
28
|
+
# Boundaries section contains lines like: - path/to/file (reason)
|
|
29
|
+
BOUNDARIES=$(sed -n '/^## Boundaries Active/,/^##/p' "$STATE_FILE" | grep '^ *- ' | sed 's/^ *- //' | sed 's/ (.*//')
|
|
30
|
+
|
|
31
|
+
if [ -z "$BOUNDARIES" ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Normalize the target file path (make relative to CWD if absolute)
|
|
36
|
+
RELATIVE_PATH="$FILE_PATH"
|
|
37
|
+
if [[ "$FILE_PATH" == "$CWD"* ]]; then
|
|
38
|
+
RELATIVE_PATH="${FILE_PATH#$CWD/}"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Check each boundary file
|
|
42
|
+
while IFS= read -r BOUNDARY_FILE; do
|
|
43
|
+
BOUNDARY_FILE=$(echo "$BOUNDARY_FILE" | xargs) # trim whitespace
|
|
44
|
+
if [ -z "$BOUNDARY_FILE" ]; then
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Check exact match or path containment
|
|
49
|
+
if [ "$RELATIVE_PATH" = "$BOUNDARY_FILE" ] || [ "$FILE_PATH" = "$BOUNDARY_FILE" ]; then
|
|
50
|
+
jq -n --arg file "$BOUNDARY_FILE" '{
|
|
51
|
+
"hookSpecificOutput": {
|
|
52
|
+
"hookEventName": "PreToolUse",
|
|
53
|
+
"permissionDecision": "deny",
|
|
54
|
+
"permissionDecisionReason": ("BOUNDARY VIOLATION: " + $file + " is in the DO NOT CHANGE list for this task. This file is protected. If you need to modify it, stop and discuss with the user first.")
|
|
55
|
+
}
|
|
56
|
+
}'
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
done <<< "$BOUNDARIES"
|
|
60
|
+
|
|
61
|
+
# File not in boundaries — allow
|
|
62
|
+
exit 0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GSD-CC Context Monitor — PostToolUse hook
|
|
3
|
+
# Injects warnings when context usage is getting high.
|
|
4
|
+
# Uses the transcript file size as a proxy for context consumption.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
8
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
|
|
9
|
+
|
|
10
|
+
# Skip if no transcript path
|
|
11
|
+
if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Check if we're in a GSD-CC project
|
|
16
|
+
if [ ! -d "$CWD/.gsd" ]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Use transcript line count as context proxy
|
|
21
|
+
# Typical context window: ~200K tokens ≈ ~2000 transcript lines for a heavy session
|
|
22
|
+
LINE_COUNT=$(wc -l < "$TRANSCRIPT" | xargs)
|
|
23
|
+
|
|
24
|
+
# Debounce: only warn every 20 tool calls
|
|
25
|
+
DEBOUNCE_FILE="/tmp/gsd-cc-ctx-monitor-$$"
|
|
26
|
+
if [ -f "$DEBOUNCE_FILE" ]; then
|
|
27
|
+
LAST_WARN=$(cat "$DEBOUNCE_FILE")
|
|
28
|
+
DIFF=$((LINE_COUNT - LAST_WARN))
|
|
29
|
+
if [ "$DIFF" -lt 50 ]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Warning thresholds (based on transcript lines)
|
|
35
|
+
if [ "$LINE_COUNT" -gt 1500 ]; then
|
|
36
|
+
echo "$LINE_COUNT" > "$DEBOUNCE_FILE"
|
|
37
|
+
jq -n '{
|
|
38
|
+
"hookSpecificOutput": {
|
|
39
|
+
"hookEventName": "PostToolUse",
|
|
40
|
+
"additionalContext": "⚠️ CRITICAL: Context window is very full. You MUST wrap up the current task immediately, write the summary, and instruct the user to start a fresh session. Do NOT start new work."
|
|
41
|
+
}
|
|
42
|
+
}'
|
|
43
|
+
exit 0
|
|
44
|
+
elif [ "$LINE_COUNT" -gt 1000 ]; then
|
|
45
|
+
echo "$LINE_COUNT" > "$DEBOUNCE_FILE"
|
|
46
|
+
jq -n '{
|
|
47
|
+
"hookSpecificOutput": {
|
|
48
|
+
"hookEventName": "PostToolUse",
|
|
49
|
+
"additionalContext": "⚠️ WARNING: Context window is filling up. Finish the current task soon and prepare to hand off to a fresh session."
|
|
50
|
+
}
|
|
51
|
+
}'
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
exit 0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GSD-CC Prompt Injection Guard — PreToolUse hook
|
|
3
|
+
# Scans Write/Edit operations targeting .gsd/ files for prompt injection patterns.
|
|
4
|
+
# Blocks suspicious content from being written into planning artifacts.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
8
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
9
|
+
|
|
10
|
+
# Only check Edit and Write operations
|
|
11
|
+
if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
16
|
+
if [ -z "$FILE_PATH" ]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Only scan writes to .gsd/ directory (planning artifacts)
|
|
21
|
+
if [[ "$FILE_PATH" != *".gsd/"* ]]; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Get the content being written
|
|
26
|
+
if [ "$TOOL_NAME" = "Write" ]; then
|
|
27
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
|
|
28
|
+
elif [ "$TOOL_NAME" = "Edit" ]; then
|
|
29
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty')
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ -z "$CONTENT" ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Check for prompt injection patterns
|
|
37
|
+
SUSPICIOUS=false
|
|
38
|
+
REASON=""
|
|
39
|
+
|
|
40
|
+
# Pattern 1: Direct instruction override attempts
|
|
41
|
+
if echo "$CONTENT" | grep -iqE 'ignore (previous|prior|above|all) (instructions|prompts|rules)'; then
|
|
42
|
+
SUSPICIOUS=true
|
|
43
|
+
REASON="Detected 'ignore previous instructions' pattern"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Pattern 2: Role reassignment
|
|
47
|
+
if echo "$CONTENT" | grep -iqE '(you are now|act as|pretend to be|your new role|forget your|disregard your)'; then
|
|
48
|
+
SUSPICIOUS=true
|
|
49
|
+
REASON="Detected role reassignment pattern"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Pattern 3: System prompt extraction
|
|
53
|
+
if echo "$CONTENT" | grep -iqE '(show|reveal|print|output|display) (your|the|system) (prompt|instructions|rules)'; then
|
|
54
|
+
SUSPICIOUS=true
|
|
55
|
+
REASON="Detected system prompt extraction attempt"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Pattern 4: Invisible Unicode characters (zero-width spaces, RTL override, etc.)
|
|
59
|
+
if echo "$CONTENT" | grep -qP '[\x{200B}\x{200C}\x{200D}\x{FEFF}\x{202A}-\x{202E}\x{2066}-\x{2069}]' 2>/dev/null; then
|
|
60
|
+
SUSPICIOUS=true
|
|
61
|
+
REASON="Detected invisible Unicode characters"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Pattern 5: Base64-encoded instructions
|
|
65
|
+
if echo "$CONTENT" | grep -qE '[A-Za-z0-9+/]{50,}={0,2}'; then
|
|
66
|
+
# Only flag if it's in a suspicious context (not normal code)
|
|
67
|
+
if echo "$CONTENT" | grep -iqE '(decode|eval|execute|base64).*[A-Za-z0-9+/]{50,}'; then
|
|
68
|
+
SUSPICIOUS=true
|
|
69
|
+
REASON="Detected potentially encoded instructions"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Pattern 6: HTML/script injection in markdown
|
|
74
|
+
if echo "$CONTENT" | grep -iqE '<script|javascript:|on(load|error|click)='; then
|
|
75
|
+
SUSPICIOUS=true
|
|
76
|
+
REASON="Detected script injection in planning artifact"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [ "$SUSPICIOUS" = true ]; then
|
|
80
|
+
jq -n --arg reason "$REASON" --arg file "$FILE_PATH" '{
|
|
81
|
+
"hookSpecificOutput": {
|
|
82
|
+
"hookEventName": "PreToolUse",
|
|
83
|
+
"permissionDecision": "deny",
|
|
84
|
+
"permissionDecisionReason": ("PROMPT INJECTION BLOCKED: " + $reason + " in " + $file + ". This content cannot be written to planning artifacts. If this is a false positive, the user can write the file manually.")
|
|
85
|
+
}
|
|
86
|
+
}'
|
|
87
|
+
exit 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Content is clean
|
|
91
|
+
exit 0
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GSD-CC Statusline — Notification hook
|
|
3
|
+
# Renders project status in the terminal statusline.
|
|
4
|
+
# Shows: current phase, milestone/slice/task position, and project type.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
8
|
+
|
|
9
|
+
# Only render if this is a GSD-CC project
|
|
10
|
+
STATE_FILE="$CWD/.gsd/STATE.md"
|
|
11
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Parse STATE.md frontmatter
|
|
16
|
+
PHASE=$(grep '^phase:' "$STATE_FILE" | head -1 | sed 's/phase: *//')
|
|
17
|
+
MILESTONE=$(grep '^milestone:' "$STATE_FILE" | head -1 | sed 's/milestone: *//')
|
|
18
|
+
SLICE=$(grep '^current_slice:' "$STATE_FILE" | head -1 | sed 's/current_slice: *//')
|
|
19
|
+
TASK=$(grep '^current_task:' "$STATE_FILE" | head -1 | sed 's/current_task: *//')
|
|
20
|
+
RIGOR=$(grep '^rigor:' "$STATE_FILE" | head -1 | sed 's/rigor: *//')
|
|
21
|
+
|
|
22
|
+
# Build position string
|
|
23
|
+
POSITION="$MILESTONE"
|
|
24
|
+
if [ "$SLICE" != "—" ] && [ -n "$SLICE" ]; then
|
|
25
|
+
POSITION="$POSITION / $SLICE"
|
|
26
|
+
fi
|
|
27
|
+
if [ "$TASK" != "—" ] && [ -n "$TASK" ]; then
|
|
28
|
+
POSITION="$POSITION / $TASK"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Map phase to display name
|
|
32
|
+
case "$PHASE" in
|
|
33
|
+
"seed") PHASE_DISPLAY="Seed" ;;
|
|
34
|
+
"seed-complete") PHASE_DISPLAY="Seed ✓" ;;
|
|
35
|
+
"stack-complete") PHASE_DISPLAY="Stack ✓" ;;
|
|
36
|
+
"roadmap-complete") PHASE_DISPLAY="Roadmap ✓" ;;
|
|
37
|
+
"discuss-complete") PHASE_DISPLAY="Discuss ✓" ;;
|
|
38
|
+
"plan-complete") PHASE_DISPLAY="Plan ✓" ;;
|
|
39
|
+
"planning") PHASE_DISPLAY="Planning..." ;;
|
|
40
|
+
"applying") PHASE_DISPLAY="Executing..." ;;
|
|
41
|
+
"apply-complete") PHASE_DISPLAY="UNIFY required" ;;
|
|
42
|
+
"unified") PHASE_DISPLAY="Unified ✓" ;;
|
|
43
|
+
*) PHASE_DISPLAY="$PHASE" ;;
|
|
44
|
+
esac
|
|
45
|
+
|
|
46
|
+
# Count progress
|
|
47
|
+
TOTAL_SLICES=$(grep -c '| S[0-9]' "$STATE_FILE" 2>/dev/null || echo "0")
|
|
48
|
+
DONE_SLICES=$(grep '| done' "$STATE_FILE" | wc -l | xargs)
|
|
49
|
+
|
|
50
|
+
# Write status to bridge file for other hooks
|
|
51
|
+
BRIDGE_FILE="/tmp/gsd-cc-status-$(echo "$CWD" | md5sum 2>/dev/null | cut -c1-8 || echo "default").json"
|
|
52
|
+
jq -n \
|
|
53
|
+
--arg phase "$PHASE" \
|
|
54
|
+
--arg position "$POSITION" \
|
|
55
|
+
--arg rigor "$RIGOR" \
|
|
56
|
+
--arg total "$TOTAL_SLICES" \
|
|
57
|
+
--arg done "$DONE_SLICES" \
|
|
58
|
+
'{phase: $phase, position: $position, rigor: $rigor, total_slices: ($total|tonumber), done_slices: ($done|tonumber)}' \
|
|
59
|
+
> "$BRIDGE_FILE" 2>/dev/null
|
|
60
|
+
|
|
61
|
+
# Output statusline data
|
|
62
|
+
jq -n \
|
|
63
|
+
--arg pos "$POSITION" \
|
|
64
|
+
--arg phase "$PHASE_DISPLAY" \
|
|
65
|
+
--arg rigor "$RIGOR" \
|
|
66
|
+
--arg progress "${DONE_SLICES}/${TOTAL_SLICES}" \
|
|
67
|
+
'{
|
|
68
|
+
"hookSpecificOutput": {
|
|
69
|
+
"hookEventName": "Notification",
|
|
70
|
+
"additionalContext": ("GSD-CC: " + $pos + " | " + $phase + " | " + $rigor + " | " + $progress + " slices")
|
|
71
|
+
}
|
|
72
|
+
}'
|
|
73
|
+
exit 0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GSD-CC Workflow Guard — PostToolUse hook
|
|
3
|
+
# Nudges Claude back into the GSD-CC flow when it drifts.
|
|
4
|
+
# Advisory only — does not block operations.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
8
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
9
|
+
|
|
10
|
+
# Only check Edit and Write on source files (not .gsd/ files)
|
|
11
|
+
if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Skip if not a GSD-CC project
|
|
16
|
+
STATE_FILE="$CWD/.gsd/STATE.md"
|
|
17
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
22
|
+
if [ -z "$FILE_PATH" ]; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Allow writes to .gsd/ directory (that's the workflow itself)
|
|
27
|
+
if [[ "$FILE_PATH" == *".gsd/"* ]] || [[ "$FILE_PATH" == *".claude/"* ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Check if we're in an active execution phase
|
|
32
|
+
PHASE=$(grep '^phase:' "$STATE_FILE" | head -1 | sed 's/phase: *//')
|
|
33
|
+
|
|
34
|
+
case "$PHASE" in
|
|
35
|
+
"seed"|"seed-complete"|"stack-complete"|"roadmap-complete"|"plan-complete"|"discuss-complete")
|
|
36
|
+
# Not in execution — source file edits are unexpected
|
|
37
|
+
jq -n --arg phase "$PHASE" --arg file "$FILE_PATH" '{
|
|
38
|
+
"hookSpecificOutput": {
|
|
39
|
+
"hookEventName": "PostToolUse",
|
|
40
|
+
"additionalContext": ("Note: You edited " + $file + " but the current GSD-CC phase is \"" + $phase + "\" which is a planning phase, not execution. Source file changes should happen during the apply phase. If this was intentional, carry on. If not, consider running /gsd-cc to check the current state.")
|
|
41
|
+
}
|
|
42
|
+
}'
|
|
43
|
+
exit 0
|
|
44
|
+
;;
|
|
45
|
+
"applying")
|
|
46
|
+
# In execution — this is expected
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
exit 0
|
|
51
|
+
;;
|
|
52
|
+
esac
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-cc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Get Shit Done on Claude Code — structured AI development with your Max plan",
|
|
5
5
|
"author": "Philipp Briese (https://github.com/0ui-labs)",
|
|
6
6
|
"homepage": "https://github.com/0ui-labs/GSD-CC#readme",
|