@windyroad/retrospective 0.16.0 → 0.17.0-preview.279
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/hooks/hooks.json +11 -0
- package/hooks/retrospective-readme-jtbd-currency.sh +203 -0
- package/hooks/test/retrospective-readme-jtbd-currency.bats +263 -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,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-briefing-budgets.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for the docs/briefing/ tree (Tier 3 of
|
|
5
|
+
# ADR-040). Walks <briefing-dir>/<topic>.md files, measures byte size
|
|
6
|
+
# per file, and reports each topic file at or above the configured
|
|
7
|
+
# threshold so run-retro Step 3 can route them through the rotation
|
|
8
|
+
# AskUserQuestion (interactive) or defer to the retro summary (AFK).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# check-briefing-budgets.sh [<briefing-dir>]
|
|
12
|
+
#
|
|
13
|
+
# Default <briefing-dir> is ./docs/briefing.
|
|
14
|
+
# Threshold is read from BRIEFING_TIER3_MAX_BYTES (default 5120 — the
|
|
15
|
+
# upper bound of ADR-040's stated "2-5 KB / topic" Tier 3 envelope).
|
|
16
|
+
#
|
|
17
|
+
# Exit codes:
|
|
18
|
+
# 0 = always (advisory only — overflow is signal, not failure)
|
|
19
|
+
# 2 = parse error (briefing dir missing or unreadable)
|
|
20
|
+
#
|
|
21
|
+
# Output format on overflow (one line per file, terse machine-readable
|
|
22
|
+
# per ADR-038 progressive-disclosure budget):
|
|
23
|
+
# OVER <basename> bytes=<N> threshold=<N>
|
|
24
|
+
#
|
|
25
|
+
# Files at >= 2.0x the threshold also emit a second line that promotes
|
|
26
|
+
# ADR-040's reassessment trigger ("≥ 3 topic files exceed 2× the
|
|
27
|
+
# configured ceiling for ≥ 2 consecutive retro cycles") from
|
|
28
|
+
# policy-revisit-time to per-cycle enforcement on the same threshold:
|
|
29
|
+
# MUST_SPLIT <basename> reason=<code>
|
|
30
|
+
#
|
|
31
|
+
# The MUST_SPLIT line is the "no defer" signal: run-retro Step 3 Tier 3
|
|
32
|
+
# silent-agent rotation is forced to pick split-by-subtopic /
|
|
33
|
+
# split-by-date for these files (the trim-noise / leave-as-is fall-
|
|
34
|
+
# throughs are not eligible). See P145.
|
|
35
|
+
#
|
|
36
|
+
# Output ordering (deterministic for stable retro-summary diffs):
|
|
37
|
+
# 1. All OVER lines, sorted by basename.
|
|
38
|
+
# 2. Then all MUST_SPLIT lines, sorted by basename.
|
|
39
|
+
#
|
|
40
|
+
# Output is empty (no lines) when no topic files exceed the threshold.
|
|
41
|
+
# README.md is excluded from the scan — it is Tier 2, not Tier 3.
|
|
42
|
+
#
|
|
43
|
+
# Read-only — does NOT mutate any briefing file. Rotation is surfaced
|
|
44
|
+
# to the user via run-retro Step 3.
|
|
45
|
+
#
|
|
46
|
+
# @problem P099 (initial OVER advisory)
|
|
47
|
+
# @problem P145 (MUST_SPLIT escalation — closes the defer-recurrence gap)
|
|
48
|
+
# @adr ADR-040 (Session-start briefing surface — Tier 3 budget; this
|
|
49
|
+
# script promotes Tier 3 from informational to advisory enforcement;
|
|
50
|
+
# MUST_SPLIT promotes the 2× reassessment trigger to per-cycle)
|
|
51
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
52
|
+
# @adr ADR-013 (Rule 1 / Rule 6 — interactive vs AFK)
|
|
53
|
+
# @adr ADR-044 (Decision-delegation contract — MUST_SPLIT is framework-
|
|
54
|
+
# resolved removal of the do-nothing options when ratio is decisive)
|
|
55
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
56
|
+
# @jtbd JTBD-001 / JTBD-006 / JTBD-101
|
|
57
|
+
|
|
58
|
+
set -uo pipefail
|
|
59
|
+
|
|
60
|
+
BRIEFING_DIR="${1:-docs/briefing}"
|
|
61
|
+
THRESHOLD="${BRIEFING_TIER3_MAX_BYTES:-5120}"
|
|
62
|
+
|
|
63
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
if [ ! -d "$BRIEFING_DIR" ]; then
|
|
66
|
+
echo "check-briefing-budgets: briefing dir not found: $BRIEFING_DIR" >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ── Scan ────────────────────────────────────────────────────────────────────
|
|
71
|
+
# Iterate markdown files at the top level of BRIEFING_DIR (not recursive).
|
|
72
|
+
# Sort by basename for stable diff output (per bats fixture contract).
|
|
73
|
+
|
|
74
|
+
shopt -s nullglob
|
|
75
|
+
files=("$BRIEFING_DIR"/*.md)
|
|
76
|
+
shopt -u nullglob
|
|
77
|
+
|
|
78
|
+
if [ "${#files[@]}" -eq 0 ]; then
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Build (basename, bytes) pairs sorted by basename.
|
|
83
|
+
declare -a entries=()
|
|
84
|
+
for path in "${files[@]}"; do
|
|
85
|
+
base="$(basename "$path")"
|
|
86
|
+
# README.md is the Tier 2 index, not a Tier 3 topic file.
|
|
87
|
+
if [ "$base" = "README.md" ]; then
|
|
88
|
+
continue
|
|
89
|
+
fi
|
|
90
|
+
bytes=$(wc -c < "$path" | tr -d ' ')
|
|
91
|
+
entries+=("$base $bytes")
|
|
92
|
+
done
|
|
93
|
+
|
|
94
|
+
# Sort entries by basename
|
|
95
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${entries[@]}" | sort))
|
|
96
|
+
unset IFS
|
|
97
|
+
|
|
98
|
+
# Pass 1: emit OVER lines for every file at or above threshold.
|
|
99
|
+
# Track MUST_SPLIT candidates (ratio >= 2.0x) for pass 2.
|
|
100
|
+
declare -a must_split=()
|
|
101
|
+
for entry in "${sorted[@]}"; do
|
|
102
|
+
base="${entry% *}"
|
|
103
|
+
bytes="${entry##* }"
|
|
104
|
+
if [ "$bytes" -ge "$THRESHOLD" ]; then
|
|
105
|
+
echo "OVER $base bytes=$bytes threshold=$THRESHOLD"
|
|
106
|
+
# Integer-arithmetic ratio test: bytes >= 2 * threshold.
|
|
107
|
+
# Avoids float math; exact at the 2.0x boundary per ADR-040
|
|
108
|
+
# reassessment trigger.
|
|
109
|
+
if [ "$bytes" -ge "$(( THRESHOLD * 2 ))" ]; then
|
|
110
|
+
must_split+=("$base")
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
# Pass 2: emit MUST_SPLIT lines (already sorted by pass-1 traversal order
|
|
116
|
+
# which is basename-sorted).
|
|
117
|
+
for base in "${must_split[@]}"; do
|
|
118
|
+
echo "MUST_SPLIT $base reason=ratio-exceeds-2x"
|
|
119
|
+
done
|
|
120
|
+
|
|
121
|
+
exit 0
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-internal-id-leaks.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for plugin-published internal-ID leaks per
|
|
5
|
+
# ADR-055 (Plugin-published artefacts use namespace-prefixed permalinks).
|
|
6
|
+
#
|
|
7
|
+
# Walks shipped-artefact surfaces under `<root-dir>/packages/<plugin>/`:
|
|
8
|
+
# - skills/<skill>/SKILL.md
|
|
9
|
+
# - agents/*.md
|
|
10
|
+
# - hooks/*.sh
|
|
11
|
+
# - CHANGELOG.md
|
|
12
|
+
#
|
|
13
|
+
# Reports each artefact carrying bare internal-ID tokens that lack the
|
|
14
|
+
# `WR-` namespace prefix. Bare tokens are tokens that resolve correctly
|
|
15
|
+
# only inside the windyroad-claude-plugin source repo's docs/decisions/,
|
|
16
|
+
# docs/jtbd/, and docs/problems/ trees — adopter projects either find
|
|
17
|
+
# nothing (failure mode 1, benign) or resolve to UNRELATED IDs in the
|
|
18
|
+
# adopter's own tree (failure mode 3, dangerous).
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# check-internal-id-leaks.sh [<root-dir>]
|
|
22
|
+
#
|
|
23
|
+
# Default <root-dir> is `.`.
|
|
24
|
+
#
|
|
25
|
+
# Token forms detected (case-sensitive, word-boundary):
|
|
26
|
+
# ADR-NNN (3+ digits)
|
|
27
|
+
# JTBD-NNN (3+ digits)
|
|
28
|
+
# PNNN (exactly 3 digits — problem ticket form)
|
|
29
|
+
#
|
|
30
|
+
# Tokens that DO NOT trigger:
|
|
31
|
+
# WR-ADR-NNN, WR-JTBD-NNN, WR-PNNN (namespace-prefixed; the strategy)
|
|
32
|
+
# docstring annotation lines beginning with `# @adr` / `# @jtbd` /
|
|
33
|
+
# `# @problem` (maintainer-facing source annotations, never expanded
|
|
34
|
+
# into adopter agent context per ADR-055 §Scope)
|
|
35
|
+
#
|
|
36
|
+
# Files NOT scanned:
|
|
37
|
+
# REFERENCE.md sibling files (lazy-loaded maintainer surface per ADR-054)
|
|
38
|
+
#
|
|
39
|
+
# Exit codes:
|
|
40
|
+
# 0 = always (advisory only — drift is signal, not failure)
|
|
41
|
+
# 2 = parse error (root dir missing or unreadable)
|
|
42
|
+
#
|
|
43
|
+
# Output format on drift (one line per file with leaks, terse machine-
|
|
44
|
+
# readable per ADR-038 progressive-disclosure budget):
|
|
45
|
+
# OVER <plugin>/<relative-path> bare_count=<N>
|
|
46
|
+
#
|
|
47
|
+
# Followed by a final aggregate summary line:
|
|
48
|
+
# TOTAL packages=<N> with_leaks=<M> drift_instances=<K>
|
|
49
|
+
#
|
|
50
|
+
# Output is empty (no lines) when no shipped artefact carries bare
|
|
51
|
+
# tokens — silent-on-pass per ADR-045 hook injection budget discipline.
|
|
52
|
+
#
|
|
53
|
+
# Output ordering (deterministic for stable retro-summary diffs):
|
|
54
|
+
# OVER lines sorted by `<plugin>/<relative-path>` identifier.
|
|
55
|
+
# TOTAL line last.
|
|
56
|
+
#
|
|
57
|
+
# Read-only — does NOT mutate any artefact. Per ADR-052, the bats fixture
|
|
58
|
+
# at scripts/test/check-internal-id-leaks.bats is BEHAVIOURAL — asserts
|
|
59
|
+
# script output on temp-fixture trees, NOT script source content.
|
|
60
|
+
#
|
|
61
|
+
# @problem P137 (Plugin-published artefacts reference internal IDs that
|
|
62
|
+
# adopter projects can't resolve)
|
|
63
|
+
# @adr ADR-055 (Plugin-published artefacts use namespace-prefixed
|
|
64
|
+
# permalinks — strategy + advisory detector)
|
|
65
|
+
# @adr ADR-038 (Progressive disclosure — terse machine-readable signal)
|
|
66
|
+
# @adr ADR-045 (Hook injection budget — silent-on-pass discipline)
|
|
67
|
+
# @adr ADR-052 (Behavioural-tests-default — fixture pattern)
|
|
68
|
+
# @adr ADR-054 (SKILL.md runtime budget — REFERENCE.md exclusion source)
|
|
69
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
70
|
+
# @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just
|
|
71
|
+
# Installed — semantic correctness axis of adopter-facing content)
|
|
72
|
+
|
|
73
|
+
set -uo pipefail
|
|
74
|
+
|
|
75
|
+
ROOT_DIR="${1:-.}"
|
|
76
|
+
|
|
77
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
if [ ! -d "$ROOT_DIR" ]; then
|
|
80
|
+
echo "check-internal-id-leaks: root dir not found: $ROOT_DIR" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Collect artefact paths ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
shopt -s nullglob
|
|
87
|
+
declare -a artefacts=()
|
|
88
|
+
for path in "$ROOT_DIR"/packages/*/skills/*/SKILL.md; do
|
|
89
|
+
artefacts+=("$path")
|
|
90
|
+
done
|
|
91
|
+
for path in "$ROOT_DIR"/packages/*/agents/*.md; do
|
|
92
|
+
artefacts+=("$path")
|
|
93
|
+
done
|
|
94
|
+
for path in "$ROOT_DIR"/packages/*/hooks/*.sh; do
|
|
95
|
+
artefacts+=("$path")
|
|
96
|
+
done
|
|
97
|
+
for path in "$ROOT_DIR"/packages/*/CHANGELOG.md; do
|
|
98
|
+
artefacts+=("$path")
|
|
99
|
+
done
|
|
100
|
+
shopt -u nullglob
|
|
101
|
+
|
|
102
|
+
if [ "${#artefacts[@]}" -eq 0 ]; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# ── Count bare tokens per file ──────────────────────────────────────────────
|
|
107
|
+
# Algorithm (perl one-liner per file):
|
|
108
|
+
# 1. Skip lines matching `^\s*#\s*@(adr|jtbd|problem)\b` (docstring annotations).
|
|
109
|
+
# 2. On surviving lines, find all `(?<!WR-)\b(ADR-\d{3,}|JTBD-\d{3,}|P\d{3})\b`
|
|
110
|
+
# matches and increment a counter.
|
|
111
|
+
# 3. Print the counter as a single integer.
|
|
112
|
+
#
|
|
113
|
+
# Negative lookbehind `(?<!WR-)` is safe in perl. Word-boundary `\b` keeps
|
|
114
|
+
# `WR-ADR-014` matched only at position 3 (after `WR-`), which the
|
|
115
|
+
# lookbehind then rejects. Bare `ADR-014` has start-of-string or non-WR-
|
|
116
|
+
# char before, lookbehind passes, match counts.
|
|
117
|
+
#
|
|
118
|
+
# Word-boundary on the `P\d{3}` form requires \b on both ends so that
|
|
119
|
+
# `Phase` (no digit follows) and `P3` (only 1 digit) don't match.
|
|
120
|
+
|
|
121
|
+
declare -a leaks=()
|
|
122
|
+
declare -A package_set=()
|
|
123
|
+
declare -i total_drift=0
|
|
124
|
+
|
|
125
|
+
# Strip ROOT_DIR + trailing slash for relative-path display.
|
|
126
|
+
strip_root() {
|
|
127
|
+
local full="$1"
|
|
128
|
+
local prefix="$ROOT_DIR/packages/"
|
|
129
|
+
echo "${full#"$prefix"}"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for path in "${artefacts[@]}"; do
|
|
133
|
+
count=$(perl -ne '
|
|
134
|
+
next if /^\s*#\s*\@(adr|jtbd|problem)\b/i;
|
|
135
|
+
while (/(?<!WR-)\b(ADR-\d{3,}|JTBD-\d{3,}|P\d{3})\b/g) {
|
|
136
|
+
$n++;
|
|
137
|
+
}
|
|
138
|
+
END { print $n // 0 }
|
|
139
|
+
' "$path")
|
|
140
|
+
|
|
141
|
+
if [ "$count" -gt 0 ]; then
|
|
142
|
+
rel="$(strip_root "$path")"
|
|
143
|
+
leaks+=("$rel $count")
|
|
144
|
+
plugin="${rel%%/*}"
|
|
145
|
+
package_set["$plugin"]=1
|
|
146
|
+
total_drift=$((total_drift + count))
|
|
147
|
+
fi
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
if [ "${#leaks[@]}" -eq 0 ]; then
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# ── Emit OVER lines (sorted) ────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${leaks[@]}" | sort))
|
|
157
|
+
unset IFS
|
|
158
|
+
|
|
159
|
+
for entry in "${sorted[@]}"; do
|
|
160
|
+
identifier="${entry% *}"
|
|
161
|
+
bare="${entry##* }"
|
|
162
|
+
echo "OVER $identifier bare_count=$bare"
|
|
163
|
+
done
|
|
164
|
+
|
|
165
|
+
# ── Emit TOTAL summary ──────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
echo "TOTAL packages=${#package_set[@]} with_leaks=${#leaks[@]} drift_instances=$total_drift"
|
|
168
|
+
|
|
169
|
+
exit 0
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-readme-jtbd-currency.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for ADR-051 (JTBD-anchored README rule).
|
|
5
|
+
# Walks packages/*/README.md and emits a drift signal per package:
|
|
6
|
+
#
|
|
7
|
+
# - has_jtbd_anchor=<yes|no> — at least one JTBD-\d{3} match in the README
|
|
8
|
+
# - cited_jobs=<count> — count of distinct JTBD IDs cited
|
|
9
|
+
# - known_jobs=<count> — count of cited IDs that resolve to a current
|
|
10
|
+
# docs/jtbd/<persona>/JTBD-NNN-*.md (any status)
|
|
11
|
+
# - drift_hints=<comma-list> — signal vocabulary:
|
|
12
|
+
# missing-jtbd-section (no JTBD-\d{3} at all)
|
|
13
|
+
# stale-jtbd-citation (cited ID has no resolving file)
|
|
14
|
+
# deprecated-jtbd-citation (cited ID resolves only to .deprecated.md / .superseded.md)
|
|
15
|
+
# skill-inventory-drift (a directory under packages/<plugin>/skills/ is not named in README)
|
|
16
|
+
#
|
|
17
|
+
# Plus a trailing TOTAL line summarising the window:
|
|
18
|
+
# TOTAL packages=<N> with_jtbd=<M> drift_instances=<K>
|
|
19
|
+
#
|
|
20
|
+
# Exit code is always 0 — the script is advisory per ADR-013 Rule 6
|
|
21
|
+
# fail-safe / ADR-040 declarative-first / ADR-051 Phase 1.
|
|
22
|
+
# Drift count is emitted as data on stdout; downstream consumers
|
|
23
|
+
# (run-retro Step 2b future wiring, release-pre-flight habit, Phase 2
|
|
24
|
+
# escalation per ADR-051 Phase 2 criterion) decide whether to act.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
# check-readme-jtbd-currency.sh [<packages-dir>] [<jtbd-dir>]
|
|
28
|
+
#
|
|
29
|
+
# Defaults:
|
|
30
|
+
# <packages-dir> = ./packages
|
|
31
|
+
# <jtbd-dir> = ./docs/jtbd
|
|
32
|
+
#
|
|
33
|
+
# Exit codes:
|
|
34
|
+
# 0 = always (advisory only — count is signal, not failure)
|
|
35
|
+
# 2 = parse error (packages-dir or jtbd-dir missing or unreadable)
|
|
36
|
+
#
|
|
37
|
+
# Output format (one line per package, alphabetical):
|
|
38
|
+
# README package=<name> has_jtbd_anchor=<yes|no> cited_jobs=<N> known_jobs=<M> drift_hints=<csv>
|
|
39
|
+
#
|
|
40
|
+
# @problem P152 (No pressure or nudge for documentation currency — the driver problem)
|
|
41
|
+
# @adr ADR-051 (JTBD-anchored README with declarative drift advisory — this script's normative source)
|
|
42
|
+
# @adr ADR-008 (JTBD directory structure — the resolution target layout)
|
|
43
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory script never blocks AFK)
|
|
44
|
+
# @adr ADR-040 (declarative-first / advisory-then-escalate precedent)
|
|
45
|
+
# @adr ADR-049 (bin/-on-PATH script resolution — paired wr-retrospective-check-readme-jtbd-currency shim)
|
|
46
|
+
# @adr ADR-005 / ADR-037 (Plugin testing strategy — behavioural tests via bats)
|
|
47
|
+
# @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just Installed — primary served job)
|
|
48
|
+
# @jtbd JTBD-007 (Keep Plugins Current Across Projects — currency expansion)
|
|
49
|
+
# @jtbd JTBD-101 (Extend the Suite with New Plugins — clear patterns the detector documents)
|
|
50
|
+
|
|
51
|
+
set -uo pipefail
|
|
52
|
+
|
|
53
|
+
PACKAGES_DIR="${1:-packages}"
|
|
54
|
+
JTBD_DIR="${2:-docs/jtbd}"
|
|
55
|
+
|
|
56
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
if [ ! -d "$PACKAGES_DIR" ]; then
|
|
59
|
+
echo "check-readme-jtbd-currency: packages dir not found: $PACKAGES_DIR" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [ ! -d "$JTBD_DIR" ]; then
|
|
64
|
+
echo "check-readme-jtbd-currency: jtbd dir not found: $JTBD_DIR" >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ── Build the JTBD index ────────────────────────────────────────────────────
|
|
69
|
+
# Map JTBD-NNN -> comma-separated status suffixes (proposed|validated|deprecated|superseded)
|
|
70
|
+
# A JTBD ID may resolve to multiple files (e.g. during a status transition);
|
|
71
|
+
# we record ALL status suffixes per ID for downstream use.
|
|
72
|
+
|
|
73
|
+
declare -A JTBD_STATUS_BY_ID
|
|
74
|
+
declare -A JTBD_RESOLVED
|
|
75
|
+
|
|
76
|
+
for jpath in "$JTBD_DIR"/*/JTBD-*.md; do
|
|
77
|
+
[ -e "$jpath" ] || continue
|
|
78
|
+
base="$(basename "$jpath")"
|
|
79
|
+
if [[ "$base" =~ ^(JTBD-[0-9]{3})-.*\.([a-z]+)\.md$ ]]; then
|
|
80
|
+
id="${BASH_REMATCH[1]}"
|
|
81
|
+
status="${BASH_REMATCH[2]}"
|
|
82
|
+
JTBD_RESOLVED["$id"]=1
|
|
83
|
+
if [ -z "${JTBD_STATUS_BY_ID[$id]:-}" ]; then
|
|
84
|
+
JTBD_STATUS_BY_ID["$id"]="$status"
|
|
85
|
+
else
|
|
86
|
+
JTBD_STATUS_BY_ID["$id"]="${JTBD_STATUS_BY_ID[$id]},$status"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
append_hint() {
|
|
94
|
+
local current="$1"
|
|
95
|
+
local hint="$2"
|
|
96
|
+
if [ -z "$current" ]; then
|
|
97
|
+
echo "$hint"
|
|
98
|
+
elif [[ ",$current," == *",$hint,"* ]]; then
|
|
99
|
+
echo "$current"
|
|
100
|
+
else
|
|
101
|
+
echo "$current,$hint"
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
is_deprecated_only() {
|
|
106
|
+
local statuses="$1"
|
|
107
|
+
IFS=',' read -ra arr <<< "$statuses"
|
|
108
|
+
for s in "${arr[@]}"; do
|
|
109
|
+
case "$s" in
|
|
110
|
+
deprecated|superseded) ;;
|
|
111
|
+
*) return 1 ;;
|
|
112
|
+
esac
|
|
113
|
+
done
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# ── Scan packages ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
total_packages=0
|
|
120
|
+
total_with_jtbd=0
|
|
121
|
+
total_drift_instances=0
|
|
122
|
+
|
|
123
|
+
package_dirs=()
|
|
124
|
+
for pdir in "$PACKAGES_DIR"/*/; do
|
|
125
|
+
[ -d "$pdir" ] || continue
|
|
126
|
+
package_dirs+=("$pdir")
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
if [ "${#package_dirs[@]}" -eq 0 ]; then
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
IFS=$'\n' sorted_dirs=($(printf '%s\n' "${package_dirs[@]}" | sort))
|
|
134
|
+
unset IFS
|
|
135
|
+
|
|
136
|
+
for pdir in "${sorted_dirs[@]}"; do
|
|
137
|
+
package="$(basename "$pdir")"
|
|
138
|
+
readme="$pdir/README.md"
|
|
139
|
+
|
|
140
|
+
# Skip packages without a README — out of scope
|
|
141
|
+
[ -f "$readme" ] || continue
|
|
142
|
+
|
|
143
|
+
total_packages=$(( total_packages + 1 ))
|
|
144
|
+
|
|
145
|
+
# Extract distinct JTBD-NNN matches from the README
|
|
146
|
+
cited_ids=()
|
|
147
|
+
while IFS= read -r id; do
|
|
148
|
+
[ -z "$id" ] && continue
|
|
149
|
+
cited_ids+=("$id")
|
|
150
|
+
done < <(grep -oE 'JTBD-[0-9]{3}' "$readme" 2>/dev/null | sort -u)
|
|
151
|
+
|
|
152
|
+
cited_count="${#cited_ids[@]}"
|
|
153
|
+
|
|
154
|
+
has_anchor="no"
|
|
155
|
+
if [ "$cited_count" -gt 0 ]; then
|
|
156
|
+
has_anchor="yes"
|
|
157
|
+
total_with_jtbd=$(( total_with_jtbd + 1 ))
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
hints=""
|
|
161
|
+
known_count=0
|
|
162
|
+
|
|
163
|
+
if [ "$cited_count" -eq 0 ]; then
|
|
164
|
+
hints=$(append_hint "$hints" "missing-jtbd-section")
|
|
165
|
+
else
|
|
166
|
+
has_stale=0
|
|
167
|
+
has_deprecated_only=0
|
|
168
|
+
for id in "${cited_ids[@]}"; do
|
|
169
|
+
if [ -n "${JTBD_RESOLVED[$id]:-}" ]; then
|
|
170
|
+
known_count=$(( known_count + 1 ))
|
|
171
|
+
if is_deprecated_only "${JTBD_STATUS_BY_ID[$id]}"; then
|
|
172
|
+
has_deprecated_only=1
|
|
173
|
+
fi
|
|
174
|
+
else
|
|
175
|
+
has_stale=1
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
[ "$has_stale" -eq 1 ] && hints=$(append_hint "$hints" "stale-jtbd-citation")
|
|
179
|
+
[ "$has_deprecated_only" -eq 1 ] && hints=$(append_hint "$hints" "deprecated-jtbd-citation")
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Soft heuristic: skill inventory drift — every directory under
|
|
183
|
+
# packages/<plugin>/skills/ should be named in the README.
|
|
184
|
+
if [ -d "$pdir/skills" ]; then
|
|
185
|
+
for sdir in "$pdir/skills"/*/; do
|
|
186
|
+
[ -d "$sdir" ] || continue
|
|
187
|
+
skill="$(basename "$sdir")"
|
|
188
|
+
if ! grep -q -F "$skill" "$readme" 2>/dev/null; then
|
|
189
|
+
hints=$(append_hint "$hints" "skill-inventory-drift")
|
|
190
|
+
break
|
|
191
|
+
fi
|
|
192
|
+
done
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Drift instance: any non-empty hint set
|
|
196
|
+
if [ -n "$hints" ]; then
|
|
197
|
+
total_drift_instances=$(( total_drift_instances + 1 ))
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
echo "README package=$package has_jtbd_anchor=$has_anchor cited_jobs=$cited_count known_jobs=$known_count drift_hints=$hints"
|
|
201
|
+
done
|
|
202
|
+
|
|
203
|
+
if [ "$total_packages" -gt 0 ]; then
|
|
204
|
+
echo "TOTAL packages=$total_packages with_jtbd=$total_with_jtbd drift_instances=$total_drift_instances"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
exit 0
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-skill-md-budgets.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for SKILL.md byte budgets per ADR-054.
|
|
5
|
+
# Walks `<root-dir>/packages/*/skills/*/SKILL.md` and
|
|
6
|
+
# `<root-dir>/.claude/skills/*/SKILL.md`, measures byte size per file, and
|
|
7
|
+
# reports each SKILL.md exceeding the WARN threshold so retro Step 2b can
|
|
8
|
+
# surface the rotation candidate (interactive) or defer to retro summary
|
|
9
|
+
# (AFK).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# check-skill-md-budgets.sh [<root-dir>]
|
|
13
|
+
#
|
|
14
|
+
# Default <root-dir> is `.`.
|
|
15
|
+
# Thresholds:
|
|
16
|
+
# WARN ≥ SKILL_MD_WARN_BYTES (default 8192)
|
|
17
|
+
# MUST_SPLIT ≥ SKILL_MD_MUST_SPLIT_BYTES (default 16384)
|
|
18
|
+
#
|
|
19
|
+
# Exit codes:
|
|
20
|
+
# 0 = always (advisory only — overflow is signal, not failure)
|
|
21
|
+
# 2 = parse error (root dir missing or unreadable)
|
|
22
|
+
#
|
|
23
|
+
# Output format on overflow (one line per file, terse machine-readable
|
|
24
|
+
# per ADR-038 progressive-disclosure budget):
|
|
25
|
+
# OVER <plugin>/<skill> bytes=<N> threshold=<N>
|
|
26
|
+
#
|
|
27
|
+
# Files at >= MUST_SPLIT also emit a second line:
|
|
28
|
+
# MUST_SPLIT <plugin>/<skill> reason=<code>
|
|
29
|
+
#
|
|
30
|
+
# This mirrors the OVER / MUST_SPLIT pair shape from `check-briefing-budgets.sh`
|
|
31
|
+
# (P099 / P145 / ADR-040) deliberately so adopters learn one concept across
|
|
32
|
+
# two surfaces.
|
|
33
|
+
#
|
|
34
|
+
# Output ordering (deterministic for stable retro-summary diffs):
|
|
35
|
+
# 1. All OVER lines, sorted by `<plugin>/<skill>` identifier.
|
|
36
|
+
# 2. Then all MUST_SPLIT lines, sorted by identifier.
|
|
37
|
+
#
|
|
38
|
+
# Output is empty (no lines) when no SKILL.md exceeds the WARN threshold.
|
|
39
|
+
# REFERENCE.md sibling files (per ADR-054) are excluded from the scan —
|
|
40
|
+
# they are intentionally lazy-loaded and not subject to the runtime budget.
|
|
41
|
+
#
|
|
42
|
+
# Read-only — does NOT mutate any SKILL.md file.
|
|
43
|
+
#
|
|
44
|
+
# @problem P097 (initial advisory — SKILL.md runtime budget surface)
|
|
45
|
+
# @adr ADR-054 (SKILL.md runtime budget policy — taxonomy + sibling pattern + budget)
|
|
46
|
+
# @adr ADR-040 (Session-start briefing surface — Tier 3 OVER / MUST_SPLIT vocabulary precedent)
|
|
47
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
48
|
+
# @adr ADR-052 (Behavioural-tests-default — fixture is behavioural)
|
|
49
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
50
|
+
# @jtbd JTBD-001 / JTBD-006 / JTBD-101
|
|
51
|
+
|
|
52
|
+
set -uo pipefail
|
|
53
|
+
|
|
54
|
+
ROOT_DIR="${1:-.}"
|
|
55
|
+
WARN_BYTES="${SKILL_MD_WARN_BYTES:-8192}"
|
|
56
|
+
MUST_SPLIT_BYTES="${SKILL_MD_MUST_SPLIT_BYTES:-16384}"
|
|
57
|
+
|
|
58
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
if [ ! -d "$ROOT_DIR" ]; then
|
|
61
|
+
echo "check-skill-md-budgets: root dir not found: $ROOT_DIR" >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ── Scan ────────────────────────────────────────────────────────────────────
|
|
66
|
+
# Collect SKILL.md paths from two surfaces:
|
|
67
|
+
# 1. <root>/packages/*/skills/*/SKILL.md
|
|
68
|
+
# 2. <root>/.claude/skills/*/SKILL.md
|
|
69
|
+
# REFERENCE.md siblings are NOT scanned (per ADR-054).
|
|
70
|
+
|
|
71
|
+
shopt -s nullglob
|
|
72
|
+
plugin_skills=("$ROOT_DIR"/packages/*/skills/*/SKILL.md)
|
|
73
|
+
local_skills=("$ROOT_DIR"/.claude/skills/*/SKILL.md)
|
|
74
|
+
shopt -u nullglob
|
|
75
|
+
|
|
76
|
+
if [ "${#plugin_skills[@]}" -eq 0 ] && [ "${#local_skills[@]}" -eq 0 ]; then
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Build (identifier, bytes) pairs.
|
|
81
|
+
# Identifier shape:
|
|
82
|
+
# plugin-skill: <plugin>/<skill> (e.g. "itil/manage-problem")
|
|
83
|
+
# project-local: .claude/<skill> (e.g. ".claude/install-updates")
|
|
84
|
+
declare -a entries=()
|
|
85
|
+
for path in "${plugin_skills[@]}"; do
|
|
86
|
+
# Path shape: <root>/packages/<plugin>/skills/<skill>/SKILL.md
|
|
87
|
+
skill_dir="$(dirname "$path")"
|
|
88
|
+
skill="$(basename "$skill_dir")"
|
|
89
|
+
plugin="$(basename "$(dirname "$(dirname "$skill_dir")")")"
|
|
90
|
+
identifier="$plugin/$skill"
|
|
91
|
+
bytes=$(wc -c < "$path" | tr -d ' ')
|
|
92
|
+
entries+=("$identifier $bytes")
|
|
93
|
+
done
|
|
94
|
+
for path in "${local_skills[@]}"; do
|
|
95
|
+
# Path shape: <root>/.claude/skills/<skill>/SKILL.md
|
|
96
|
+
skill_dir="$(dirname "$path")"
|
|
97
|
+
skill="$(basename "$skill_dir")"
|
|
98
|
+
identifier=".claude/$skill"
|
|
99
|
+
bytes=$(wc -c < "$path" | tr -d ' ')
|
|
100
|
+
entries+=("$identifier $bytes")
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
if [ "${#entries[@]}" -eq 0 ]; then
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Sort entries by identifier for deterministic output
|
|
108
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${entries[@]}" | sort))
|
|
109
|
+
unset IFS
|
|
110
|
+
|
|
111
|
+
# Pass 1: emit OVER lines for every file at or above WARN threshold.
|
|
112
|
+
# Track MUST_SPLIT candidates (>= MUST_SPLIT_BYTES) for pass 2.
|
|
113
|
+
declare -a must_split=()
|
|
114
|
+
for entry in "${sorted[@]}"; do
|
|
115
|
+
identifier="${entry% *}"
|
|
116
|
+
bytes="${entry##* }"
|
|
117
|
+
if [ "$bytes" -ge "$WARN_BYTES" ]; then
|
|
118
|
+
echo "OVER $identifier bytes=$bytes threshold=$WARN_BYTES"
|
|
119
|
+
if [ "$bytes" -ge "$MUST_SPLIT_BYTES" ]; then
|
|
120
|
+
must_split+=("$identifier")
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
|
|
125
|
+
# Pass 2: emit MUST_SPLIT lines (already in basename-sorted order from pass 1).
|
|
126
|
+
for identifier in "${must_split[@]}"; do
|
|
127
|
+
echo "MUST_SPLIT $identifier reason=ratio-exceeds-must-split"
|
|
128
|
+
done
|
|
129
|
+
|
|
130
|
+
exit 0
|