@windyroad/retrospective 0.16.0 → 0.17.0-preview.277
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 +1 -1
- package/bin/wr-retrospective-check-tarball-shipped-shims +2 -0
- package/package.json +2 -1
- package/scripts/check-ask-hygiene.sh +163 -0
- package/scripts/check-briefing-budgets.sh +121 -0
- package/scripts/check-internal-id-leaks.sh +169 -0
- package/scripts/check-readme-jtbd-currency.sh +207 -0
- package/scripts/check-skill-md-budgets.sh +130 -0
- package/scripts/check-tarball-shipped-shims.sh +227 -0
- package/scripts/check-tickets-deferred-cause.sh +220 -0
- package/scripts/list-plugin-attribution.sh +139 -0
- package/scripts/measure-context-budget.sh +270 -0
- package/scripts/test/check-ask-hygiene.bats +248 -0
- package/scripts/test/check-briefing-budgets.bats +307 -0
- package/scripts/test/check-internal-id-leaks.bats +286 -0
- package/scripts/test/check-readme-jtbd-currency.bats +217 -0
- package/scripts/test/check-skill-md-budgets.bats +336 -0
- package/scripts/test/check-tarball-shipped-shims.bats +284 -0
- package/scripts/test/check-tickets-deferred-cause.bats +330 -0
- package/scripts/test/list-plugin-attribution.bats +149 -0
- package/scripts/test/measure-context-budget.bats +236 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/list-plugin-attribution.sh
|
|
3
|
+
#
|
|
4
|
+
# Read-only diagnostic for per-plugin hooks/skills byte attribution
|
|
5
|
+
# consumed by the deep-layer skill `/wr-retrospective:analyze-context`
|
|
6
|
+
# (Step 2 — Decompose per-plugin attribution).
|
|
7
|
+
#
|
|
8
|
+
# Replaces the inline `for plugin_dir in packages/*/hooks; do ... done`
|
|
9
|
+
# loops that lived in SKILL.md before P153/ADR-049-reassessment.clause-3.
|
|
10
|
+
# Those loops worked in source-repo dev sessions but expanded to nothing
|
|
11
|
+
# in adopter sessions (no `packages/` dir under adopter project root),
|
|
12
|
+
# emitting zero PLUGIN-HOOKS / PLUGIN-SKILLS rows with no error signal.
|
|
13
|
+
#
|
|
14
|
+
# This helper resolves both modes:
|
|
15
|
+
# 1. Source-tree mode — walk `<project-root>/packages/<plugin>/{hooks,skills}`.
|
|
16
|
+
# 2. Cache-fallback — sniff `$PATH` for entries shaped like
|
|
17
|
+
# `*/cache/<owner>/<plugin>/<version>/bin`
|
|
18
|
+
# and back-walk to each plugin's root.
|
|
19
|
+
# 3. Neither resolves — emit `PLUGIN-ATTRIBUTION not-measured
|
|
20
|
+
# reason=no-plugin-source-resolvable` per ADR-026.
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# list-plugin-attribution.sh [<project-root>]
|
|
24
|
+
#
|
|
25
|
+
# Default <project-root> is the current working directory.
|
|
26
|
+
#
|
|
27
|
+
# Output (one row per plugin, terse machine-readable per ADR-038 ≤150 bytes):
|
|
28
|
+
# PLUGIN-HOOKS <plugin> bytes=<N>
|
|
29
|
+
# PLUGIN-SKILLS <plugin> bytes=<N>
|
|
30
|
+
# PLUGIN-ATTRIBUTION not-measured reason=<reason>
|
|
31
|
+
#
|
|
32
|
+
# Sorted by row-type then plugin name for stable diffs.
|
|
33
|
+
#
|
|
34
|
+
# Exit code: 0 always (advisory only — matches measure-context-budget.sh
|
|
35
|
+
# contract; missing data is signal, not failure).
|
|
36
|
+
#
|
|
37
|
+
# @problem P153
|
|
38
|
+
# @adr ADR-049 (Plugin-bundled scripts via `bin/` on `$PATH` —
|
|
39
|
+
# reassessment-criteria clause 3 explicitly anticipates this surface)
|
|
40
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
41
|
+
# @adr ADR-026 (Agent output grounding — explicit not-measured sentinels
|
|
42
|
+
# when neither resolution mode resolves)
|
|
43
|
+
# @jtbd JTBD-301 (Plugin-user) / JTBD-101 (Plugin-developer)
|
|
44
|
+
|
|
45
|
+
set -uo pipefail
|
|
46
|
+
|
|
47
|
+
PROJECT_ROOT="${1:-$(pwd)}"
|
|
48
|
+
|
|
49
|
+
sum_dir_bytes() {
|
|
50
|
+
local dir="$1"
|
|
51
|
+
local pattern="$2"
|
|
52
|
+
if [ ! -d "$dir" ] || [ ! -r "$dir" ]; then
|
|
53
|
+
echo 0
|
|
54
|
+
return
|
|
55
|
+
fi
|
|
56
|
+
local total=0 b
|
|
57
|
+
while IFS= read -r -d '' f; do
|
|
58
|
+
b=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
|
|
59
|
+
total=$(( total + ${b:-0} ))
|
|
60
|
+
done < <(find "$dir" -type f -name "$pattern" -print0 2>/dev/null)
|
|
61
|
+
echo "$total"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare -a ROWS=()
|
|
65
|
+
|
|
66
|
+
emit_plugin_row() {
|
|
67
|
+
local row_type="$1"
|
|
68
|
+
local plugin="$2"
|
|
69
|
+
local bytes="$3"
|
|
70
|
+
ROWS+=( "$row_type $plugin bytes=$bytes" )
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
source_resolved=0
|
|
74
|
+
|
|
75
|
+
if [ -d "$PROJECT_ROOT/packages" ]; then
|
|
76
|
+
shopt -s nullglob
|
|
77
|
+
hook_dirs=( "$PROJECT_ROOT"/packages/*/hooks )
|
|
78
|
+
skill_dirs=( "$PROJECT_ROOT"/packages/*/skills )
|
|
79
|
+
shopt -u nullglob
|
|
80
|
+
|
|
81
|
+
for d in ${hook_dirs[@]+"${hook_dirs[@]}"}; do
|
|
82
|
+
plugin=$(basename "$(dirname "$d")")
|
|
83
|
+
bytes=$(sum_dir_bytes "$d" '*.sh')
|
|
84
|
+
emit_plugin_row PLUGIN-HOOKS "$plugin" "$bytes"
|
|
85
|
+
source_resolved=1
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
for d in ${skill_dirs[@]+"${skill_dirs[@]}"}; do
|
|
89
|
+
plugin=$(basename "$(dirname "$d")")
|
|
90
|
+
bytes=$(sum_dir_bytes "$d" 'SKILL.md')
|
|
91
|
+
emit_plugin_row PLUGIN-SKILLS "$plugin" "$bytes"
|
|
92
|
+
source_resolved=1
|
|
93
|
+
done
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
cache_resolved=0
|
|
97
|
+
|
|
98
|
+
if [ "$source_resolved" -eq 0 ] && [ -n "${PATH:-}" ]; then
|
|
99
|
+
# bash 3.2 on macOS lacks `declare -A` — track seen plugins in a
|
|
100
|
+
# delimiter-bounded string. Membership probe: case "$SEEN_PLUGINS" in
|
|
101
|
+
# *"|$plugin|"*) ;; esac.
|
|
102
|
+
SEEN_PLUGINS="|"
|
|
103
|
+
IFS=':' read -r -a path_entries <<< "$PATH"
|
|
104
|
+
|
|
105
|
+
for entry in ${path_entries[@]+"${path_entries[@]}"}; do
|
|
106
|
+
[ -z "$entry" ] && continue
|
|
107
|
+
entry="${entry%/}"
|
|
108
|
+
[[ "$entry" == */bin ]] || continue
|
|
109
|
+
[[ "$entry" == */cache/* ]] || continue
|
|
110
|
+
|
|
111
|
+
plugin_root="${entry%/bin}"
|
|
112
|
+
plugin=$(basename "$(dirname "$plugin_root")")
|
|
113
|
+
[ -z "$plugin" ] && continue
|
|
114
|
+
case "$SEEN_PLUGINS" in *"|$plugin|"*) continue ;; esac
|
|
115
|
+
SEEN_PLUGINS="${SEEN_PLUGINS}${plugin}|"
|
|
116
|
+
|
|
117
|
+
hooks_dir="$plugin_root/hooks"
|
|
118
|
+
skills_dir="$plugin_root/skills"
|
|
119
|
+
|
|
120
|
+
if [ -d "$hooks_dir" ]; then
|
|
121
|
+
bytes=$(sum_dir_bytes "$hooks_dir" '*.sh')
|
|
122
|
+
emit_plugin_row PLUGIN-HOOKS "$plugin" "$bytes"
|
|
123
|
+
cache_resolved=1
|
|
124
|
+
fi
|
|
125
|
+
if [ -d "$skills_dir" ]; then
|
|
126
|
+
bytes=$(sum_dir_bytes "$skills_dir" 'SKILL.md')
|
|
127
|
+
emit_plugin_row PLUGIN-SKILLS "$plugin" "$bytes"
|
|
128
|
+
cache_resolved=1
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [ "$source_resolved" -eq 1 ] || [ "$cache_resolved" -eq 1 ]; then
|
|
134
|
+
printf '%s\n' ${ROWS[@]+"${ROWS[@]}"} | LC_ALL=C sort
|
|
135
|
+
else
|
|
136
|
+
echo "PLUGIN-ATTRIBUTION not-measured reason=no-plugin-source-resolvable"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
exit 0
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/measure-context-budget.sh
|
|
3
|
+
#
|
|
4
|
+
# Read-only diagnostic script for context-usage measurement (P101 / ADR-043
|
|
5
|
+
# Progressive context-usage measurement and reporting for retrospective sessions).
|
|
6
|
+
# Walks the session's on-disk context contributors and reports per-source
|
|
7
|
+
# bucket byte totals so run-retro Step 2c can render the cheap-layer table
|
|
8
|
+
# (interactive or AFK; same output shape) and the deep-layer skill can
|
|
9
|
+
# consume the same data as its baseline.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# measure-context-budget.sh [<project-root>]
|
|
13
|
+
#
|
|
14
|
+
# Default <project-root> is $CLAUDE_PROJECT_DIR if set, else the current
|
|
15
|
+
# working directory.
|
|
16
|
+
#
|
|
17
|
+
# Threshold for the optional fail-open ceiling is read from
|
|
18
|
+
# CONTEXT_BUDGET_MAX_BYTES (default 10240 — the 5% / 200K cheap-layer
|
|
19
|
+
# envelope per ADR-043). The script does NOT enforce the threshold; it
|
|
20
|
+
# is exposed for the bats fixture and for Step 2c's defensive trip.
|
|
21
|
+
#
|
|
22
|
+
# Exit codes:
|
|
23
|
+
# 0 = always (advisory only — overflow is signal, not failure)
|
|
24
|
+
# 2 = parse error (project root missing or unreadable)
|
|
25
|
+
#
|
|
26
|
+
# Output format (one line per bucket, terse machine-readable per ADR-038
|
|
27
|
+
# progressive-disclosure budget — ≤150 bytes per row):
|
|
28
|
+
# BUCKET <name> bytes=<N>
|
|
29
|
+
# BUCKET <name> not-measured reason=<reason>
|
|
30
|
+
#
|
|
31
|
+
# The output is sorted by bucket name for stable diffs (per the
|
|
32
|
+
# check-briefing-budgets.sh precedent + bats fixture contract).
|
|
33
|
+
#
|
|
34
|
+
# Read-only — does NOT mutate any project file. Snapshot persistence
|
|
35
|
+
# (HTML-comment trailer in docs/retros/<date>-context-analysis.md) is the
|
|
36
|
+
# deep-layer skill's responsibility, not this script's.
|
|
37
|
+
#
|
|
38
|
+
# @problem P101
|
|
39
|
+
# @adr ADR-043 (Progressive context-usage measurement and reporting for
|
|
40
|
+
# retrospective sessions; this script is the measurement primitive)
|
|
41
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
42
|
+
# @adr ADR-026 (Agent output grounding — explicit not-measured sentinels
|
|
43
|
+
# for surfaces without an on-disk source)
|
|
44
|
+
# @adr ADR-013 (Rule 1 / Rule 6 — interactive vs AFK; this script's
|
|
45
|
+
# advisory exit-0 contract supports both)
|
|
46
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
47
|
+
# @adr ADR-037 (Skill testing strategy — bats-contract precedent)
|
|
48
|
+
# @jtbd JTBD-001 / JTBD-005 / JTBD-006
|
|
49
|
+
|
|
50
|
+
set -uo pipefail
|
|
51
|
+
|
|
52
|
+
PROJECT_ROOT="${1:-${CLAUDE_PROJECT_DIR:-$(pwd)}}"
|
|
53
|
+
THRESHOLD="${CONTEXT_BUDGET_MAX_BYTES:-10240}"
|
|
54
|
+
|
|
55
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
if [ ! -d "$PROJECT_ROOT" ]; then
|
|
58
|
+
echo "measure-context-budget: project root not found: $PROJECT_ROOT" >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Helper — sum byte sizes of files matching a glob. Returns 0 (zero bytes)
|
|
63
|
+
# if the glob matches nothing, distinguishing "scanned, empty" from
|
|
64
|
+
# "surface not present".
|
|
65
|
+
sum_globs() {
|
|
66
|
+
local total=0
|
|
67
|
+
local file
|
|
68
|
+
for pattern in "$@"; do
|
|
69
|
+
# Use shopt nullglob so an empty match expands to nothing
|
|
70
|
+
shopt -s nullglob
|
|
71
|
+
local matches=( $pattern )
|
|
72
|
+
shopt -u nullglob
|
|
73
|
+
for file in "${matches[@]}"; do
|
|
74
|
+
if [ -f "$file" ] && [ -r "$file" ]; then
|
|
75
|
+
local bytes
|
|
76
|
+
bytes=$(wc -c < "$file" 2>/dev/null | tr -d ' ')
|
|
77
|
+
total=$(( total + ${bytes:-0} ))
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
done
|
|
81
|
+
echo "$total"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Helper — emit one bucket row. Use a "not-measured" sentinel when the
|
|
85
|
+
# surface is absent (e.g. project has no docs/jtbd/) per ADR-026's
|
|
86
|
+
# ungrounded-field rule.
|
|
87
|
+
emit_bucket() {
|
|
88
|
+
local name="$1"
|
|
89
|
+
local bytes="$2"
|
|
90
|
+
local present="$3" # 1 if surface present, 0 if absent
|
|
91
|
+
local reason="${4:-source-absent}"
|
|
92
|
+
if [ "$present" = "1" ]; then
|
|
93
|
+
echo "BUCKET $name bytes=$bytes"
|
|
94
|
+
else
|
|
95
|
+
echo "BUCKET $name not-measured reason=$reason"
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ── Bucket: hooks ───────────────────────────────────────────────────────────
|
|
100
|
+
# Aggregate over packages/*/hooks/**/*.sh + project-local .claude/hooks/**/*.sh
|
|
101
|
+
# Surface present if either exists.
|
|
102
|
+
|
|
103
|
+
hooks_present=0
|
|
104
|
+
if [ -d "$PROJECT_ROOT/packages" ] || [ -d "$PROJECT_ROOT/.claude/hooks" ]; then
|
|
105
|
+
hooks_present=1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if [ "$hooks_present" = "1" ]; then
|
|
109
|
+
hooks_bytes=$(
|
|
110
|
+
cd "$PROJECT_ROOT" 2>/dev/null && {
|
|
111
|
+
shopt -s nullglob globstar
|
|
112
|
+
pkg_files=( packages/*/hooks/**/*.sh )
|
|
113
|
+
proj_files=( .claude/hooks/**/*.sh )
|
|
114
|
+
shopt -u nullglob globstar
|
|
115
|
+
total=0
|
|
116
|
+
for f in "${pkg_files[@]}" "${proj_files[@]}"; do
|
|
117
|
+
if [ -f "$f" ] && [ -r "$f" ]; then
|
|
118
|
+
b=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
|
|
119
|
+
total=$(( total + ${b:-0} ))
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
echo "$total"
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
fi
|
|
126
|
+
emit_bucket hooks "${hooks_bytes:-0}" "$hooks_present"
|
|
127
|
+
|
|
128
|
+
# ── Bucket: skills ──────────────────────────────────────────────────────────
|
|
129
|
+
# Aggregate over packages/*/skills/**/SKILL.md + .claude/skills/**/SKILL.md
|
|
130
|
+
|
|
131
|
+
skills_present=0
|
|
132
|
+
if [ -d "$PROJECT_ROOT/packages" ] || [ -d "$PROJECT_ROOT/.claude/skills" ]; then
|
|
133
|
+
skills_present=1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if [ "$skills_present" = "1" ]; then
|
|
137
|
+
skills_bytes=$(
|
|
138
|
+
cd "$PROJECT_ROOT" 2>/dev/null && {
|
|
139
|
+
shopt -s nullglob globstar
|
|
140
|
+
pkg_files=( packages/*/skills/**/SKILL.md )
|
|
141
|
+
proj_files=( .claude/skills/**/SKILL.md )
|
|
142
|
+
shopt -u nullglob globstar
|
|
143
|
+
total=0
|
|
144
|
+
for f in "${pkg_files[@]}" "${proj_files[@]}"; do
|
|
145
|
+
if [ -f "$f" ] && [ -r "$f" ]; then
|
|
146
|
+
b=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
|
|
147
|
+
total=$(( total + ${b:-0} ))
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
echo "$total"
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
fi
|
|
154
|
+
emit_bucket skills "${skills_bytes:-0}" "$skills_present"
|
|
155
|
+
|
|
156
|
+
# ── Bucket: briefing ────────────────────────────────────────────────────────
|
|
157
|
+
# Aggregate over docs/briefing/*.md (top-level only; nested archives are
|
|
158
|
+
# the deep layer's concern). Single bucket row aggregating per-file detail
|
|
159
|
+
# already exposed via P099's check-briefing-budgets.sh.
|
|
160
|
+
|
|
161
|
+
briefing_dir="$PROJECT_ROOT/docs/briefing"
|
|
162
|
+
briefing_present=0
|
|
163
|
+
briefing_bytes=0
|
|
164
|
+
if [ -d "$briefing_dir" ]; then
|
|
165
|
+
briefing_present=1
|
|
166
|
+
briefing_bytes=$( cd "$PROJECT_ROOT" && sum_globs "docs/briefing/*.md" )
|
|
167
|
+
fi
|
|
168
|
+
emit_bucket briefing "$briefing_bytes" "$briefing_present"
|
|
169
|
+
|
|
170
|
+
# ── Bucket: decisions ───────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
decisions_dir="$PROJECT_ROOT/docs/decisions"
|
|
173
|
+
decisions_present=0
|
|
174
|
+
decisions_bytes=0
|
|
175
|
+
if [ -d "$decisions_dir" ]; then
|
|
176
|
+
decisions_present=1
|
|
177
|
+
decisions_bytes=$( cd "$PROJECT_ROOT" && sum_globs "docs/decisions/*.md" )
|
|
178
|
+
fi
|
|
179
|
+
emit_bucket decisions "$decisions_bytes" "$decisions_present"
|
|
180
|
+
|
|
181
|
+
# ── Bucket: problems ────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
problems_dir="$PROJECT_ROOT/docs/problems"
|
|
184
|
+
problems_present=0
|
|
185
|
+
problems_bytes=0
|
|
186
|
+
if [ -d "$problems_dir" ]; then
|
|
187
|
+
problems_present=1
|
|
188
|
+
problems_bytes=$( cd "$PROJECT_ROOT" && sum_globs "docs/problems/*.md" )
|
|
189
|
+
fi
|
|
190
|
+
emit_bucket problems "$problems_bytes" "$problems_present"
|
|
191
|
+
|
|
192
|
+
# ── Bucket: jtbd ────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
jtbd_dir="$PROJECT_ROOT/docs/jtbd"
|
|
195
|
+
jtbd_present=0
|
|
196
|
+
jtbd_bytes=0
|
|
197
|
+
if [ -d "$jtbd_dir" ]; then
|
|
198
|
+
jtbd_present=1
|
|
199
|
+
jtbd_bytes=$(
|
|
200
|
+
cd "$PROJECT_ROOT" 2>/dev/null && {
|
|
201
|
+
shopt -s nullglob globstar
|
|
202
|
+
files=( docs/jtbd/**/*.md )
|
|
203
|
+
shopt -u nullglob globstar
|
|
204
|
+
total=0
|
|
205
|
+
for f in "${files[@]}"; do
|
|
206
|
+
if [ -f "$f" ] && [ -r "$f" ]; then
|
|
207
|
+
b=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
|
|
208
|
+
total=$(( total + ${b:-0} ))
|
|
209
|
+
fi
|
|
210
|
+
done
|
|
211
|
+
echo "$total"
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
fi
|
|
215
|
+
emit_bucket jtbd "$jtbd_bytes" "$jtbd_present"
|
|
216
|
+
|
|
217
|
+
# ── Bucket: project-claude-md ───────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
project_claude_md="$PROJECT_ROOT/CLAUDE.md"
|
|
220
|
+
claude_md_present=0
|
|
221
|
+
claude_md_bytes=0
|
|
222
|
+
if [ -f "$project_claude_md" ] && [ -r "$project_claude_md" ]; then
|
|
223
|
+
claude_md_present=1
|
|
224
|
+
claude_md_bytes=$( wc -c < "$project_claude_md" 2>/dev/null | tr -d ' ' )
|
|
225
|
+
fi
|
|
226
|
+
emit_bucket project-claude-md "${claude_md_bytes:-0}" "$claude_md_present"
|
|
227
|
+
|
|
228
|
+
# ── Bucket: memory ──────────────────────────────────────────────────────────
|
|
229
|
+
# User-owned per-project memory files. Read-only attempt; emit not-measured
|
|
230
|
+
# sentinel when the directory is inaccessible (e.g. the running agent is
|
|
231
|
+
# in a different user account or the path doesn't exist for this project).
|
|
232
|
+
|
|
233
|
+
memory_root="${HOME:-/tmp}/.claude/projects"
|
|
234
|
+
memory_present=0
|
|
235
|
+
memory_bytes=0
|
|
236
|
+
if [ -d "$memory_root" ] && [ -r "$memory_root" ]; then
|
|
237
|
+
# Best-effort: sum *.md files under any subdirectory of memory_root.
|
|
238
|
+
# Per-project filtering is the deep layer's concern.
|
|
239
|
+
memory_present=1
|
|
240
|
+
shopt -s nullglob globstar
|
|
241
|
+
mem_files=( "$memory_root"/**/memory/*.md )
|
|
242
|
+
shopt -u nullglob globstar
|
|
243
|
+
for f in "${mem_files[@]}"; do
|
|
244
|
+
if [ -f "$f" ] && [ -r "$f" ]; then
|
|
245
|
+
b=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
|
|
246
|
+
memory_bytes=$(( memory_bytes + ${b:-0} ))
|
|
247
|
+
fi
|
|
248
|
+
done
|
|
249
|
+
fi
|
|
250
|
+
if [ "$memory_present" = "1" ]; then
|
|
251
|
+
emit_bucket memory "$memory_bytes" 1
|
|
252
|
+
else
|
|
253
|
+
emit_bucket memory 0 0 user-memory-inaccessible
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# ── Bucket: framework-injected ──────────────────────────────────────────────
|
|
257
|
+
# Available-skills, subagent-types, deferred-tools listings are emitted by
|
|
258
|
+
# the framework on every turn but are NOT byte-countable from the project
|
|
259
|
+
# filesystem. Per ADR-026 ungrounded-field rule, emit explicit sentinel.
|
|
260
|
+
|
|
261
|
+
emit_bucket framework-injected 0 0 framework-injected-no-on-disk-source
|
|
262
|
+
|
|
263
|
+
# ── Done ────────────────────────────────────────────────────────────────────
|
|
264
|
+
# Threshold is exposed for callers (Step 2c defensive trip + bats fixture).
|
|
265
|
+
# Echoed as a trailing diagnostic line — callers can grep for `THRESHOLD `
|
|
266
|
+
# to retrieve it without parsing every BUCKET row.
|
|
267
|
+
|
|
268
|
+
echo "THRESHOLD bytes=$THRESHOLD"
|
|
269
|
+
|
|
270
|
+
exit 0
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# packages/retrospective/scripts/test/check-ask-hygiene.bats
|
|
4
|
+
#
|
|
5
|
+
# Behavioural tests for `check-ask-hygiene.sh` — the ask-hygiene trail
|
|
6
|
+
# advisory script (P135 Phase 5 / ADR-044). Mirrors the test pattern of
|
|
7
|
+
# `check-briefing-budgets.bats`.
|
|
8
|
+
#
|
|
9
|
+
# Tests are behavioural per ADR-005 / ADR-037 — they exercise the
|
|
10
|
+
# script end-to-end against fixture trail directories and assert on
|
|
11
|
+
# stdout / stderr / exit shape. No structural greps of the script
|
|
12
|
+
# source itself per ADR-044's deviation-default to behavioural-by-
|
|
13
|
+
# default for skill / script testing.
|
|
14
|
+
#
|
|
15
|
+
# @problem P135 (Phase 5 measurement)
|
|
16
|
+
# @adr ADR-044 (Decision-Delegation Contract — lazy-count metric)
|
|
17
|
+
# @adr ADR-040 (Tier 3 advisory-not-fail-closed)
|
|
18
|
+
# @adr ADR-005 / ADR-037 (Plugin testing strategy — behavioural tests)
|
|
19
|
+
# @jtbd JTBD-001 / JTBD-006 / JTBD-201
|
|
20
|
+
|
|
21
|
+
SCRIPT="${BATS_TEST_DIRNAME}/../check-ask-hygiene.sh"
|
|
22
|
+
|
|
23
|
+
setup() {
|
|
24
|
+
TEST_DIR="$(mktemp -d)"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
teardown() {
|
|
28
|
+
rm -rf "$TEST_DIR"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
@test "script file exists and is executable" {
|
|
34
|
+
[ -f "$SCRIPT" ]
|
|
35
|
+
[ -x "$SCRIPT" ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "missing retros dir exits 2 with error message on stderr" {
|
|
39
|
+
run bash "$SCRIPT" "$TEST_DIR/does-not-exist"
|
|
40
|
+
[ "$status" -eq 2 ]
|
|
41
|
+
[[ "$output" == *"retros dir not found"* ]]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "empty retros dir exits 0 with empty stdout" {
|
|
45
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
46
|
+
[ "$status" -eq 0 ]
|
|
47
|
+
[ -z "$output" ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@test "default retros-dir argument is docs/retros (when omitted)" {
|
|
51
|
+
# Behavioural: script must accept no-arg invocation when invoked from a project root.
|
|
52
|
+
# We exercise from a fresh tmp dir with no docs/retros to assert the missing-dir
|
|
53
|
+
# behaviour fires when the default path is used.
|
|
54
|
+
cd "$TEST_DIR"
|
|
55
|
+
run bash "$SCRIPT"
|
|
56
|
+
[ "$status" -eq 2 ]
|
|
57
|
+
[[ "$output" == *"docs/retros"* ]]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# ── Single-entry behaviour ──────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
@test "single trail entry emits one RETRO line and no TREND" {
|
|
63
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
64
|
+
**Lazy count: 5**
|
|
65
|
+
**Direction count: 2**
|
|
66
|
+
TRAIL
|
|
67
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
68
|
+
[ "$status" -eq 0 ]
|
|
69
|
+
[[ "$output" == *"RETRO 2026-04-27 lazy=5 direction=2"* ]]
|
|
70
|
+
[[ "$output" != *"TREND"* ]]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "trail entry without lazy-count line is skipped silently" {
|
|
74
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
75
|
+
# Some retro file with no lazy count
|
|
76
|
+
**Direction count: 3**
|
|
77
|
+
TRAIL
|
|
78
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
79
|
+
[ "$status" -eq 0 ]
|
|
80
|
+
[ -z "$output" ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ── Multi-entry behaviour ───────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
@test "multiple trail entries emit RETRO lines sorted oldest-first by date" {
|
|
86
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
87
|
+
**Lazy count: 1**
|
|
88
|
+
TRAIL
|
|
89
|
+
cat > "$TEST_DIR/2026-04-25-ask-hygiene.md" <<'TRAIL'
|
|
90
|
+
**Lazy count: 5**
|
|
91
|
+
TRAIL
|
|
92
|
+
cat > "$TEST_DIR/2026-04-26-ask-hygiene.md" <<'TRAIL'
|
|
93
|
+
**Lazy count: 3**
|
|
94
|
+
TRAIL
|
|
95
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
96
|
+
[ "$status" -eq 0 ]
|
|
97
|
+
# Assert order: 04-25 (oldest) → 04-26 → 04-27 (newest)
|
|
98
|
+
line1="${lines[0]}"
|
|
99
|
+
line2="${lines[1]}"
|
|
100
|
+
line3="${lines[2]}"
|
|
101
|
+
[[ "$line1" == *"2026-04-25"* ]]
|
|
102
|
+
[[ "$line2" == *"2026-04-26"* ]]
|
|
103
|
+
[[ "$line3" == *"2026-04-27"* ]]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@test "two-or-more entries emit TREND line with first/last/delta" {
|
|
107
|
+
cat > "$TEST_DIR/2026-04-25-ask-hygiene.md" <<'TRAIL'
|
|
108
|
+
**Lazy count: 5**
|
|
109
|
+
TRAIL
|
|
110
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
111
|
+
**Lazy count: 1**
|
|
112
|
+
TRAIL
|
|
113
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
114
|
+
[ "$status" -eq 0 ]
|
|
115
|
+
[[ "$output" == *"TREND lazy_first=5 lazy_last=1 delta=-4"* ]]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@test "TREND line shows positive delta when lazy count grew" {
|
|
119
|
+
cat > "$TEST_DIR/2026-04-25-ask-hygiene.md" <<'TRAIL'
|
|
120
|
+
**Lazy count: 1**
|
|
121
|
+
TRAIL
|
|
122
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
123
|
+
**Lazy count: 4**
|
|
124
|
+
TRAIL
|
|
125
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
126
|
+
[ "$status" -eq 0 ]
|
|
127
|
+
[[ "$output" == *"TREND lazy_first=1 lazy_last=4 delta=+3"* ]]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@test "TREND line shows zero delta when lazy count unchanged" {
|
|
131
|
+
cat > "$TEST_DIR/2026-04-25-ask-hygiene.md" <<'TRAIL'
|
|
132
|
+
**Lazy count: 2**
|
|
133
|
+
TRAIL
|
|
134
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
135
|
+
**Lazy count: 2**
|
|
136
|
+
TRAIL
|
|
137
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
138
|
+
[ "$status" -eq 0 ]
|
|
139
|
+
[[ "$output" == *"TREND lazy_first=2 lazy_last=2 delta=+0"* ]]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ── Window override ──────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
@test "ASK_HYGIENE_WINDOW=2 keeps only the most-recent 2 entries" {
|
|
145
|
+
for d in 25 26 27; do
|
|
146
|
+
cat > "$TEST_DIR/2026-04-$d-ask-hygiene.md" <<TRAIL
|
|
147
|
+
**Lazy count: $d**
|
|
148
|
+
TRAIL
|
|
149
|
+
done
|
|
150
|
+
ASK_HYGIENE_WINDOW=2 run bash "$SCRIPT" "$TEST_DIR"
|
|
151
|
+
[ "$status" -eq 0 ]
|
|
152
|
+
# Assert: 04-25 not present; 04-26 first; 04-27 last
|
|
153
|
+
[[ "$output" != *"2026-04-25"* ]]
|
|
154
|
+
[[ "$output" == *"RETRO 2026-04-26"* ]]
|
|
155
|
+
[[ "$output" == *"RETRO 2026-04-27"* ]]
|
|
156
|
+
# TREND should reflect the windowed pair, not the full 3
|
|
157
|
+
[[ "$output" == *"TREND lazy_first=26 lazy_last=27 delta=+1"* ]]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@test "default window 10 keeps all entries when fewer than 10 exist" {
|
|
161
|
+
cat > "$TEST_DIR/2026-04-25-ask-hygiene.md" <<'TRAIL'
|
|
162
|
+
**Lazy count: 5**
|
|
163
|
+
TRAIL
|
|
164
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
165
|
+
**Lazy count: 1**
|
|
166
|
+
TRAIL
|
|
167
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
168
|
+
[ "$status" -eq 0 ]
|
|
169
|
+
[[ "$output" == *"2026-04-25"* ]]
|
|
170
|
+
[[ "$output" == *"2026-04-27"* ]]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# ── Category-coverage shape ──────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
@test "RETRO line includes all 6 category counts (lazy + 5 non-lazy)" {
|
|
176
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
177
|
+
**Lazy count: 0**
|
|
178
|
+
**Direction count: 3**
|
|
179
|
+
**Override count: 1**
|
|
180
|
+
**Silent-framework count: 1**
|
|
181
|
+
**Taste count: 0**
|
|
182
|
+
**Correction-followup count: 0**
|
|
183
|
+
TRAIL
|
|
184
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
185
|
+
[ "$status" -eq 0 ]
|
|
186
|
+
[[ "$output" == *"lazy=0"* ]]
|
|
187
|
+
[[ "$output" == *"direction=3"* ]]
|
|
188
|
+
[[ "$output" == *"override=1"* ]]
|
|
189
|
+
[[ "$output" == *"silent=1"* ]]
|
|
190
|
+
[[ "$output" == *"taste=0"* ]]
|
|
191
|
+
[[ "$output" == *"correction=0"* ]]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@test "missing non-lazy categories default to 0" {
|
|
195
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
196
|
+
**Lazy count: 4**
|
|
197
|
+
TRAIL
|
|
198
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
199
|
+
[ "$status" -eq 0 ]
|
|
200
|
+
[[ "$output" == *"lazy=4"* ]]
|
|
201
|
+
[[ "$output" == *"direction=0"* ]]
|
|
202
|
+
[[ "$output" == *"override=0"* ]]
|
|
203
|
+
[[ "$output" == *"silent=0"* ]]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# ── Format tolerance ──────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
@test "lazy-count line works without bold markdown asterisks" {
|
|
209
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
210
|
+
Lazy count: 7
|
|
211
|
+
TRAIL
|
|
212
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
213
|
+
[ "$status" -eq 0 ]
|
|
214
|
+
[[ "$output" == *"lazy=7"* ]]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@test "lazy-count line is case-insensitive on the LAZY label" {
|
|
218
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
219
|
+
**lazy count: 8**
|
|
220
|
+
TRAIL
|
|
221
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
222
|
+
[ "$status" -eq 0 ]
|
|
223
|
+
[[ "$output" == *"lazy=8"* ]]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# ── Cross-shell portability (P124 / P133 lessons) ───────────────────────────
|
|
227
|
+
|
|
228
|
+
@test "script glob iteration uses portable for-loop existence check (no shopt nullglob)" {
|
|
229
|
+
# Behavioural: invoking an empty retros dir must NOT emit a literal
|
|
230
|
+
# `*-ask-hygiene.md` because the glob unexpanded to a literal pattern.
|
|
231
|
+
# The script's portable iteration handles this without zsh `shopt -s nullglob`.
|
|
232
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
233
|
+
[ "$status" -eq 0 ]
|
|
234
|
+
[[ "$output" != *"*-ask-hygiene.md"* ]]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# ── Read-only contract ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
@test "script is read-only — fixture tree unchanged after run" {
|
|
240
|
+
cat > "$TEST_DIR/2026-04-27-ask-hygiene.md" <<'TRAIL'
|
|
241
|
+
**Lazy count: 3**
|
|
242
|
+
TRAIL
|
|
243
|
+
pre_hash=$(find "$TEST_DIR" -type f -exec cksum {} \; 2>/dev/null | sort | cksum | awk '{print $1}')
|
|
244
|
+
run bash "$SCRIPT" "$TEST_DIR"
|
|
245
|
+
[ "$status" -eq 0 ]
|
|
246
|
+
post_hash=$(find "$TEST_DIR" -type f -exec cksum {} \; 2>/dev/null | sort | cksum | awk '{print $1}')
|
|
247
|
+
[ "$pre_hash" = "$post_hash" ]
|
|
248
|
+
}
|