@windyroad/itil 0.39.0-preview.472 → 0.40.0-preview.479
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-itil-evaluate-relevance +2 -0
- package/package.json +1 -1
- package/scripts/evaluate-relevance.sh +135 -0
- package/scripts/test/evaluate-relevance.bats +329 -0
- package/skills/manage-problem/SKILL.md +1 -1
- package/skills/review-problems/SKILL.md +84 -0
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/evaluate-relevance.sh
|
|
3
|
+
#
|
|
4
|
+
# Evaluate whether a problem ticket has become "no longer relevant" by
|
|
5
|
+
# checking observable evidence per ADR-026 grounding. Phase 1 scope per
|
|
6
|
+
# ADR-079: ONE evidence shape — "file no longer exists in codebase" —
|
|
7
|
+
# closest analog to P334/P336 close-on-evidence.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# evaluate-relevance.sh <ticket-file> [<min-age-days>]
|
|
11
|
+
#
|
|
12
|
+
# Default <min-age-days> is 7. Age gate is a GATING condition, not the
|
|
13
|
+
# closing condition (per user direction 2026-05-31: "not just because
|
|
14
|
+
# they are old").
|
|
15
|
+
#
|
|
16
|
+
# Algorithm:
|
|
17
|
+
# 1. Read **Reported**: YYYY-MM-DD from the ticket frontmatter.
|
|
18
|
+
# If absent or unparseable → SKIP no-reported-date.
|
|
19
|
+
# 2. If today - Reported < min-age-days → SKIP too-fresh.
|
|
20
|
+
# 3. Extract file-path candidates from the ticket body matching
|
|
21
|
+
# (packages|docs|.changeset|src|test|scripts)/<path>.<known-extension>
|
|
22
|
+
# then drop self-references (docs/problems/*).
|
|
23
|
+
# 4. If no candidates remain → SKIP no-extractable-paths.
|
|
24
|
+
# 5. For each candidate, run `git ls-files --error-unmatch <path>`.
|
|
25
|
+
# Count present vs missing.
|
|
26
|
+
# 6. If ALL candidates missing AND at least 1 was extracted →
|
|
27
|
+
# CLOSE-CANDIDATE. Otherwise → KEEP.
|
|
28
|
+
#
|
|
29
|
+
# Output (stdout, one line):
|
|
30
|
+
# CLOSE-CANDIDATE <basename> — all <N> file paths absent: <semicolon list>
|
|
31
|
+
# KEEP <basename> — <M>/<N> paths still present
|
|
32
|
+
# SKIP <basename> — <reason>
|
|
33
|
+
#
|
|
34
|
+
# Exit codes:
|
|
35
|
+
# 0 = CLOSE-CANDIDATE (close action recommended)
|
|
36
|
+
# 1 = KEEP (no action)
|
|
37
|
+
# 2 = SKIP (no action; gating condition or unparseable)
|
|
38
|
+
# 3 = error (ticket file not found / git not available)
|
|
39
|
+
#
|
|
40
|
+
# Set LC_ALL=C for portable byte-grep per P328 (BSD grep on macOS
|
|
41
|
+
# silently misbehaves on UTF-8 without an explicit locale).
|
|
42
|
+
#
|
|
43
|
+
# ADR-049: never source repo-relative `packages/...` paths from a SKILL.
|
|
44
|
+
# This script is invoked via the `wr-itil-evaluate-relevance` PATH shim.
|
|
45
|
+
# ADR-026: every CLOSE-CANDIDATE verdict cites the paths checked AND
|
|
46
|
+
# the verdict is reversible (`git mv` back if a rename was missed).
|
|
47
|
+
# ADR-052: behavioural bats coverage at scripts/test/evaluate-relevance.bats.
|
|
48
|
+
|
|
49
|
+
set -euo pipefail
|
|
50
|
+
export LC_ALL=C
|
|
51
|
+
|
|
52
|
+
ticket_file="${1:-}"
|
|
53
|
+
min_age_days="${2:-7}"
|
|
54
|
+
|
|
55
|
+
if [ -z "$ticket_file" ]; then
|
|
56
|
+
echo "evaluate-relevance: usage: $0 <ticket-file> [<min-age-days>]" >&2
|
|
57
|
+
exit 3
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ ! -f "$ticket_file" ]; then
|
|
61
|
+
echo "evaluate-relevance: ticket file not found: $ticket_file" >&2
|
|
62
|
+
exit 3
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
basename=$(basename "$ticket_file")
|
|
66
|
+
|
|
67
|
+
# ── Age gate ────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
reported=$(grep -m1 -oE '^\*\*Reported\*\*: [0-9]{4}-[0-9]{2}-[0-9]{2}' "$ticket_file" 2>/dev/null | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || true)
|
|
70
|
+
if [ -z "$reported" ]; then
|
|
71
|
+
echo "SKIP $basename — no Reported date"
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Portable date arithmetic: compute cutoff date min_age_days ago as
|
|
76
|
+
# YYYY-MM-DD, then ISO string-compare. Works on both BSD (macOS) and
|
|
77
|
+
# GNU date.
|
|
78
|
+
cutoff=$(date -u -v-"${min_age_days}"d "+%Y-%m-%d" 2>/dev/null || date -u -d "${min_age_days} days ago" "+%Y-%m-%d" 2>/dev/null || true)
|
|
79
|
+
if [ -z "$cutoff" ]; then
|
|
80
|
+
echo "SKIP $basename — could not compute cutoff date (date binary missing both BSD and GNU forms)"
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ISO strings sort lexicographically. If reported > cutoff, the ticket
|
|
85
|
+
# is younger than min_age_days → skip.
|
|
86
|
+
if [ "$reported" \> "$cutoff" ]; then
|
|
87
|
+
echo "SKIP $basename — age gate (reported=$reported newer than cutoff=$cutoff, gate=${min_age_days}d)"
|
|
88
|
+
exit 2
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# ── Path extraction ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
# Regex restricts candidates to well-known repo subdirs with known
|
|
94
|
+
# file extensions. Tight on purpose — false-positive-resistant.
|
|
95
|
+
# Extension list mirrors the file types typically referenced in
|
|
96
|
+
# problem-ticket bodies (markdown, shell scripts, source, configs).
|
|
97
|
+
candidates=$(grep -oE '(packages|docs|\.changeset|src|test|scripts)/[A-Za-z0-9._/-]+\.(md|sh|ts|tsx|js|jsx|json|yml|yaml|bats|py|txt|html)' "$ticket_file" 2>/dev/null \
|
|
98
|
+
| sort -u \
|
|
99
|
+
| grep -v '^docs/problems/' \
|
|
100
|
+
|| true)
|
|
101
|
+
|
|
102
|
+
if [ -z "$candidates" ]; then
|
|
103
|
+
echo "SKIP $basename — no extractable file paths (after self-reference exclusion)"
|
|
104
|
+
exit 2
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# ── Existence check via git ls-files ────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
missing=0
|
|
110
|
+
present=0
|
|
111
|
+
missing_list=""
|
|
112
|
+
|
|
113
|
+
while IFS= read -r path; do
|
|
114
|
+
[ -z "$path" ] && continue
|
|
115
|
+
if git ls-files --error-unmatch "$path" >/dev/null 2>&1; then
|
|
116
|
+
present=$((present + 1))
|
|
117
|
+
else
|
|
118
|
+
missing=$((missing + 1))
|
|
119
|
+
if [ -z "$missing_list" ]; then
|
|
120
|
+
missing_list="$path"
|
|
121
|
+
else
|
|
122
|
+
missing_list="$missing_list;$path"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
done <<< "$candidates"
|
|
126
|
+
|
|
127
|
+
total=$((missing + present))
|
|
128
|
+
|
|
129
|
+
if [ "$missing" -eq "$total" ] && [ "$missing" -ge 1 ]; then
|
|
130
|
+
echo "CLOSE-CANDIDATE $basename — all ${total} file paths absent: ${missing_list}"
|
|
131
|
+
exit 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
echo "KEEP $basename — ${present}/${total} paths still present"
|
|
135
|
+
exit 1
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P346 — `/wr-itil:review-problems` has no path to close tickets that
|
|
4
|
+
# are no longer relevant (evidence-based, NOT age-based) —
|
|
5
|
+
# structural outflow gap drives monotonic backlog growth.
|
|
6
|
+
# Phase 1 ships the auto-close on "file no longer exists in
|
|
7
|
+
# codebase" evidence shape.
|
|
8
|
+
#
|
|
9
|
+
# Contract: `evaluate-relevance.sh <ticket-file> [<min-age-days>]` reads the
|
|
10
|
+
# ticket frontmatter for **Reported**:, applies an age gate, extracts file
|
|
11
|
+
# paths matching well-known repo subdirs from the ticket body (excluding
|
|
12
|
+
# self-references to docs/problems/*), runs `git ls-files --error-unmatch`
|
|
13
|
+
# on each, and emits a structured verdict.
|
|
14
|
+
#
|
|
15
|
+
# Output (stdout, one line):
|
|
16
|
+
# CLOSE-CANDIDATE <basename> — all <N> file paths absent: <semicolon list>
|
|
17
|
+
# KEEP <basename> — <M>/<N> paths still present
|
|
18
|
+
# SKIP <basename> — <reason>
|
|
19
|
+
#
|
|
20
|
+
# Exit codes:
|
|
21
|
+
# 0 = CLOSE-CANDIDATE
|
|
22
|
+
# 1 = KEEP
|
|
23
|
+
# 2 = SKIP
|
|
24
|
+
# 3 = error
|
|
25
|
+
#
|
|
26
|
+
# @adr ADR-079 (Evidence-based relevance-close pass — Phase 1)
|
|
27
|
+
# @adr ADR-049 (bin/ on PATH shim — adopter-safe script resolution)
|
|
28
|
+
# @adr ADR-022 (Lifecycle extension — Open|Known Error → Closed-with-reason)
|
|
29
|
+
# @adr ADR-026 (Agent output grounding — cite + persist + uncertainty)
|
|
30
|
+
# @adr ADR-052 (Behavioural bats default)
|
|
31
|
+
# @jtbd JTBD-001 (Enforce Governance — under-60s review-flow served by smaller queue)
|
|
32
|
+
# @jtbd JTBD-006 (AFK — mechanical evidence not judgment-call)
|
|
33
|
+
# @jtbd JTBD-101 (Extend the Suite — extensible pattern per evidence shape)
|
|
34
|
+
# @jtbd JTBD-201 (Audit trail — closed-ticket section preserves close reason)
|
|
35
|
+
|
|
36
|
+
setup() {
|
|
37
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
38
|
+
SCRIPT="$SCRIPTS_DIR/evaluate-relevance.sh"
|
|
39
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
40
|
+
cd "$FIXTURE_DIR"
|
|
41
|
+
git init -q -b main
|
|
42
|
+
git config user.email test@example.com
|
|
43
|
+
git config user.name "Test"
|
|
44
|
+
mkdir -p docs/problems/open docs/problems/known-error packages/itil/scripts docs/decisions
|
|
45
|
+
|
|
46
|
+
# An "old" Reported date: 60 days before today. ISO date arithmetic
|
|
47
|
+
# portable across BSD + GNU date.
|
|
48
|
+
OLD_DATE=$(date -u -v-60d "+%Y-%m-%d" 2>/dev/null || date -u -d '60 days ago' "+%Y-%m-%d")
|
|
49
|
+
# A "fresh" Reported date: 1 day before today.
|
|
50
|
+
FRESH_DATE=$(date -u -v-1d "+%Y-%m-%d" 2>/dev/null || date -u -d '1 day ago' "+%Y-%m-%d")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
teardown() {
|
|
54
|
+
cd /
|
|
55
|
+
rm -rf "$FIXTURE_DIR"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ── Existence + executable ──────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
@test "evaluate-relevance: script exists" {
|
|
61
|
+
[ -f "$SCRIPT" ]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@test "evaluate-relevance: script is executable" {
|
|
65
|
+
[ -x "$SCRIPT" ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "evaluate-relevance: PATH shim exists and dispatches" {
|
|
69
|
+
SHIM="$SCRIPTS_DIR/../bin/wr-itil-evaluate-relevance"
|
|
70
|
+
[ -f "$SHIM" ]
|
|
71
|
+
[ -x "$SHIM" ]
|
|
72
|
+
# Shim exec's the canonical script body.
|
|
73
|
+
grep -q "evaluate-relevance.sh" "$SHIM"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ── Usage / error path ──────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
@test "evaluate-relevance: no args → exit 3 with usage stderr" {
|
|
79
|
+
run "$SCRIPT"
|
|
80
|
+
[ "$status" -eq 3 ]
|
|
81
|
+
[[ "$output" == *"usage"* ]]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@test "evaluate-relevance: nonexistent ticket file → exit 3" {
|
|
85
|
+
run "$SCRIPT" /nonexistent/ticket.md
|
|
86
|
+
[ "$status" -eq 3 ]
|
|
87
|
+
[[ "$output" == *"not found"* ]]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ── Age gate (SKIP exit 2) ──────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
@test "evaluate-relevance: fresh ticket (< 7 days) → SKIP exit 2" {
|
|
93
|
+
cat > docs/problems/open/100-foo.md <<EOF
|
|
94
|
+
# Problem 100: foo
|
|
95
|
+
|
|
96
|
+
**Status**: Open
|
|
97
|
+
**Reported**: $FRESH_DATE
|
|
98
|
+
|
|
99
|
+
## Description
|
|
100
|
+
|
|
101
|
+
Bug in packages/itil/scripts/imaginary.sh
|
|
102
|
+
EOF
|
|
103
|
+
run "$SCRIPT" docs/problems/open/100-foo.md
|
|
104
|
+
[ "$status" -eq 2 ]
|
|
105
|
+
[[ "$output" == "SKIP "*"age gate"* ]]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@test "evaluate-relevance: no Reported date → SKIP exit 2" {
|
|
109
|
+
cat > docs/problems/open/101-bar.md <<EOF
|
|
110
|
+
# Problem 101: bar
|
|
111
|
+
|
|
112
|
+
**Status**: Open
|
|
113
|
+
|
|
114
|
+
## Description
|
|
115
|
+
|
|
116
|
+
packages/itil/scripts/missing.sh
|
|
117
|
+
EOF
|
|
118
|
+
run "$SCRIPT" docs/problems/open/101-bar.md
|
|
119
|
+
[ "$status" -eq 2 ]
|
|
120
|
+
[[ "$output" == *"no Reported date"* ]]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ── No extractable paths (SKIP exit 2) ──────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
@test "evaluate-relevance: no extractable file paths → SKIP exit 2" {
|
|
126
|
+
cat > docs/problems/open/102-baz.md <<EOF
|
|
127
|
+
# Problem 102: baz
|
|
128
|
+
|
|
129
|
+
**Status**: Open
|
|
130
|
+
**Reported**: $OLD_DATE
|
|
131
|
+
|
|
132
|
+
## Description
|
|
133
|
+
|
|
134
|
+
A general complaint about agent behaviour with no file references.
|
|
135
|
+
EOF
|
|
136
|
+
run "$SCRIPT" docs/problems/open/102-baz.md
|
|
137
|
+
[ "$status" -eq 2 ]
|
|
138
|
+
[[ "$output" == *"no extractable file paths"* ]]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@test "evaluate-relevance: only self-references to docs/problems/* → SKIP exit 2" {
|
|
142
|
+
cat > docs/problems/open/103-qux.md <<EOF
|
|
143
|
+
# Problem 103: qux
|
|
144
|
+
|
|
145
|
+
**Status**: Open
|
|
146
|
+
**Reported**: $OLD_DATE
|
|
147
|
+
|
|
148
|
+
## Description
|
|
149
|
+
|
|
150
|
+
Duplicate concern with docs/problems/open/099-other.md and docs/problems/known-error/088-third.md.
|
|
151
|
+
EOF
|
|
152
|
+
run "$SCRIPT" docs/problems/open/103-qux.md
|
|
153
|
+
[ "$status" -eq 2 ]
|
|
154
|
+
[[ "$output" == *"no extractable file paths"* ]]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ── CLOSE-CANDIDATE path (exit 0) ───────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
@test "evaluate-relevance: old ticket + all paths absent → CLOSE-CANDIDATE exit 0" {
|
|
160
|
+
cat > docs/problems/open/104-stale.md <<EOF
|
|
161
|
+
# Problem 104: stale
|
|
162
|
+
|
|
163
|
+
**Status**: Open
|
|
164
|
+
**Reported**: $OLD_DATE
|
|
165
|
+
|
|
166
|
+
## Description
|
|
167
|
+
|
|
168
|
+
Bug in packages/itil/scripts/imaginary-helper.sh that no longer exists.
|
|
169
|
+
Related: docs/decisions/999-imaginary-adr.proposed.md.
|
|
170
|
+
EOF
|
|
171
|
+
run "$SCRIPT" docs/problems/open/104-stale.md
|
|
172
|
+
[ "$status" -eq 0 ]
|
|
173
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"104-stale.md"*"all 2 file paths absent"* ]]
|
|
174
|
+
[[ "$output" == *"packages/itil/scripts/imaginary-helper.sh"* ]]
|
|
175
|
+
[[ "$output" == *"docs/decisions/999-imaginary-adr.proposed.md"* ]]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@test "evaluate-relevance: old ticket + single absent path → CLOSE-CANDIDATE exit 0" {
|
|
179
|
+
cat > docs/problems/open/105-single.md <<EOF
|
|
180
|
+
# Problem 105: single
|
|
181
|
+
|
|
182
|
+
**Status**: Open
|
|
183
|
+
**Reported**: $OLD_DATE
|
|
184
|
+
|
|
185
|
+
## Description
|
|
186
|
+
|
|
187
|
+
The script at packages/itil/scripts/dead-helper.sh fails.
|
|
188
|
+
EOF
|
|
189
|
+
run "$SCRIPT" docs/problems/open/105-single.md
|
|
190
|
+
[ "$status" -eq 0 ]
|
|
191
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"all 1 file paths absent"* ]]
|
|
192
|
+
[[ "$output" == *"packages/itil/scripts/dead-helper.sh"* ]]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# ── KEEP path (exit 1) ──────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
@test "evaluate-relevance: old ticket + all paths present → KEEP exit 1" {
|
|
198
|
+
# Create + stage the file so git ls-files sees it
|
|
199
|
+
echo "live" > packages/itil/scripts/live-helper.sh
|
|
200
|
+
git add packages/itil/scripts/live-helper.sh
|
|
201
|
+
|
|
202
|
+
cat > docs/problems/open/106-live.md <<EOF
|
|
203
|
+
# Problem 106: live
|
|
204
|
+
|
|
205
|
+
**Status**: Open
|
|
206
|
+
**Reported**: $OLD_DATE
|
|
207
|
+
|
|
208
|
+
## Description
|
|
209
|
+
|
|
210
|
+
Bug in packages/itil/scripts/live-helper.sh.
|
|
211
|
+
EOF
|
|
212
|
+
run "$SCRIPT" docs/problems/open/106-live.md
|
|
213
|
+
[ "$status" -eq 1 ]
|
|
214
|
+
[[ "$output" == "KEEP "*"1/1 paths still present"* ]]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@test "evaluate-relevance: old ticket + mixed paths (one present, one absent) → KEEP exit 1" {
|
|
218
|
+
echo "live" > packages/itil/scripts/exists.sh
|
|
219
|
+
git add packages/itil/scripts/exists.sh
|
|
220
|
+
|
|
221
|
+
cat > docs/problems/open/107-mixed.md <<EOF
|
|
222
|
+
# Problem 107: mixed
|
|
223
|
+
|
|
224
|
+
**Status**: Open
|
|
225
|
+
**Reported**: $OLD_DATE
|
|
226
|
+
|
|
227
|
+
## Description
|
|
228
|
+
|
|
229
|
+
Interaction between packages/itil/scripts/exists.sh and
|
|
230
|
+
packages/itil/scripts/gone.sh produces wrong result.
|
|
231
|
+
EOF
|
|
232
|
+
run "$SCRIPT" docs/problems/open/107-mixed.md
|
|
233
|
+
[ "$status" -eq 1 ]
|
|
234
|
+
[[ "$output" == "KEEP "*"1/2 paths still present"* ]]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# ── Known Error tickets (exit 0) ────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
@test "evaluate-relevance: known-error ticket with all paths absent → CLOSE-CANDIDATE exit 0" {
|
|
240
|
+
cat > docs/problems/known-error/108-ke-stale.md <<EOF
|
|
241
|
+
# Problem 108: ke-stale
|
|
242
|
+
|
|
243
|
+
**Status**: Known Error
|
|
244
|
+
**Reported**: $OLD_DATE
|
|
245
|
+
|
|
246
|
+
## Description
|
|
247
|
+
|
|
248
|
+
Fix strategy referenced packages/itil/scripts/abandoned-fix.sh.
|
|
249
|
+
|
|
250
|
+
## Root Cause Analysis
|
|
251
|
+
|
|
252
|
+
Root cause was identified in packages/itil/scripts/abandoned-fix.sh.
|
|
253
|
+
EOF
|
|
254
|
+
run "$SCRIPT" docs/problems/known-error/108-ke-stale.md
|
|
255
|
+
[ "$status" -eq 0 ]
|
|
256
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"108-ke-stale.md"* ]]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# ── Custom age gate ─────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
@test "evaluate-relevance: custom min-age-days=30 keeps a 15-day-old ticket as SKIP" {
|
|
262
|
+
MED_DATE=$(date -u -v-15d "+%Y-%m-%d" 2>/dev/null || date -u -d '15 days ago' "+%Y-%m-%d")
|
|
263
|
+
cat > docs/problems/open/109-medium.md <<EOF
|
|
264
|
+
# Problem 109: medium
|
|
265
|
+
|
|
266
|
+
**Status**: Open
|
|
267
|
+
**Reported**: $MED_DATE
|
|
268
|
+
|
|
269
|
+
## Description
|
|
270
|
+
|
|
271
|
+
packages/itil/scripts/missing.sh is broken.
|
|
272
|
+
EOF
|
|
273
|
+
run "$SCRIPT" docs/problems/open/109-medium.md 30
|
|
274
|
+
[ "$status" -eq 2 ]
|
|
275
|
+
[[ "$output" == *"age gate"* ]]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ── Output contract: verdict starts with the keyword ────────────────────────
|
|
279
|
+
|
|
280
|
+
@test "evaluate-relevance: CLOSE-CANDIDATE verdict begins with the literal keyword" {
|
|
281
|
+
cat > docs/problems/open/110-verdict.md <<EOF
|
|
282
|
+
# Problem 110: verdict
|
|
283
|
+
|
|
284
|
+
**Status**: Open
|
|
285
|
+
**Reported**: $OLD_DATE
|
|
286
|
+
|
|
287
|
+
## Description
|
|
288
|
+
|
|
289
|
+
packages/itil/scripts/missing-x.sh
|
|
290
|
+
EOF
|
|
291
|
+
run "$SCRIPT" docs/problems/open/110-verdict.md
|
|
292
|
+
[ "$status" -eq 0 ]
|
|
293
|
+
[[ "${lines[0]}" == "CLOSE-CANDIDATE "* ]]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@test "evaluate-relevance: KEEP verdict begins with the literal keyword" {
|
|
297
|
+
echo "x" > packages/itil/scripts/present-x.sh
|
|
298
|
+
git add packages/itil/scripts/present-x.sh
|
|
299
|
+
|
|
300
|
+
cat > docs/problems/open/111-keep-verdict.md <<EOF
|
|
301
|
+
# Problem 111: keep-verdict
|
|
302
|
+
|
|
303
|
+
**Status**: Open
|
|
304
|
+
**Reported**: $OLD_DATE
|
|
305
|
+
|
|
306
|
+
## Description
|
|
307
|
+
|
|
308
|
+
packages/itil/scripts/present-x.sh
|
|
309
|
+
EOF
|
|
310
|
+
run "$SCRIPT" docs/problems/open/111-keep-verdict.md
|
|
311
|
+
[ "$status" -eq 1 ]
|
|
312
|
+
[[ "${lines[0]}" == "KEEP "* ]]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@test "evaluate-relevance: SKIP verdict begins with the literal keyword" {
|
|
316
|
+
cat > docs/problems/open/112-skip-verdict.md <<EOF
|
|
317
|
+
# Problem 112: skip-verdict
|
|
318
|
+
|
|
319
|
+
**Status**: Open
|
|
320
|
+
**Reported**: $FRESH_DATE
|
|
321
|
+
|
|
322
|
+
## Description
|
|
323
|
+
|
|
324
|
+
packages/itil/scripts/anything.sh
|
|
325
|
+
EOF
|
|
326
|
+
run "$SCRIPT" docs/problems/open/112-skip-verdict.md
|
|
327
|
+
[ "$status" -eq 2 ]
|
|
328
|
+
[[ "${lines[0]}" == "SKIP "* ]]
|
|
329
|
+
}
|
|
@@ -56,7 +56,7 @@ The `.verifying.md` suffix distinguishes "fix released, awaiting user verificati
|
|
|
56
56
|
| **Known Error** | `.known-error.md` | Root cause confirmed, fix path clear, **fix NOT yet released** | Root cause documented, reproduction test exists, workaround in place |
|
|
57
57
|
| **Verification Pending** | `.verifying.md` | Fix released, awaiting user verification (ADR-022) | Fix shipped; `## Fix Released` section written; user action remaining |
|
|
58
58
|
| **Parked** | `.parked.md` | Blocked on upstream or suspended by user decision | Upstream blocker identified, or user explicitly suspends; reason and un-park trigger documented |
|
|
59
|
-
| **Closed** | `.closed.md` | Fix verified in production | User explicitly confirms the released fix works |
|
|
59
|
+
| **Closed** | `.closed.md` | Fix verified in production OR ticket determined no longer relevant via evidence | (a) User explicitly confirms the released fix works (canonical Verifying → Closed path), OR (b) auto-closed by `/wr-itil:review-problems` Step 4.6 relevance-close pass per ADR-079 Phase 1 — file-no-longer-exists evidence shape with `## Closed as no longer relevant` audit section per ADR-026 grounding (extends ADR-022 lifecycle: Open\|Known Error → Closed bypasses Verifying when no fix was released) |
|
|
60
60
|
|
|
61
61
|
**Parked problems** are excluded from WSJF ranking and work selection. They are listed separately in review output so users can see them without them polluting the backlog. To park a problem:
|
|
62
62
|
1. **If the park reason is `upstream-blocked`**, run the external-root-cause detection block at Step 7 first (see "External-root-cause detection (P063)"). Park without recording the upstream dependency in `## Related` would be the canonical audit-trail gap this block closes.
|
|
@@ -208,6 +208,90 @@ The `## Inbound Upstream Reports` README section (ADR-062 § Step 9e renderer pe
|
|
|
208
208
|
|
|
209
209
|
When invoked from `/wr-itil:work-problems` Step 6.5 (AFK orchestrator), Step 4.5 runs silently per the mechanical-stage carve-out. The only user-attention surface during AFK is the existing external-comms gate UX (a known interrupt class per ADR-028 amended); per-branch `AskUserQuestion` would re-introduce the friction P132 was engineered to remove.
|
|
210
210
|
|
|
211
|
+
### 4.6. Relevance-close pass (P346 / ADR-079 Phase 1)
|
|
212
|
+
|
|
213
|
+
For each `.open.md` / `.known-error.md` ticket aged ≥ 7 days, evaluate whether the ticket has become **no longer relevant** by checking observable evidence per ADR-026 grounding. Phase 1 scope: auto-close on the single evidence shape "file no longer exists in codebase" — closest analog to P334/P336 close-on-evidence patterns. Other evidence shapes (ADR-supersession, duplicate-of-X, "concern no longer concerning", SKILL-contract-superseded) are deferred to sibling tickets per ADR-079 scope discipline.
|
|
214
|
+
|
|
215
|
+
**User direction (verbatim, 2026-05-31)**: *"Ok, I'm happy for a skill executed as part of review problems that closes tickets that are no longer relevant, but not just because they are old"* — the relevance signal MUST be observable; age is a **gating** condition (don't bother evaluating fresh tickets), never the **closing** condition. The 7-day gate is conservative; tickets younger than that are likely still actionable.
|
|
216
|
+
|
|
217
|
+
#### 4.6a. Invoke the canonical evaluator script
|
|
218
|
+
|
|
219
|
+
For each ticket in the dual-tolerant glob `docs/problems/*.open.md docs/problems/open/*.md docs/problems/*.known-error.md docs/problems/known-error/*.md` (RFC-002 migration window), invoke the evaluator script via the ADR-049 PATH shim:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
wr-itil-evaluate-relevance "$ticket"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The `wr-itil-evaluate-relevance` command is a `$PATH`-resolved shim shipped in `packages/itil/bin/` that dispatches the canonical `packages/itil/scripts/evaluate-relevance.sh` body. ADR-049 — never invoke the canonical script via repo-relative path; the path does not resolve in adopter trees (P317 / RFC-009).
|
|
226
|
+
|
|
227
|
+
Exit-code routing (one verdict line per ticket on stdout):
|
|
228
|
+
|
|
229
|
+
| Exit | Stdout prefix | Action |
|
|
230
|
+
|------|--------------|--------|
|
|
231
|
+
| 0 | `CLOSE-CANDIDATE <basename> — all <N> file paths absent: <semicolon list>` | Auto-close branch (4.6b). |
|
|
232
|
+
| 1 | `KEEP <basename> — <M>/<N> paths still present` | No action; log only. |
|
|
233
|
+
| 2 | `SKIP <basename> — <reason>` | No action (age gate, no Reported date, no extractable paths). |
|
|
234
|
+
| 3 | error | Log advisory; do not abort the pass — relevance-close is non-blocking per the Step 4.5 fail-soft precedent. |
|
|
235
|
+
|
|
236
|
+
**Algorithm (canonical body)**: extracts file-path candidates from the ticket body matching `(packages|docs|.changeset|src|test|scripts)/[A-Za-z0-9._/-]+\.(md|sh|ts|tsx|js|jsx|json|yml|yaml|bats|py|txt|html)`, drops self-references to `docs/problems/*`, then runs `git ls-files --error-unmatch <path>` for each surviving candidate. The `CLOSE-CANDIDATE` verdict fires only when **all** extracted paths return zero AND **at least one** path was extracted. This is intentionally conservative — tickets with no extractable file paths route to `SKIP no-extractable-paths`, not auto-close.
|
|
237
|
+
|
|
238
|
+
#### 4.6b. Auto-close action per CLOSE-CANDIDATE
|
|
239
|
+
|
|
240
|
+
For each `CLOSE-CANDIDATE` ticket, perform the following BEFORE the `git mv`:
|
|
241
|
+
|
|
242
|
+
1. Use the `Edit` tool to append a `## Closed as no longer relevant` section to the ticket body (cite + persist + uncertainty per ADR-026):
|
|
243
|
+
|
|
244
|
+
```markdown
|
|
245
|
+
## Closed as no longer relevant
|
|
246
|
+
|
|
247
|
+
- **Evidence shape**: file-no-longer-exists (ADR-079 Phase 1)
|
|
248
|
+
- **Closed on**: <YYYY-MM-DD>
|
|
249
|
+
- **Closed by**: /wr-itil:review-problems Step 4.6 relevance-close pass
|
|
250
|
+
- **Cite (paths checked, all absent in `git ls-files`)**: <semicolon-separated list from the CLOSE-CANDIDATE verdict>
|
|
251
|
+
- **Persist**: this section is committed in the ticket file itself; the script body at `packages/itil/scripts/evaluate-relevance.sh` is the re-runnable verdict source per ADR-026
|
|
252
|
+
- **Uncertainty / reversibility**: verdict is deterministic given the path set. False-positive remediation: `git revert` the relevance-close commit OR `git mv` the ticket back to its prior state if a missed rename surfaces. The ≥7-day age gate guards against premature evaluation.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
2. `git mv` the ticket from its current state directory to `closed/` (lifecycle extension per ADR-079 — Open|Known Error → Closed bypasses Verifying because no fix was released; conclusion is "no fix needed"):
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
git mv docs/problems/open/<NNN>-<title>.md docs/problems/closed/<NNN>-<title>.md
|
|
259
|
+
# or known-error/<NNN>-<title>.md → closed/<NNN>-<title>.md
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
3. Use the `Edit` tool to update the `**Status**:` field to `Closed`.
|
|
263
|
+
|
|
264
|
+
4. **Re-stage explicitly per the P057 staging trap** — `git mv` alone stages the rename; the subsequent `Edit` content changes are not in the rename's index entry:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
git add docs/problems/closed/<NNN>-<title>.md
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### 4.6c. Batch commit grain (per ADR-014 / P139)
|
|
271
|
+
|
|
272
|
+
All relevance-closes from THIS review pass batch into ONE commit, mirroring `/wr-itil:transition-problems` batch grain (P139). The commit message names the count and the closure class:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
git commit -m "chore(problems): relevance-close pass — close <N> tickets as no longer relevant
|
|
276
|
+
|
|
277
|
+
Auto-closed via /wr-itil:review-problems Step 4.6 (ADR-079 Phase 1
|
|
278
|
+
file-no-longer-exists evidence shape). Each closed ticket carries
|
|
279
|
+
a ## Closed as no longer relevant section citing the file paths
|
|
280
|
+
no longer present in git ls-files. Reversible via git revert.
|
|
281
|
+
|
|
282
|
+
Closed: P<NNN>, P<NNN>, ..."
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Step 5's README refresh rides the same commit per ADR-014 single-commit grain — `docs/problems/README.md` gets re-rendered with the closed tickets dropped from WSJF Rankings.
|
|
286
|
+
|
|
287
|
+
#### 4.6d. AFK-policy-authorised silent proceed (ADR-013 Rule 5 / ADR-044 category 4)
|
|
288
|
+
|
|
289
|
+
The relevance-close pass runs **unconditionally** during AFK orchestration (`/wr-itil:work-problems` Step 6.5). File existence is empirical, not user-judgment — the mechanical-stage carve-out (P132) applies per ADR-044 category-4 silent framework action. Do NOT fire `AskUserQuestion` per CLOSE-CANDIDATE; the framework has already resolved the close-on-empirical-evidence question.
|
|
290
|
+
|
|
291
|
+
**Worked example (real backlog smoke test, 2026-05-31)**: across 143 open / known-error tickets, Phase 1 surfaced 6 CLOSE-CANDIDATEs (4.2%) — tickets referencing files renamed or removed but whose lifecycle close was missed. 44 routed to KEEP (file paths still present), 93 to SKIP (age gate, no extractable paths, or no Reported date). The 4.2% close rate matches expectations for a 47-day-old backlog with one-time outflow gap closure.
|
|
292
|
+
|
|
293
|
+
**Cross-references**: ADR-079 (this pass's design ADR), ADR-026 (grounding), ADR-022 + ADR-079 lifecycle extension (Open|Known Error → Closed bypassing Verifying for no-fix-needed conclusions), ADR-049 (PATH shim), ADR-052 (behavioural bats at `packages/itil/scripts/test/evaluate-relevance.bats`), ADR-014 (commit grain), P057 (staging trap), P346 (driver ticket).
|
|
294
|
+
|
|
211
295
|
### 5. Rewrite `docs/problems/README.md`
|
|
212
296
|
|
|
213
297
|
Write / overwrite `docs/problems/README.md` with the refreshed ranking so future `work-problem` / `list-problems` fast-paths can skip the full re-scan. Rendering rules match the SKILL.md `Present the refreshed ranking` section above — driven off globs, not file-body scans:
|