@windyroad/retrospective 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +5 -0
- package/bin/check-deps.sh +65 -0
- package/bin/install.mjs +42 -0
- package/hooks/hooks.json +13 -0
- package/hooks/lib/gate-helpers.sh +174 -0
- package/hooks/lib/review-gate.sh +102 -0
- package/hooks/retrospective-reminder.sh +9 -0
- package/hooks/review-plan-enforce.sh +73 -0
- package/package.json +28 -0
- package/skills/wr:retrospective/SKILL.md +72 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared dependency checker for windyroad-plugins marketplace.
|
|
3
|
+
# Called by each plugin's SessionStart hook with a list of required plugins.
|
|
4
|
+
#
|
|
5
|
+
# Usage: check-deps.sh <this-plugin-name> <required-plugin-1> [required-plugin-2] ...
|
|
6
|
+
#
|
|
7
|
+
# Checks if required sibling plugins are installed by looking for their
|
|
8
|
+
# .claude-plugin/plugin.json in the plugin cache. Outputs a warning to
|
|
9
|
+
# stderr (which surfaces as hook output) if any are missing.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
PLUGIN_NAME="${1:?Usage: check-deps.sh <plugin-name> <dep1> [dep2] ...}"
|
|
14
|
+
shift
|
|
15
|
+
|
|
16
|
+
MISSING=()
|
|
17
|
+
|
|
18
|
+
for DEP in "$@"; do
|
|
19
|
+
# Check if the dependency plugin is installed by looking for its marker.
|
|
20
|
+
# Installed plugins have their hooks loaded, so we check if the dep's
|
|
21
|
+
# hooks are present in the session. Simplest check: look for the plugin
|
|
22
|
+
# name in the installed plugins list.
|
|
23
|
+
FOUND=false
|
|
24
|
+
|
|
25
|
+
# Method 1: Check installed_plugins.json
|
|
26
|
+
if [ -f "$HOME/.claude/plugins/installed_plugins.json" ]; then
|
|
27
|
+
if python3 -c "
|
|
28
|
+
import json, sys
|
|
29
|
+
data = json.load(open('$HOME/.claude/plugins/installed_plugins.json'))
|
|
30
|
+
plugins = data.get('plugins', {})
|
|
31
|
+
for key in plugins:
|
|
32
|
+
if key.startswith('${DEP}@'):
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
" 2>/dev/null; then
|
|
36
|
+
FOUND=true
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Method 2: Check if plugin dir exists in cache
|
|
41
|
+
if [ "$FOUND" = false ]; then
|
|
42
|
+
for dir in "$HOME/.claude/plugins/cache/"*/"$DEP"/*/; do
|
|
43
|
+
if [ -f "${dir}.claude-plugin/plugin.json" ] 2>/dev/null; then
|
|
44
|
+
FOUND=true
|
|
45
|
+
break
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Method 3: Check if loaded via --plugin-dir (plugin hooks would be active)
|
|
51
|
+
# We can't easily check this, so we rely on methods 1 and 2.
|
|
52
|
+
|
|
53
|
+
if [ "$FOUND" = false ]; then
|
|
54
|
+
MISSING+=("$DEP")
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
59
|
+
echo ""
|
|
60
|
+
echo "WARNING: Plugin '$PLUGIN_NAME' requires the following plugins that may not be installed:"
|
|
61
|
+
for m in "${MISSING[@]}"; do
|
|
62
|
+
echo " - $m (install with: /plugin install $m@windyroad-plugins)"
|
|
63
|
+
done
|
|
64
|
+
echo ""
|
|
65
|
+
fi
|
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const utils = await import(resolve(__dirname, "../../shared/install-utils.mjs"));
|
|
8
|
+
|
|
9
|
+
const PLUGIN = "wr-retrospective";
|
|
10
|
+
const DEPS = ["wr-problem", "wr-risk-scorer"];
|
|
11
|
+
|
|
12
|
+
const flags = utils.parseStandardArgs(process.argv);
|
|
13
|
+
|
|
14
|
+
if (flags.help) {
|
|
15
|
+
console.log(`
|
|
16
|
+
Usage: npx @windyroad/retrospective [options]
|
|
17
|
+
|
|
18
|
+
Session retrospectives that update briefings and create problem tickets
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--update Update this plugin and its skills
|
|
22
|
+
--uninstall Remove this plugin
|
|
23
|
+
--dry-run Show what would be done without executing
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (flags.dryRun) {
|
|
30
|
+
utils.setDryRun(true);
|
|
31
|
+
console.log("[dry-run mode — no commands will be executed]\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
utils.checkPrerequisites();
|
|
35
|
+
|
|
36
|
+
if (flags.uninstall) {
|
|
37
|
+
utils.uninstallPackage(PLUGIN);
|
|
38
|
+
} else if (flags.update) {
|
|
39
|
+
utils.updatePackage(PLUGIN);
|
|
40
|
+
} else {
|
|
41
|
+
utils.installPackage(PLUGIN, { deps: DEPS });
|
|
42
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/bin/check-deps.sh wr-retrospective wr-problem wr-risk-scorer" }] }
|
|
5
|
+
],
|
|
6
|
+
"PreToolUse": [
|
|
7
|
+
{ "matcher": "ExitPlanMode", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/review-plan-enforce.sh" }] }
|
|
8
|
+
],
|
|
9
|
+
"Stop": [
|
|
10
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/retrospective-reminder.sh" }] }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared portable helpers for gate enforcement hooks.
|
|
3
|
+
# Sourced by architect-gate.sh, risk-gate.sh, and all hook scripts.
|
|
4
|
+
# Provides: _mtime, _hashcmd, _doc_exclusions, _err_trap, _get_*
|
|
5
|
+
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# Portable utilities
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
# Portable mtime: tries GNU stat, falls back to macOS stat
|
|
11
|
+
_mtime() { stat -c%Y "$1" 2>/dev/null || /usr/bin/stat -f%m "$1" 2>/dev/null || echo 0; }
|
|
12
|
+
|
|
13
|
+
# Portable hash: tries md5sum, falls back to md5 -r, then shasum
|
|
14
|
+
_hashcmd() { md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null; }
|
|
15
|
+
|
|
16
|
+
# Paths excluded from pipeline state hashing and docs-only detection.
|
|
17
|
+
_doc_exclusions() {
|
|
18
|
+
echo ':!docs/' ':!.risk-reports/' ':!.changeset/' ':!governance/' ':!.claude/plans/' ':!CLAUDE.md' ':!AGENTS.md' ':!PRINCIPLES.md' ':!DECISION-MANAGEMENT.md' ':!AGENTIC_RISK_REGISTER.md' ':!PROBLEM-MANAGEMENT.md'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# ERR trap: outputs diagnostic JSON on hook errors (P010)
|
|
23
|
+
# Usage: source gate-helpers.sh at top of hook, then call _enable_err_trap
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_enable_err_trap() {
|
|
27
|
+
trap '_err_trap_handler "$BASH_SOURCE" "$LINENO" "$BASH_COMMAND"' ERR
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_err_trap_handler() {
|
|
31
|
+
local script="$1" line="$2" cmd="$3"
|
|
32
|
+
local name
|
|
33
|
+
name=$(basename "$script" 2>/dev/null || echo "$script")
|
|
34
|
+
# Output diagnostic as systemMessage so it's visible in conversation
|
|
35
|
+
cat <<EOF
|
|
36
|
+
{
|
|
37
|
+
"systemMessage": "Hook error in ${name} at line ${line}: ${cmd}"
|
|
38
|
+
}
|
|
39
|
+
EOF
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# JSON input parsing: standardised helpers replacing inline python3
|
|
44
|
+
# Each reads from _HOOK_INPUT (set by the hook before calling these)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
# Store hook input for reuse by parsing helpers
|
|
48
|
+
_HOOK_INPUT=""
|
|
49
|
+
|
|
50
|
+
_parse_input() {
|
|
51
|
+
_HOOK_INPUT=$(cat)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_get_tool_name() {
|
|
55
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
56
|
+
import sys, json
|
|
57
|
+
try:
|
|
58
|
+
data = json.load(sys.stdin)
|
|
59
|
+
print(data.get('tool_name', ''))
|
|
60
|
+
except:
|
|
61
|
+
print('')
|
|
62
|
+
" 2>/dev/null || echo ""
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_get_session_id() {
|
|
66
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
67
|
+
import sys, json
|
|
68
|
+
try:
|
|
69
|
+
data = json.load(sys.stdin)
|
|
70
|
+
print(data.get('session_id', ''))
|
|
71
|
+
except:
|
|
72
|
+
print('')
|
|
73
|
+
" 2>/dev/null || echo ""
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_get_command() {
|
|
77
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
78
|
+
import sys, json
|
|
79
|
+
try:
|
|
80
|
+
data = json.load(sys.stdin)
|
|
81
|
+
print(data.get('tool_input', {}).get('command', ''))
|
|
82
|
+
except:
|
|
83
|
+
print('')
|
|
84
|
+
" 2>/dev/null || echo ""
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_get_file_path() {
|
|
88
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
89
|
+
import sys, json
|
|
90
|
+
try:
|
|
91
|
+
data = json.load(sys.stdin)
|
|
92
|
+
ti = data.get('tool_input', {})
|
|
93
|
+
print(ti.get('file_path', ti.get('path', '')))
|
|
94
|
+
except:
|
|
95
|
+
print('')
|
|
96
|
+
" 2>/dev/null || echo ""
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_get_subagent_type() {
|
|
100
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
101
|
+
import sys, json
|
|
102
|
+
try:
|
|
103
|
+
data = json.load(sys.stdin)
|
|
104
|
+
print(data.get('tool_input', {}).get('subagent_type', ''))
|
|
105
|
+
except:
|
|
106
|
+
print('')
|
|
107
|
+
" 2>/dev/null || echo ""
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_get_user_prompt() {
|
|
111
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
112
|
+
import sys, json
|
|
113
|
+
try:
|
|
114
|
+
data = json.load(sys.stdin)
|
|
115
|
+
print(data.get('user_prompt', ''))
|
|
116
|
+
except:
|
|
117
|
+
print('')
|
|
118
|
+
" 2>/dev/null || echo ""
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_get_tool_output() {
|
|
122
|
+
echo "$_HOOK_INPUT" | python3 -c "
|
|
123
|
+
import sys, json
|
|
124
|
+
try:
|
|
125
|
+
data = json.load(sys.stdin)
|
|
126
|
+
# PostToolUse provides tool_response (dict with content array), not tool_output
|
|
127
|
+
tr = data.get('tool_response', {})
|
|
128
|
+
if isinstance(tr, dict):
|
|
129
|
+
content = tr.get('content', [])
|
|
130
|
+
if isinstance(content, list):
|
|
131
|
+
texts = [c.get('text', '') for c in content if isinstance(c, dict) and c.get('type') == 'text']
|
|
132
|
+
if texts:
|
|
133
|
+
print('\n'.join(texts))
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
# Fallback for older/different hook formats
|
|
136
|
+
print(data.get('tool_output', ''))
|
|
137
|
+
except:
|
|
138
|
+
print('')
|
|
139
|
+
" 2>/dev/null || echo ""
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Session-scoped tmp directory for risk files
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
# Returns the session-scoped directory for risk temp files.
|
|
147
|
+
# Creates the directory if it doesn't exist.
|
|
148
|
+
# Usage: DIR=$(_risk_dir "$SESSION_ID"); echo "1" > "$DIR/commit"
|
|
149
|
+
_risk_dir() {
|
|
150
|
+
local sid="$1"
|
|
151
|
+
local dir="${TMPDIR:-/tmp}/claude-risk-${sid}"
|
|
152
|
+
mkdir -p "$dir"
|
|
153
|
+
echo "$dir"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Non-doc file detection for WIP gating
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
_is_doc_file() {
|
|
161
|
+
local file_path="$1"
|
|
162
|
+
local EXCL
|
|
163
|
+
EXCL=$(_doc_exclusions)
|
|
164
|
+
for pattern in $EXCL; do
|
|
165
|
+
local clean="${pattern#:!}"
|
|
166
|
+
case "$file_path" in
|
|
167
|
+
*"$clean"*) return 0 ;;
|
|
168
|
+
esac
|
|
169
|
+
done
|
|
170
|
+
case "$file_path" in
|
|
171
|
+
*.claude/*|*.risk-reports/*|*RISK-POLICY.md) return 0 ;;
|
|
172
|
+
esac
|
|
173
|
+
return 1
|
|
174
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared gate logic for review enforcement hooks (a11y, voice-tone, style-guide).
|
|
3
|
+
# Sourced by *-enforce-edit.sh hooks and review-plan-enforce.sh.
|
|
4
|
+
# Provides: check_review_gate, review_gate_deny, review_gate_parse_error
|
|
5
|
+
|
|
6
|
+
# Source shared portable helpers (_mtime, _hashcmd)
|
|
7
|
+
_REVIEW_GATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
source "$_REVIEW_GATE_DIR/gate-helpers.sh"
|
|
9
|
+
|
|
10
|
+
# Check review gate marker. Returns 0 if marker is valid (allow), 1 if invalid (deny).
|
|
11
|
+
# Sets REVIEW_GATE_REASON on failure.
|
|
12
|
+
# Usage: check_review_gate "$SESSION_ID" "style-guide" "docs/STYLE-GUIDE.md"
|
|
13
|
+
check_review_gate() {
|
|
14
|
+
local SESSION_ID="$1"
|
|
15
|
+
local SYSTEM="$2" # e.g., "a11y", "voice-tone", "style-guide"
|
|
16
|
+
local POLICY_FILE="$3" # e.g., "docs/STYLE-GUIDE.md"
|
|
17
|
+
local MARKER="/tmp/${SYSTEM}-reviewed-${SESSION_ID}"
|
|
18
|
+
local HASH_FILE="/tmp/${SYSTEM}-reviewed-${SESSION_ID}.hash"
|
|
19
|
+
local TTL_SECONDS="${REVIEW_TTL:-600}"
|
|
20
|
+
|
|
21
|
+
# 1. Marker must exist
|
|
22
|
+
if [ ! -f "$MARKER" ]; then
|
|
23
|
+
REVIEW_GATE_REASON="No ${SYSTEM} review marker found. The ${SYSTEM} agent must review first."
|
|
24
|
+
return 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 2. TTL check — marker mtime must be within TTL
|
|
28
|
+
local NOW=$(date +%s)
|
|
29
|
+
local MARKER_TIME=$(_mtime "$MARKER")
|
|
30
|
+
local AGE=$(( NOW - MARKER_TIME ))
|
|
31
|
+
if [ "$AGE" -ge "$TTL_SECONDS" ]; then
|
|
32
|
+
rm -f "$MARKER" "$HASH_FILE"
|
|
33
|
+
REVIEW_GATE_REASON="${SYSTEM} review expired (${AGE}s old, TTL ${TTL_SECONDS}s). Re-run the ${SYSTEM} agent."
|
|
34
|
+
return 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 3. Drift detection — policy file hash must match
|
|
38
|
+
if [ -f "$HASH_FILE" ] && [ -n "$POLICY_FILE" ]; then
|
|
39
|
+
local STORED_HASH=$(cat "$HASH_FILE")
|
|
40
|
+
local CURRENT_HASH=""
|
|
41
|
+
if [ -f "$POLICY_FILE" ]; then
|
|
42
|
+
CURRENT_HASH=$(cat "$POLICY_FILE" | _hashcmd | cut -d' ' -f1)
|
|
43
|
+
elif [ -d "$POLICY_FILE" ]; then
|
|
44
|
+
# Directory (e.g., docs/decisions/) — hash all .md files
|
|
45
|
+
CURRENT_HASH=$(find "$POLICY_FILE" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
46
|
+
else
|
|
47
|
+
CURRENT_HASH="missing"
|
|
48
|
+
fi
|
|
49
|
+
if [ "$STORED_HASH" != "$CURRENT_HASH" ]; then
|
|
50
|
+
rm -f "$MARKER" "$HASH_FILE"
|
|
51
|
+
REVIEW_GATE_REASON="${SYSTEM} policy file changed since last review. Re-run the ${SYSTEM} agent."
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Slide TTL window forward
|
|
57
|
+
touch "$MARKER"
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Store policy file hash after a successful review.
|
|
62
|
+
# Usage: store_review_hash "$SESSION_ID" "style-guide" "docs/STYLE-GUIDE.md"
|
|
63
|
+
store_review_hash() {
|
|
64
|
+
local SESSION_ID="$1"
|
|
65
|
+
local SYSTEM="$2"
|
|
66
|
+
local POLICY_FILE="$3"
|
|
67
|
+
local HASH_FILE="/tmp/${SYSTEM}-reviewed-${SESSION_ID}.hash"
|
|
68
|
+
|
|
69
|
+
if [ -n "$POLICY_FILE" ]; then
|
|
70
|
+
local HASH=""
|
|
71
|
+
if [ -f "$POLICY_FILE" ]; then
|
|
72
|
+
HASH=$(cat "$POLICY_FILE" | _hashcmd | cut -d' ' -f1)
|
|
73
|
+
elif [ -d "$POLICY_FILE" ]; then
|
|
74
|
+
HASH=$(find "$POLICY_FILE" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
75
|
+
else
|
|
76
|
+
HASH="missing"
|
|
77
|
+
fi
|
|
78
|
+
echo "$HASH" > "$HASH_FILE"
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Emit fail-closed deny JSON for PreToolUse hooks.
|
|
83
|
+
review_gate_deny() {
|
|
84
|
+
local REASON="$1"
|
|
85
|
+
cat <<EOF
|
|
86
|
+
{
|
|
87
|
+
"hookSpecificOutput": {
|
|
88
|
+
"hookEventName": "PreToolUse",
|
|
89
|
+
"permissionDecision": "deny",
|
|
90
|
+
"permissionDecisionReason": "$REASON"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
EOF
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Emit fail-closed deny JSON for parse failures.
|
|
97
|
+
review_gate_parse_error() {
|
|
98
|
+
cat <<'EOF'
|
|
99
|
+
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny",
|
|
100
|
+
"permissionDecisionReason": "BLOCKED: Could not parse hook input. Gate is fail-closed." } }
|
|
101
|
+
EOF
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Stop hook: Reminds the user to run /retrospective before ending the session.
|
|
3
|
+
|
|
4
|
+
cat <<'EOF'
|
|
5
|
+
{
|
|
6
|
+
"stopReason": "Before ending: run /retrospective to capture session learnings, update docs/BRIEFING.md, and use the /problem skill to create problem tickets for anything that failed or was harder than it should have been."
|
|
7
|
+
}
|
|
8
|
+
EOF
|
|
9
|
+
exit 0
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: Denies ExitPlanMode until review specialists have
|
|
3
|
+
# reviewed the plan. Skips UI specialists (a11y, voice-tone, style-guide,
|
|
4
|
+
# jtbd) when the plan only touches non-UI files (P008 optimization).
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/review-gate.sh"
|
|
8
|
+
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
|
|
12
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
13
|
+
|
|
14
|
+
if [ -z "$SESSION_ID" ]; then
|
|
15
|
+
review_gate_parse_error
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Detect if the plan touches UI files by checking uncommitted changes
|
|
20
|
+
# UI patterns: *.html, *.jsx, *.tsx, *.vue, *.svelte, *.astro, *.css, *.scss
|
|
21
|
+
HAS_UI_FILES=false
|
|
22
|
+
UI_PATTERNS='\.html$|\.jsx$|\.tsx$|\.vue$|\.svelte$|\.astro$|\.css$|\.scss$|\.ejs$|\.hbs$|\.erb$|\.leaf$'
|
|
23
|
+
if git diff --cached --name-only 2>/dev/null | grep -qE "$UI_PATTERNS"; then
|
|
24
|
+
HAS_UI_FILES=true
|
|
25
|
+
elif git diff --name-only 2>/dev/null | grep -qE "$UI_PATTERNS"; then
|
|
26
|
+
HAS_UI_FILES=true
|
|
27
|
+
elif git ls-files --others --exclude-standard 2>/dev/null | grep -qE "$UI_PATTERNS"; then
|
|
28
|
+
HAS_UI_FILES=true
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Also check the plan file itself for mentions of UI files
|
|
32
|
+
PLAN_DIR="$HOME/.claude/plans"
|
|
33
|
+
if [ -d "$PLAN_DIR" ] && [ "$HAS_UI_FILES" = false ]; then
|
|
34
|
+
LATEST_PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | head -1)
|
|
35
|
+
if [ -n "$LATEST_PLAN" ] && grep -qiE '\.html|\.jsx|\.tsx|\.vue|\.svelte|\.css|component|page|form|modal|dialog' "$LATEST_PLAN" 2>/dev/null; then
|
|
36
|
+
HAS_UI_FILES=true
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
MISSING=""
|
|
41
|
+
|
|
42
|
+
if [ "$HAS_UI_FILES" = true ]; then
|
|
43
|
+
# UI files detected — require all specialists
|
|
44
|
+
for SYSTEM in a11y voice-tone style-guide jtbd; do
|
|
45
|
+
MARKER="/tmp/${SYSTEM}-plan-reviewed-${SESSION_ID}"
|
|
46
|
+
if [ ! -f "$MARKER" ]; then
|
|
47
|
+
case "$SYSTEM" in
|
|
48
|
+
a11y) AGENT="accessibility-agents:accessibility-lead" ;;
|
|
49
|
+
voice-tone) AGENT="voice-and-tone-lead" ;;
|
|
50
|
+
style-guide) AGENT="style-guide-lead" ;;
|
|
51
|
+
jtbd) AGENT="jtbd-lead" ;;
|
|
52
|
+
esac
|
|
53
|
+
if [ -z "$MISSING" ]; then
|
|
54
|
+
MISSING="$AGENT"
|
|
55
|
+
else
|
|
56
|
+
MISSING="${MISSING}, ${AGENT}"
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
else
|
|
61
|
+
# No UI files — skip a11y, voice-tone, style-guide, jtbd
|
|
62
|
+
# Auto-create their markers so the gate passes
|
|
63
|
+
for SYSTEM in a11y voice-tone style-guide jtbd; do
|
|
64
|
+
touch "/tmp/${SYSTEM}-plan-reviewed-${SESSION_ID}"
|
|
65
|
+
done
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ -n "$MISSING" ]; then
|
|
69
|
+
review_gate_deny "BLOCKED: Cannot approve plan without specialist review. Missing: ${MISSING}. Delegate to each agent to review the plan."
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@windyroad/retrospective",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Session retrospectives that update briefings and create problem tickets",
|
|
5
|
+
"bin": {
|
|
6
|
+
"windyroad-retrospective": "./bin/install.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/windyroad/agent-plugins.git",
|
|
13
|
+
"directory": "packages/retrospective"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"claude-code-plugin",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"ai-coding"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"agents/",
|
|
24
|
+
"hooks/",
|
|
25
|
+
"skills/",
|
|
26
|
+
".claude-plugin/"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wr:retrospective
|
|
3
|
+
description: Run a session retrospective. Updates docs/BRIEFING.md with learnings and creates problem tickets for failures and friction.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Session Retrospective
|
|
8
|
+
|
|
9
|
+
Reflect on the current session, update the project briefing, and create problem tickets for failures and friction.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
### 1. Read the current briefing
|
|
14
|
+
|
|
15
|
+
Read `docs/BRIEFING.md` to understand what previous sessions already captured.
|
|
16
|
+
|
|
17
|
+
### 2. Reflect on this session
|
|
18
|
+
|
|
19
|
+
Consider the work done in this session and identify:
|
|
20
|
+
|
|
21
|
+
**What you wish you'd been told up front** — things that were non-obvious and caused wasted effort or wrong assumptions. These should be added to BRIEFING.md "What You Need to Know" if they aren't already there.
|
|
22
|
+
|
|
23
|
+
**What surprised you** — things that contradicted reasonable expectations. These should be added to BRIEFING.md "What Will Surprise You" if they aren't already there.
|
|
24
|
+
|
|
25
|
+
**What was harder than it should have been** — friction points, tool limitations, process overhead, confusing code. These should become problem tickets via the `/problem` skill.
|
|
26
|
+
|
|
27
|
+
**What failed** — things that broke, bugs encountered, hooks that errored, tests that failed unexpectedly. These should become problem tickets via the `/problem` skill.
|
|
28
|
+
|
|
29
|
+
**What should we make easier or automate** — repetitive manual steps, missing tooling, things that could be scripted. These should become problem tickets via the `/problem` skill.
|
|
30
|
+
|
|
31
|
+
### 3. Update BRIEFING.md
|
|
32
|
+
|
|
33
|
+
Edit `docs/BRIEFING.md`:
|
|
34
|
+
|
|
35
|
+
- **Add** new learnings to the appropriate section ("What You Need to Know" or "What Will Surprise You")
|
|
36
|
+
- **Remove** stale items that are no longer true. A learning is stale when:
|
|
37
|
+
- The issue has been fixed (e.g., "CI doesn't test v2" after v2 tests are added)
|
|
38
|
+
- It's now documented elsewhere (e.g., in an ADR, CLAUDE.md, or README)
|
|
39
|
+
- The codebase has changed enough that it's no longer relevant
|
|
40
|
+
- **Update** items where the details have changed
|
|
41
|
+
- Keep the file concise — under 2000 tokens. Each item should be 1-2 lines.
|
|
42
|
+
|
|
43
|
+
Use the AskUserQuestion tool to confirm any removals: "I'd like to remove [item] from BRIEFING.md because [reason]. Is this correct?"
|
|
44
|
+
|
|
45
|
+
### 4. Create or update problem tickets
|
|
46
|
+
|
|
47
|
+
For each item identified in "What was harder than it should have been", "What failed", and "What should we make easier or automate", use the `/problem` skill to:
|
|
48
|
+
|
|
49
|
+
- Check if a problem ticket already exists in `docs/problems/`
|
|
50
|
+
- If yes: update it with new evidence from this session
|
|
51
|
+
- If no: create a new problem ticket
|
|
52
|
+
|
|
53
|
+
### 5. Summary
|
|
54
|
+
|
|
55
|
+
Present a summary to the user:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
## Session Retrospective
|
|
59
|
+
|
|
60
|
+
### BRIEFING.md Changes
|
|
61
|
+
- Added: [items added]
|
|
62
|
+
- Removed: [items removed with reasons]
|
|
63
|
+
- Updated: [items modified]
|
|
64
|
+
|
|
65
|
+
### Problems Created/Updated
|
|
66
|
+
- [problem ticket]: [summary]
|
|
67
|
+
|
|
68
|
+
### No Action Needed
|
|
69
|
+
- [learnings that were already captured]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
$ARGUMENTS
|