@windyroad/itil 0.39.0 → 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.
@@ -484,5 +484,5 @@
484
484
  }
485
485
  },
486
486
  "name": "wr-itil",
487
- "version": "0.39.0"
487
+ "version": "0.40.0"
488
488
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/../scripts/evaluate-relevance.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.39.0",
3
+ "version": "0.40.0-preview.479",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -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: