@windyroad/itil 0.50.3 → 0.51.0
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.
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generated by scripts/sync-shim-wrappers.sh from
|
|
3
|
+
# packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
|
|
4
|
+
# shim files in packages/*/bin/wr-* directly; edit the template + run
|
|
5
|
+
# `npm run sync:shim-wrappers` to regenerate.
|
|
6
|
+
#
|
|
7
|
+
# Resolution (ADR-080):
|
|
8
|
+
# 1. If the wrapper's parent dir is semver-shaped, treat as installed-
|
|
9
|
+
# cache execution and resolve to the highest-version sibling's
|
|
10
|
+
# scripts/ entry below.
|
|
11
|
+
# 2. Otherwise (parent dir is e.g. `architect`), treat as source-
|
|
12
|
+
# monorepo execution and dispatch to own scripts/. The source-repo-
|
|
13
|
+
# guard `exec` is the anchor parsed by
|
|
14
|
+
# packages/retrospective/scripts/check-tarball-shipped-shims.sh.
|
|
15
|
+
# 3. If the cache parent contains zero semver-shaped siblings, exit
|
|
16
|
+
# 127 with a stderr message naming the cache parent (per SQ-080-2).
|
|
17
|
+
#
|
|
18
|
+
# @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
|
|
19
|
+
# @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
|
|
20
|
+
# @problem P343 (mid-session staleness window)
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
|
|
26
|
+
OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
|
|
27
|
+
CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
|
|
28
|
+
|
|
29
|
+
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
|
|
30
|
+
|
|
31
|
+
# Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
|
|
32
|
+
if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
|
|
33
|
+
exec "$SHIM_DIR/../scripts/catchup-scan.sh" "$@"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Cache execution: pick the highest-semver sibling under CACHE_PARENT.
|
|
37
|
+
HIGHEST=""
|
|
38
|
+
while IFS= read -r dir; do
|
|
39
|
+
name="$(basename "$dir")"
|
|
40
|
+
[[ "$name" =~ $SEMVER_RE ]] || continue
|
|
41
|
+
if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
|
|
42
|
+
HIGHEST="$name"
|
|
43
|
+
fi
|
|
44
|
+
done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
|
45
|
+
|
|
46
|
+
if [[ -z "$HIGHEST" ]]; then
|
|
47
|
+
printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
|
|
48
|
+
exit 127
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exec "$CACHE_PARENT/$HIGHEST/scripts/catchup-scan.sh" "$@"
|
package/package.json
CHANGED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/catchup-scan.sh
|
|
3
|
+
#
|
|
4
|
+
# Phase 2 of P080 — `--catchup` migration-mode worklist scanner for
|
|
5
|
+
# `/wr-itil:update-upstream`. Walks the EXISTING `.verifying.md` +
|
|
6
|
+
# `.closed.md` ticket corpus, filters to tickets carrying a `## Reported
|
|
7
|
+
# Upstream` back-link section (written by `/wr-itil:report-upstream`
|
|
8
|
+
# Step 7), and emits a per-ticket worklist of upstream issues that still
|
|
9
|
+
# need a retroactive lifecycle-update comment.
|
|
10
|
+
#
|
|
11
|
+
# Read-only / local-only: this script makes NO `gh` calls and NO writes.
|
|
12
|
+
# It only reads local ticket files and prints a worklist to stdout. The
|
|
13
|
+
# actual `gh issue comment` / `gh issue close` posts — and the ADR-028
|
|
14
|
+
# external-comms + voice-tone gate composition that guards them — stay in
|
|
15
|
+
# the `/wr-itil:update-upstream` SKILL's per-ticket Step 4-6 loop, which
|
|
16
|
+
# consumes this worklist. Keeping the writes out of the scanner keeps it
|
|
17
|
+
# AFK-safe and behaviourally testable without a live upstream.
|
|
18
|
+
#
|
|
19
|
+
# Idempotency (P080 Phase 2 acceptance criterion 3): a ticket whose
|
|
20
|
+
# `## Upstream Lifecycle Updates` log already records an entry for the
|
|
21
|
+
# current target-state transition is reported as SKIP/already-logged, NOT
|
|
22
|
+
# re-posted. The marker-on-body log (append-only per the ADR-024 P080
|
|
23
|
+
# amendment) is the source of truth — re-running catchup is safe.
|
|
24
|
+
#
|
|
25
|
+
# Usage:
|
|
26
|
+
# catchup-scan.sh
|
|
27
|
+
# [--problems-dir <dir>] default: docs/problems
|
|
28
|
+
# [--ticket P<NNN>] restrict the scan to one ticket
|
|
29
|
+
#
|
|
30
|
+
# Exit codes:
|
|
31
|
+
# 0 = success (zero or more worklist lines on stdout)
|
|
32
|
+
# 1 = error (problems-dir missing, malformed CLI args)
|
|
33
|
+
#
|
|
34
|
+
# Structured stdout (one per actionable upstream entry; <= 150 bytes per
|
|
35
|
+
# line per ADR-038). ASCII `->` for the transition arrow per the P334
|
|
36
|
+
# awk/script portability lesson (no Unicode in machine-read output):
|
|
37
|
+
# CATCHUP P<NNN> <url> state=<state> transition=<KE->Verifying|Verifying->Closed>
|
|
38
|
+
# SKIP P<NNN> <url> reason=already-logged
|
|
39
|
+
# SKIP P<NNN> <url> reason=out-of-band
|
|
40
|
+
# Tickets with no `## Reported Upstream` section are skipped silently (the
|
|
41
|
+
# common case — most tickets were never reported upstream).
|
|
42
|
+
#
|
|
43
|
+
# Trailing summary line (stderr) for the SKILL / human reader:
|
|
44
|
+
# SUMMARY scanned=<N> catchup=<N> skip-logged=<N> skip-out-of-band=<N>
|
|
45
|
+
#
|
|
46
|
+
# @problem P080 — no bidirectional update of upstream-reported problems (Phase 2 --catchup)
|
|
47
|
+
# @adr ADR-024 (amended P080 Phase 2 — --catchup migration mode + idempotency contract)
|
|
48
|
+
# @adr ADR-014 (governance skills commit their own work)
|
|
49
|
+
# @adr ADR-038 (progressive disclosure — per-row byte budget)
|
|
50
|
+
# @adr ADR-049 (invoked via wr-itil-catchup-scan bin shim, never repo-relative path)
|
|
51
|
+
# @adr ADR-032 (foreground synchronous skill)
|
|
52
|
+
# @jtbd JTBD-301 (reporter feedback loop — the catchup's primary job)
|
|
53
|
+
# @jtbd JTBD-006 (AFK-safe worklist scanner)
|
|
54
|
+
# @jtbd JTBD-004 (cross-repo coordination — reconcile local corpus vs upstream trackers)
|
|
55
|
+
# @jtbd JTBD-001 (governance without slowing down — derive the worklist, no manual policing)
|
|
56
|
+
# @jtbd JTBD-201 (symmetric local/upstream audit trail)
|
|
57
|
+
|
|
58
|
+
set -uo pipefail
|
|
59
|
+
|
|
60
|
+
# ── Parse CLI args ──────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
PROBLEMS_DIR="docs/problems"
|
|
63
|
+
TICKET_FILTER=""
|
|
64
|
+
|
|
65
|
+
while [ $# -gt 0 ]; do
|
|
66
|
+
case "$1" in
|
|
67
|
+
--problems-dir) PROBLEMS_DIR="$2"; shift 2 ;;
|
|
68
|
+
--ticket) TICKET_FILTER="$2"; shift 2 ;;
|
|
69
|
+
-h|--help)
|
|
70
|
+
sed -n '/^# Usage:/,/^# Exit codes:/p' "$0" | sed 's/^# //'
|
|
71
|
+
exit 0
|
|
72
|
+
;;
|
|
73
|
+
*) echo "ERROR: unknown argument: $1" >&2; exit 1 ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
if [ ! -d "$PROBLEMS_DIR" ]; then
|
|
80
|
+
echo "ERROR: problems-dir not found: $PROBLEMS_DIR" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Discover the post-fix corpus (.verifying.md + .closed.md only) ──────────
|
|
85
|
+
#
|
|
86
|
+
# Catchup only covers tickets PAST the fix point — the lifecycle updates a
|
|
87
|
+
# reporter most wants (fix released / closed) are the ones the pre-Phase-1
|
|
88
|
+
# corpus is missing. Open / Known-Error / Parked tickets are out of scope
|
|
89
|
+
# for the migration (their transitions, if linked, fire the per-ticket
|
|
90
|
+
# path going forward). Dual-tolerant per RFC-002: flat layout
|
|
91
|
+
# `<NNN>-*.<status>.md` AND per-state subdir `<status>/<NNN>-*.md`.
|
|
92
|
+
|
|
93
|
+
shopt -s nullglob
|
|
94
|
+
|
|
95
|
+
declare -a TICKET_FILES
|
|
96
|
+
TICKET_FILES=()
|
|
97
|
+
for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.verifying.md \
|
|
98
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.closed.md \
|
|
99
|
+
"$PROBLEMS_DIR"/verifying/[0-9][0-9][0-9]-*.md \
|
|
100
|
+
"$PROBLEMS_DIR"/closed/[0-9][0-9][0-9]-*.md ; do
|
|
101
|
+
TICKET_FILES+=("$f")
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
# Extract the first `## Reported Upstream` URL from a ticket file.
|
|
105
|
+
extract_upstream_url() {
|
|
106
|
+
awk '
|
|
107
|
+
/^## Reported Upstream/ { in_section = 1; next }
|
|
108
|
+
/^## / && in_section { in_section = 0 }
|
|
109
|
+
in_section && /^- \*\*URL\*\*:/ {
|
|
110
|
+
sub(/^- \*\*URL\*\*: */, "")
|
|
111
|
+
sub(/[[:space:]].*$/, "")
|
|
112
|
+
print
|
|
113
|
+
exit
|
|
114
|
+
}
|
|
115
|
+
' "$1"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Extract the `## Reported Upstream` disclosure-path line (lower-cased).
|
|
119
|
+
extract_disclosure_path() {
|
|
120
|
+
awk '
|
|
121
|
+
/^## Reported Upstream/ { in_section = 1; next }
|
|
122
|
+
/^## / && in_section { in_section = 0 }
|
|
123
|
+
in_section && /^- \*\*Disclosure path\*\*:/ {
|
|
124
|
+
sub(/^- \*\*Disclosure path\*\*: */, "")
|
|
125
|
+
print
|
|
126
|
+
exit
|
|
127
|
+
}
|
|
128
|
+
' "$1" | tr "[:upper:]" "[:lower:]"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Does the `## Upstream Lifecycle Updates` log already record the target
|
|
132
|
+
# transition? `needle` is the ASCII-or-Unicode transition suffix to match
|
|
133
|
+
# (e.g. "Verification Pending" target, or "Closed" target). The back-write
|
|
134
|
+
# format is `- **<date>** — <from> → <to>`, so we match the `<to>` token.
|
|
135
|
+
log_has_target() {
|
|
136
|
+
local file="$1" target="$2"
|
|
137
|
+
awk -v target="$target" '
|
|
138
|
+
/^## Upstream Lifecycle Updates/ { in_section = 1; next }
|
|
139
|
+
/^## / && in_section { in_section = 0 }
|
|
140
|
+
in_section && index($0, target) > 0 { found = 1 }
|
|
141
|
+
END { exit(found ? 0 : 1) }
|
|
142
|
+
' "$file"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
extract_ticket_id() {
|
|
146
|
+
local base
|
|
147
|
+
base="$(basename "$1")"
|
|
148
|
+
echo "P${base%%-*}"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# ── Per-ticket scan loop ────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
SCANNED=0
|
|
154
|
+
CATCHUP_COUNT=0
|
|
155
|
+
SKIP_LOGGED=0
|
|
156
|
+
SKIP_OUT_OF_BAND=0
|
|
157
|
+
|
|
158
|
+
declare -A SEEN_IDS
|
|
159
|
+
|
|
160
|
+
for ticket_file in "${TICKET_FILES[@]}"; do
|
|
161
|
+
ticket_id="$(extract_ticket_id "$ticket_file")"
|
|
162
|
+
|
|
163
|
+
if [ -n "$TICKET_FILTER" ] && [ "$ticket_id" != "$TICKET_FILTER" ]; then
|
|
164
|
+
continue
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Dedup: if the same ID appears via both layouts (mid-migration), the
|
|
168
|
+
# per-state subdir copy wins (same rule as check-upstream-responses.sh).
|
|
169
|
+
if [ -n "${SEEN_IDS[$ticket_id]:-}" ]; then
|
|
170
|
+
if [[ "$ticket_file" != *"/verifying/"* && "$ticket_file" != *"/closed/"* ]]; then
|
|
171
|
+
continue
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
SEEN_IDS[$ticket_id]="$ticket_file"
|
|
175
|
+
|
|
176
|
+
# Filter to tickets carrying a `## Reported Upstream` section.
|
|
177
|
+
if ! grep -q '^## Reported Upstream' "$ticket_file"; then
|
|
178
|
+
continue
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
SCANNED=$((SCANNED + 1))
|
|
182
|
+
|
|
183
|
+
# Derive the transition the current suffix implies.
|
|
184
|
+
case "$ticket_file" in
|
|
185
|
+
*.verifying.md|*/verifying/*)
|
|
186
|
+
state="verifying"
|
|
187
|
+
transition="KE->Verifying"
|
|
188
|
+
log_target="Verification Pending" ;;
|
|
189
|
+
*.closed.md|*/closed/*)
|
|
190
|
+
state="closed"
|
|
191
|
+
transition="Verifying->Closed"
|
|
192
|
+
log_target="Closed" ;;
|
|
193
|
+
*)
|
|
194
|
+
continue ;;
|
|
195
|
+
esac
|
|
196
|
+
|
|
197
|
+
upstream_url="$(extract_upstream_url "$ticket_file")"
|
|
198
|
+
disclosure="$(extract_disclosure_path "$ticket_file")"
|
|
199
|
+
|
|
200
|
+
# Out-of-band / non-gh disclosure path, or no actionable URL → SKIP.
|
|
201
|
+
if [ -z "$upstream_url" ] \
|
|
202
|
+
|| [[ "$disclosure" == *out-of-band* ]] \
|
|
203
|
+
|| [[ "$disclosure" == *mailbox* ]]; then
|
|
204
|
+
printf "SKIP %s %s reason=out-of-band\n" "$ticket_id" "${upstream_url:-none}"
|
|
205
|
+
SKIP_OUT_OF_BAND=$((SKIP_OUT_OF_BAND + 1))
|
|
206
|
+
continue
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Idempotency: the lifecycle log already records this target → SKIP.
|
|
210
|
+
if log_has_target "$ticket_file" "$log_target"; then
|
|
211
|
+
printf "SKIP %s %s reason=already-logged\n" "$ticket_id" "$upstream_url"
|
|
212
|
+
SKIP_LOGGED=$((SKIP_LOGGED + 1))
|
|
213
|
+
continue
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
printf "CATCHUP %s %s state=%s transition=%s\n" \
|
|
217
|
+
"$ticket_id" "$upstream_url" "$state" "$transition"
|
|
218
|
+
CATCHUP_COUNT=$((CATCHUP_COUNT + 1))
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
printf "SUMMARY scanned=%s catchup=%s skip-logged=%s skip-out-of-band=%s\n" \
|
|
222
|
+
"$SCANNED" "$CATCHUP_COUNT" "$SKIP_LOGGED" "$SKIP_OUT_OF_BAND" >&2
|
|
223
|
+
|
|
224
|
+
exit 0
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Behavioural test for packages/itil/scripts/catchup-scan.sh — the P080
|
|
4
|
+
# Phase 2 `--catchup` worklist scanner. Exercises the script against a
|
|
5
|
+
# synthetic `.verifying.md` / `.closed.md` fixture corpus and asserts on
|
|
6
|
+
# its emitted worklist (stdout) + summary (stderr) — NOT on SKILL.md prose.
|
|
7
|
+
# This is a genuinely behavioural test per ADR-052: it runs the target and
|
|
8
|
+
# asserts on its outputs, covering acceptance criterion 6 (fixture +
|
|
9
|
+
# idempotency assertion).
|
|
10
|
+
#
|
|
11
|
+
# Coverage:
|
|
12
|
+
# - CATCHUP emitted for a post-fix ticket with `## Reported Upstream` and
|
|
13
|
+
# no lifecycle log entry for the target state.
|
|
14
|
+
# - SKIP/already-logged (idempotency) for a ticket whose
|
|
15
|
+
# `## Upstream Lifecycle Updates` log already records the target state.
|
|
16
|
+
# - Silent skip for tickets without a `## Reported Upstream` section.
|
|
17
|
+
# - SKIP/out-of-band for out-of-band / mailbox disclosure paths.
|
|
18
|
+
# - Open / Known-Error / Parked tickets are out of the catchup corpus.
|
|
19
|
+
# - Dual-tolerant flat-layout AND per-state subdir layout (RFC-002).
|
|
20
|
+
# - --ticket restricts the scan; bad args / missing dir error cleanly.
|
|
21
|
+
#
|
|
22
|
+
# @problem P080 (Phase 2 --catchup)
|
|
23
|
+
# @adr ADR-052 (behavioural-tests default)
|
|
24
|
+
|
|
25
|
+
setup() {
|
|
26
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
27
|
+
SCRIPT="$REPO_ROOT/packages/itil/scripts/catchup-scan.sh"
|
|
28
|
+
FIX="$BATS_TEST_TMPDIR/problems"
|
|
29
|
+
mkdir -p "$FIX"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Write a ticket with a `## Reported Upstream` section. Args:
|
|
33
|
+
# $1 = filename (relative to $FIX)
|
|
34
|
+
# $2 = upstream URL (or "" for none)
|
|
35
|
+
# $3 = disclosure path text
|
|
36
|
+
make_reported_ticket() {
|
|
37
|
+
local path="$FIX/$1" url="$2" disclosure="$3"
|
|
38
|
+
mkdir -p "$(dirname "$path")"
|
|
39
|
+
{
|
|
40
|
+
echo "# Problem: fixture"
|
|
41
|
+
echo ""
|
|
42
|
+
echo "**Status**: fixture"
|
|
43
|
+
echo ""
|
|
44
|
+
echo "## Reported Upstream"
|
|
45
|
+
echo ""
|
|
46
|
+
[ -n "$url" ] && echo "- **URL**: $url"
|
|
47
|
+
echo "- **Reported**: 2026-01-01"
|
|
48
|
+
echo "- **Disclosure path**: $disclosure"
|
|
49
|
+
} > "$path"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Append an `## Upstream Lifecycle Updates` log entry recording a target.
|
|
53
|
+
append_lifecycle_log() {
|
|
54
|
+
local path="$FIX/$1" transition="$2"
|
|
55
|
+
{
|
|
56
|
+
echo ""
|
|
57
|
+
echo "## Upstream Lifecycle Updates"
|
|
58
|
+
echo ""
|
|
59
|
+
echo "- **2026-02-02** — $transition"
|
|
60
|
+
echo " - **Disclosure path**: posted-comment"
|
|
61
|
+
} >> "$path"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@test "catchup-scan: emits CATCHUP for a closed ticket with Reported Upstream and no lifecycle log" {
|
|
65
|
+
make_reported_ticket "113-foo.closed.md" "https://github.com/o/r/issues/5" "public issue"
|
|
66
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
67
|
+
[ "$status" -eq 0 ]
|
|
68
|
+
[[ "$output" == *"CATCHUP P113 https://github.com/o/r/issues/5 state=closed transition=Verifying->Closed"* ]]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@test "catchup-scan: emits CATCHUP for a verifying ticket with the KE->Verifying transition" {
|
|
72
|
+
make_reported_ticket "090-bar.verifying.md" "https://github.com/o/r/issues/9" "public issue"
|
|
73
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
74
|
+
[ "$status" -eq 0 ]
|
|
75
|
+
[[ "$output" == *"CATCHUP P090 https://github.com/o/r/issues/9 state=verifying transition=KE->Verifying"* ]]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@test "catchup-scan: idempotency — SKIP/already-logged when log records the target state (closed)" {
|
|
79
|
+
make_reported_ticket "113-foo.closed.md" "https://github.com/o/r/issues/5" "public issue"
|
|
80
|
+
append_lifecycle_log "113-foo.closed.md" "Verification Pending → Closed"
|
|
81
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
82
|
+
[ "$status" -eq 0 ]
|
|
83
|
+
[[ "$output" == *"SKIP P113 https://github.com/o/r/issues/5 reason=already-logged"* ]]
|
|
84
|
+
[[ "$output" != *"CATCHUP P113"* ]]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@test "catchup-scan: idempotency — SKIP/already-logged when log records the target state (verifying)" {
|
|
88
|
+
make_reported_ticket "090-bar.verifying.md" "https://github.com/o/r/issues/9" "public issue"
|
|
89
|
+
append_lifecycle_log "090-bar.verifying.md" "Known Error → Verification Pending"
|
|
90
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
91
|
+
[ "$status" -eq 0 ]
|
|
92
|
+
[[ "$output" == *"SKIP P090 https://github.com/o/r/issues/9 reason=already-logged"* ]]
|
|
93
|
+
[[ "$output" != *"CATCHUP P090"* ]]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@test "catchup-scan: idempotency is re-run-safe — a logged closed ticket alongside a fresh one" {
|
|
97
|
+
make_reported_ticket "113-done.closed.md" "https://github.com/o/r/issues/5" "public issue"
|
|
98
|
+
append_lifecycle_log "113-done.closed.md" "Verification Pending → Closed"
|
|
99
|
+
make_reported_ticket "114-fresh.closed.md" "https://github.com/o/r/issues/6" "public issue"
|
|
100
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
101
|
+
[ "$status" -eq 0 ]
|
|
102
|
+
[[ "$output" == *"SKIP P113 https://github.com/o/r/issues/5 reason=already-logged"* ]]
|
|
103
|
+
[[ "$output" == *"CATCHUP P114 https://github.com/o/r/issues/6"* ]]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@test "catchup-scan: silently skips tickets with no Reported Upstream section" {
|
|
107
|
+
mkdir -p "$FIX"
|
|
108
|
+
printf '# Problem\n\n**Status**: Closed\n\nNo upstream link here.\n' > "$FIX/200-nolink.closed.md"
|
|
109
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
110
|
+
[ "$status" -eq 0 ]
|
|
111
|
+
[[ "$output" != *"P200"* ]]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@test "catchup-scan: SKIP/out-of-band for a mailbox / out-of-band disclosure path" {
|
|
115
|
+
make_reported_ticket "150-sec.closed.md" "" "drafted-and-saved (mailbox / out-of-band)"
|
|
116
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
117
|
+
[ "$status" -eq 0 ]
|
|
118
|
+
[[ "$output" == *"SKIP P150"* ]]
|
|
119
|
+
[[ "$output" == *"reason=out-of-band"* ]]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "catchup-scan: Open / Known-Error / Parked tickets are out of the catchup corpus" {
|
|
123
|
+
make_reported_ticket "300-open.open.md" "https://github.com/o/r/issues/3" "public issue"
|
|
124
|
+
make_reported_ticket "301-ke.known-error.md" "https://github.com/o/r/issues/4" "public issue"
|
|
125
|
+
make_reported_ticket "302-park.parked.md" "https://github.com/o/r/issues/7" "public issue"
|
|
126
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
127
|
+
[ "$status" -eq 0 ]
|
|
128
|
+
[[ "$output" != *"P300"* ]]
|
|
129
|
+
[[ "$output" != *"P301"* ]]
|
|
130
|
+
[[ "$output" != *"P302"* ]]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@test "catchup-scan: dual-tolerant — per-state subdir layout (RFC-002) is scanned" {
|
|
134
|
+
make_reported_ticket "closed/113-sub.md" "https://github.com/o/r/issues/8" "public issue"
|
|
135
|
+
run bash "$SCRIPT" --problems-dir "$FIX"
|
|
136
|
+
[ "$status" -eq 0 ]
|
|
137
|
+
[[ "$output" == *"CATCHUP P113 https://github.com/o/r/issues/8 state=closed transition=Verifying->Closed"* ]]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@test "catchup-scan: --ticket restricts the scan to one ticket" {
|
|
141
|
+
make_reported_ticket "113-foo.closed.md" "https://github.com/o/r/issues/5" "public issue"
|
|
142
|
+
make_reported_ticket "114-bar.closed.md" "https://github.com/o/r/issues/6" "public issue"
|
|
143
|
+
run bash "$SCRIPT" --problems-dir "$FIX" --ticket P114
|
|
144
|
+
[ "$status" -eq 0 ]
|
|
145
|
+
[[ "$output" == *"CATCHUP P114"* ]]
|
|
146
|
+
[[ "$output" != *"P113"* ]]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@test "catchup-scan: SUMMARY line reports counts on stderr" {
|
|
150
|
+
make_reported_ticket "113-foo.closed.md" "https://github.com/o/r/issues/5" "public issue"
|
|
151
|
+
make_reported_ticket "114-bar.closed.md" "https://github.com/o/r/issues/6" "public issue"
|
|
152
|
+
append_lifecycle_log "114-bar.closed.md" "Verification Pending → Closed"
|
|
153
|
+
run bash -c "bash '$SCRIPT' --problems-dir '$FIX' 2>&1 1>/dev/null"
|
|
154
|
+
[ "$status" -eq 0 ]
|
|
155
|
+
[[ "$output" == *"SUMMARY scanned=2 catchup=1 skip-logged=1 skip-out-of-band=0"* ]]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@test "catchup-scan: missing problems-dir exits 1 with error" {
|
|
159
|
+
run bash "$SCRIPT" --problems-dir "$BATS_TEST_TMPDIR/does-not-exist"
|
|
160
|
+
[ "$status" -eq 1 ]
|
|
161
|
+
[[ "$output" == *"ERROR"* ]]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@test "catchup-scan: unknown argument exits 1" {
|
|
165
|
+
run bash "$SCRIPT" --bogus
|
|
166
|
+
[ "$status" -eq 1 ]
|
|
167
|
+
[[ "$output" == *"ERROR"* ]]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@test "catchup-scan: --help prints usage and exits 0" {
|
|
171
|
+
run bash "$SCRIPT" --help
|
|
172
|
+
[ "$status" -eq 0 ]
|
|
173
|
+
[[ "$output" == *"Usage:"* ]]
|
|
174
|
+
[[ "$output" == *"--catchup"* ]] || [[ "$output" == *"catchup-scan"* ]]
|
|
175
|
+
}
|
|
@@ -10,7 +10,8 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill, Agen
|
|
|
10
10
|
@jtbd JTBD-201 (Restore Service Fast with an Audit Trail — symmetric local/upstream audit trail)
|
|
11
11
|
@jtbd JTBD-101 (Extend the Suite with Clear Patterns — downstream adopters inherit bidirectional contract)
|
|
12
12
|
@problem P080
|
|
13
|
-
@adr ADR-024 (amended P080 — bidirectional lifecycle updates)
|
|
13
|
+
@adr ADR-024 (amended P080 — bidirectional lifecycle updates; Phase 2 — --catchup migration mode + idempotency)
|
|
14
|
+
@adr ADR-049 (catchup worklist scanner invoked via wr-itil-catchup-scan bin shim)
|
|
14
15
|
@adr ADR-028 (voice-tone gate on `gh issue comment` / `gh issue close`)
|
|
15
16
|
@adr ADR-013 (Rule 1 AskUserQuestion; Rule 6 AFK fail-safe)
|
|
16
17
|
@adr ADR-014 (single-commit grain — transition + back-write + upstream comment)
|
|
@@ -31,12 +32,14 @@ This skill implements the bidirectional extension to ADR-024's outbound contract
|
|
|
31
32
|
## Invocation
|
|
32
33
|
|
|
33
34
|
```
|
|
34
|
-
/wr-itil:update-upstream <NNN>
|
|
35
|
+
/wr-itil:update-upstream <NNN> # single-ticket lifecycle update
|
|
36
|
+
/wr-itil:update-upstream --catchup # batch-retroactive migration (Phase 2)
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
- `<NNN>`: the three-digit local ticket ID (e.g. `080`). The ticket file is discovered via the same dual-tolerant lookup as [`/wr-itil:report-upstream`](../report-upstream/SKILL.md) (flat layout + per-state subdir per RFC-002 migration window).
|
|
40
|
+
- `--catchup`: one-shot batch-retroactive migration mode (P080 Phase 2 — see [§ Catchup migration mode](#catchup-migration-mode-phase-2)). Walks the existing `.verifying.md` + `.closed.md` corpus and posts the lifecycle update each ticket should have received but did not (because it was reported upstream / transitioned before the per-ticket auto-update path shipped). Idempotent — already-updated tickets are skipped.
|
|
38
41
|
|
|
39
|
-
The
|
|
42
|
+
The single-ticket form is typically invoked from `/wr-itil:transition-problem` Step 7's advisory subsection when the transitioning ticket carries a `## Reported Upstream` section (per ADR-024 Confirmation criterion 3a — the back-write that `/wr-itil:report-upstream` Step 7 writes). User-initiated single-ticket invocation is also supported. The `--catchup` form is user-initiated only (a deliberate one-shot migration, never auto-fired from a transition).
|
|
40
43
|
|
|
41
44
|
## Scope
|
|
42
45
|
|
|
@@ -48,10 +51,10 @@ The skill is typically invoked from `/wr-itil:transition-problem` Step 7's advis
|
|
|
48
51
|
- Within appetite → post via `gh issue comment <n>`; on Verifying→Closed also run `gh issue close <n>`.
|
|
49
52
|
- Above appetite → AskUserQuestion (interactive) / queue `outstanding_questions` (AFK, per P352 queue-and-continue).
|
|
50
53
|
- Back-write a `## Upstream Lifecycle Updates` log entry to the local ticket recording the transition, the matched URL, the posted comment URL, and the disclosure path.
|
|
54
|
+
- **Historical catch-up migration (`--catchup`, P080 Phase 2)** — one-shot retroactive scan of the existing `.verifying.md` + `.closed.md` corpus; posts the lifecycle update each linked-upstream ticket should already carry. Idempotent — re-running is safe. See [§ Catchup migration mode](#catchup-migration-mode-phase-2).
|
|
51
55
|
|
|
52
56
|
**Out of scope:**
|
|
53
57
|
- Initial upstream filing — that's `/wr-itil:report-upstream`.
|
|
54
|
-
- Historical catch-up migration (one-shot retroactive scan of all closed/verifying tickets with `## Reported Upstream`) — that's a separate orchestration concern; per-ticket invocation suffices for the Phase 1 contract. A future amendment may add a `--catchup` mode.
|
|
55
58
|
- Cross-tracker propagation (linking the upstream update back into a different upstream's parallel issue) — out of scope; one local ticket → N upstream URLs is supported, but each URL update is independent.
|
|
56
59
|
|
|
57
60
|
## Step-0 deferral (ADR-027)
|
|
@@ -295,6 +298,57 @@ When invoked user-initiatedly (no transition in this session, e.g. retroactive c
|
|
|
295
298
|
|
|
296
299
|
If the cumulative pipeline risk lands above appetite and `AskUserQuestion` is unavailable, apply the [ADR-013 Rule 6](../../../docs/decisions/013-structured-user-interaction-for-governance-decisions.proposed.md) non-interactive fail-safe: skip the commit and report the uncommitted state. Do NOT auto-commit above appetite without the user's call.
|
|
297
300
|
|
|
301
|
+
## Catchup migration mode (Phase 2)
|
|
302
|
+
|
|
303
|
+
`/wr-itil:update-upstream --catchup` runs a one-shot batch-retroactive migration. It exists because the per-ticket auto-update path (Phase 1) only fires on transitions that happen *after* it shipped — every ticket reported upstream and transitioned *before* Phase 1 silently missed its lifecycle update, leaving upstream issues looking abandoned. Catchup back-fills that history. Authority: [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) amendment (P080 Phase 2).
|
|
304
|
+
|
|
305
|
+
This mode is **user-initiated only** — it is never auto-fired from a transition. It is a deliberate corpus-wide migration the maintainer runs once (or re-runs safely, thanks to idempotency).
|
|
306
|
+
|
|
307
|
+
### C1. Build the worklist (read-only scan)
|
|
308
|
+
|
|
309
|
+
Invoke the worklist scanner via its [ADR-049](../../../docs/decisions/049-plugin-script-resolution-via-bin-on-path.proposed.md) `$PATH` shim — **never** via a repo-relative `packages/...` path (that path does not resolve in adopter trees):
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
wr-itil-catchup-scan
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The scanner (`packages/itil/scripts/catchup-scan.sh`, dispatched by the `wr-itil-catchup-scan` bin shim) is **read-only and local** — it makes no `gh` calls and writes nothing. It walks the `.verifying.md` + `.closed.md` corpus (dual-tolerant flat + per-state subdir per RFC-002), filters to tickets carrying a `## Reported Upstream` section, applies marker-based idempotency, and prints a worklist:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
CATCHUP P<NNN> <url> state=<verifying|closed> transition=<KE->Verifying|Verifying->Closed>
|
|
319
|
+
SKIP P<NNN> <url> reason=already-logged
|
|
320
|
+
SKIP P<NNN> <url> reason=out-of-band
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
plus a `SUMMARY scanned=… catchup=… skip-logged=… skip-out-of-band=…` line on stderr. Tickets with no `## Reported Upstream` section produce no line (the common case). Open / Known-Error / Parked tickets are out of the catchup corpus — only post-fix states (Verifying, Closed) carry the lifecycle updates a reporter most wants retroactively.
|
|
324
|
+
|
|
325
|
+
### C2. Idempotency contract
|
|
326
|
+
|
|
327
|
+
Catchup is **idempotent** (P080 Phase 2 acceptance criterion 3). The scanner skips a ticket whose `## Upstream Lifecycle Updates` log already records an entry for the current target state:
|
|
328
|
+
|
|
329
|
+
- `.verifying.md` → already-logged iff the log contains a `→ Verification Pending` entry.
|
|
330
|
+
- `.closed.md` → already-logged iff the log contains a `→ Closed` entry.
|
|
331
|
+
|
|
332
|
+
The append-only log (written by Step 6 on every post) is the source of truth — the same marker the per-ticket path writes. Re-running `--catchup` therefore never double-posts. As defence-in-depth, before posting each `CATCHUP` entry the SKILL MAY also scan the upstream issue for a prior `Update from …` comment authored by the posting account (`gh issue view <n> --json comments`); if one already matches the target transition, treat it as already-logged, back-write the log entry to reconcile, and skip the post. The body-marker check is primary (cheap, no `gh` round-trip); the comment scan is the belt-and-braces fallback for tickets whose log predates Phase 1's back-write.
|
|
333
|
+
|
|
334
|
+
### C3. Process each CATCHUP entry
|
|
335
|
+
|
|
336
|
+
For each `CATCHUP` line, run the **existing per-ticket flow** (Steps 4–6) against that ticket ID:
|
|
337
|
+
|
|
338
|
+
1. Draft the transition template (Step 4) for the entry's transition (`KE->Verifying` → Known Error → Verification Pending template; `Verifying->Closed` → Verification Pending → Closed template, which also runs `gh issue close`).
|
|
339
|
+
2. Compose through the external-comms + voice-tone gates (Step 5) — **identical** dual-gate composition as the per-ticket path. Above-appetite handling (Step 5c) is unchanged: silent risk-reduce + re-score, then queue to `## Queued Upstream Update` + `outstanding_questions` (category `deviation-approval`) per P352 if still above. Catchup does NOT bypass the gates.
|
|
340
|
+
3. Post within appetite (Step 5b final) and back-write the `## Upstream Lifecycle Updates` log (Step 6).
|
|
341
|
+
|
|
342
|
+
Process entries one at a time so a single above-appetite entry queues only itself; the rest proceed. There is no batch-cap on the number of catchup posts — the gate composition is the rate-limit, and the corpus is bounded (one pass over local tickets).
|
|
343
|
+
|
|
344
|
+
### C4. Commit per ADR-014
|
|
345
|
+
|
|
346
|
+
The catchup migration is user-initiated, so it owns its commit per the Step 7 user-initiated path: stage every touched ticket's back-write (and any `## Queued Upstream Update` appendage), score commit/push/release risk via `wr-risk-scorer:pipeline`, and commit once covering the whole pass — `docs(problems): upstream lifecycle catchup migration — <N> tickets (P080 Phase 2)`. Above-appetite-and-no-AskUserQuestion → ADR-013 Rule 6 fail-safe (report the uncommitted state, do not auto-commit).
|
|
347
|
+
|
|
348
|
+
### C5. Verification
|
|
349
|
+
|
|
350
|
+
The live-upstream end-to-end confirmation (P080 acceptance criterion 7 — a catchup comment actually lands on a real upstream issue) is the overall P080 verification step. Running `--catchup` against the real corpus (e.g. P113's `https://github.com/anthropics/claude-code/issues/52831`) and confirming the comment posts is what closes P080 to Verifying once a fresh release ships the mode.
|
|
351
|
+
|
|
298
352
|
## AFK behaviour summary
|
|
299
353
|
|
|
300
354
|
Four distinct AFK branches. Per the [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) amendment (P080) — same composition shape as the post-P270 initial-filing path — ALL pre-post branches route through the `wr-risk-scorer:external-comms` + `wr-voice-tone:external-comms` gates. Below-appetite proceeds; above-appetite silent risk-reduces + re-scores; if still above, queues per P352 queue-and-continue without halting the loop.
|
|
@@ -322,7 +376,9 @@ The skill's no-op exit (Step 1) means firing the trigger unconditionally on ever
|
|
|
322
376
|
|
|
323
377
|
## References
|
|
324
378
|
|
|
325
|
-
- [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) — primary contract this skill extends. The P080 amendment in `## Amendments` authorises the bidirectional lifecycle-update sibling skill, the transition-template shape, and the external-comms + voice-tone gate composition.
|
|
379
|
+
- [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) — primary contract this skill extends. The P080 amendment in `## Amendments` authorises the bidirectional lifecycle-update sibling skill, the transition-template shape, and the external-comms + voice-tone gate composition; the **P080 Phase 2 amendment** authorises the `--catchup` migration mode, the read-only worklist scanner, and the marker-based idempotency contract.
|
|
380
|
+
- [ADR-049](../../../docs/decisions/049-plugin-script-resolution-via-bin-on-path.proposed.md) — the catchup worklist scanner is invoked as `wr-itil-catchup-scan` ($PATH shim), never via a repo-relative `packages/...` path.
|
|
381
|
+
- [`packages/itil/scripts/catchup-scan.sh`](../../scripts/catchup-scan.sh) — read-only local worklist scanner for `--catchup`; behavioural bats at `packages/itil/scripts/test/catchup-scan.bats`.
|
|
326
382
|
- [ADR-028](../../../docs/decisions/028-voice-tone-gate-external-comms.proposed.md) — voice-tone gate on `gh issue comment` and `gh issue close`.
|
|
327
383
|
- [ADR-013](../../../docs/decisions/013-structured-user-interaction-for-governance-decisions.proposed.md) — interaction policy; Rule 1 governs the interactive above-appetite path; Rule 6 governs the AFK fail-safe.
|
|
328
384
|
- [ADR-014](../../../docs/decisions/014-governance-skills-commit-their-own-work.proposed.md) — single-commit grain for transition + back-write + upstream post.
|