@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,227 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-tarball-shipped-shims.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for plugin-published tarball-shipped-shim
|
|
5
|
+
# integrity per WR-P154 (P137 detector must run against npm pack output not
|
|
6
|
+
# source tree) and the WR-ADR-049 plugin-script-resolution-via-bin-on-PATH
|
|
7
|
+
# contract.
|
|
8
|
+
#
|
|
9
|
+
# Walks workspace packages under `<root-dir>/packages/<plugin>/`. For each
|
|
10
|
+
# workspace, runs `npm pack --dry-run --json` to enumerate the file set that
|
|
11
|
+
# WOULD ship to npm (per `package.json#files` filtering). For every
|
|
12
|
+
# WR-ADR-049-grammar bin shim (`bin/wr-<plugin>-<name>`) in that file set,
|
|
13
|
+
# parses the shim source to extract the `exec`'d `scripts/<name>.sh` target
|
|
14
|
+
# and asserts the target path is also in the tarball file set.
|
|
15
|
+
#
|
|
16
|
+
# The asymmetry between source-tree state and tarball state is the gap
|
|
17
|
+
# WR-P137 Phase 1 (check-internal-id-leaks.sh, source-tree-walking) does
|
|
18
|
+
# not see — `scripts/` exists on disk but is omitted from `package.json#files`
|
|
19
|
+
# so the shim shipped to adopters exec-fails with `no such file or directory`
|
|
20
|
+
# at invocation time. WR-P154 closes the prevention surface from the
|
|
21
|
+
# publish-manifest side.
|
|
22
|
+
#
|
|
23
|
+
# Usage:
|
|
24
|
+
# check-tarball-shipped-shims.sh [<root-dir>]
|
|
25
|
+
#
|
|
26
|
+
# Default <root-dir> is `.`.
|
|
27
|
+
#
|
|
28
|
+
# WR-ADR-049-grammar shims that this script considers:
|
|
29
|
+
# bin/wr-<plugin>-<name>
|
|
30
|
+
# Non-grammar bins (e.g. `bin/install.mjs`, `bin/check-deps.sh`,
|
|
31
|
+
# `bin/windyroad-<plugin>` legacy installers) are skipped — they don't
|
|
32
|
+
# follow the script-resolution-via-bin-on-PATH WR-ADR-049 contract.
|
|
33
|
+
#
|
|
34
|
+
# Exit codes:
|
|
35
|
+
# 0 = always (advisory only — drift is signal, not failure)
|
|
36
|
+
# 2 = parse error (root dir missing or unreadable, npm unavailable)
|
|
37
|
+
#
|
|
38
|
+
# Output format on drift (terse machine-readable per WR-ADR-038):
|
|
39
|
+
# TARBALL_DRIFT package=<name> shim=<bin/wr-...> target=<scripts/...> tarball-status=missing
|
|
40
|
+
#
|
|
41
|
+
# Followed by a final aggregate summary line:
|
|
42
|
+
# TOTAL packages=<N> with_drift=<M> missing_targets=<K>
|
|
43
|
+
#
|
|
44
|
+
# Output is empty (no lines) when no shipped artefact carries broken
|
|
45
|
+
# shims — silent-on-pass per WR-ADR-045 hook injection budget discipline.
|
|
46
|
+
#
|
|
47
|
+
# Output ordering (deterministic for stable retro-summary diffs):
|
|
48
|
+
# TARBALL_DRIFT lines sorted by `<package>/<shim>` identifier.
|
|
49
|
+
# TOTAL line last.
|
|
50
|
+
#
|
|
51
|
+
# Read-only — does NOT mutate any artefact. Per WR-ADR-052, the bats
|
|
52
|
+
# fixture at scripts/test/check-tarball-shipped-shims.bats is BEHAVIOURAL —
|
|
53
|
+
# asserts script output on temp-fixture trees, NOT script source content.
|
|
54
|
+
#
|
|
55
|
+
# @problem P154 (P137 namespace-prefix detector must run against
|
|
56
|
+
# npm pack output not source tree)
|
|
57
|
+
# @problem P140 (Step 6.5 fix-and-continue — same prevention surface
|
|
58
|
+
# from the publisher side)
|
|
59
|
+
# @adr ADR-049 (Plugin script resolution via bin/ on PATH)
|
|
60
|
+
# @adr ADR-038 (Progressive disclosure — terse machine-readable signal)
|
|
61
|
+
# @adr ADR-045 (Hook injection budget — silent-on-pass discipline)
|
|
62
|
+
# @adr ADR-052 (Behavioural-tests-default — fixture pattern)
|
|
63
|
+
# @adr ADR-055 (Plugin-published namespace-prefixed permalinks —
|
|
64
|
+
# sibling adopter-context decision)
|
|
65
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
66
|
+
# @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just
|
|
67
|
+
# Installed — executable correctness axis of adopter-facing content)
|
|
68
|
+
# @jtbd JTBD-101 (Extend the Suite with New Plugins — secondary
|
|
69
|
+
# plugin-developer feedback surface)
|
|
70
|
+
|
|
71
|
+
set -uo pipefail
|
|
72
|
+
|
|
73
|
+
ROOT_DIR="${1:-.}"
|
|
74
|
+
|
|
75
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
if [ ! -d "$ROOT_DIR" ]; then
|
|
78
|
+
echo "check-tarball-shipped-shims: root dir not found: $ROOT_DIR" >&2
|
|
79
|
+
exit 2
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
83
|
+
echo "check-tarball-shipped-shims: npm not found on PATH" >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ── Walk workspaces ─────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
shopt -s nullglob
|
|
90
|
+
declare -a workspaces=()
|
|
91
|
+
for pkg_json in "$ROOT_DIR"/packages/*/package.json; do
|
|
92
|
+
workspaces+=("$(dirname "$pkg_json")")
|
|
93
|
+
done
|
|
94
|
+
shopt -u nullglob
|
|
95
|
+
|
|
96
|
+
if [ "${#workspaces[@]}" -eq 0 ]; then
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ── Per-workspace pack + drift detection ────────────────────────────────────
|
|
101
|
+
# For each workspace:
|
|
102
|
+
# 1. cd into workspace dir; run `npm pack --dry-run --json` to get the
|
|
103
|
+
# shipped file list. Independent of monorepo workspaces config — works
|
|
104
|
+
# in adopter trees too.
|
|
105
|
+
# 2. Parse JSON via python3 (always available on the host) to extract
|
|
106
|
+
# the file paths AND the package name.
|
|
107
|
+
# 3. For each `bin/wr-<plugin>-<name>` shim in the file list, parse the
|
|
108
|
+
# shim source on disk (NOT from the tarball — same content) to find
|
|
109
|
+
# the `exec`'d `../scripts/<name>.sh` target.
|
|
110
|
+
# 4. Check whether the resolved target path is in the tarball file list.
|
|
111
|
+
# 5. Emit `TARBALL_DRIFT` lines for missing targets.
|
|
112
|
+
|
|
113
|
+
declare -a drifts=()
|
|
114
|
+
declare -A package_set=()
|
|
115
|
+
declare -i missing_total=0
|
|
116
|
+
|
|
117
|
+
for ws in "${workspaces[@]}"; do
|
|
118
|
+
# Pack the workspace. Capture stdout; suppress stderr (npm chatter).
|
|
119
|
+
pack_json=$(cd "$ws" && npm pack --dry-run --json 2>/dev/null) || continue
|
|
120
|
+
if [ -z "$pack_json" ]; then
|
|
121
|
+
continue
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Extract package name + file path list via python3.
|
|
125
|
+
# Output format (one record per line):
|
|
126
|
+
# NAME <package-name>
|
|
127
|
+
# FILE <relative-path>
|
|
128
|
+
# ...
|
|
129
|
+
parsed=$(printf '%s' "$pack_json" | python3 -c '
|
|
130
|
+
import sys, json
|
|
131
|
+
data = json.load(sys.stdin)
|
|
132
|
+
if not data:
|
|
133
|
+
sys.exit(0)
|
|
134
|
+
entry = data[0] if isinstance(data, list) else data
|
|
135
|
+
name = entry.get("name", "")
|
|
136
|
+
files = entry.get("files", []) or []
|
|
137
|
+
print("NAME " + name)
|
|
138
|
+
for f in files:
|
|
139
|
+
p = f.get("path", "")
|
|
140
|
+
if p:
|
|
141
|
+
print("FILE " + p)
|
|
142
|
+
') || continue
|
|
143
|
+
|
|
144
|
+
pkg_name=""
|
|
145
|
+
declare -a tarball_files=()
|
|
146
|
+
while IFS= read -r line; do
|
|
147
|
+
case "$line" in
|
|
148
|
+
"NAME "*)
|
|
149
|
+
pkg_name="${line#NAME }"
|
|
150
|
+
;;
|
|
151
|
+
"FILE "*)
|
|
152
|
+
tarball_files+=("${line#FILE }")
|
|
153
|
+
;;
|
|
154
|
+
esac
|
|
155
|
+
done <<< "$parsed"
|
|
156
|
+
|
|
157
|
+
if [ -z "$pkg_name" ]; then
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Build a lookup set of tarball file paths.
|
|
162
|
+
declare -A tarball_set=()
|
|
163
|
+
for f in "${tarball_files[@]}"; do
|
|
164
|
+
tarball_set["$f"]=1
|
|
165
|
+
done
|
|
166
|
+
|
|
167
|
+
# Find WR-ADR-049-grammar shims in the tarball file list.
|
|
168
|
+
# Grammar: bin/wr-<plugin>-<name> (excludes bin/install.mjs, bin/check-deps.sh,
|
|
169
|
+
# bin/windyroad-<plugin>, bin/anything-without-wr-prefix).
|
|
170
|
+
workspace_has_drift=0
|
|
171
|
+
for f in "${tarball_files[@]}"; do
|
|
172
|
+
case "$f" in
|
|
173
|
+
bin/wr-*)
|
|
174
|
+
# Parse the shim source on disk for the exec target.
|
|
175
|
+
shim_path="$ws/$f"
|
|
176
|
+
if [ ! -f "$shim_path" ]; then
|
|
177
|
+
continue
|
|
178
|
+
fi
|
|
179
|
+
# Heuristic: extract the relative path after `$(dirname "$0")/../`.
|
|
180
|
+
# Matches the WR-ADR-049 grammar both for `exec "$(dirname "$0")/../scripts/foo.sh" "$@"`
|
|
181
|
+
# and the same shape with single quotes / no quotes around dirname.
|
|
182
|
+
target=$(perl -ne '
|
|
183
|
+
if (/\$\(dirname\s+["'\'']?\$0["'\'']?\)\/\.\.\/(\S+?)["'\'']?\s/) {
|
|
184
|
+
print $1; exit;
|
|
185
|
+
}
|
|
186
|
+
' "$shim_path")
|
|
187
|
+
|
|
188
|
+
if [ -z "$target" ]; then
|
|
189
|
+
continue
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if [ -z "${tarball_set[$target]:-}" ]; then
|
|
193
|
+
drifts+=("$pkg_name|$f|$target")
|
|
194
|
+
missing_total=$((missing_total + 1))
|
|
195
|
+
workspace_has_drift=1
|
|
196
|
+
fi
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
done
|
|
200
|
+
|
|
201
|
+
if [ "$workspace_has_drift" -eq 1 ]; then
|
|
202
|
+
package_set["$pkg_name"]=1
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
unset tarball_set
|
|
206
|
+
unset tarball_files
|
|
207
|
+
done
|
|
208
|
+
|
|
209
|
+
if [ "${#drifts[@]}" -eq 0 ]; then
|
|
210
|
+
exit 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ── Emit TARBALL_DRIFT lines (sorted) ───────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${drifts[@]}" | sort))
|
|
216
|
+
unset IFS
|
|
217
|
+
|
|
218
|
+
for entry in "${sorted[@]}"; do
|
|
219
|
+
IFS='|' read -r pkg shim target <<< "$entry"
|
|
220
|
+
echo "TARBALL_DRIFT package=$pkg shim=$shim target=$target tarball-status=missing"
|
|
221
|
+
done
|
|
222
|
+
|
|
223
|
+
# ── Emit TOTAL summary ──────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
echo "TOTAL packages=${#package_set[@]} with_drift=${#package_set[@]} missing_targets=$missing_total"
|
|
226
|
+
|
|
227
|
+
exit 0
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-tickets-deferred-cause.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for Step 4b Stage 1's "Tickets Deferred"
|
|
5
|
+
# fallback (P148). Walks docs/retros/*.md retro summary files, parses the
|
|
6
|
+
# `### Tickets Deferred` table in each, and counts entries whose `Cause`
|
|
7
|
+
# column is not in the allowlist `{skill_unavailable}`. Emits one line
|
|
8
|
+
# per retro file plus a trailing TOTAL summary line.
|
|
9
|
+
#
|
|
10
|
+
# Exit code is always 0 — the script is advisory per ADR-040
|
|
11
|
+
# declarative-first / ADR-013 Rule 6 fail-safe. Violation count is
|
|
12
|
+
# emitted as data on stdout; downstream consumers (retro summary
|
|
13
|
+
# rendering, future enforcement-hook escalation per P135 R6 trajectory)
|
|
14
|
+
# decide whether to act on the count.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# check-tickets-deferred-cause.sh [<retros-dir>]
|
|
18
|
+
#
|
|
19
|
+
# Default <retros-dir> is ./docs/retros.
|
|
20
|
+
#
|
|
21
|
+
# Exit codes:
|
|
22
|
+
# 0 = always (advisory only — count is signal, not failure)
|
|
23
|
+
# 2 = parse error (retros dir missing or unreadable)
|
|
24
|
+
#
|
|
25
|
+
# Output format (one line per retro file with a Tickets Deferred section,
|
|
26
|
+
# oldest first by date prefix):
|
|
27
|
+
# RETRO <YYYY-MM-DD> file=<basename> deferred=<N> with_valid_cause=<M> violations=<K>
|
|
28
|
+
#
|
|
29
|
+
# Plus a trailing TOTAL line summarising the window:
|
|
30
|
+
# TOTAL files=<N> deferred=<N> with_valid_cause=<M> violations=<K>
|
|
31
|
+
#
|
|
32
|
+
# Output is empty (no lines) when no retro files contain a Tickets
|
|
33
|
+
# Deferred section — this is the expected steady state when Stage 1
|
|
34
|
+
# ticketing is firing as designed.
|
|
35
|
+
#
|
|
36
|
+
# Tickets Deferred table shape (per run-retro SKILL.md Step 5 template):
|
|
37
|
+
#
|
|
38
|
+
# ### Tickets Deferred
|
|
39
|
+
#
|
|
40
|
+
# | Observation | Cause | Citation |
|
|
41
|
+
# |-------------|-------|----------|
|
|
42
|
+
# | <text> | `skill_unavailable` | <text> |
|
|
43
|
+
#
|
|
44
|
+
# The script tolerates:
|
|
45
|
+
# - Cause values wrapped in backticks or bold markers
|
|
46
|
+
# - Missing Cause column entirely (treated as a violation)
|
|
47
|
+
# - Out-of-allowlist cause values (treated as a violation)
|
|
48
|
+
# - Empty Cause cell (treated as a violation)
|
|
49
|
+
#
|
|
50
|
+
# Read-only — does NOT mutate any retro file.
|
|
51
|
+
#
|
|
52
|
+
# @problem P148 (Agent defers ticket creation — broadens Stage 1 fallback gate)
|
|
53
|
+
# @problem P145 (Sibling defer-pattern at Tier 3 rotation — same class of behaviour)
|
|
54
|
+
# @adr ADR-044 (Decision-Delegation Contract — framework-resolution boundary)
|
|
55
|
+
# @adr ADR-040 (Tier 3 advisory-not-fail-closed — declarative-first precedent)
|
|
56
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory script never blocks AFK)
|
|
57
|
+
# @adr ADR-005 / ADR-037 (Plugin testing strategy — behavioural tests)
|
|
58
|
+
# @jtbd JTBD-001 (enforce governance without slowing down)
|
|
59
|
+
# @jtbd JTBD-006 (progress backlog while AFK — anti-defer is the load-bearing job)
|
|
60
|
+
# @jtbd JTBD-201 (audit trail for AI-assisted work — Cause field IS the audit trail)
|
|
61
|
+
|
|
62
|
+
set -uo pipefail
|
|
63
|
+
|
|
64
|
+
RETROS_DIR="${1:-docs/retros}"
|
|
65
|
+
|
|
66
|
+
# Allowlist — single source of truth for valid Cause values. The
|
|
67
|
+
# allowlist intentionally has ONE entry. Adding entries requires a
|
|
68
|
+
# matching SKILL.md AFK-branch update (and ideally a sibling ADR).
|
|
69
|
+
ALLOWLIST_CAUSES=("skill_unavailable")
|
|
70
|
+
|
|
71
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
if [ ! -d "$RETROS_DIR" ]; then
|
|
74
|
+
echo "check-tickets-deferred-cause: retros dir not found: $RETROS_DIR" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── Scan ────────────────────────────────────────────────────────────────────
|
|
79
|
+
# Iterate retro summary files at the top level of RETROS_DIR. Skip
|
|
80
|
+
# ask-hygiene trail files (those are check-ask-hygiene.sh's surface);
|
|
81
|
+
# skip context-analysis files. Glob expansion uses a portable for-loop
|
|
82
|
+
# (P124 lesson — `shopt -s nullglob` is bash-only).
|
|
83
|
+
|
|
84
|
+
retro_files=()
|
|
85
|
+
for path in "$RETROS_DIR"/*.md; do
|
|
86
|
+
[ -e "$path" ] || continue
|
|
87
|
+
# Skip non-summary files
|
|
88
|
+
basename="$(basename "$path")"
|
|
89
|
+
case "$basename" in
|
|
90
|
+
*-ask-hygiene.md) continue ;;
|
|
91
|
+
*-context-analysis.md) continue ;;
|
|
92
|
+
*)
|
|
93
|
+
# Only consider files whose basename starts with a YYYY-MM-DD date prefix
|
|
94
|
+
if [[ "$basename" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then
|
|
95
|
+
retro_files+=("$path")
|
|
96
|
+
fi
|
|
97
|
+
;;
|
|
98
|
+
esac
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
if [ "${#retro_files[@]}" -eq 0 ]; then
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Sort by basename date prefix, oldest first
|
|
106
|
+
IFS=$'\n' sorted_files=($(printf '%s\n' "${retro_files[@]}" | sort))
|
|
107
|
+
unset IFS
|
|
108
|
+
|
|
109
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
extract_date() {
|
|
112
|
+
local basename
|
|
113
|
+
basename="$(basename "$1")"
|
|
114
|
+
# Take the leading YYYY-MM-DD prefix
|
|
115
|
+
echo "${basename:0:10}"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Determine if a cause value is in the allowlist
|
|
119
|
+
is_valid_cause() {
|
|
120
|
+
local raw="$1"
|
|
121
|
+
# Strip backticks, bold asterisks, and surrounding whitespace
|
|
122
|
+
local cleaned
|
|
123
|
+
cleaned=$(echo "$raw" | sed 's/`//g' | sed 's/\*\*//g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
|
|
124
|
+
if [ -z "$cleaned" ]; then
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
for allowed in "${ALLOWLIST_CAUSES[@]}"; do
|
|
128
|
+
if [ "$cleaned" = "$allowed" ]; then
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
done
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Parse a single retro file's Tickets Deferred section. Sets the global
|
|
136
|
+
# variables `parsed_deferred`, `parsed_valid`, `parsed_violations` for
|
|
137
|
+
# the caller to read.
|
|
138
|
+
parse_retro_file() {
|
|
139
|
+
local path="$1"
|
|
140
|
+
parsed_deferred=0
|
|
141
|
+
parsed_valid=0
|
|
142
|
+
parsed_violations=0
|
|
143
|
+
|
|
144
|
+
# Extract the lines between `### Tickets Deferred` and the next `###`
|
|
145
|
+
# heading (or EOF). awk's range pattern handles this idiomatically.
|
|
146
|
+
local section
|
|
147
|
+
section=$(awk '
|
|
148
|
+
/^### Tickets Deferred[[:space:]]*$/ { in_section=1; next }
|
|
149
|
+
in_section && /^### / { in_section=0 }
|
|
150
|
+
in_section { print }
|
|
151
|
+
' "$path")
|
|
152
|
+
|
|
153
|
+
if [ -z "$section" ]; then
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Walk the table rows. A table row starts with `|`, has at least 3
|
|
158
|
+
# cells, and is not the header or separator row.
|
|
159
|
+
while IFS= read -r line; do
|
|
160
|
+
# Skip blank lines, prose, and non-table content
|
|
161
|
+
[[ "$line" =~ ^\| ]] || continue
|
|
162
|
+
# Skip the separator row (cells of dashes)
|
|
163
|
+
[[ "$line" =~ ^\|[[:space:]]*-+[[:space:]]*\| ]] && continue
|
|
164
|
+
# Skip the header row — detected by literal "Observation" in cell 1
|
|
165
|
+
if [[ "$line" =~ ^\|[[:space:]]*Observation[[:space:]]*\| ]]; then
|
|
166
|
+
continue
|
|
167
|
+
fi
|
|
168
|
+
# Skip the placeholder example row — detected by `<one-line` template marker
|
|
169
|
+
if [[ "$line" =~ \<one-line\ observation\ summary\> ]]; then
|
|
170
|
+
continue
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
parsed_deferred=$(( parsed_deferred + 1 ))
|
|
174
|
+
|
|
175
|
+
# Split the row into cells. The first cell is empty (leading `|`).
|
|
176
|
+
# Use a perl-free awk split on `|`.
|
|
177
|
+
local cause_cell
|
|
178
|
+
cause_cell=$(echo "$line" | awk -F'|' '{print $3}')
|
|
179
|
+
|
|
180
|
+
if [ -z "$cause_cell" ]; then
|
|
181
|
+
# No Cause column at all — violation
|
|
182
|
+
parsed_violations=$(( parsed_violations + 1 ))
|
|
183
|
+
continue
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
if is_valid_cause "$cause_cell"; then
|
|
187
|
+
parsed_valid=$(( parsed_valid + 1 ))
|
|
188
|
+
else
|
|
189
|
+
parsed_violations=$(( parsed_violations + 1 ))
|
|
190
|
+
fi
|
|
191
|
+
done <<< "$section"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# ── Emit per-file lines ─────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
total_files=0
|
|
197
|
+
total_deferred=0
|
|
198
|
+
total_valid=0
|
|
199
|
+
total_violations=0
|
|
200
|
+
|
|
201
|
+
for path in "${sorted_files[@]}"; do
|
|
202
|
+
parse_retro_file "$path"
|
|
203
|
+
if [ "$parsed_deferred" -eq 0 ]; then
|
|
204
|
+
continue
|
|
205
|
+
fi
|
|
206
|
+
date=$(extract_date "$path")
|
|
207
|
+
basename="$(basename "$path")"
|
|
208
|
+
echo "RETRO $date file=$basename deferred=$parsed_deferred with_valid_cause=$parsed_valid violations=$parsed_violations"
|
|
209
|
+
total_files=$(( total_files + 1 ))
|
|
210
|
+
total_deferred=$(( total_deferred + parsed_deferred ))
|
|
211
|
+
total_valid=$(( total_valid + parsed_valid ))
|
|
212
|
+
total_violations=$(( total_violations + parsed_violations ))
|
|
213
|
+
done
|
|
214
|
+
|
|
215
|
+
# Emit TOTAL line when at least one file contributed
|
|
216
|
+
if [ "$total_files" -gt 0 ]; then
|
|
217
|
+
echo "TOTAL files=$total_files deferred=$total_deferred with_valid_cause=$total_valid violations=$total_violations"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
exit 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
|