@windyroad/retrospective 0.1.2-preview.13 → 0.1.3-preview.26

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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @windyroad/retrospective
2
+
3
+ **Session retrospectives for Claude Code.** Captures learnings at the end of each session and creates problem tickets for failures and friction.
4
+
5
+ Part of [Windy Road Agent Plugins](../../README.md).
6
+
7
+ ## What It Does
8
+
9
+ Every coding session produces learnings -- things that went well, things that broke, things that were harder than expected. Without a retrospective, those learnings evaporate.
10
+
11
+ The retrospective plugin:
12
+
13
+ - **Reminds** you to run a retro when a session ends
14
+ - **Updates** `docs/BRIEFING.md` with session learnings so future sessions start with context
15
+ - **Creates problem tickets** (via [`@windyroad/problem`](../problem/)) for failures and friction encountered during the session
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npx @windyroad/retrospective
21
+ ```
22
+
23
+ Restart Claude Code after installing.
24
+
25
+ > **Requires:** [`@windyroad/problem`](../problem/) and [`@windyroad/risk-scorer`](../risk-scorer/). The installer warns if they're missing.
26
+
27
+ ## Usage
28
+
29
+ **Run a session retrospective:**
30
+
31
+ ```
32
+ /wr-retrospective:run-retro
33
+ ```
34
+
35
+ This walks through the session's work, identifies what went well and what didn't, updates `docs/BRIEFING.md`, and creates problem tickets for any failures.
36
+
37
+ The plugin also triggers a reminder via a `Stop` hook when a session ends naturally.
38
+
39
+ ## How It Works
40
+
41
+ | Hook | Trigger | What it does |
42
+ |------|---------|-------------|
43
+ | `check-deps.sh` | Session start | Verifies that `wr-problem` and `wr-risk-scorer` are installed |
44
+ | `retrospective-reminder.sh` | Session end | Reminds you to run a retrospective |
45
+
46
+ ## Updating and Uninstalling
47
+
48
+ ```bash
49
+ npx @windyroad/retrospective --update
50
+ npx @windyroad/retrospective --uninstall
51
+ ```
52
+
53
+ ## Licence
54
+
55
+ [MIT](../../LICENSE)
package/hooks/hooks.json CHANGED
@@ -3,9 +3,6 @@
3
3
  "SessionStart": [
4
4
  { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/bin/check-deps.sh wr-retrospective wr-problem wr-risk-scorer" }] }
5
5
  ],
6
- "PreToolUse": [
7
- { "matcher": "ExitPlanMode", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/review-plan-enforce.sh" }] }
8
- ],
9
6
  "Stop": [
10
7
  { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/retrospective-reminder.sh" }] }
11
8
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/retrospective",
3
- "version": "0.1.2-preview.13",
3
+ "version": "0.1.3-preview.26",
4
4
  "description": "Session retrospectives that update briefings and create problem tickets",
5
5
  "bin": {
6
6
  "windyroad-retrospective": "./bin/install.mjs"
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: wr:retrospective
2
+ name: wr-retrospective:run-retro
3
3
  description: Run a session retrospective. Updates docs/BRIEFING.md with learnings and creates problem tickets for failures and friction.
4
4
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill
5
5
  ---
@@ -1,174 +0,0 @@
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
- }
@@ -1,102 +0,0 @@
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
- }
@@ -1,73 +0,0 @@
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