@windyroad/retrospective 0.16.0-preview.273 → 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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-retrospective",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Session retrospective reminders and plan review for Claude Code"
5
5
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/../scripts/check-tarball-shipped-shims.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/retrospective",
3
- "version": "0.16.0-preview.273",
3
+ "version": "0.17.0-preview.277",
4
4
  "description": "Session retrospectives that update briefings and create problem tickets",
5
5
  "bin": {
6
6
  "windyroad-retrospective": "./bin/install.mjs"
@@ -23,6 +23,7 @@
23
23
  "agents/",
24
24
  "hooks/",
25
25
  "skills/",
26
+ "scripts/",
26
27
  ".claude-plugin/",
27
28
  "lib/"
28
29
  ]
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bash
2
+ # packages/retrospective/scripts/check-ask-hygiene.sh
3
+ #
4
+ # Diagnose-only advisory script for the ask-hygiene trail (per Step 2d
5
+ # in run-retro, ADR-044 / P135 Phase 5). Walks docs/retros/*-ask-hygiene.md
6
+ # trail files, extracts the lazy AskUserQuestion count from each, and
7
+ # emits a tabular trend over the last N retros so the user can see
8
+ # whether ADR-044's framework-resolution boundary is taking hold.
9
+ #
10
+ # Usage:
11
+ # check-ask-hygiene.sh [<retros-dir>]
12
+ #
13
+ # Default <retros-dir> is ./docs/retros.
14
+ # Window is read from ASK_HYGIENE_WINDOW (default 10 — last N retros).
15
+ #
16
+ # Exit codes:
17
+ # 0 = always (advisory only — count is signal, not failure)
18
+ # 2 = parse error (retros dir missing or unreadable)
19
+ #
20
+ # Output format on populated trail (one line per retro, oldest first):
21
+ # RETRO <YYYY-MM-DD> lazy=<N> direction=<N> override=<N> silent=<N> taste=<N> correction=<N>
22
+ #
23
+ # Plus a trailing TREND line summarising first vs last lazy count
24
+ # (when 2+ entries are in the window):
25
+ # TREND lazy_first=<N> lazy_last=<N> delta=<+|-N>
26
+ #
27
+ # Output is empty (no lines) when no retro trail entries are found —
28
+ # this is the expected first-run state, not an error.
29
+ #
30
+ # Trail file shape (per Step 2d contract — written by run-retro):
31
+ # docs/retros/<YYYY-MM-DD>-ask-hygiene.md
32
+ #
33
+ # Each trail file MUST contain a single line of the shape:
34
+ # `Lazy count: <N>` (case-insensitive; allows a leading `**` for bold)
35
+ #
36
+ # And SHOULD contain matching counts for each non-lazy category:
37
+ # `Direction count: <N>`
38
+ # `Override count: <N>`
39
+ # `Silent-framework count: <N>`
40
+ # `Taste count: <N>`
41
+ # `Correction-followup count: <N>`
42
+ #
43
+ # The script tolerates missing non-lazy categories (defaults to 0).
44
+ # The lazy-count line is the only required field; without it the file
45
+ # is skipped silently.
46
+ #
47
+ # Read-only — does NOT mutate any retro file.
48
+ #
49
+ # @problem P135 (Phase 5 measurement)
50
+ # @adr ADR-044 (Decision-Delegation Contract — framework-resolution boundary; lazy-count metric is the regression signal)
51
+ # @adr ADR-040 (Tier 3 advisory-not-fail-closed — declarative-first precedent)
52
+ # @adr ADR-038 (Progressive disclosure — per-row byte budget)
53
+ # @adr ADR-026 (Cost-source grounding — trail entries cite specific tool invocations per retro)
54
+ # @adr ADR-013 Rule 5 (Policy-authorised silent proceed — script proceeds silently per ADR-040 advisory pattern)
55
+ # @adr ADR-005 (Plugin testing strategy)
56
+ # @jtbd JTBD-001 / JTBD-006 / JTBD-201
57
+
58
+ set -uo pipefail
59
+
60
+ RETROS_DIR="${1:-docs/retros}"
61
+ WINDOW="${ASK_HYGIENE_WINDOW:-10}"
62
+
63
+ # ── Pre-checks ──────────────────────────────────────────────────────────────
64
+
65
+ if [ ! -d "$RETROS_DIR" ]; then
66
+ echo "check-ask-hygiene: retros dir not found: $RETROS_DIR" >&2
67
+ exit 2
68
+ fi
69
+
70
+ # ── Scan ────────────────────────────────────────────────────────────────────
71
+ # Iterate ask-hygiene trail files at the top level of RETROS_DIR.
72
+ # Glob expansion uses an existence-check loop for cross-shell portability
73
+ # (P124 lesson — bash's `shopt -s nullglob` fails on zsh).
74
+
75
+ trail_files=()
76
+ for path in "$RETROS_DIR"/*-ask-hygiene.md; do
77
+ [ -e "$path" ] || continue
78
+ trail_files+=("$path")
79
+ done
80
+
81
+ if [ "${#trail_files[@]}" -eq 0 ]; then
82
+ exit 0
83
+ fi
84
+
85
+ # Sort by basename (which starts with the YYYY-MM-DD date prefix), oldest first
86
+ IFS=$'\n' sorted_files=($(printf '%s\n' "${trail_files[@]}" | sort))
87
+ unset IFS
88
+
89
+ # Apply the window — keep only the last N entries (most-recent N)
90
+ total="${#sorted_files[@]}"
91
+ if [ "$total" -gt "$WINDOW" ]; then
92
+ start=$(( total - WINDOW ))
93
+ windowed=("${sorted_files[@]:$start}")
94
+ else
95
+ windowed=("${sorted_files[@]}")
96
+ fi
97
+
98
+ # Extract counts per file
99
+ extract_count() {
100
+ local path="$1"
101
+ local label="$2"
102
+ local default="${3:-0}"
103
+ # Match lines like "Lazy count: 5" or "**Lazy count: 5**" (case-insensitive).
104
+ # Markdown bold is an enclosing pair, so leading **<text>: <N>** has the
105
+ # trailing ** AFTER the number, not after the label.
106
+ local match
107
+ match=$(grep -iE "^\*{0,2}$label count:[[:space:]]+[0-9]+" "$path" 2>/dev/null \
108
+ | head -1 \
109
+ | grep -oE '[0-9]+' \
110
+ | head -1)
111
+ if [ -z "$match" ]; then
112
+ echo "$default"
113
+ else
114
+ echo "$match"
115
+ fi
116
+ }
117
+
118
+ extract_date() {
119
+ local basename
120
+ basename="$(basename "$1")"
121
+ # Strip the -ask-hygiene.md suffix; what remains is the YYYY-MM-DD prefix
122
+ echo "${basename%-ask-hygiene.md}"
123
+ }
124
+
125
+ # Emit per-retro lines
126
+ declare -a lazy_counts=()
127
+ for path in "${windowed[@]}"; do
128
+ date=$(extract_date "$path")
129
+ # Lazy count is required (no default); if missing, skip the file silently.
130
+ # Inline rather than calling extract_count() because its `local default=...`
131
+ # falls back to "0" on empty (bash `${3:-0}` semantics), which would
132
+ # mask the "no lazy line" case.
133
+ lazy=$(grep -iE "^\*{0,2}Lazy count:[[:space:]]+[0-9]+" "$path" 2>/dev/null \
134
+ | head -1 \
135
+ | grep -oE '[0-9]+' \
136
+ | head -1)
137
+ if [ -z "$lazy" ]; then
138
+ continue
139
+ fi
140
+ # Other categories default to 0 if the trail entry omits them
141
+ direction=$(extract_count "$path" "Direction" "0")
142
+ override=$(extract_count "$path" "Override" "0")
143
+ silent=$(extract_count "$path" "Silent-framework" "0")
144
+ taste=$(extract_count "$path" "Taste" "0")
145
+ correction=$(extract_count "$path" "Correction-followup" "0")
146
+ echo "RETRO $date lazy=$lazy direction=$direction override=$override silent=$silent taste=$taste correction=$correction"
147
+ lazy_counts+=("$lazy")
148
+ done
149
+
150
+ # Emit trend line when 2+ entries are in the window
151
+ if [ "${#lazy_counts[@]}" -ge 2 ]; then
152
+ first="${lazy_counts[0]}"
153
+ last="${lazy_counts[${#lazy_counts[@]}-1]}"
154
+ delta=$(( last - first ))
155
+ if [ "$delta" -ge 0 ]; then
156
+ delta_s="+$delta"
157
+ else
158
+ delta_s="$delta"
159
+ fi
160
+ echo "TREND lazy_first=$first lazy_last=$last delta=$delta_s"
161
+ fi
162
+
163
+ exit 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