@windyroad/retrospective 0.19.0-preview.341 → 0.20.0-preview.344

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.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Session retrospective reminders and plan review for Claude Code"
5
5
  }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # packages/retrospective/bin/wr-retrospective-check-plugin-maturity-drift
3
+ #
4
+ # ADR-049 shim — resolves the canonical body in this package's scripts/
5
+ # dir. Phase 3b drift detector entrypoint; sibling to ADR-051's
6
+ # `wr-retrospective-check-readme-jtbd-currency`.
7
+
8
+ exec "$(dirname "$0")/../scripts/check-plugin-maturity-drift.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/retrospective",
3
- "version": "0.19.0-preview.341",
3
+ "version": "0.20.0-preview.344",
4
4
  "description": "Session retrospectives that update briefings and create problem tickets",
5
5
  "bin": {
6
6
  "windyroad-retrospective": "./bin/install.mjs"
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ # packages/retrospective/scripts/check-plugin-maturity-drift.sh
3
+ #
4
+ # Phase 3b (P087 / P238) plugin-maturity drift advisory.
5
+ #
6
+ # Compares each plugin's rendered README.md maturity badge against the
7
+ # canonical `plugin.json` `maturity:` field and emits an NDJSON-shaped
8
+ # drift signal per plugin to stdout. Sibling to ADR-051's
9
+ # `check-readme-jtbd-currency.sh` — same detector pattern, different
10
+ # anchor (maturity rollup vs JTBD citation), different failure mode
11
+ # (render-vs-canonical drift vs citation drift).
12
+ #
13
+ # Drift hint vocabulary:
14
+ #
15
+ # missing-badge — plugin.json carries `maturity:` but the README
16
+ # has no `*Maturity: ...*` prose-woven badge
17
+ # stale-band — README badge band mismatches canonical record
18
+ # orphan-badge — README has badge but plugin.json has no
19
+ # `maturity:` field (renderer ran ahead of
20
+ # populate; or populate was reverted)
21
+ # anti-pattern-section — README has a standalone `## Maturity` heading
22
+ # (ADR-063 §"README badge rendering format"
23
+ # rejects the section shape)
24
+ # anti-pattern-url — README has a shields.io URL or inline SVG
25
+ # (ADR-063 §F5 rejects external-dependency
26
+ # rendering)
27
+ #
28
+ # Output format (one line per package, alphabetical):
29
+ # README package=<name> badge_band=<band|none> record_band=<band|none>
30
+ # drift_hints=<csv>
31
+ # Plus a trailing TOTAL line:
32
+ # TOTAL packages=<N> drift_instances=<K>
33
+ #
34
+ # Exit code is always 0 — advisory only per ADR-013 Rule 6 / ADR-040
35
+ # declarative-first / ADR-051 Phase 1 precedent. Drift count is emitted
36
+ # as data on stdout; downstream consumers (run-retro Step 2b wiring,
37
+ # release-pre-flight habit, Phase 4 escalation per ADR-051 Phase 2
38
+ # criterion) decide whether to act.
39
+ #
40
+ # Usage:
41
+ # check-plugin-maturity-drift.sh [<packages-dir>]
42
+ #
43
+ # Default:
44
+ # <packages-dir> = ./packages
45
+ #
46
+ # Exit codes:
47
+ # 0 = always (advisory only — count is signal, not failure)
48
+ # 2 = parse error (packages-dir missing or unreadable)
49
+ #
50
+ # @problem P238 (Phase 3b — drift detector)
51
+ # @problem P087 (parent — no maturity signal on plugin features)
52
+ # @problem P152 (sibling drift-detector pattern — JTBD-currency)
53
+ # @adr ADR-063 (Plugin maturity presentation layer — Phase 3b contract)
54
+ # @adr ADR-051 (Sibling drift-detector pattern)
55
+ # @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory exit 0)
56
+ # @adr ADR-040 (Declarative-first / advisory-then-escalate)
57
+ # @adr ADR-049 (Shim grammar — `wr-retrospective-check-plugin-maturity-drift`)
58
+ # @adr ADR-052 (Behavioural tests default)
59
+ # @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just Installed)
60
+ # @jtbd JTBD-007 (Keep Plugins Current Across Projects — maturity-band-currency
61
+ # as third currency dimension alongside code + JTBD-content)
62
+ # @jtbd JTBD-101 (Extend the Suite — clear patterns include stability signal)
63
+
64
+ set -uo pipefail
65
+
66
+ PACKAGES_DIR="${1:-packages}"
67
+
68
+ # ── Pre-checks ──────────────────────────────────────────────────────────────
69
+
70
+ if [ ! -d "$PACKAGES_DIR" ]; then
71
+ echo "check-plugin-maturity-drift: packages dir not found: $PACKAGES_DIR" >&2
72
+ exit 2
73
+ fi
74
+
75
+ # ── Python body ─────────────────────────────────────────────────────────────
76
+
77
+ export CPMD_PACKAGES_DIR="$PACKAGES_DIR"
78
+
79
+ python3 - <<'PYEOF'
80
+ import json, os, re, sys
81
+ from pathlib import Path
82
+
83
+ packages_dir = Path(os.environ["CPMD_PACKAGES_DIR"]).resolve()
84
+
85
+ if not packages_dir.is_dir():
86
+ print(f"check-plugin-maturity-drift: packages dir not found: {packages_dir}", file=sys.stderr)
87
+ sys.exit(2)
88
+
89
+ # Badge prose-woven pattern: `*Maturity: <Band>...*` per ADR-063
90
+ # §"README badge rendering format". Captures the band token as the
91
+ # first word after `Maturity:`.
92
+ BADGE_RE = re.compile(r"\*Maturity:\s+([A-Za-z]+)[^*]*\*")
93
+ ANTI_SECTION_RE = re.compile(r"(?m)^#{1,3}\s+Maturity\s*$")
94
+ ANTI_URL_RE = re.compile(r"shields\.io|img\.shields", re.IGNORECASE)
95
+
96
+
97
+ def extract_badge_band(readme_text):
98
+ """Returns the band token from a `*Maturity: <Band>...*` span, or
99
+ None when no such span is present.
100
+ """
101
+ m = BADGE_RE.search(readme_text)
102
+ if m:
103
+ return m.group(1)
104
+ return None
105
+
106
+
107
+ def append_hint(current, hint):
108
+ if not current:
109
+ return hint
110
+ parts = current.split(",")
111
+ if hint in parts:
112
+ return current
113
+ return current + "," + hint
114
+
115
+
116
+ def evaluate_plugin(pkg_dir):
117
+ """Returns (line_dict, hint_count) for one plugin, or None when the
118
+ plugin should be silently skipped (no README).
119
+ """
120
+ plugin_json_path = pkg_dir / ".claude-plugin" / "plugin.json"
121
+ readme_path = pkg_dir / "README.md"
122
+ if not readme_path.is_file():
123
+ return None
124
+ try:
125
+ plugin_doc = json.loads(plugin_json_path.read_text(encoding="utf-8"))
126
+ except Exception:
127
+ plugin_doc = {}
128
+ if not isinstance(plugin_doc, dict):
129
+ plugin_doc = {}
130
+
131
+ record_band = None
132
+ maturity = plugin_doc.get("maturity")
133
+ if isinstance(maturity, dict):
134
+ record_band = maturity.get("band")
135
+
136
+ readme_text = readme_path.read_text(encoding="utf-8")
137
+ badge_band = extract_badge_band(readme_text)
138
+
139
+ hints = ""
140
+
141
+ if record_band and not badge_band:
142
+ hints = append_hint(hints, "missing-badge")
143
+ elif badge_band and not record_band:
144
+ hints = append_hint(hints, "orphan-badge")
145
+ elif record_band and badge_band and record_band != badge_band:
146
+ hints = append_hint(hints, "stale-band")
147
+
148
+ if ANTI_SECTION_RE.search(readme_text):
149
+ hints = append_hint(hints, "anti-pattern-section")
150
+ if ANTI_URL_RE.search(readme_text):
151
+ hints = append_hint(hints, "anti-pattern-url")
152
+
153
+ line = {
154
+ "package": pkg_dir.name,
155
+ "badge_band": badge_band or "none",
156
+ "record_band": record_band or "none",
157
+ "drift_hints": hints,
158
+ }
159
+ return (line, 1 if hints else 0)
160
+
161
+
162
+ # ── Walk packages/ ─────────────────────────────────────────────────────────
163
+
164
+ plugin_dirs = sorted(
165
+ [d for d in packages_dir.iterdir()
166
+ if d.is_dir() and (d / ".claude-plugin" / "plugin.json").is_file()
167
+ or (d.is_dir() and (d / "README.md").is_file())]
168
+ )
169
+
170
+ total_packages = 0
171
+ total_drift_instances = 0
172
+
173
+ for pkg_dir in plugin_dirs:
174
+ result = evaluate_plugin(pkg_dir)
175
+ if result is None:
176
+ continue
177
+ line, drift = result
178
+ total_packages += 1
179
+ total_drift_instances += drift
180
+ print(
181
+ f"README package={line['package']} "
182
+ f"badge_band={line['badge_band']} "
183
+ f"record_band={line['record_band']} "
184
+ f"drift_hints={line['drift_hints']}"
185
+ )
186
+
187
+ if total_packages > 0:
188
+ print(f"TOTAL packages={total_packages} drift_instances={total_drift_instances}")
189
+
190
+ PYEOF
191
+
192
+ exit 0
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # @problem P238 — Phase 3b drift detector behavioural confirmation.
4
+ # @problem P087 — parent: plugin maturity battle-hardening signal.
5
+ # @problem P152 — sibling drift-detector pattern (JTBD-currency).
6
+ #
7
+ # Contract under test:
8
+ # `packages/retrospective/scripts/check-plugin-maturity-drift.sh` is an
9
+ # advisory drift detector. It compares each plugin's rendered README
10
+ # maturity badge against the canonical `plugin.json` `maturity:` field
11
+ # and emits NDJSON-per-drift signals to stdout. Exit code 0 always per
12
+ # ADR-013 Rule 6 — drift is data, not failure.
13
+ #
14
+ # Drift classes:
15
+ # missing-badge — plugin.json has maturity but README has no badge
16
+ # stale-band — badge band mismatches canonical record
17
+ # orphan-badge — README has badge but plugin.json has no maturity
18
+ # anti-pattern-section — README has standalone ## Maturity section
19
+ # anti-pattern-url — README has shields.io / SVG badge URL
20
+ #
21
+ # @adr ADR-063 (Plugin maturity presentation layer — Phase 3b contract)
22
+ # @adr ADR-051 (Sibling drift-detector pattern — `check-readme-jtbd-currency.sh`)
23
+ # @adr ADR-013 Rule 6 (non-interactive fail-safe — exit 0 always)
24
+ # @adr ADR-049 (Shim grammar — `wr-retrospective-check-plugin-maturity-drift` on $PATH)
25
+ # @adr ADR-052 (Behavioural tests default)
26
+
27
+ setup() {
28
+ SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
29
+ SCRIPT="$SCRIPTS_DIR/check-plugin-maturity-drift.sh"
30
+ FIXTURE_DIR="$(mktemp -d)"
31
+ PROJECT_ROOT="$FIXTURE_DIR/project"
32
+ PKG_DIR="$PROJECT_ROOT/packages"
33
+ mkdir -p "$PKG_DIR"
34
+ }
35
+
36
+ teardown() {
37
+ rm -rf "$FIXTURE_DIR"
38
+ }
39
+
40
+ # Helper: write a synthetic plugin layout with plugin.json + README.md
41
+ make_plugin() {
42
+ local name="$1"
43
+ local plugin_json="$2"
44
+ local readme="$3"
45
+ local pkg="$PKG_DIR/$name"
46
+ mkdir -p "$pkg/.claude-plugin"
47
+ printf '%s\n' "$plugin_json" > "$pkg/.claude-plugin/plugin.json"
48
+ printf '%s\n' "$readme" > "$pkg/README.md"
49
+ }
50
+
51
+ # ── Pre-checks ──────────────────────────────────────────────────────────────
52
+
53
+ @test "script file exists and is executable" {
54
+ [ -f "$SCRIPT" ]
55
+ [ -x "$SCRIPT" ]
56
+ }
57
+
58
+ @test "missing packages dir exits 2 with error message on stderr" {
59
+ run bash "$SCRIPT" "$FIXTURE_DIR/does-not-exist"
60
+ [ "$status" -eq 2 ]
61
+ [[ "$output" == *"packages dir not found"* ]]
62
+ }
63
+
64
+ @test "empty packages dir exits 0 with empty stdout" {
65
+ run bash "$SCRIPT" "$PKG_DIR"
66
+ [ "$status" -eq 0 ]
67
+ [[ "$output" == *"TOTAL"* ]] || [ -z "$output" ]
68
+ }
69
+
70
+ # ── Clean fixture: badge matches plugin.json record ─────────────────────────
71
+
72
+ @test "clean fixture: matching badge + record emits no drift hints" {
73
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
74
+ "# @windyroad/stub
75
+
76
+ **Stub plugin description.** *Maturity: Alpha.*
77
+
78
+ ## Skills
79
+ "
80
+
81
+ run bash "$SCRIPT" "$PKG_DIR"
82
+ [ "$status" -eq 0 ]
83
+ [[ "$output" == *"package=stub"* ]]
84
+ [[ "$output" == *"drift_hints="* ]]
85
+ [[ "$output" != *"stale-band"* ]]
86
+ [[ "$output" != *"missing-badge"* ]]
87
+ }
88
+
89
+ # ── Stale-band fixture: badge != canonical record ───────────────────────────
90
+
91
+ @test "stale-band fixture: README badge band mismatches plugin.json record" {
92
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Beta"}}' \
93
+ "# @windyroad/stub
94
+
95
+ **Stub plugin description.** *Maturity: Alpha.*
96
+
97
+ ## Skills
98
+ "
99
+
100
+ run bash "$SCRIPT" "$PKG_DIR"
101
+ [ "$status" -eq 0 ]
102
+ [[ "$output" == *"package=stub"* ]]
103
+ [[ "$output" == *"stale-band"* ]]
104
+ }
105
+
106
+ # ── Missing-badge fixture: plugin.json has maturity, README has no badge ────
107
+
108
+ @test "missing-badge fixture: plugin.json has maturity but README has no badge" {
109
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
110
+ "# @windyroad/stub
111
+
112
+ **Stub plugin description.**
113
+
114
+ ## Skills
115
+ "
116
+
117
+ run bash "$SCRIPT" "$PKG_DIR"
118
+ [ "$status" -eq 0 ]
119
+ [[ "$output" == *"package=stub"* ]]
120
+ [[ "$output" == *"missing-badge"* ]]
121
+ }
122
+
123
+ # ── Orphan-badge fixture: README has badge, plugin.json has no maturity ─────
124
+
125
+ @test "orphan-badge fixture: README badge present but plugin.json lacks maturity field" {
126
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub"}' \
127
+ "# @windyroad/stub
128
+
129
+ **Stub plugin description.** *Maturity: Alpha.*
130
+
131
+ ## Skills
132
+ "
133
+
134
+ run bash "$SCRIPT" "$PKG_DIR"
135
+ [ "$status" -eq 0 ]
136
+ [[ "$output" == *"package=stub"* ]]
137
+ [[ "$output" == *"orphan-badge"* ]]
138
+ }
139
+
140
+ # ── Anti-pattern: standalone ## Maturity section ────────────────────────────
141
+
142
+ @test "anti-pattern: standalone ## Maturity section emits anti-pattern-section hint" {
143
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
144
+ "# @windyroad/stub
145
+
146
+ **Stub plugin description.** *Maturity: Alpha.*
147
+
148
+ ## Maturity
149
+
150
+ Alpha band.
151
+
152
+ ## Skills
153
+ "
154
+
155
+ run bash "$SCRIPT" "$PKG_DIR"
156
+ [ "$status" -eq 0 ]
157
+ [[ "$output" == *"package=stub"* ]]
158
+ [[ "$output" == *"anti-pattern-section"* ]]
159
+ }
160
+
161
+ # ── Anti-pattern: shields.io URL ────────────────────────────────────────────
162
+
163
+ @test "anti-pattern: shields.io URL emits anti-pattern-url hint" {
164
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
165
+ "# @windyroad/stub
166
+
167
+ **Stub plugin description.** *Maturity: Alpha.* ![Maturity](https://img.shields.io/badge/maturity-alpha-orange)
168
+
169
+ ## Skills
170
+ "
171
+
172
+ run bash "$SCRIPT" "$PKG_DIR"
173
+ [ "$status" -eq 0 ]
174
+ [[ "$output" == *"package=stub"* ]]
175
+ [[ "$output" == *"anti-pattern-url"* ]]
176
+ }
177
+
178
+ # ── TOTAL summary line ──────────────────────────────────────────────────────
179
+
180
+ @test "multi-plugin aggregation: emits TOTAL summary with drift count" {
181
+ make_plugin "alpha" '{"name":"wr-alpha","version":"0.1.0","description":"Alpha","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
182
+ "# @windyroad/alpha
183
+
184
+ **Alpha plugin.** *Maturity: Alpha.*
185
+
186
+ ## Skills
187
+ "
188
+ make_plugin "bravo" '{"name":"wr-bravo","version":"0.1.0","description":"Bravo","maturity":{"schema_version":"1.0","band":"Beta"}}' \
189
+ "# @windyroad/bravo
190
+
191
+ **Bravo plugin.**
192
+
193
+ ## Skills
194
+ "
195
+
196
+ run bash "$SCRIPT" "$PKG_DIR"
197
+ [ "$status" -eq 0 ]
198
+ [[ "$output" == *"package=alpha"* ]]
199
+ [[ "$output" == *"package=bravo"* ]]
200
+ [[ "$output" == *"TOTAL packages=2"* ]]
201
+ [[ "$output" == *"drift_instances=1"* ]]
202
+ }
203
+
204
+ # ── Exit-0-always invariant ─────────────────────────────────────────────────
205
+
206
+ @test "exit-0-always: even with multiple drift entries, exit code is 0" {
207
+ make_plugin "alpha" '{"name":"wr-alpha","version":"0.1.0","description":"Alpha","maturity":{"schema_version":"1.0","band":"Beta"}}' \
208
+ "# @windyroad/alpha
209
+
210
+ **Alpha plugin.** *Maturity: Alpha.*
211
+
212
+ ## Skills
213
+ "
214
+ make_plugin "bravo" '{"name":"wr-bravo","version":"0.1.0","description":"Bravo","maturity":{"schema_version":"1.0","band":"Stable"}}' \
215
+ "# @windyroad/bravo
216
+
217
+ **Bravo plugin.**
218
+
219
+ ## Skills
220
+ "
221
+
222
+ run bash "$SCRIPT" "$PKG_DIR"
223
+ [ "$status" -eq 0 ]
224
+ }
225
+
226
+ # ── Package without README is silently skipped ──────────────────────────────
227
+
228
+ @test "package without README.md is silently skipped" {
229
+ local pkg="$PKG_DIR/no-readme"
230
+ mkdir -p "$pkg/.claude-plugin"
231
+ echo '{"name":"wr-no-readme","version":"0.1.0","description":"No README","maturity":{"schema_version":"1.0","band":"Alpha"}}' > "$pkg/.claude-plugin/plugin.json"
232
+
233
+ make_plugin "with-readme" '{"name":"wr-with-readme","version":"0.1.0","description":"With README","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
234
+ "# @windyroad/with-readme
235
+
236
+ **With README.** *Maturity: Alpha.*
237
+ "
238
+
239
+ run bash "$SCRIPT" "$PKG_DIR"
240
+ [ "$status" -eq 0 ]
241
+ [[ "$output" != *"package=no-readme"* ]]
242
+ [[ "$output" == *"package=with-readme"* ]]
243
+ }
244
+
245
+ # ── No-network primitive: ADR-035 privacy posture ───────────────────────────
246
+
247
+ @test "ADR-035: script body invokes no network primitive" {
248
+ run grep -E "(curl|wget|nc -|netcat|ssh |scp |rsync|http\.client|urllib|requests)" "$SCRIPT"
249
+ [ "$status" -ne 0 ]
250
+ }
251
+
252
+ # ── NDJSON output: per-line shape parseable as structured signal ────────────
253
+
254
+ @test "NDJSON output: each README line follows package=<name> <key>=<value> shape" {
255
+ make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
256
+ "# @windyroad/stub
257
+
258
+ **Stub plugin description.** *Maturity: Alpha.*
259
+
260
+ ## Skills
261
+ "
262
+
263
+ run bash "$SCRIPT" "$PKG_DIR"
264
+ [ "$status" -eq 0 ]
265
+ # Per-package README line is space-separated key=value tokens
266
+ [[ "$output" == *"README package=stub"* ]]
267
+ [[ "$output" == *"badge_band="* ]]
268
+ [[ "$output" == *"record_band="* ]]
269
+ }