@windyroad/itil 0.40.0-preview.479 → 0.41.0-preview.484
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/package.json
CHANGED
|
@@ -2,49 +2,46 @@
|
|
|
2
2
|
# packages/itil/scripts/evaluate-relevance.sh
|
|
3
3
|
#
|
|
4
4
|
# Evaluate whether a problem ticket has become "no longer relevant" by
|
|
5
|
-
# checking observable evidence per ADR-026 grounding.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# checking observable evidence per ADR-026 grounding. Implements 5
|
|
6
|
+
# evidence shapes per ADR-079 (Phase 1 + Phase 2):
|
|
7
|
+
#
|
|
8
|
+
# Shape 1 — file-no-longer-exists (Phase 1, original)
|
|
9
|
+
# Shape 2 — ADR-shipped with `human-oversight: confirmed` (Phase 2)
|
|
10
|
+
# Shape 3 — named-skill-or-feature-exists (Phase 2)
|
|
11
|
+
# Shape 4 — self-marker-in-body (line-anchored) (Phase 2)
|
|
12
|
+
# Shape 5 — driver-child-ticket-closed (Phase 2)
|
|
13
|
+
#
|
|
14
|
+
# Phase 1 false-positive fixes (Phase 2):
|
|
15
|
+
# - state-suffix detection (P180): per-state subdirs + .<state>.md
|
|
16
|
+
# - sibling-file detection (P244): dir-glob slug-prefix
|
|
17
|
+
# - rename detection (P251): git log --follow --diff-filter=AD
|
|
8
18
|
#
|
|
9
19
|
# Usage:
|
|
10
20
|
# evaluate-relevance.sh <ticket-file> [<min-age-days>]
|
|
11
21
|
#
|
|
12
|
-
# Default <min-age-days> is 7. Age gate is a GATING condition
|
|
13
|
-
#
|
|
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.
|
|
22
|
+
# Default <min-age-days> is 7. Age gate is a GATING condition per user
|
|
23
|
+
# direction 2026-05-31 "not just because they are old".
|
|
28
24
|
#
|
|
29
25
|
# Output (stdout, one line):
|
|
30
|
-
# CLOSE-CANDIDATE <basename> —
|
|
31
|
-
#
|
|
32
|
-
#
|
|
26
|
+
# CLOSE-CANDIDATE <basename> — shapes: <comma-list> — <per-shape cite>; ...
|
|
27
|
+
# CLOSE-CANDIDATE-WITH-CAVEAT <basename> — shapes: <list> — caveat: <tag>: <one-line>
|
|
28
|
+
# KEEP-WITH-NOTE <basename> — <note>: <evidence>
|
|
29
|
+
# KEEP <basename> — <M>/<N> paths still present
|
|
30
|
+
# SKIP <basename> — <reason>
|
|
33
31
|
#
|
|
34
32
|
# Exit codes:
|
|
35
|
-
# 0 = CLOSE-CANDIDATE
|
|
36
|
-
# 1 = KEEP
|
|
37
|
-
# 2 = SKIP
|
|
38
|
-
# 3 = error
|
|
33
|
+
# 0 = CLOSE-CANDIDATE or CLOSE-CANDIDATE-WITH-CAVEAT
|
|
34
|
+
# 1 = KEEP or KEEP-WITH-NOTE
|
|
35
|
+
# 2 = SKIP
|
|
36
|
+
# 3 = error
|
|
39
37
|
#
|
|
40
38
|
# Set LC_ALL=C for portable byte-grep per P328 (BSD grep on macOS
|
|
41
39
|
# silently misbehaves on UTF-8 without an explicit locale).
|
|
42
40
|
#
|
|
43
|
-
# ADR-049:
|
|
44
|
-
#
|
|
45
|
-
# ADR-
|
|
46
|
-
#
|
|
47
|
-
# ADR-052: behavioural bats coverage at scripts/test/evaluate-relevance.bats.
|
|
41
|
+
# ADR-049: invoked via the `wr-itil-evaluate-relevance` PATH shim.
|
|
42
|
+
# ADR-026: every CLOSE-CANDIDATE verdict cites the evidence per shape.
|
|
43
|
+
# ADR-052: behavioural bats at scripts/test/evaluate-relevance.bats.
|
|
44
|
+
# ADR-079: design (Phase 1 + Phase 2).
|
|
48
45
|
|
|
49
46
|
set -euo pipefail
|
|
50
47
|
export LC_ALL=C
|
|
@@ -72,64 +69,462 @@ if [ -z "$reported" ]; then
|
|
|
72
69
|
exit 2
|
|
73
70
|
fi
|
|
74
71
|
|
|
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
72
|
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
73
|
if [ -z "$cutoff" ]; then
|
|
80
74
|
echo "SKIP $basename — could not compute cutoff date (date binary missing both BSD and GNU forms)"
|
|
81
75
|
exit 2
|
|
82
76
|
fi
|
|
83
77
|
|
|
84
|
-
# ISO strings sort lexicographically. If reported > cutoff, the ticket
|
|
85
|
-
# is younger than min_age_days → skip.
|
|
86
78
|
if [ "$reported" \> "$cutoff" ]; then
|
|
87
79
|
echo "SKIP $basename — age gate (reported=$reported newer than cutoff=$cutoff, gate=${min_age_days}d)"
|
|
88
80
|
exit 2
|
|
89
81
|
fi
|
|
90
82
|
|
|
91
|
-
# ──
|
|
83
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
# Check whether <path> is tracked by git (`git ls-files --error-unmatch`).
|
|
86
|
+
# Falls back to filesystem check when not in a git repo or when the file
|
|
87
|
+
# is staged-but-untracked-by-HEAD. Returns 0 if present.
|
|
88
|
+
path_exists() {
|
|
89
|
+
local p="$1"
|
|
90
|
+
if git ls-files --error-unmatch "$p" >/dev/null 2>&1; then
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
if [ -e "$p" ]; then
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Detect state-suffix variants of an incident/problem/RFC path (P180 fix).
|
|
100
|
+
# Given a path like `docs/incidents/I002-foo.investigating.md`, also checks
|
|
101
|
+
# `docs/incidents/I002-foo.{restored,mitigating,closed}.md`. Returns 0 if
|
|
102
|
+
# any sibling state-suffix variant exists.
|
|
103
|
+
state_suffix_variant_exists() {
|
|
104
|
+
local p="$1"
|
|
105
|
+
local dir base ext
|
|
106
|
+
dir=$(dirname "$p")
|
|
107
|
+
base=$(basename "$p")
|
|
108
|
+
# Strip trailing .<state>.md to a slug-prefix and recombine with each
|
|
109
|
+
# state. Conservative — only fires for paths in docs/incidents/,
|
|
110
|
+
# docs/problems/, docs/rfcs/, docs/stories/, docs/story-maps/ where the
|
|
111
|
+
# state-suffix convention applies (ADR-031 / ADR-060).
|
|
112
|
+
case "$dir" in
|
|
113
|
+
docs/incidents|docs/problems|docs/problems/*|docs/rfcs|docs/rfcs/*|docs/stories|docs/stories/*|docs/story-maps|docs/story-maps/*)
|
|
114
|
+
;;
|
|
115
|
+
*)
|
|
116
|
+
return 1
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
# Strip <state>.md suffix
|
|
120
|
+
local slug
|
|
121
|
+
slug=$(echo "$base" | sed -E 's/\.(open|known-error|verifying|closed|parked|investigating|mitigating|restored|draft|accepted|in-progress|done|archived|proposed|superseded)\.md$//')
|
|
122
|
+
if [ "$slug" = "$base" ]; then
|
|
123
|
+
return 1
|
|
124
|
+
fi
|
|
125
|
+
# Also try the per-state subdir layout (RFC-002 migration window).
|
|
126
|
+
local parent
|
|
127
|
+
parent=$(dirname "$dir")
|
|
128
|
+
local entity
|
|
129
|
+
entity=$(basename "$dir")
|
|
130
|
+
for state in open known-error verifying closed parked investigating mitigating restored draft accepted in-progress done archived proposed superseded; do
|
|
131
|
+
if [ -f "$dir/$slug.$state.md" ] && [ "$dir/$slug.$state.md" != "$p" ]; then
|
|
132
|
+
echo "$dir/$slug.$state.md"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
# Per-state subdir form: docs/problems/<state>/<slug>.md
|
|
136
|
+
case "$dir" in
|
|
137
|
+
docs/incidents|docs/problems|docs/rfcs|docs/stories|docs/story-maps)
|
|
138
|
+
if [ -f "$dir/$state/$slug.md" ]; then
|
|
139
|
+
echo "$dir/$state/$slug.md"
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
;;
|
|
143
|
+
docs/problems/*|docs/rfcs/*|docs/stories/*|docs/story-maps/*|docs/incidents/*)
|
|
144
|
+
if [ -f "$parent/$state/$slug.md" ]; then
|
|
145
|
+
echo "$parent/$state/$slug.md"
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
;;
|
|
149
|
+
esac
|
|
150
|
+
done
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Detect sibling files with similar slug-prefix in the same parent dir
|
|
155
|
+
# (P244 fix). Given `packages/foo/scripts/bar-list.sh`, finds
|
|
156
|
+
# `packages/foo/scripts/bar-render.sh` / `bar-populate.sh` etc.
|
|
157
|
+
# Returns 0 (and echoes the matched sibling) if a sibling exists.
|
|
158
|
+
sibling_file_exists() {
|
|
159
|
+
local p="$1"
|
|
160
|
+
local dir base ext stem prefix
|
|
161
|
+
dir=$(dirname "$p")
|
|
162
|
+
base=$(basename "$p")
|
|
163
|
+
ext="${base##*.}"
|
|
164
|
+
stem="${base%.*}"
|
|
165
|
+
# slug-prefix = first 2 dash-separated tokens (e.g. "plugin-maturity")
|
|
166
|
+
# for "plugin-maturity-list" we take "plugin-maturity"; for "foo-bar"
|
|
167
|
+
# we take "foo". Conservative — too-short prefixes (single token) skip.
|
|
168
|
+
prefix=$(echo "$stem" | cut -d- -f1-2)
|
|
169
|
+
if [ "$prefix" = "$stem" ]; then
|
|
170
|
+
# Single-token stem — no sibling-pattern to detect.
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
# Require at least 2 dash-separated tokens AND a multi-char first token
|
|
174
|
+
local token_count
|
|
175
|
+
token_count=$(echo "$stem" | tr '-' '\n' | wc -l | tr -d ' ')
|
|
176
|
+
if [ "$token_count" -lt 2 ]; then
|
|
177
|
+
return 1
|
|
178
|
+
fi
|
|
179
|
+
if [ ! -d "$dir" ]; then
|
|
180
|
+
return 1
|
|
181
|
+
fi
|
|
182
|
+
local sibling
|
|
183
|
+
# Use ls/find to enumerate; nullglob via shell would expand to literal
|
|
184
|
+
# if no matches. Use find for portability.
|
|
185
|
+
while IFS= read -r sibling; do
|
|
186
|
+
[ -z "$sibling" ] && continue
|
|
187
|
+
if [ "$sibling" = "$p" ]; then
|
|
188
|
+
continue
|
|
189
|
+
fi
|
|
190
|
+
if [ -f "$sibling" ]; then
|
|
191
|
+
echo "$sibling"
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
done < <(find "$dir" -maxdepth 1 -type f -name "$prefix-*.$ext" 2>/dev/null)
|
|
195
|
+
return 1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Detect rename via git log --follow (P251 fix). Returns 0 (and echoes the
|
|
199
|
+
# detected new name) if the file was renamed away from <path>.
|
|
200
|
+
rename_detected() {
|
|
201
|
+
local p="$1"
|
|
202
|
+
local log
|
|
203
|
+
log=$(git log --follow --diff-filter=AD --name-only --pretty=format: -- "$p" 2>/dev/null | grep -v '^$' || true)
|
|
204
|
+
if [ -z "$log" ]; then
|
|
205
|
+
return 1
|
|
206
|
+
fi
|
|
207
|
+
# If the most recent name is different from the queried path, it was renamed.
|
|
208
|
+
local most_recent
|
|
209
|
+
most_recent=$(echo "$log" | head -1)
|
|
210
|
+
if [ -n "$most_recent" ] && [ "$most_recent" != "$p" ]; then
|
|
211
|
+
if [ -f "$most_recent" ] || git ls-files --error-unmatch "$most_recent" >/dev/null 2>&1; then
|
|
212
|
+
echo "$most_recent"
|
|
213
|
+
return 0
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
return 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# ── Shape detection ─────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
shapes=""
|
|
222
|
+
cites=""
|
|
223
|
+
caveat_tag=""
|
|
224
|
+
caveat_msg=""
|
|
225
|
+
keep_with_note=""
|
|
226
|
+
|
|
227
|
+
# Append a shape to the cumulative shape list + emit per-shape cite.
|
|
228
|
+
record_shape() {
|
|
229
|
+
local shape="$1" cite="$2"
|
|
230
|
+
if [ -z "$shapes" ]; then
|
|
231
|
+
shapes="$shape"
|
|
232
|
+
cites="$cite"
|
|
233
|
+
else
|
|
234
|
+
case ",$shapes," in
|
|
235
|
+
*",$shape,"*) return 0 ;; # already recorded
|
|
236
|
+
esac
|
|
237
|
+
shapes="$shapes,$shape"
|
|
238
|
+
cites="$cites; $cite"
|
|
239
|
+
fi
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Shape 1 — file-no-longer-exists (Phase 1 original).
|
|
243
|
+
# Extracts candidate paths, drops self-refs, runs path_exists per
|
|
244
|
+
# candidate. Detects state-suffix / sibling-file / rename to avoid Phase 1
|
|
245
|
+
# false-positives (P180/P244/P251); on detection routes to KEEP-WITH-NOTE.
|
|
92
246
|
|
|
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
247
|
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
248
|
| sort -u \
|
|
99
249
|
| grep -v '^docs/problems/' \
|
|
100
250
|
|| true)
|
|
101
251
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
252
|
+
shape1_missing=0
|
|
253
|
+
shape1_present=0
|
|
254
|
+
shape1_missing_list=""
|
|
255
|
+
|
|
256
|
+
if [ -n "$candidates" ]; then
|
|
257
|
+
while IFS= read -r path; do
|
|
258
|
+
[ -z "$path" ] && continue
|
|
259
|
+
if path_exists "$path"; then
|
|
260
|
+
shape1_present=$((shape1_present + 1))
|
|
261
|
+
else
|
|
262
|
+
# Phase 1 false-positive fixes — check state-suffix / sibling-file
|
|
263
|
+
# / rename BEFORE counting as missing.
|
|
264
|
+
variant=$(state_suffix_variant_exists "$path" 2>/dev/null || true)
|
|
265
|
+
if [ -n "$variant" ]; then
|
|
266
|
+
keep_with_note="state-suffix variant exists: $variant"
|
|
267
|
+
break
|
|
268
|
+
fi
|
|
269
|
+
sibling=$(sibling_file_exists "$path" 2>/dev/null || true)
|
|
270
|
+
if [ -n "$sibling" ]; then
|
|
271
|
+
keep_with_note="sibling file with similar slug-prefix exists: $sibling"
|
|
272
|
+
break
|
|
273
|
+
fi
|
|
274
|
+
renamed=$(rename_detected "$path" 2>/dev/null || true)
|
|
275
|
+
if [ -n "$renamed" ]; then
|
|
276
|
+
keep_with_note="renamed (git log --follow): $path → $renamed"
|
|
277
|
+
break
|
|
278
|
+
fi
|
|
279
|
+
shape1_missing=$((shape1_missing + 1))
|
|
280
|
+
if [ -z "$shape1_missing_list" ]; then
|
|
281
|
+
shape1_missing_list="$path"
|
|
282
|
+
else
|
|
283
|
+
shape1_missing_list="$shape1_missing_list;$path"
|
|
284
|
+
fi
|
|
285
|
+
fi
|
|
286
|
+
done <<< "$candidates"
|
|
105
287
|
fi
|
|
106
288
|
|
|
107
|
-
#
|
|
289
|
+
# Shape 1 fires only when ALL extracted candidates are absent AND at
|
|
290
|
+
# least one was extracted.
|
|
291
|
+
if [ -z "$keep_with_note" ] && [ -n "$candidates" ] && [ "$shape1_missing" -ge 1 ] && [ "$shape1_present" -eq 0 ]; then
|
|
292
|
+
shape1_total=$shape1_missing
|
|
293
|
+
record_shape "file-no-longer-exists" "all ${shape1_total} file paths absent: ${shape1_missing_list}"
|
|
294
|
+
fi
|
|
108
295
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
296
|
+
# Shape 2 — ADR-shipped with `human-oversight: confirmed`.
|
|
297
|
+
# grep ticket body for ADR-NNN; for each, check docs/decisions/<NNN>-*.md
|
|
298
|
+
# exists AND frontmatter contains `human-oversight: confirmed`.
|
|
112
299
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
300
|
+
adr_refs=$(grep -oE '\bADR-[0-9]{3}\b' "$ticket_file" 2>/dev/null | sort -u || true)
|
|
301
|
+
shape2_confirmed=""
|
|
302
|
+
if [ -n "$adr_refs" ]; then
|
|
303
|
+
while IFS= read -r adr; do
|
|
304
|
+
[ -z "$adr" ] && continue
|
|
305
|
+
num="${adr#ADR-}"
|
|
306
|
+
# Find any docs/decisions/<num>-*.md (any state suffix).
|
|
307
|
+
while IFS= read -r adr_file; do
|
|
308
|
+
[ -z "$adr_file" ] && continue
|
|
309
|
+
if grep -qE '^human-oversight: confirmed' "$adr_file" 2>/dev/null; then
|
|
310
|
+
if [ -z "$shape2_confirmed" ]; then
|
|
311
|
+
shape2_confirmed="$adr ($adr_file)"
|
|
312
|
+
else
|
|
313
|
+
shape2_confirmed="$shape2_confirmed, $adr ($adr_file)"
|
|
314
|
+
fi
|
|
315
|
+
break
|
|
316
|
+
fi
|
|
317
|
+
done < <(find docs/decisions -maxdepth 1 -type f -name "${num}-*.md" 2>/dev/null)
|
|
318
|
+
done <<< "$adr_refs"
|
|
319
|
+
fi
|
|
320
|
+
if [ -n "$shape2_confirmed" ]; then
|
|
321
|
+
record_shape "ADR-shipped-confirmed" "ADRs human-oversight-confirmed: ${shape2_confirmed}"
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
# Shape 3 — named-skill-or-feature-exists.
|
|
325
|
+
# Detects SKILL.md / hook / agent / slash-command surfaces that exist.
|
|
326
|
+
|
|
327
|
+
shape3_hits=""
|
|
328
|
+
|
|
329
|
+
# (a) SKILL.md paths.
|
|
330
|
+
while IFS= read -r skill_path; do
|
|
331
|
+
[ -z "$skill_path" ] && continue
|
|
332
|
+
if path_exists "$skill_path"; then
|
|
333
|
+
if [ -z "$shape3_hits" ]; then
|
|
334
|
+
shape3_hits="$skill_path"
|
|
121
335
|
else
|
|
122
|
-
|
|
336
|
+
shape3_hits="$shape3_hits; $skill_path"
|
|
123
337
|
fi
|
|
124
338
|
fi
|
|
125
|
-
done
|
|
339
|
+
done < <(grep -oE 'packages/[A-Za-z0-9_-]+/skills/[A-Za-z0-9_-]+/SKILL\.md' "$ticket_file" 2>/dev/null | sort -u || true)
|
|
126
340
|
|
|
127
|
-
|
|
341
|
+
# (b) Hook paths.
|
|
342
|
+
while IFS= read -r hook_path; do
|
|
343
|
+
[ -z "$hook_path" ] && continue
|
|
344
|
+
if path_exists "$hook_path"; then
|
|
345
|
+
if [ -z "$shape3_hits" ]; then
|
|
346
|
+
shape3_hits="$hook_path"
|
|
347
|
+
else
|
|
348
|
+
shape3_hits="$shape3_hits; $hook_path"
|
|
349
|
+
fi
|
|
350
|
+
fi
|
|
351
|
+
done < <(grep -oE 'packages/[A-Za-z0-9_-]+/hooks/[A-Za-z0-9._-]+\.sh' "$ticket_file" 2>/dev/null | sort -u || true)
|
|
128
352
|
|
|
129
|
-
|
|
130
|
-
|
|
353
|
+
# (c) Agent paths.
|
|
354
|
+
while IFS= read -r agent_path; do
|
|
355
|
+
[ -z "$agent_path" ] && continue
|
|
356
|
+
if path_exists "$agent_path"; then
|
|
357
|
+
if [ -z "$shape3_hits" ]; then
|
|
358
|
+
shape3_hits="$agent_path"
|
|
359
|
+
else
|
|
360
|
+
shape3_hits="$shape3_hits; $agent_path"
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
done < <(grep -oE 'packages/[A-Za-z0-9_-]+/agents/[A-Za-z0-9._-]+\.md' "$ticket_file" 2>/dev/null | sort -u || true)
|
|
364
|
+
|
|
365
|
+
# (d) Slash-command refs — resolve to packages/<plugin>/skills/<skill>/SKILL.md
|
|
366
|
+
while IFS= read -r slash; do
|
|
367
|
+
[ -z "$slash" ] && continue
|
|
368
|
+
plugin=$(echo "$slash" | sed -E 's|/wr-([a-z0-9-]+):.*|\1|')
|
|
369
|
+
skill=$(echo "$slash" | sed -E 's|/wr-[a-z0-9-]+:([a-z0-9-]+).*|\1|')
|
|
370
|
+
candidate="packages/$plugin/skills/$skill/SKILL.md"
|
|
371
|
+
if path_exists "$candidate"; then
|
|
372
|
+
if [ -z "$shape3_hits" ]; then
|
|
373
|
+
shape3_hits="$slash → $candidate"
|
|
374
|
+
else
|
|
375
|
+
shape3_hits="$shape3_hits; $slash → $candidate"
|
|
376
|
+
fi
|
|
377
|
+
fi
|
|
378
|
+
done < <(grep -oE '/wr-[a-z0-9-]+:[a-z0-9-]+' "$ticket_file" 2>/dev/null | sort -u || true)
|
|
379
|
+
|
|
380
|
+
if [ -n "$shape3_hits" ]; then
|
|
381
|
+
record_shape "named-skill-or-feature-exists" "feature surfaces exist: ${shape3_hits}"
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# Shape 4 — self-marker-in-body (line-anchored regex per architect A2).
|
|
385
|
+
# Patterns:
|
|
386
|
+
# ^.* Close to (Verifying|Closed)\b
|
|
387
|
+
# ^.* DONE 2026-
|
|
388
|
+
# ^.* fix shipped session
|
|
389
|
+
# ^.* awaiting K→V
|
|
390
|
+
# ^## Fix Released
|
|
391
|
+
# Line-anchored: must appear at line-start (with optional leading bullet
|
|
392
|
+
# or heading prefix) — prevents mid-prose narrative false-positives.
|
|
393
|
+
|
|
394
|
+
shape4_marker=""
|
|
395
|
+
if grep -qE '^[[:space:]]*[#>*-]*[[:space:]]*.*Close to (Verifying|Closed)\b' "$ticket_file" 2>/dev/null; then
|
|
396
|
+
shape4_marker="'Close to Verifying|Closed' line marker"
|
|
397
|
+
elif grep -qE '^[[:space:]]*[#>*-]*[[:space:]]*\[?[x ]?\]?[[:space:]]*\*?\*?DONE 2026-' "$ticket_file" 2>/dev/null; then
|
|
398
|
+
shape4_marker="'DONE 2026-' line marker"
|
|
399
|
+
elif grep -qE '^## Fix Released' "$ticket_file" 2>/dev/null; then
|
|
400
|
+
shape4_marker="'## Fix Released' heading"
|
|
401
|
+
elif grep -qE '^[[:space:]]*[#>*-]*[[:space:]]*.*fix shipped session' "$ticket_file" 2>/dev/null; then
|
|
402
|
+
shape4_marker="'fix shipped session' line marker"
|
|
403
|
+
elif grep -qE '^[[:space:]]*[#>*-]*[[:space:]]*.*awaiting K→V' "$ticket_file" 2>/dev/null; then
|
|
404
|
+
shape4_marker="'awaiting K→V' line marker"
|
|
405
|
+
fi
|
|
406
|
+
if [ -n "$shape4_marker" ]; then
|
|
407
|
+
record_shape "self-marker-in-body" "self-marker: ${shape4_marker}"
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
# Shape 5 — driver-child-ticket-closed.
|
|
411
|
+
# Parse `## Related` section for P<NNN> refs; check if any are in
|
|
412
|
+
# docs/problems/closed/ (dual-tolerant: subdir OR .closed.md suffix).
|
|
413
|
+
# Per advisory A1, only fires when the child has NO unresolved
|
|
414
|
+
# investigation items of its own (rough heuristic: no unticked checkboxes
|
|
415
|
+
# OR no extractable open file refs).
|
|
416
|
+
|
|
417
|
+
# Extract section starting at "## Related" through end of file.
|
|
418
|
+
related_section=$(awk '/^## Related/{flag=1;next} /^## /{flag=0} flag' "$ticket_file" 2>/dev/null || true)
|
|
419
|
+
closed_drivers=""
|
|
420
|
+
if [ -n "$related_section" ]; then
|
|
421
|
+
while IFS= read -r pnum; do
|
|
422
|
+
[ -z "$pnum" ] && continue
|
|
423
|
+
n="${pnum#P}"
|
|
424
|
+
n="${n#p}"
|
|
425
|
+
# Strip leading zeros before printf so bash doesn't interpret 034 as
|
|
426
|
+
# octal (would yield decimal 28 — silent attribution bug). Use a
|
|
427
|
+
# while-loop strip rather than $((10#$n)) so we don't trip on
|
|
428
|
+
# malformed input.
|
|
429
|
+
n_clean="$n"
|
|
430
|
+
while [ "${n_clean#0}" != "$n_clean" ] && [ -n "${n_clean#0}" ]; do
|
|
431
|
+
n_clean="${n_clean#0}"
|
|
432
|
+
done
|
|
433
|
+
pattern_num=$(printf "%03d" "$n_clean" 2>/dev/null || echo "$n")
|
|
434
|
+
while IFS= read -r closed_file; do
|
|
435
|
+
[ -z "$closed_file" ] && continue
|
|
436
|
+
if [ -z "$closed_drivers" ]; then
|
|
437
|
+
closed_drivers="$pnum ($closed_file)"
|
|
438
|
+
else
|
|
439
|
+
closed_drivers="$closed_drivers, $pnum ($closed_file)"
|
|
440
|
+
fi
|
|
441
|
+
break
|
|
442
|
+
done < <(find docs/problems/closed -maxdepth 1 -type f \( -name "${pattern_num}-*.md" -o -name "${pattern_num}-*.closed.md" \) 2>/dev/null; find docs/problems -maxdepth 1 -type f -name "${pattern_num}-*.closed.md" 2>/dev/null)
|
|
443
|
+
done < <(echo "$related_section" | grep -oE '\bP[0-9]{2,4}\b' | sort -u || true)
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
# A1 guard — "child has independent open work" disambiguation.
|
|
447
|
+
#
|
|
448
|
+
# Shape 5 says "the driver in ## Related is closed". This is contributory
|
|
449
|
+
# evidence at best — the driver's closure does not prove the child's work
|
|
450
|
+
# is done. Per architect advisory A1, shape 5 should NOT fire when the
|
|
451
|
+
# child clearly names independent outstanding scope.
|
|
452
|
+
#
|
|
453
|
+
# Detection: if the ticket body references a `packages/.../skills/<name>/
|
|
454
|
+
# SKILL.md` or `packages/.../agents/<name>.md` that does NOT exist on
|
|
455
|
+
# disk, the umbrella is naming a feature that hasn't been built. This is
|
|
456
|
+
# future-work, not stale-work — suppress both shape 5 AND shape 1 for the
|
|
457
|
+
# missing future-work path; emit KEEP-WITH-NOTE.
|
|
458
|
+
future_work_skill_ref=""
|
|
459
|
+
while IFS= read -r future_skill; do
|
|
460
|
+
[ -z "$future_skill" ] && continue
|
|
461
|
+
if ! path_exists "$future_skill"; then
|
|
462
|
+
future_work_skill_ref="$future_skill"
|
|
463
|
+
break
|
|
464
|
+
fi
|
|
465
|
+
done < <(grep -oE 'packages/[A-Za-z0-9_-]+/(skills/[A-Za-z0-9_-]+/SKILL\.md|agents/[A-Za-z0-9._-]+\.md)' "$ticket_file" 2>/dev/null | sort -u || true)
|
|
466
|
+
|
|
467
|
+
if [ -n "$closed_drivers" ] && [ -n "$future_work_skill_ref" ]; then
|
|
468
|
+
# Closed driver AND child names a SKILL/agent that hasn't been built
|
|
469
|
+
# yet — future work, not stale. KEEP-WITH-NOTE.
|
|
470
|
+
keep_with_note="closed driver(s) ${closed_drivers}, but child names unbuilt SKILL/agent: $future_work_skill_ref"
|
|
471
|
+
# Reset cumulative shapes so the KEEP-WITH-NOTE branch routes cleanly.
|
|
472
|
+
shapes=""
|
|
473
|
+
cites=""
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
if [ -n "$closed_drivers" ] && [ -z "$keep_with_note" ]; then
|
|
477
|
+
record_shape "driver-child-ticket-closed" "drivers closed: ${closed_drivers}"
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
# ── Caveat detection ────────────────────────────────────────────────────────
|
|
481
|
+
# Architect condition C2: when shape detection is partial (umbrella with
|
|
482
|
+
# mixed-phase progress), emit CLOSE-CANDIDATE-WITH-CAVEAT with structured
|
|
483
|
+
# caveat short-tag + one-line prose so the SKILL Step 4.6b template can
|
|
484
|
+
# splice the **Caveat** field directly.
|
|
485
|
+
|
|
486
|
+
if [ -n "$shapes" ]; then
|
|
487
|
+
# Multi-phase umbrella detection: unticked checkboxes in the ticket
|
|
488
|
+
# body + at least one shipped-evidence shape match. The shape match
|
|
489
|
+
# itself is the "progress made" signal — unticked tasks indicate
|
|
490
|
+
# outstanding scope the maintainer must confirm before close.
|
|
491
|
+
#
|
|
492
|
+
# Use `grep | wc -l` so a no-match scenario exits 0 (wc always returns
|
|
493
|
+
# 0) — avoids the pipefail + `grep -c` trap where grep prints "0" then
|
|
494
|
+
# exits 1, tripping set -e on assignment.
|
|
495
|
+
unticked_count=$(grep -cE '^[[:space:]]*-[[:space:]]*\[[[:space:]]\][[:space:]]' "$ticket_file" 2>/dev/null || true)
|
|
496
|
+
ticked_count=$(grep -cE '^[[:space:]]*-[[:space:]]*\[x\][[:space:]]' "$ticket_file" 2>/dev/null || true)
|
|
497
|
+
unticked_count=${unticked_count:-0}
|
|
498
|
+
ticked_count=${ticked_count:-0}
|
|
499
|
+
if [ "$unticked_count" -ge 1 ]; then
|
|
500
|
+
caveat_tag="multi-phase-mixed-progress"
|
|
501
|
+
caveat_msg="${ticked_count} task(s) done, ${unticked_count} outstanding — confirm umbrella scope before close"
|
|
502
|
+
fi
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
# ── Verdict emission ────────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
if [ -n "$keep_with_note" ]; then
|
|
508
|
+
echo "KEEP-WITH-NOTE $basename — $keep_with_note"
|
|
509
|
+
exit 1
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
if [ -n "$shapes" ]; then
|
|
513
|
+
if [ -n "$caveat_tag" ]; then
|
|
514
|
+
echo "CLOSE-CANDIDATE-WITH-CAVEAT $basename — shapes: $shapes — caveat: $caveat_tag: $caveat_msg — cites: $cites"
|
|
515
|
+
else
|
|
516
|
+
echo "CLOSE-CANDIDATE $basename — shapes: $shapes — $cites"
|
|
517
|
+
fi
|
|
131
518
|
exit 0
|
|
132
519
|
fi
|
|
133
520
|
|
|
134
|
-
|
|
521
|
+
# No shape fired — fall back to the legacy KEEP / SKIP routing.
|
|
522
|
+
|
|
523
|
+
if [ -z "$candidates" ]; then
|
|
524
|
+
echo "SKIP $basename — no extractable file paths (after self-reference exclusion)"
|
|
525
|
+
exit 2
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
total=$((shape1_missing + shape1_present))
|
|
529
|
+
echo "KEEP $basename — ${shape1_present}/${total} paths still present"
|
|
135
530
|
exit 1
|
|
@@ -41,7 +41,7 @@ setup() {
|
|
|
41
41
|
git init -q -b main
|
|
42
42
|
git config user.email test@example.com
|
|
43
43
|
git config user.name "Test"
|
|
44
|
-
mkdir -p docs/problems/open docs/problems/known-error packages/itil/scripts docs/decisions
|
|
44
|
+
mkdir -p docs/problems/open docs/problems/known-error docs/problems/closed packages/itil/scripts docs/decisions
|
|
45
45
|
|
|
46
46
|
# An "old" Reported date: 60 days before today. ISO date arithmetic
|
|
47
47
|
# portable across BSD + GNU date.
|
|
@@ -327,3 +327,447 @@ EOF
|
|
|
327
327
|
[ "$status" -eq 2 ]
|
|
328
328
|
[[ "${lines[0]}" == "SKIP "* ]]
|
|
329
329
|
}
|
|
330
|
+
|
|
331
|
+
# ── Phase 2 Shape 2: ADR-shipped with `human-oversight: confirmed` ───────────
|
|
332
|
+
#
|
|
333
|
+
# @adr ADR-079 Phase 2 — shape 2 covers 8 of 14 closes in the 2026-05-31
|
|
334
|
+
# labeled fixture set (P012/P015/P018/P022/P033/P039/
|
|
335
|
+
# P194/P292). Mechanical check: grep ticket body for
|
|
336
|
+
# ADR-NNN refs; for each, verify
|
|
337
|
+
# docs/decisions/<NNN>-*.md exists AND frontmatter has
|
|
338
|
+
# `human-oversight: confirmed`.
|
|
339
|
+
|
|
340
|
+
@test "evaluate-relevance: Phase 2 shape 2 — ADR-shipped-confirmed → CLOSE-CANDIDATE exit 0" {
|
|
341
|
+
cat > docs/decisions/037-confirmed-adr.proposed.md <<EOF
|
|
342
|
+
---
|
|
343
|
+
status: "proposed"
|
|
344
|
+
human-oversight: confirmed
|
|
345
|
+
oversight-date: 2026-05-25
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
# ADR-037: Test confirmed ADR
|
|
349
|
+
EOF
|
|
350
|
+
git add docs/decisions/037-confirmed-adr.proposed.md
|
|
351
|
+
|
|
352
|
+
cat > docs/problems/open/120-adr-confirmed.md <<EOF
|
|
353
|
+
# Problem 120: adr-confirmed
|
|
354
|
+
|
|
355
|
+
**Status**: Open
|
|
356
|
+
**Reported**: $OLD_DATE
|
|
357
|
+
|
|
358
|
+
## Description
|
|
359
|
+
|
|
360
|
+
ADR-037 was the design decision; the implementation has landed and the
|
|
361
|
+
ADR is human-oversight: confirmed. Concern no longer concerning.
|
|
362
|
+
EOF
|
|
363
|
+
run "$SCRIPT" docs/problems/open/120-adr-confirmed.md
|
|
364
|
+
[ "$status" -eq 0 ]
|
|
365
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"shapes: "*"ADR-shipped-confirmed"* ]]
|
|
366
|
+
[[ "$output" == *"ADR-037"* ]]
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@test "evaluate-relevance: Phase 2 shape 2 — ADR exists but NOT confirmed → no shape 2 fire" {
|
|
370
|
+
cat > docs/decisions/038-proposed-but-unconfirmed.proposed.md <<EOF
|
|
371
|
+
---
|
|
372
|
+
status: "proposed"
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
# ADR-038: Not yet confirmed
|
|
376
|
+
EOF
|
|
377
|
+
git add docs/decisions/038-proposed-but-unconfirmed.proposed.md
|
|
378
|
+
|
|
379
|
+
cat > docs/problems/open/121-adr-unconfirmed.md <<EOF
|
|
380
|
+
# Problem 121: adr-unconfirmed
|
|
381
|
+
|
|
382
|
+
**Status**: Open
|
|
383
|
+
**Reported**: $OLD_DATE
|
|
384
|
+
|
|
385
|
+
## Description
|
|
386
|
+
|
|
387
|
+
ADR-038 captured the design but is not yet confirmed by human review.
|
|
388
|
+
EOF
|
|
389
|
+
run "$SCRIPT" docs/problems/open/121-adr-unconfirmed.md
|
|
390
|
+
# Without other shape matches, shape 2 alone (unconfirmed ADR) MUST NOT
|
|
391
|
+
# produce a CLOSE-CANDIDATE. Verdict routes to SKIP (no extractable
|
|
392
|
+
# paths to ground shape 1) or KEEP — never CLOSE-CANDIDATE.
|
|
393
|
+
[ "$status" -ne 0 ]
|
|
394
|
+
[[ "${lines[0]}" != "CLOSE-CANDIDATE "* ]]
|
|
395
|
+
[[ "${lines[0]}" != "CLOSE-CANDIDATE-WITH-CAVEAT "* ]]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# ── Phase 2 Shape 3: named-skill-or-feature-exists ───────────────────────────
|
|
399
|
+
#
|
|
400
|
+
# @adr ADR-079 Phase 2 — shape 3 covers 6 of 14 closes (P014/P034/P045/P079/
|
|
401
|
+
# P190/P289). Verifies the cited SKILL.md / hook /
|
|
402
|
+
# agent / slash-command surface exists.
|
|
403
|
+
|
|
404
|
+
@test "evaluate-relevance: Phase 2 shape 3 — SKILL.md exists → CLOSE-CANDIDATE exit 0" {
|
|
405
|
+
mkdir -p packages/itil/skills/some-feature
|
|
406
|
+
cat > packages/itil/skills/some-feature/SKILL.md <<EOF
|
|
407
|
+
---
|
|
408
|
+
name: wr-itil:some-feature
|
|
409
|
+
---
|
|
410
|
+
# Some Feature
|
|
411
|
+
EOF
|
|
412
|
+
git add packages/itil/skills/some-feature/SKILL.md
|
|
413
|
+
|
|
414
|
+
cat > docs/problems/open/130-feature-shipped.md <<EOF
|
|
415
|
+
# Problem 130: feature-shipped
|
|
416
|
+
|
|
417
|
+
**Status**: Open
|
|
418
|
+
**Reported**: $OLD_DATE
|
|
419
|
+
|
|
420
|
+
## Description
|
|
421
|
+
|
|
422
|
+
The feature this ticket asks for has shipped at
|
|
423
|
+
\`packages/itil/skills/some-feature/SKILL.md\`. Concern resolved.
|
|
424
|
+
EOF
|
|
425
|
+
run "$SCRIPT" docs/problems/open/130-feature-shipped.md
|
|
426
|
+
[ "$status" -eq 0 ]
|
|
427
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"shapes: "*"named-skill-or-feature-exists"* ]]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@test "evaluate-relevance: Phase 2 shape 3 — slash-command ref + SKILL exists → CLOSE-CANDIDATE exit 0" {
|
|
431
|
+
mkdir -p packages/architect/skills/capture-adr
|
|
432
|
+
cat > packages/architect/skills/capture-adr/SKILL.md <<EOF
|
|
433
|
+
---
|
|
434
|
+
name: wr-architect:capture-adr
|
|
435
|
+
---
|
|
436
|
+
EOF
|
|
437
|
+
git add packages/architect/skills/capture-adr/SKILL.md
|
|
438
|
+
|
|
439
|
+
cat > docs/problems/open/131-slash-command.md <<EOF
|
|
440
|
+
# Problem 131: slash-command
|
|
441
|
+
|
|
442
|
+
**Status**: Open
|
|
443
|
+
**Reported**: $OLD_DATE
|
|
444
|
+
|
|
445
|
+
## Description
|
|
446
|
+
|
|
447
|
+
The aside-invocation surface /wr-architect:capture-adr now exists and
|
|
448
|
+
covers the original concern.
|
|
449
|
+
EOF
|
|
450
|
+
run "$SCRIPT" docs/problems/open/131-slash-command.md
|
|
451
|
+
[ "$status" -eq 0 ]
|
|
452
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"named-skill-or-feature-exists"* ]]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# ── Phase 2 Shape 4: self-marker-in-body (line-anchored regex per A2) ────────
|
|
456
|
+
#
|
|
457
|
+
# @adr ADR-079 Phase 2 — shape 4 explicit in P289 ("Close to Verifying"),
|
|
458
|
+
# contributory in P033 ("## Fix Released"). Regex
|
|
459
|
+
# line-anchored per architect advisory A2 to avoid
|
|
460
|
+
# mid-prose false-positives.
|
|
461
|
+
|
|
462
|
+
@test "evaluate-relevance: Phase 2 shape 4 — 'Close to Verifying' line marker → CLOSE-CANDIDATE exit 0" {
|
|
463
|
+
cat > docs/problems/open/140-self-marker.md <<EOF
|
|
464
|
+
# Problem 140: self-marker
|
|
465
|
+
|
|
466
|
+
**Status**: Open
|
|
467
|
+
**Reported**: $OLD_DATE
|
|
468
|
+
|
|
469
|
+
## Description
|
|
470
|
+
|
|
471
|
+
Work has been done.
|
|
472
|
+
|
|
473
|
+
## Resolution
|
|
474
|
+
|
|
475
|
+
The fix shipped 2026-05-27. Close to Verifying.
|
|
476
|
+
EOF
|
|
477
|
+
run "$SCRIPT" docs/problems/open/140-self-marker.md
|
|
478
|
+
[ "$status" -eq 0 ]
|
|
479
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"self-marker-in-body"* ]]
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
@test "evaluate-relevance: Phase 2 shape 4 — '## Fix Released' heading → CLOSE-CANDIDATE exit 0" {
|
|
483
|
+
cat > docs/problems/open/141-fix-released.md <<EOF
|
|
484
|
+
# Problem 141: fix-released
|
|
485
|
+
|
|
486
|
+
**Status**: Open
|
|
487
|
+
**Reported**: $OLD_DATE
|
|
488
|
+
|
|
489
|
+
## Description
|
|
490
|
+
|
|
491
|
+
Bug.
|
|
492
|
+
|
|
493
|
+
## Fix Released
|
|
494
|
+
|
|
495
|
+
Implemented 2026-04-17.
|
|
496
|
+
EOF
|
|
497
|
+
run "$SCRIPT" docs/problems/open/141-fix-released.md
|
|
498
|
+
[ "$status" -eq 0 ]
|
|
499
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"self-marker-in-body"* ]]
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@test "evaluate-relevance: Phase 2 shape 4 — 'DONE 2026-' line marker → CLOSE-CANDIDATE exit 0" {
|
|
503
|
+
cat > docs/problems/open/142-done-marker.md <<EOF
|
|
504
|
+
# Problem 142: done-marker
|
|
505
|
+
|
|
506
|
+
**Status**: Open
|
|
507
|
+
**Reported**: $OLD_DATE
|
|
508
|
+
|
|
509
|
+
## Description
|
|
510
|
+
|
|
511
|
+
Description.
|
|
512
|
+
|
|
513
|
+
### Investigation Tasks
|
|
514
|
+
|
|
515
|
+
- [x] DONE 2026-05-27 — Migration-strategy decision executed.
|
|
516
|
+
EOF
|
|
517
|
+
run "$SCRIPT" docs/problems/open/142-done-marker.md
|
|
518
|
+
[ "$status" -eq 0 ]
|
|
519
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"self-marker-in-body"* ]]
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@test "evaluate-relevance: Phase 2 shape 4 — mid-prose 'close to verifying' (lowercase, narrative) → KEEP exit 1 (negative fixture per advisory A2)" {
|
|
523
|
+
cat > docs/problems/open/143-mid-prose.md <<EOF
|
|
524
|
+
# Problem 143: mid-prose
|
|
525
|
+
|
|
526
|
+
**Status**: Open
|
|
527
|
+
**Reported**: $OLD_DATE
|
|
528
|
+
|
|
529
|
+
## Description
|
|
530
|
+
|
|
531
|
+
The team thinks this is close to verifying our hypothesis but no concrete
|
|
532
|
+
shipped evidence exists yet — still investigating.
|
|
533
|
+
EOF
|
|
534
|
+
run "$SCRIPT" docs/problems/open/143-mid-prose.md
|
|
535
|
+
# Without other shape matches, mid-prose narrative MUST NOT fire shape 4.
|
|
536
|
+
# Verdict should be SKIP (no extractable paths) or KEEP — never CLOSE-CANDIDATE.
|
|
537
|
+
[ "$status" -ne 0 ]
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
# ── Phase 2 Shape 5: driver-child-ticket-closed ──────────────────────────────
|
|
541
|
+
#
|
|
542
|
+
# @adr ADR-079 Phase 2 — shape 5 contributory in several closes (e.g. P014
|
|
543
|
+
# cites closed P155 driver). Parses ## Related for
|
|
544
|
+
# P<NNN> refs; checks if any are in
|
|
545
|
+
# docs/problems/closed/.
|
|
546
|
+
|
|
547
|
+
@test "evaluate-relevance: Phase 2 shape 5 — Related cites closed driver → CLOSE-CANDIDATE exit 0" {
|
|
548
|
+
cat > docs/problems/closed/155-driver-done.md <<EOF
|
|
549
|
+
# Problem 155: driver-done
|
|
550
|
+
|
|
551
|
+
**Status**: Closed
|
|
552
|
+
EOF
|
|
553
|
+
git add docs/problems/closed/155-driver-done.md
|
|
554
|
+
|
|
555
|
+
cat > docs/problems/open/150-child-of-closed.md <<EOF
|
|
556
|
+
# Problem 150: child-of-closed
|
|
557
|
+
|
|
558
|
+
**Status**: Open
|
|
559
|
+
**Reported**: $OLD_DATE
|
|
560
|
+
|
|
561
|
+
## Description
|
|
562
|
+
|
|
563
|
+
This is the umbrella for several driver tickets.
|
|
564
|
+
|
|
565
|
+
## Related
|
|
566
|
+
|
|
567
|
+
- **P155** — implementation driver.
|
|
568
|
+
EOF
|
|
569
|
+
run "$SCRIPT" docs/problems/open/150-child-of-closed.md
|
|
570
|
+
[ "$status" -eq 0 ]
|
|
571
|
+
[[ "$output" == "CLOSE-CANDIDATE "*"driver-child-ticket-closed"* ]]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
@test "evaluate-relevance: Phase 2 shape 5 — Related cites closed driver BUT child has independent open work → KEEP exit 1 (advisory A1 negative fixture)" {
|
|
575
|
+
cat > docs/problems/closed/156-driver-done.md <<EOF
|
|
576
|
+
# Problem 156: driver-done
|
|
577
|
+
|
|
578
|
+
**Status**: Closed
|
|
579
|
+
EOF
|
|
580
|
+
git add docs/problems/closed/156-driver-done.md
|
|
581
|
+
|
|
582
|
+
cat > docs/problems/open/151-independent.md <<EOF
|
|
583
|
+
# Problem 151: independent
|
|
584
|
+
|
|
585
|
+
**Status**: Open
|
|
586
|
+
**Reported**: $OLD_DATE
|
|
587
|
+
|
|
588
|
+
## Description
|
|
589
|
+
|
|
590
|
+
While the driver P156 is closed, this ticket has its own outstanding
|
|
591
|
+
investigation around \`packages/itil/skills/unrelated-future-skill/SKILL.md\`
|
|
592
|
+
which has not been implemented yet.
|
|
593
|
+
|
|
594
|
+
## Related
|
|
595
|
+
|
|
596
|
+
- **P156** — original driver (closed); this ticket carries new scope beyond P156.
|
|
597
|
+
EOF
|
|
598
|
+
run "$SCRIPT" docs/problems/open/151-independent.md
|
|
599
|
+
# The script extracts \`packages/itil/skills/unrelated-future-skill/SKILL.md\`
|
|
600
|
+
# which does NOT exist — shape 1 (file-no-longer-exists) would fire. But the
|
|
601
|
+
# ticket independent-work signal lives in the unimplemented-file class; the
|
|
602
|
+
# KEEP requirement here is: shape 5 must NOT fire when an existing file
|
|
603
|
+
# reference is unresolvable (i.e. the umbrella has unfinished scope).
|
|
604
|
+
# Per architect advisory A1, we surface this as KEEP-WITH-NOTE rather than
|
|
605
|
+
# silent CLOSE-CANDIDATE.
|
|
606
|
+
[ "$status" -ne 0 ]
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
# ── Phase 1 false-positive fixes ─────────────────────────────────────────────
|
|
610
|
+
#
|
|
611
|
+
# @adr ADR-079 Phase 2 — Phase 1 false-positive class fixes:
|
|
612
|
+
# - P180: state-suffix detection (incident I002.investigating.md vs I002.restored.md)
|
|
613
|
+
# - P244: sibling-file detection (plugin-maturity-list.sh vs plugin-maturity-render.sh)
|
|
614
|
+
# - P251: rename detection via git log --follow
|
|
615
|
+
|
|
616
|
+
@test "evaluate-relevance: Phase 1 fix — state-suffix variant exists at different suffix → KEEP-WITH-NOTE (not CLOSE-CANDIDATE)" {
|
|
617
|
+
mkdir -p docs/incidents
|
|
618
|
+
cat > docs/incidents/I002-renamed.restored.md <<EOF
|
|
619
|
+
# Incident I002
|
|
620
|
+
EOF
|
|
621
|
+
git add docs/incidents/I002-renamed.restored.md
|
|
622
|
+
|
|
623
|
+
cat > docs/problems/open/160-state-suffix.md <<EOF
|
|
624
|
+
# Problem 160: state-suffix
|
|
625
|
+
|
|
626
|
+
**Status**: Open
|
|
627
|
+
**Reported**: $OLD_DATE
|
|
628
|
+
|
|
629
|
+
## Description
|
|
630
|
+
|
|
631
|
+
Investigation references docs/incidents/I002-renamed.investigating.md
|
|
632
|
+
which has since transitioned state.
|
|
633
|
+
EOF
|
|
634
|
+
run "$SCRIPT" docs/problems/open/160-state-suffix.md
|
|
635
|
+
# Phase 1 would falsely declare the file gone. Phase 2 detects the
|
|
636
|
+
# restored.md state-suffix variant and routes to KEEP-WITH-NOTE.
|
|
637
|
+
[ "$status" -eq 1 ]
|
|
638
|
+
[[ "$output" == "KEEP-WITH-NOTE "* ]] || [[ "$output" == "KEEP "* ]]
|
|
639
|
+
[[ "$output" == *"state-suffix"* ]] || [[ "$output" == *"renamed.restored.md"* ]]
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
@test "evaluate-relevance: Phase 1 fix — sibling file with similar slug-prefix → KEEP-WITH-NOTE" {
|
|
643
|
+
mkdir -p packages/architect/scripts
|
|
644
|
+
cat > packages/architect/scripts/plugin-maturity-render.sh <<EOF
|
|
645
|
+
#!/bin/bash
|
|
646
|
+
EOF
|
|
647
|
+
cat > packages/architect/scripts/plugin-maturity-populate.sh <<EOF
|
|
648
|
+
#!/bin/bash
|
|
649
|
+
EOF
|
|
650
|
+
git add packages/architect/scripts/plugin-maturity-render.sh packages/architect/scripts/plugin-maturity-populate.sh
|
|
651
|
+
|
|
652
|
+
cat > docs/problems/open/161-sibling-file.md <<EOF
|
|
653
|
+
# Problem 161: sibling-file
|
|
654
|
+
|
|
655
|
+
**Status**: Open
|
|
656
|
+
**Reported**: $OLD_DATE
|
|
657
|
+
|
|
658
|
+
## Description
|
|
659
|
+
|
|
660
|
+
Bug in packages/architect/scripts/plugin-maturity-list.sh
|
|
661
|
+
EOF
|
|
662
|
+
run "$SCRIPT" docs/problems/open/161-sibling-file.md
|
|
663
|
+
# Phase 1 would declare plugin-maturity-list.sh gone. Phase 2 detects
|
|
664
|
+
# the sibling-file class (plugin-maturity-* slug-prefix) and routes to
|
|
665
|
+
# KEEP-WITH-NOTE.
|
|
666
|
+
[ "$status" -eq 1 ]
|
|
667
|
+
[[ "$output" == "KEEP-WITH-NOTE "* ]] || [[ "$output" == "KEEP "* ]]
|
|
668
|
+
[[ "$output" == *"sibling"* ]] || [[ "$output" == *"plugin-maturity-"* ]]
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# ── CLOSE-CANDIDATE-WITH-CAVEAT structured emission (architect condition C2) ──
|
|
672
|
+
|
|
673
|
+
@test "evaluate-relevance: CLOSE-CANDIDATE-WITH-CAVEAT emits structured caveat format" {
|
|
674
|
+
cat > docs/decisions/040-multi-phase-confirmed.proposed.md <<EOF
|
|
675
|
+
---
|
|
676
|
+
status: "proposed"
|
|
677
|
+
human-oversight: confirmed
|
|
678
|
+
oversight-date: 2026-05-25
|
|
679
|
+
---
|
|
680
|
+
# ADR-040
|
|
681
|
+
EOF
|
|
682
|
+
git add docs/decisions/040-multi-phase-confirmed.proposed.md
|
|
683
|
+
|
|
684
|
+
cat > docs/problems/open/170-umbrella-caveat.md <<EOF
|
|
685
|
+
# Problem 170: umbrella-caveat
|
|
686
|
+
|
|
687
|
+
**Status**: Open
|
|
688
|
+
**Reported**: $OLD_DATE
|
|
689
|
+
|
|
690
|
+
## Description
|
|
691
|
+
|
|
692
|
+
Multi-phase umbrella. ADR-040 covers the design; Phase 2 done, Phase 3
|
|
693
|
+
outstanding work \`packages/jtbd/lib/phase3-helper.sh\`.
|
|
694
|
+
|
|
695
|
+
## Phase 3 progress
|
|
696
|
+
|
|
697
|
+
- [ ] Phase 3 work
|
|
698
|
+
EOF
|
|
699
|
+
# Phase 3 cited path does not exist; ADR-040 confirmed → shape 2 fires + caveat
|
|
700
|
+
run "$SCRIPT" docs/problems/open/170-umbrella-caveat.md
|
|
701
|
+
# Architect condition C2 — structured caveat emission:
|
|
702
|
+
# CLOSE-CANDIDATE-WITH-CAVEAT <basename> — shapes: <list> — caveat: <tag>: <one-line>
|
|
703
|
+
# For multi-phase umbrellas with unticked checkboxes the caveat tag is
|
|
704
|
+
# 'multi-phase-mixed-progress'.
|
|
705
|
+
[[ "$output" == "CLOSE-CANDIDATE-WITH-CAVEAT "* ]]
|
|
706
|
+
[[ "$output" == *"shapes:"* ]]
|
|
707
|
+
[[ "$output" == *"caveat:"* ]]
|
|
708
|
+
[[ "$output" == *"multi-phase-mixed-progress"* ]]
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
# ── KEEP fixtures from the 2026-05-31 labeled negative set ────────────────────
|
|
712
|
+
#
|
|
713
|
+
# @adr ADR-079 Phase 2 — KEEP regression suite: P136 multi-phase umbrella,
|
|
714
|
+
# P303/P326 recent-observation-no-shipped-evidence.
|
|
715
|
+
|
|
716
|
+
@test "evaluate-relevance: KEEP fixture — recent observation, no shipped evidence (P303/P326 class)" {
|
|
717
|
+
cat > docs/problems/open/180-recent-observation.md <<EOF
|
|
718
|
+
# Problem 180: recent-observation
|
|
719
|
+
|
|
720
|
+
**Status**: Open
|
|
721
|
+
**Reported**: $OLD_DATE
|
|
722
|
+
|
|
723
|
+
## Description
|
|
724
|
+
|
|
725
|
+
Observed friction with the risk-scorer pipeline staging. Composes-with
|
|
726
|
+
P057 / P125 / P273 (sibling staging traps) but no shipped fix yet.
|
|
727
|
+
|
|
728
|
+
## Related
|
|
729
|
+
|
|
730
|
+
- **P057** — sibling.
|
|
731
|
+
EOF
|
|
732
|
+
# P057 not closed (no file); no ADR refs that are confirmed; no SKILL refs;
|
|
733
|
+
# no self-markers. Should KEEP.
|
|
734
|
+
run "$SCRIPT" docs/problems/open/180-recent-observation.md
|
|
735
|
+
[ "$status" -ne 0 ]
|
|
736
|
+
# Either SKIP (no extractable paths) or KEEP — never CLOSE-CANDIDATE.
|
|
737
|
+
[[ "${lines[0]}" != "CLOSE-CANDIDATE "* ]]
|
|
738
|
+
[[ "${lines[0]}" != "CLOSE-CANDIDATE-WITH-CAVEAT "* ]]
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
# ── Cumulative shape annotation (architect Q4 — cumulative is correct) ───────
|
|
742
|
+
|
|
743
|
+
@test "evaluate-relevance: multi-shape match emits cumulative shapes: list" {
|
|
744
|
+
cat > docs/decisions/041-multi-match.proposed.md <<EOF
|
|
745
|
+
---
|
|
746
|
+
status: "proposed"
|
|
747
|
+
human-oversight: confirmed
|
|
748
|
+
oversight-date: 2026-05-25
|
|
749
|
+
---
|
|
750
|
+
# ADR-041
|
|
751
|
+
EOF
|
|
752
|
+
git add docs/decisions/041-multi-match.proposed.md
|
|
753
|
+
|
|
754
|
+
cat > docs/problems/open/190-multi-shape.md <<EOF
|
|
755
|
+
# Problem 190: multi-shape
|
|
756
|
+
|
|
757
|
+
**Status**: Open
|
|
758
|
+
**Reported**: $OLD_DATE
|
|
759
|
+
|
|
760
|
+
## Description
|
|
761
|
+
|
|
762
|
+
ADR-041 captures the design. Fix shipped.
|
|
763
|
+
|
|
764
|
+
## Fix Released
|
|
765
|
+
|
|
766
|
+
Implemented and confirmed.
|
|
767
|
+
EOF
|
|
768
|
+
run "$SCRIPT" docs/problems/open/190-multi-shape.md
|
|
769
|
+
[ "$status" -eq 0 ]
|
|
770
|
+
# Both shape 2 (ADR-confirmed) and shape 4 (Fix Released line) match.
|
|
771
|
+
[[ "$output" == *"ADR-shipped-confirmed"* ]]
|
|
772
|
+
[[ "$output" == *"self-marker-in-body"* ]]
|
|
773
|
+
}
|
|
@@ -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 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
|
|
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 + Phase 2 evidence shapes — `file-no-longer-exists` / `ADR-shipped-confirmed` / `named-skill-or-feature-exists` / `self-marker-in-body` / `driver-child-ticket-closed` (cumulative; multi-shape matches emit comma-joined) 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). Partial-scope umbrellas emit `CLOSE-CANDIDATE-WITH-CAVEAT` and ride the maintainer's `AskUserQuestion` surface-batch-confirm path. |
|
|
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,9 +208,25 @@ 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)
|
|
211
|
+
### 4.6. Relevance-close pass (P346 / P347 / ADR-079 Phase 1 + Phase 2)
|
|
212
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
|
|
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 + Phase 2 cover **five evidence shapes** grounded in the 14-fixture labeled close-on-evidence set from the 2026-05-31 foreground relevance-scan (the regression suite per ADR-052 lives at `packages/itil/scripts/test/evaluate-relevance.bats`):
|
|
214
|
+
|
|
215
|
+
| Shape | Phase | Mechanical check | Empirical closes (2026-05-31) |
|
|
216
|
+
|---|---|---|---|
|
|
217
|
+
| 1. `file-no-longer-exists` | Phase 1 | grep ticket body for `(packages\|docs\|...)/...\.(md\|sh\|...)`; verify each via `git ls-files --error-unmatch` | 0 of 14 |
|
|
218
|
+
| 2. `ADR-shipped-confirmed` | Phase 2 | grep ticket body for `ADR-NNN`; for each, verify `docs/decisions/<NNN>-*.md` exists AND frontmatter has `human-oversight: confirmed` | 8 of 14 — P012/P015/P018/P022/P033/P039/P194/P292 |
|
|
219
|
+
| 3. `named-skill-or-feature-exists` | Phase 2 | grep for SKILL.md / hook / agent paths + `/wr-<plugin>:<skill>` slash-command refs; verify each via `git ls-files` | 6 of 14 — P014/P034/P045/P079/P190/P289 |
|
|
220
|
+
| 4. `self-marker-in-body` | Phase 2 | line-anchored grep for `Close to (Verifying\|Closed)`, `DONE 2026-`, `## Fix Released` heading, `fix shipped session`, `awaiting K→V`. Pattern MUST anchor to line-start to avoid mid-prose false-positives (architect advisory A2) | explicit in P289; contributory in P033 |
|
|
221
|
+
| 5. `driver-child-ticket-closed` | Phase 2 | parse `## Related` for `P<NNN>` refs; check if any are in `docs/problems/closed/`. Suppressed when child names an unbuilt SKILL/agent path (future work, not stale; architect advisory A1) | contributory in several closes |
|
|
222
|
+
|
|
223
|
+
**Phase 1 false-positive fixes** (the iter-4 60% false-positive rate is structurally addressed; each fix routes the candidate to `KEEP-WITH-NOTE` rather than auto-close):
|
|
224
|
+
|
|
225
|
+
- **P180 — state-suffix detection**: per-state subdirs (`open|known-error|verifying|closed|parked` for problems; `investigating|mitigating|restored` for incidents) AND `.<state>.md` suffix variants.
|
|
226
|
+
- **P244 — sibling-file detection**: dir-glob the parent dir for files with similar slug-prefix (first 2 dash-tokens).
|
|
227
|
+
- **P251 — rename detection**: `git log --follow --diff-filter=AD --name-only` surfaces the renamed-to path.
|
|
228
|
+
|
|
229
|
+
Tickets with no extractable evidence (no file refs, no ADR refs, no SKILL refs, no self-markers, no closed drivers) route to `SKIP`. Other evidence shapes (ADR-supersession via `.superseded.md`, duplicate-of-X, "concern no longer concerning", test-passes-without-issue) are deferred to sibling tickets per ADR-079 scope discipline.
|
|
214
230
|
|
|
215
231
|
**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
232
|
|
|
@@ -228,28 +244,40 @@ Exit-code routing (one verdict line per ticket on stdout):
|
|
|
228
244
|
|
|
229
245
|
| Exit | Stdout prefix | Action |
|
|
230
246
|
|------|--------------|--------|
|
|
231
|
-
| 0 | `CLOSE-CANDIDATE <basename> —
|
|
247
|
+
| 0 | `CLOSE-CANDIDATE <basename> — shapes: <comma-list> — <per-shape cite>; ...` | Auto-close branch (4.6b). |
|
|
248
|
+
| 0 | `CLOSE-CANDIDATE-WITH-CAVEAT <basename> — shapes: <comma-list> — caveat: <short-tag>: <one-line> — cites: ...` | Surface-batch-confirm branch (4.6b-with-caveat); the caveat short-tag + one-line splices verbatim into the audit section's **Caveat** field per architect condition C2. |
|
|
232
249
|
| 1 | `KEEP <basename> — <M>/<N> paths still present` | No action; log only. |
|
|
233
|
-
|
|
|
250
|
+
| 1 | `KEEP-WITH-NOTE <basename> — <note>: <evidence>` | Phase 1 false-positive class (state-suffix / sibling-file / rename) OR architect-A1 future-work disambiguation. No action; log only. |
|
|
251
|
+
| 2 | `SKIP <basename> — <reason>` | No action (age gate, no Reported date, no extractable evidence). |
|
|
234
252
|
| 3 | error | Log advisory; do not abort the pass — relevance-close is non-blocking per the Step 4.5 fail-soft precedent. |
|
|
235
253
|
|
|
236
|
-
**Algorithm (canonical body)**:
|
|
254
|
+
**Algorithm (canonical body)**: runs each of the five shape detectors over the ticket body. Multi-shape matches emit cumulatively (corroborating evidence is stronger than first-match-wins per ADR-026): the `shapes:` field carries a comma-joined list, the trailing fragment carries per-shape cites semicolon-separated. The caveat fires when at least one shape matches AND the body has any unticked checkboxes (multi-phase mixed-progress umbrella class). The verdict is intentionally conservative — tickets with no shape match AND no extractable evidence route to `SKIP`, not auto-close.
|
|
255
|
+
|
|
256
|
+
**Surface-batch-confirm flow** (the methodology that produced today's 14 closes — codified for repeatable use):
|
|
257
|
+
|
|
258
|
+
1. **Surface a batch** — run the evaluator across the dual-tolerant glob in 4.6a; group all `CLOSE-CANDIDATE` and `CLOSE-CANDIDATE-WITH-CAVEAT` verdicts as a batch.
|
|
259
|
+
2. **Interactive surface (`AskUserQuestion`)** — present each batch of ~5 candidates with their shape annotations + caveats; user confirms close / amend / defer. Surface caveat tickets adjacent to their clean-close siblings so the maintainer sees the full batch class together. The interactive batch is the one-and-only `AskUserQuestion` per relevance-close pass (mechanical-stage carve-out per ADR-044 cat 4 + P132 — do NOT ask per-ticket; ask per-batch).
|
|
260
|
+
3. **AFK (`/wr-itil:work-problems` Step 6.5)** — close clean `CLOSE-CANDIDATE` verdicts silently per ADR-013 Rule 5 + ADR-044 cat 4 (file existence + frontmatter inspection + line-anchored grep are empirical). Route `CLOSE-CANDIDATE-WITH-CAVEAT` verdicts to the next interactive review's `AskUserQuestion` surface — the caveat short-tag is the maintainer's decision input.
|
|
261
|
+
4. **Batched closure commit per ADR-014** — all relevance-closes from one review pass batch into ONE commit (mirroring `/wr-itil:transition-problems` P139 batch grain).
|
|
262
|
+
|
|
263
|
+
Real-backlog smoke test 2026-05-31 against today's labeled fixtures: P012 → `CLOSE-CANDIDATE-WITH-CAVEAT` (shapes 2 + 5 + multi-phase-mixed-progress caveat); P136 → `KEEP-WITH-NOTE` (sibling-file class); P303/P326 → `SKIP` (age gate, recent observations).
|
|
237
264
|
|
|
238
265
|
#### 4.6b. Auto-close action per CLOSE-CANDIDATE
|
|
239
266
|
|
|
240
|
-
For each `CLOSE-CANDIDATE` ticket, perform the following BEFORE the `git mv`:
|
|
267
|
+
For each `CLOSE-CANDIDATE` or `CLOSE-CANDIDATE-WITH-CAVEAT` ticket, perform the following BEFORE the `git mv`:
|
|
241
268
|
|
|
242
269
|
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
270
|
|
|
244
271
|
```markdown
|
|
245
272
|
## Closed as no longer relevant
|
|
246
273
|
|
|
247
|
-
- **Evidence shape**:
|
|
274
|
+
- **Evidence shape**: <comma-joined list from the verdict's `shapes:` field — e.g. `ADR-shipped-confirmed, self-marker-in-body` for multi-shape match> (ADR-079 Phase 1 + Phase 2)
|
|
248
275
|
- **Closed on**: <YYYY-MM-DD>
|
|
249
276
|
- **Closed by**: /wr-itil:review-problems Step 4.6 relevance-close pass
|
|
250
|
-
- **Cite (
|
|
277
|
+
- **Cite (per-shape evidence)**: <semicolon-separated per-shape cites from the trailing fragment of the verdict line>
|
|
278
|
+
- **Caveat (if CLOSE-CANDIDATE-WITH-CAVEAT)**: `<short-tag>: <one-line>` from the verdict's `caveat:` field (splice verbatim — preserves ADR-026 uncertainty leg structurally per architect condition C2). Omit this field for plain CLOSE-CANDIDATE verdicts.
|
|
251
279
|
- **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
|
|
280
|
+
- **Uncertainty / reversibility**: verdict is deterministic given the body + git state. False-positive remediation: `git revert` the relevance-close commit OR `git mv` the ticket back to its prior state. The ≥7-day age gate + Phase 1 false-positive fixes (state-suffix / sibling-file / rename) + shape-cumulative annotation guard against premature evaluation.
|
|
253
281
|
```
|
|
254
282
|
|
|
255
283
|
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"):
|
|
@@ -288,9 +316,11 @@ Step 5's README refresh rides the same commit per ADR-014 single-commit grain
|
|
|
288
316
|
|
|
289
317
|
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
318
|
|
|
291
|
-
**Worked example (
|
|
319
|
+
**Worked example (Phase 1 smoke test, 2026-05-31)**: across 143 open / known-error tickets, Phase 1 surfaced 6 CLOSE-CANDIDATEs (4.2%) — but the post-batch-1 verification showed 60% of those were false-positives (state-suffix / sibling-file / rename class). Phase 2's false-positive fixes route those to `KEEP-WITH-NOTE`. The same-day foreground relevance-scan that used the broader Phase 2 shape vocabulary produced 14 actual closes across 5 batches — empirically calibrating the Phase 2 shape set.
|
|
320
|
+
|
|
321
|
+
**Worked example (Phase 2 surface-batch-confirm, 2026-05-31)**: 14 closes across 5 batches using shapes 2-5. Each batch surfaced via `AskUserQuestion` (≤ 5 candidates per batch); maintainer confirmed clean closes and routed caveat candidates with explicit caveat acknowledgement (e.g. P039 `shared-template-not-built`; P194 `deep-dive-bloat-remains`). All closures batched into per-batch commits per ADR-014. The 14-fixture labeled set is the regression suite (`packages/itil/scripts/test/evaluate-relevance.bats` covers each shape positive + the architect A1/A2 advisory negatives).
|
|
292
322
|
|
|
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
|
|
323
|
+
**Cross-references**: ADR-079 (this pass's design ADR, Phase 1 + Phase 2), ADR-026 (grounding, cumulative shape cite + structured caveat field), ADR-022 + ADR-079 lifecycle extension (Open|Known Error → Closed bypassing Verifying for no-fix-needed conclusions; the Closed-row entry at `/wr-itil:manage-problem` SKILL.md line 59 names Phase 1 + Phase 2 shapes), ADR-049 (PATH shim), ADR-052 (behavioural bats at `packages/itil/scripts/test/evaluate-relevance.bats` — 33/33 GREEN), ADR-014 (batched closure commit grain per pass), ADR-044 cat 4 + P132 (mechanical-stage carve-out: ask per-batch, not per-ticket), P057 (staging trap), P346 (Phase 1 driver), P347 (Phase 2 driver).
|
|
294
324
|
|
|
295
325
|
### 5. Rewrite `docs/problems/README.md`
|
|
296
326
|
|