@windyroad/itil 0.33.0 → 0.34.0-preview.341
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -0
- package/bin/wr-itil-check-upstream-responses +2 -0
- package/package.json +1 -1
- package/scripts/check-upstream-responses.sh +308 -0
- package/scripts/test/check-upstream-responses.bats +503 -0
- package/skills/check-upstream-responses/SKILL.md +146 -0
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
|
|
|
86
86
|
| `/wr-itil:review-problems` | Re-rate every open and known-error ticket and refresh the WSJF ranking |
|
|
87
87
|
| `/wr-itil:reconcile-readme` | Detect and correct drift between `docs/problems/README.md` and on-disk ticket inventory |
|
|
88
88
|
| `/wr-itil:report-upstream` | Report a local problem as a structured issue against an upstream repository (ADR-024) |
|
|
89
|
+
| `/wr-itil:check-upstream-responses` | Poll upstream issues we filed via `/wr-itil:report-upstream` and surface new comments / state changes / label changes since last check (P249 Phase 1; outbound symmetric counterpart to ADR-062 inbound discovery; serves [JTBD-004](../../docs/jtbd/solo-developer/JTBD-004-connect-agents.proposed.md)) |
|
|
89
90
|
| `/wr-itil:capture-rfc` | Lightweight RFC-capture skill — mandatory problem-trace per ADR-060 I1 invariant; opens a coordinated multi-commit change traceable to ≥ 1 driving problem (Phase 1 of the Problem-RFC-Story framework, P170 / ADR-060) |
|
|
90
91
|
| `/wr-itil:manage-rfc` | Heavyweight RFC intake + lifecycle management — proposed → accepted → in-progress → verifying → closed; sibling to `manage-problem` at the RFC tier (ADR-060) |
|
|
91
92
|
| `/wr-itil:capture-story` | Lightweight story-capture skill — mandatory problem-trace AND JTBD-trace per ADR-060 I6 + I9 invariants; optional `--rfc` / `--story-map` flags (I7 + I8 enforce at `accepted` transition); drafts an INVEST-shaped sub-workstream entity under a parent RFC (Phase 2 of the Problem-RFC-Story framework, P170 / ADR-060) |
|
package/package.json
CHANGED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/check-upstream-responses.sh
|
|
3
|
+
#
|
|
4
|
+
# Phase 1 of P249 — outbound symmetric counterpart to ADR-062's inbound
|
|
5
|
+
# discovery pipeline. Scans local problem tickets for `## Reported
|
|
6
|
+
# Upstream` back-link sections (written by `/wr-itil:report-upstream`
|
|
7
|
+
# Step 7), polls each upstream issue via `gh issue view`, diffs against
|
|
8
|
+
# cache, and surfaces new comments / state changes / label changes since
|
|
9
|
+
# last check.
|
|
10
|
+
#
|
|
11
|
+
# Read-only externally: only `gh issue view` (read-only) — no
|
|
12
|
+
# `gh issue comment` / `gh issue create`. Does NOT trip ADR-028
|
|
13
|
+
# external-comms gate. AFK-safe.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# check-upstream-responses.sh
|
|
17
|
+
# [--problems-dir <dir>] default: docs/problems
|
|
18
|
+
# [--cache-file <path>] default: <problems-dir>/.outbound-responses-cache.json
|
|
19
|
+
# [--audit-log <path>] default: docs/audits/outbound-responses-log.md
|
|
20
|
+
# [--ticket P<NNN>] restrict polling to one ticket
|
|
21
|
+
# [--force-recheck] ignore cache; treat all as new
|
|
22
|
+
# [--gh-bin <path>] gh binary (default: gh) — testability seam
|
|
23
|
+
#
|
|
24
|
+
# Exit codes:
|
|
25
|
+
# 0 = success (zero or more new responses surfaced; stdout has per-ticket lines)
|
|
26
|
+
# 1 = error (problems-dir missing, malformed cache, malformed CLI args)
|
|
27
|
+
# 2 = partial — some upstream polls failed; successful ones are still
|
|
28
|
+
# written to cache + audit-log
|
|
29
|
+
#
|
|
30
|
+
# Structured stdout (one per ticket; ≤ 150 bytes per line per ADR-038):
|
|
31
|
+
# NEW P<NNN> <url> state=<state> new-comments=<N>
|
|
32
|
+
# STATE P<NNN> <url> state=<old>→<new>
|
|
33
|
+
# LABEL P<NNN> <url> labels-added=<csv> labels-removed=<csv>
|
|
34
|
+
# NONE P<NNN> <url> no-change-since=<last-checked>
|
|
35
|
+
# FAIL P<NNN> <url> reason=<gh-error-short>
|
|
36
|
+
#
|
|
37
|
+
# Precedence when multiple change classes apply: STATE > NEW (comments) > LABEL > NONE.
|
|
38
|
+
#
|
|
39
|
+
# @problem P249 — no process for issue reporters to check for responses (Phase 1)
|
|
40
|
+
# @adr ADR-014 (governance skills commit their own work)
|
|
41
|
+
# @adr ADR-024 (back-link source of truth — Reported Upstream URL)
|
|
42
|
+
# @adr ADR-031 (cache file placement under docs/problems/)
|
|
43
|
+
# @adr ADR-032 (foreground synchronous skill)
|
|
44
|
+
# @adr ADR-038 (progressive disclosure — per-row byte budget)
|
|
45
|
+
# @adr ADR-049 (invoked via wr-itil-check-upstream-responses bin shim)
|
|
46
|
+
# @adr ADR-062 (inbound discovery — symmetric counterpart)
|
|
47
|
+
# @jtbd JTBD-004 (cross-repo coordination — primary anchor)
|
|
48
|
+
# @jtbd JTBD-006 (AFK-safe)
|
|
49
|
+
# @jtbd JTBD-001 (governance without slowing down)
|
|
50
|
+
# @jtbd JTBD-201 (audit trail)
|
|
51
|
+
|
|
52
|
+
set -uo pipefail
|
|
53
|
+
|
|
54
|
+
# ── Parse CLI args ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
PROBLEMS_DIR="docs/problems"
|
|
57
|
+
CACHE_FILE=""
|
|
58
|
+
AUDIT_LOG="docs/audits/outbound-responses-log.md"
|
|
59
|
+
TICKET_FILTER=""
|
|
60
|
+
FORCE_RECHECK=0
|
|
61
|
+
GH_BIN="gh"
|
|
62
|
+
|
|
63
|
+
while [ $# -gt 0 ]; do
|
|
64
|
+
case "$1" in
|
|
65
|
+
--problems-dir) PROBLEMS_DIR="$2"; shift 2 ;;
|
|
66
|
+
--cache-file) CACHE_FILE="$2"; shift 2 ;;
|
|
67
|
+
--audit-log) AUDIT_LOG="$2"; shift 2 ;;
|
|
68
|
+
--ticket) TICKET_FILTER="$2"; shift 2 ;;
|
|
69
|
+
--force-recheck) FORCE_RECHECK=1; shift ;;
|
|
70
|
+
--gh-bin) GH_BIN="$2"; shift 2 ;;
|
|
71
|
+
-h|--help)
|
|
72
|
+
sed -n '/^# Usage:/,/^# Exit codes:/p' "$0" | sed 's/^# //'
|
|
73
|
+
exit 0
|
|
74
|
+
;;
|
|
75
|
+
*) echo "ERROR: unknown argument: $1" >&2; exit 1 ;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# Default cache path lives under problems-dir.
|
|
80
|
+
if [ -z "$CACHE_FILE" ]; then
|
|
81
|
+
CACHE_FILE="${PROBLEMS_DIR}/.outbound-responses-cache.json"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
if [ ! -d "$PROBLEMS_DIR" ]; then
|
|
87
|
+
echo "ERROR: problems-dir not found: $PROBLEMS_DIR" >&2
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
92
|
+
echo "ERROR: jq is required but not installed" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── Read existing cache (or start fresh) ────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
99
|
+
if ! jq -e . "$CACHE_FILE" >/dev/null 2>&1; then
|
|
100
|
+
echo "ERROR: cache file is malformed: $CACHE_FILE" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
CACHE_JSON="$(cat "$CACHE_FILE")"
|
|
104
|
+
else
|
|
105
|
+
CACHE_JSON='{"last_checked":null,"tickets":{}}'
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Build a fresh updated cache JSON in memory; flush at end.
|
|
109
|
+
UPDATED_CACHE="$CACHE_JSON"
|
|
110
|
+
|
|
111
|
+
# ── Discover tickets with `## Reported Upstream` URL ────────────────────────
|
|
112
|
+
#
|
|
113
|
+
# Dual-tolerant per RFC-002: flat layout `<NNN>-*.<status>.md` AND
|
|
114
|
+
# per-state subdir layout `<status>/<NNN>-*.md`.
|
|
115
|
+
|
|
116
|
+
shopt -s nullglob
|
|
117
|
+
|
|
118
|
+
declare -a TICKET_FILES
|
|
119
|
+
TICKET_FILES=()
|
|
120
|
+
for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.open.md \
|
|
121
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.known-error.md \
|
|
122
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.verifying.md \
|
|
123
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.parked.md \
|
|
124
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.closed.md \
|
|
125
|
+
"$PROBLEMS_DIR"/open/[0-9][0-9][0-9]-*.md \
|
|
126
|
+
"$PROBLEMS_DIR"/known-error/[0-9][0-9][0-9]-*.md \
|
|
127
|
+
"$PROBLEMS_DIR"/verifying/[0-9][0-9][0-9]-*.md \
|
|
128
|
+
"$PROBLEMS_DIR"/parked/[0-9][0-9][0-9]-*.md \
|
|
129
|
+
"$PROBLEMS_DIR"/closed/[0-9][0-9][0-9]-*.md ; do
|
|
130
|
+
TICKET_FILES+=("$f")
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
# Extract `## Reported Upstream` URL from a ticket file.
|
|
134
|
+
# Returns the first URL found in a `- **URL**:` line within the section.
|
|
135
|
+
extract_upstream_url() {
|
|
136
|
+
awk '
|
|
137
|
+
/^## Reported Upstream/ { in_section = 1; next }
|
|
138
|
+
/^## / && in_section { in_section = 0 }
|
|
139
|
+
in_section && /^- \*\*URL\*\*:/ {
|
|
140
|
+
# Strip prefix; the URL is the first whitespace-delimited token after.
|
|
141
|
+
sub(/^- \*\*URL\*\*: */, "")
|
|
142
|
+
sub(/[[:space:]].*$/, "")
|
|
143
|
+
print
|
|
144
|
+
exit
|
|
145
|
+
}
|
|
146
|
+
' "$1"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Extract numeric ID prefix from a ticket file basename.
|
|
150
|
+
extract_ticket_id() {
|
|
151
|
+
local base
|
|
152
|
+
base="$(basename "$1")"
|
|
153
|
+
echo "P${base%%-*}"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ── Per-ticket polling loop ─────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
PARTIAL_FAILURE=0
|
|
159
|
+
POLL_COUNT=0
|
|
160
|
+
NEW_COUNT=0
|
|
161
|
+
STATE_CHANGE_COUNT=0
|
|
162
|
+
LABEL_CHANGE_COUNT=0
|
|
163
|
+
NONE_COUNT=0
|
|
164
|
+
FAIL_COUNT=0
|
|
165
|
+
|
|
166
|
+
declare -A SEEN_IDS
|
|
167
|
+
|
|
168
|
+
for ticket_file in "${TICKET_FILES[@]}"; do
|
|
169
|
+
ticket_id="$(extract_ticket_id "$ticket_file")"
|
|
170
|
+
|
|
171
|
+
# Filter: --ticket only processes the named ticket.
|
|
172
|
+
if [ -n "$TICKET_FILTER" ] && [ "$ticket_id" != "$TICKET_FILTER" ]; then
|
|
173
|
+
continue
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Dedup: if the same ID appears via both layouts (mid-migration), per-state subdir wins.
|
|
177
|
+
if [ -n "${SEEN_IDS[$ticket_id]:-}" ]; then
|
|
178
|
+
if [[ "$ticket_file" != *"/"+(open|known-error|verifying|parked|closed)"/"* ]]; then
|
|
179
|
+
continue
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
SEEN_IDS[$ticket_id]="$ticket_file"
|
|
183
|
+
|
|
184
|
+
upstream_url="$(extract_upstream_url "$ticket_file")"
|
|
185
|
+
if [ -z "$upstream_url" ]; then
|
|
186
|
+
# No upstream link — silently skip.
|
|
187
|
+
continue
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
POLL_COUNT=$((POLL_COUNT + 1))
|
|
191
|
+
|
|
192
|
+
# Poll the upstream.
|
|
193
|
+
if ! gh_output="$("$GH_BIN" issue view "$upstream_url" --json comments,state,labels,updatedAt 2>&1)"; then
|
|
194
|
+
short_reason="$(echo "$gh_output" | head -1 | cut -c1-80)"
|
|
195
|
+
printf "FAIL %s %s reason=%s\n" "$ticket_id" "$upstream_url" "$short_reason"
|
|
196
|
+
PARTIAL_FAILURE=1
|
|
197
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
198
|
+
continue
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# Parse upstream state.
|
|
202
|
+
current_state="$(echo "$gh_output" | jq -r '.state // "UNKNOWN"')"
|
|
203
|
+
current_comment_count="$(echo "$gh_output" | jq -r '.comments | length')"
|
|
204
|
+
current_labels_csv="$(echo "$gh_output" | jq -r '[.labels[].name] | sort | join(",")')"
|
|
205
|
+
current_updated_at="$(echo "$gh_output" | jq -r '.updatedAt // ""')"
|
|
206
|
+
|
|
207
|
+
# Look up cache entry.
|
|
208
|
+
cache_state="$(echo "$UPDATED_CACHE" | jq -r ".tickets[\"$ticket_id\"].last_seen_state // \"\"")"
|
|
209
|
+
cache_comment_count="$(echo "$UPDATED_CACHE" | jq -r ".tickets[\"$ticket_id\"].last_seen_comment_count // -1")"
|
|
210
|
+
cache_labels_csv="$(echo "$UPDATED_CACHE" | jq -r ".tickets[\"$ticket_id\"].last_seen_labels // [] | sort | join(\",\")")"
|
|
211
|
+
cache_last_checked="$(echo "$UPDATED_CACHE" | jq -r ".tickets[\"$ticket_id\"].last_checked_at // \"\"")"
|
|
212
|
+
|
|
213
|
+
# Decide the change class (precedence: STATE > NEW (comments) > LABEL > NONE).
|
|
214
|
+
no_cache_entry=0
|
|
215
|
+
if [ -z "$cache_state" ] || [ "$cache_comment_count" = "-1" ]; then
|
|
216
|
+
no_cache_entry=1
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
if [ "$FORCE_RECHECK" -eq 1 ] || [ "$no_cache_entry" -eq 1 ]; then
|
|
220
|
+
delta="$current_comment_count"
|
|
221
|
+
if [ "$no_cache_entry" -eq 0 ]; then
|
|
222
|
+
delta=$((current_comment_count - cache_comment_count))
|
|
223
|
+
[ "$delta" -lt 0 ] && delta=0
|
|
224
|
+
fi
|
|
225
|
+
printf "NEW %s %s state=%s new-comments=%s\n" "$ticket_id" "$upstream_url" "$current_state" "$delta"
|
|
226
|
+
NEW_COUNT=$((NEW_COUNT + 1))
|
|
227
|
+
elif [ "$current_state" != "$cache_state" ]; then
|
|
228
|
+
printf "STATE %s %s state=%s→%s\n" "$ticket_id" "$upstream_url" "$cache_state" "$current_state"
|
|
229
|
+
STATE_CHANGE_COUNT=$((STATE_CHANGE_COUNT + 1))
|
|
230
|
+
elif [ "$current_comment_count" -ne "$cache_comment_count" ]; then
|
|
231
|
+
delta=$((current_comment_count - cache_comment_count))
|
|
232
|
+
[ "$delta" -lt 0 ] && delta=0
|
|
233
|
+
printf "NEW %s %s state=%s new-comments=%s\n" "$ticket_id" "$upstream_url" "$current_state" "$delta"
|
|
234
|
+
NEW_COUNT=$((NEW_COUNT + 1))
|
|
235
|
+
elif [ "$current_labels_csv" != "$cache_labels_csv" ]; then
|
|
236
|
+
# Compute labels added/removed.
|
|
237
|
+
added="$(comm -13 <(echo "$cache_labels_csv" | tr ',' '\n' | sort) <(echo "$current_labels_csv" | tr ',' '\n' | sort) | grep -v '^$' | paste -sd',' -)"
|
|
238
|
+
removed="$(comm -23 <(echo "$cache_labels_csv" | tr ',' '\n' | sort) <(echo "$current_labels_csv" | tr ',' '\n' | sort) | grep -v '^$' | paste -sd',' -)"
|
|
239
|
+
printf "LABEL %s %s labels-added=%s labels-removed=%s\n" "$ticket_id" "$upstream_url" "$added" "$removed"
|
|
240
|
+
LABEL_CHANGE_COUNT=$((LABEL_CHANGE_COUNT + 1))
|
|
241
|
+
else
|
|
242
|
+
printf "NONE %s %s no-change-since=%s\n" "$ticket_id" "$upstream_url" "$cache_last_checked"
|
|
243
|
+
NONE_COUNT=$((NONE_COUNT + 1))
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# Update cache entry for this ticket.
|
|
247
|
+
now_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
248
|
+
UPDATED_CACHE="$(echo "$UPDATED_CACHE" | jq \
|
|
249
|
+
--arg id "$ticket_id" \
|
|
250
|
+
--arg url "$upstream_url" \
|
|
251
|
+
--arg state "$current_state" \
|
|
252
|
+
--arg checked "$now_iso" \
|
|
253
|
+
--arg updated "$current_updated_at" \
|
|
254
|
+
--argjson count "$current_comment_count" \
|
|
255
|
+
--argjson labels "$(echo "$gh_output" | jq '[.labels[].name] | sort')" \
|
|
256
|
+
'.tickets[$id] = {
|
|
257
|
+
upstream_url: $url,
|
|
258
|
+
last_checked_at: $checked,
|
|
259
|
+
last_seen_state: $state,
|
|
260
|
+
last_seen_comment_count: $count,
|
|
261
|
+
last_seen_labels: $labels,
|
|
262
|
+
last_seen_updated_at: $updated
|
|
263
|
+
}')"
|
|
264
|
+
done
|
|
265
|
+
|
|
266
|
+
# ── Flush cache + append audit-log ──────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
269
|
+
UPDATED_CACHE="$(echo "$UPDATED_CACHE" | jq --arg now "$NOW_ISO" '.last_checked = $now')"
|
|
270
|
+
|
|
271
|
+
# Ensure cache file parent dir exists.
|
|
272
|
+
mkdir -p "$(dirname "$CACHE_FILE")"
|
|
273
|
+
echo "$UPDATED_CACHE" | jq . > "$CACHE_FILE"
|
|
274
|
+
|
|
275
|
+
# Append audit-log entry.
|
|
276
|
+
mkdir -p "$(dirname "$AUDIT_LOG")"
|
|
277
|
+
if [ ! -f "$AUDIT_LOG" ]; then
|
|
278
|
+
cat > "$AUDIT_LOG" <<'EOF'
|
|
279
|
+
# Outbound upstream-response check — audit log
|
|
280
|
+
|
|
281
|
+
> Forward-chronology audit trail of `/wr-itil:check-upstream-responses` passes (P249 Phase 1). Each pass appends a `## YYYY-MM-DDTHH:MM:SSZ` heading with tickets polled, response classes observed, and cache refresh confirmation. Mirrors `docs/audits/inbound-discovery-log.md` shape per ADR-062's audit-log surface contract.
|
|
282
|
+
>
|
|
283
|
+
> Path is intentional per CLAUDE.md P131 — project-generated artefacts go under `docs/`, never `.claude/`.
|
|
284
|
+
|
|
285
|
+
EOF
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
{
|
|
289
|
+
echo ""
|
|
290
|
+
echo "## ${NOW_ISO} — Outbound response check pass"
|
|
291
|
+
echo ""
|
|
292
|
+
echo "- Tickets polled: ${POLL_COUNT}"
|
|
293
|
+
echo "- New responses: ${NEW_COUNT}"
|
|
294
|
+
echo "- State changes: ${STATE_CHANGE_COUNT}"
|
|
295
|
+
echo "- Label changes: ${LABEL_CHANGE_COUNT}"
|
|
296
|
+
echo "- No changes: ${NONE_COUNT}"
|
|
297
|
+
echo "- Poll failures: ${FAIL_COUNT}"
|
|
298
|
+
echo "- Cache: ${CACHE_FILE}"
|
|
299
|
+
echo "- Force recheck: $([ "$FORCE_RECHECK" -eq 1 ] && echo "yes" || echo "no")"
|
|
300
|
+
if [ -n "$TICKET_FILTER" ]; then
|
|
301
|
+
echo "- Filter: ${TICKET_FILTER}"
|
|
302
|
+
fi
|
|
303
|
+
} >> "$AUDIT_LOG"
|
|
304
|
+
|
|
305
|
+
if [ "$PARTIAL_FAILURE" -eq 1 ]; then
|
|
306
|
+
exit 2
|
|
307
|
+
fi
|
|
308
|
+
exit 0
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P249 — no process for issue reporters to check for responses
|
|
4
|
+
# (symmetric gap to ADR-062 inbound discovery). Phase 1: us-as-upstream-
|
|
5
|
+
# reporter side — scan local tickets for `## Reported Upstream` back-
|
|
6
|
+
# link sections (written by `/wr-itil:report-upstream` Step 7), poll
|
|
7
|
+
# each upstream issue via `gh issue view`, diff against cache, surface
|
|
8
|
+
# new comments / state changes / label changes.
|
|
9
|
+
#
|
|
10
|
+
# Contract: `check-upstream-responses.sh [--problems-dir <dir>]
|
|
11
|
+
# [--cache-file <path>] [--audit-log <path>] [--force-recheck]
|
|
12
|
+
# [--ticket P<NNN>] [--gh-bin <path>]` is a foreground governance
|
|
13
|
+
# script that polls upstream issues, writes cache + audit-log, and
|
|
14
|
+
# emits a structured one-line-per-ticket report to stdout.
|
|
15
|
+
#
|
|
16
|
+
# Read-soft externally: only `gh issue view` (read-only) — no
|
|
17
|
+
# `gh issue comment` / `gh issue create`. Does NOT trip ADR-028
|
|
18
|
+
# external-comms gate. AFK-safe.
|
|
19
|
+
#
|
|
20
|
+
# Cache: `docs/problems/.outbound-responses-cache.json` (mirrors
|
|
21
|
+
# ADR-062 inbound cache shape under same dir per ADR-031 §"Cache
|
|
22
|
+
# files live under docs/problems/"). Committed for replay
|
|
23
|
+
# determinism; rebuild via --force-recheck.
|
|
24
|
+
#
|
|
25
|
+
# Audit-log: `docs/audits/outbound-responses-log.md` (mirrors
|
|
26
|
+
# ADR-062 inbound audit-log under docs/audits/ per CLAUDE.md P131).
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 = success (zero or more new responses surfaced; check stdout
|
|
30
|
+
# for per-ticket lines)
|
|
31
|
+
# 1 = error (cache file malformed, upstream URL malformed, gh CLI
|
|
32
|
+
# missing, problems-dir not found)
|
|
33
|
+
# 2 = partial — some upstream polls failed (network / 404); the
|
|
34
|
+
# successful ones are still written to cache + audit-log
|
|
35
|
+
#
|
|
36
|
+
# Structured stdout format (≤ 150 bytes per line per ADR-038
|
|
37
|
+
# progressive-disclosure budget):
|
|
38
|
+
# NEW P<NNN> <url> state=<state> new-comments=<N>
|
|
39
|
+
# STATE P<NNN> <url> state=<old>→<new>
|
|
40
|
+
# LABEL P<NNN> <url> labels-added=<csv> labels-removed=<csv>
|
|
41
|
+
# NONE P<NNN> <url> no-change-since=<last-checked>
|
|
42
|
+
# SKIP P<NNN> reason=no-reported-upstream-section
|
|
43
|
+
# FAIL P<NNN> <url> reason=<gh-error-short>
|
|
44
|
+
#
|
|
45
|
+
# @adr ADR-014 (governance skills commit their own work — cache +
|
|
46
|
+
# audit-log ride one commit per pass)
|
|
47
|
+
# @adr ADR-024 (back-link source of truth — `## Reported Upstream`
|
|
48
|
+
# URL field is what this script reads)
|
|
49
|
+
# @adr ADR-031 (cache file placement under docs/problems/)
|
|
50
|
+
# @adr ADR-032 (foreground synchronous skill — no AskUserQuestion)
|
|
51
|
+
# @adr ADR-038 (progressive disclosure — per-row byte budget on diff)
|
|
52
|
+
# @adr ADR-049 (script invoked via wr-itil-check-upstream-responses
|
|
53
|
+
# bin shim from SKILL.md)
|
|
54
|
+
# @adr ADR-062 (inbound discovery pattern — this script is the
|
|
55
|
+
# outbound symmetric counterpart)
|
|
56
|
+
# @jtbd JTBD-004 (cross-repo coordination — primary anchor)
|
|
57
|
+
# @jtbd JTBD-006 (AFK-safe surface)
|
|
58
|
+
# @jtbd JTBD-001 (governance without slowing down — eliminates
|
|
59
|
+
# manual upstream polling)
|
|
60
|
+
# @jtbd JTBD-201 (audit trail — outbound-responses-log.md replay)
|
|
61
|
+
|
|
62
|
+
setup() {
|
|
63
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
64
|
+
SCRIPT="$SCRIPTS_DIR/check-upstream-responses.sh"
|
|
65
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
66
|
+
PROBLEMS_DIR="$FIXTURE_DIR/problems"
|
|
67
|
+
AUDITS_DIR="$FIXTURE_DIR/audits"
|
|
68
|
+
GHFAKE_DIR="$FIXTURE_DIR/ghfake"
|
|
69
|
+
mkdir -p "$PROBLEMS_DIR" "$AUDITS_DIR" "$GHFAKE_DIR"
|
|
70
|
+
CACHE_FILE="$PROBLEMS_DIR/.outbound-responses-cache.json"
|
|
71
|
+
AUDIT_LOG="$AUDITS_DIR/outbound-responses-log.md"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
teardown() {
|
|
75
|
+
rm -rf "$FIXTURE_DIR"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ── Helper: install a fake `gh` binary that prints a canned JSON
|
|
79
|
+
# ── response keyed by upstream URL. Tests prime $GHFAKE_DIR/<url-hash>
|
|
80
|
+
# ── with the JSON they want returned.
|
|
81
|
+
|
|
82
|
+
make_fake_gh() {
|
|
83
|
+
cat > "$GHFAKE_DIR/gh" <<'EOF'
|
|
84
|
+
#!/usr/bin/env bash
|
|
85
|
+
# Fake gh shim. Recognised invocations:
|
|
86
|
+
# gh issue view <url> --json comments,state,labels,updatedAt
|
|
87
|
+
# Looks up canned response by url-hash from $GHFAKE_DATA dir.
|
|
88
|
+
if [ "$1" = "issue" ] && [ "$2" = "view" ]; then
|
|
89
|
+
URL="$3"
|
|
90
|
+
HASH=$(echo -n "$URL" | shasum | awk '{print $1}')
|
|
91
|
+
if [ -f "$GHFAKE_DATA/$HASH.json" ]; then
|
|
92
|
+
cat "$GHFAKE_DATA/$HASH.json"
|
|
93
|
+
exit 0
|
|
94
|
+
else
|
|
95
|
+
echo "could not resolve issue: $URL" >&2
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
echo "fake-gh: unrecognised invocation: $*" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
EOF
|
|
102
|
+
chmod +x "$GHFAKE_DIR/gh"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
prime_gh_response() {
|
|
106
|
+
local url="$1"
|
|
107
|
+
local payload="$2"
|
|
108
|
+
local hash
|
|
109
|
+
hash=$(echo -n "$url" | shasum | awk '{print $1}')
|
|
110
|
+
printf '%s' "$payload" > "$GHFAKE_DIR/$hash.json"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ── Existence + executable ──────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
@test "check-upstream-responses: script exists" {
|
|
116
|
+
[ -f "$SCRIPT" ]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@test "check-upstream-responses: script is executable" {
|
|
120
|
+
[ -x "$SCRIPT" ]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ── Behavioural: ticket with `## Reported Upstream` is polled ───────────────
|
|
124
|
+
|
|
125
|
+
@test "ticket with Reported Upstream section is polled and surfaces NEW state on first check" {
|
|
126
|
+
cat > "$PROBLEMS_DIR/100-foo.open.md" <<'EOF'
|
|
127
|
+
# Problem 100: Foo
|
|
128
|
+
|
|
129
|
+
**Status**: Open
|
|
130
|
+
|
|
131
|
+
## Description
|
|
132
|
+
|
|
133
|
+
Some description.
|
|
134
|
+
|
|
135
|
+
## Reported Upstream
|
|
136
|
+
|
|
137
|
+
- **URL**: https://github.com/example/repo/issues/42
|
|
138
|
+
- **Reported**: 2026-05-01
|
|
139
|
+
- **Template used**: bug_report.yml
|
|
140
|
+
- **Disclosure path**: public issue
|
|
141
|
+
- **Cross-reference confirmed**: yes
|
|
142
|
+
EOF
|
|
143
|
+
|
|
144
|
+
make_fake_gh
|
|
145
|
+
prime_gh_response "https://github.com/example/repo/issues/42" '{"state":"OPEN","comments":[{"id":1,"author":{"login":"someone"},"createdAt":"2026-05-10T10:00:00Z","body":"thanks"}],"labels":[{"name":"triage"}],"updatedAt":"2026-05-10T10:00:00Z"}'
|
|
146
|
+
|
|
147
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
148
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
149
|
+
[ "$status" -eq 0 ]
|
|
150
|
+
echo "$output" | grep -qE "^NEW *P100"
|
|
151
|
+
echo "$output" | grep -q "https://github.com/example/repo/issues/42"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ── Behavioural: ticket without Reported Upstream is skipped ────────────────
|
|
155
|
+
|
|
156
|
+
@test "ticket without Reported Upstream section is skipped" {
|
|
157
|
+
cat > "$PROBLEMS_DIR/101-bar.open.md" <<'EOF'
|
|
158
|
+
# Problem 101: Bar
|
|
159
|
+
|
|
160
|
+
**Status**: Open
|
|
161
|
+
|
|
162
|
+
## Description
|
|
163
|
+
|
|
164
|
+
No upstream link here.
|
|
165
|
+
EOF
|
|
166
|
+
|
|
167
|
+
make_fake_gh
|
|
168
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
169
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
170
|
+
[ "$status" -eq 0 ]
|
|
171
|
+
# P101 should not appear in output at all (no Reported Upstream → silently skipped).
|
|
172
|
+
! echo "$output" | grep -q "P101"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# ── Behavioural: cache fresh → no new responses surfaced ────────────────────
|
|
176
|
+
|
|
177
|
+
@test "ticket with cached state matching upstream surfaces NONE" {
|
|
178
|
+
cat > "$PROBLEMS_DIR/102-baz.open.md" <<'EOF'
|
|
179
|
+
# Problem 102: Baz
|
|
180
|
+
|
|
181
|
+
**Status**: Open
|
|
182
|
+
|
|
183
|
+
## Reported Upstream
|
|
184
|
+
|
|
185
|
+
- **URL**: https://github.com/example/repo/issues/77
|
|
186
|
+
- **Reported**: 2026-05-01
|
|
187
|
+
- **Template used**: structured default
|
|
188
|
+
- **Disclosure path**: public issue
|
|
189
|
+
- **Cross-reference confirmed**: yes
|
|
190
|
+
EOF
|
|
191
|
+
|
|
192
|
+
# Seed cache to match upstream state.
|
|
193
|
+
cat > "$CACHE_FILE" <<'EOF'
|
|
194
|
+
{
|
|
195
|
+
"last_checked": "2026-05-15T00:00:00Z",
|
|
196
|
+
"tickets": {
|
|
197
|
+
"P102": {
|
|
198
|
+
"upstream_url": "https://github.com/example/repo/issues/77",
|
|
199
|
+
"last_checked_at": "2026-05-15T00:00:00Z",
|
|
200
|
+
"last_seen_state": "OPEN",
|
|
201
|
+
"last_seen_comment_count": 3,
|
|
202
|
+
"last_seen_labels": ["triage"],
|
|
203
|
+
"last_seen_updated_at": "2026-05-14T12:00:00Z"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
EOF
|
|
208
|
+
|
|
209
|
+
make_fake_gh
|
|
210
|
+
prime_gh_response "https://github.com/example/repo/issues/77" '{"state":"OPEN","comments":[{"id":1},{"id":2},{"id":3}],"labels":[{"name":"triage"}],"updatedAt":"2026-05-14T12:00:00Z"}'
|
|
211
|
+
|
|
212
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
213
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
214
|
+
[ "$status" -eq 0 ]
|
|
215
|
+
echo "$output" | grep -qE "^NONE *P102"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# ── Behavioural: new comments surface as NEW with delta count ───────────────
|
|
219
|
+
|
|
220
|
+
@test "ticket with new comments since last check surfaces NEW with count" {
|
|
221
|
+
cat > "$PROBLEMS_DIR/103-qux.open.md" <<'EOF'
|
|
222
|
+
# Problem 103: Qux
|
|
223
|
+
|
|
224
|
+
**Status**: Open
|
|
225
|
+
|
|
226
|
+
## Reported Upstream
|
|
227
|
+
|
|
228
|
+
- **URL**: https://github.com/example/repo/issues/88
|
|
229
|
+
- **Reported**: 2026-05-01
|
|
230
|
+
- **Template used**: bug_report.yml
|
|
231
|
+
- **Disclosure path**: public issue
|
|
232
|
+
- **Cross-reference confirmed**: yes
|
|
233
|
+
EOF
|
|
234
|
+
|
|
235
|
+
cat > "$CACHE_FILE" <<'EOF'
|
|
236
|
+
{
|
|
237
|
+
"last_checked": "2026-05-15T00:00:00Z",
|
|
238
|
+
"tickets": {
|
|
239
|
+
"P103": {
|
|
240
|
+
"upstream_url": "https://github.com/example/repo/issues/88",
|
|
241
|
+
"last_checked_at": "2026-05-15T00:00:00Z",
|
|
242
|
+
"last_seen_state": "OPEN",
|
|
243
|
+
"last_seen_comment_count": 1,
|
|
244
|
+
"last_seen_labels": [],
|
|
245
|
+
"last_seen_updated_at": "2026-05-14T12:00:00Z"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
EOF
|
|
250
|
+
|
|
251
|
+
make_fake_gh
|
|
252
|
+
prime_gh_response "https://github.com/example/repo/issues/88" '{"state":"OPEN","comments":[{"id":1},{"id":2},{"id":3}],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
253
|
+
|
|
254
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
255
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
256
|
+
[ "$status" -eq 0 ]
|
|
257
|
+
echo "$output" | grep -qE "^NEW *P103"
|
|
258
|
+
echo "$output" | grep -q "new-comments=2"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ── Behavioural: state change surfaces STATE marker ─────────────────────────
|
|
262
|
+
|
|
263
|
+
@test "ticket with state change OPEN to CLOSED surfaces STATE marker" {
|
|
264
|
+
cat > "$PROBLEMS_DIR/104-baz.open.md" <<'EOF'
|
|
265
|
+
# Problem 104: Baz
|
|
266
|
+
|
|
267
|
+
**Status**: Open
|
|
268
|
+
|
|
269
|
+
## Reported Upstream
|
|
270
|
+
|
|
271
|
+
- **URL**: https://github.com/example/repo/issues/99
|
|
272
|
+
- **Reported**: 2026-05-01
|
|
273
|
+
- **Template used**: structured default
|
|
274
|
+
- **Disclosure path**: public issue
|
|
275
|
+
- **Cross-reference confirmed**: yes
|
|
276
|
+
EOF
|
|
277
|
+
|
|
278
|
+
cat > "$CACHE_FILE" <<'EOF'
|
|
279
|
+
{
|
|
280
|
+
"last_checked": "2026-05-15T00:00:00Z",
|
|
281
|
+
"tickets": {
|
|
282
|
+
"P104": {
|
|
283
|
+
"upstream_url": "https://github.com/example/repo/issues/99",
|
|
284
|
+
"last_checked_at": "2026-05-15T00:00:00Z",
|
|
285
|
+
"last_seen_state": "OPEN",
|
|
286
|
+
"last_seen_comment_count": 2,
|
|
287
|
+
"last_seen_labels": [],
|
|
288
|
+
"last_seen_updated_at": "2026-05-14T12:00:00Z"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
EOF
|
|
293
|
+
|
|
294
|
+
make_fake_gh
|
|
295
|
+
prime_gh_response "https://github.com/example/repo/issues/99" '{"state":"CLOSED","comments":[{"id":1},{"id":2}],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
296
|
+
|
|
297
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
298
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
299
|
+
[ "$status" -eq 0 ]
|
|
300
|
+
echo "$output" | grep -qE "^STATE *P104"
|
|
301
|
+
echo "$output" | grep -q "state=OPEN.*CLOSED"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# ── Behavioural: cache file is created/updated after a pass ─────────────────
|
|
305
|
+
|
|
306
|
+
@test "cache file is written with the latest state after the pass" {
|
|
307
|
+
cat > "$PROBLEMS_DIR/105-quux.open.md" <<'EOF'
|
|
308
|
+
# Problem 105: Quux
|
|
309
|
+
|
|
310
|
+
**Status**: Open
|
|
311
|
+
|
|
312
|
+
## Reported Upstream
|
|
313
|
+
|
|
314
|
+
- **URL**: https://github.com/example/repo/issues/55
|
|
315
|
+
- **Reported**: 2026-05-01
|
|
316
|
+
- **Template used**: structured default
|
|
317
|
+
- **Disclosure path**: public issue
|
|
318
|
+
- **Cross-reference confirmed**: yes
|
|
319
|
+
EOF
|
|
320
|
+
|
|
321
|
+
make_fake_gh
|
|
322
|
+
prime_gh_response "https://github.com/example/repo/issues/55" '{"state":"OPEN","comments":[{"id":1},{"id":2}],"labels":[{"name":"triage"},{"name":"bug"}],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
323
|
+
|
|
324
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
325
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
326
|
+
[ "$status" -eq 0 ]
|
|
327
|
+
[ -f "$CACHE_FILE" ]
|
|
328
|
+
# Cache file contains P105 entry with the current state.
|
|
329
|
+
grep -q '"P105"' "$CACHE_FILE"
|
|
330
|
+
grep -q '"upstream_url": *"https://github.com/example/repo/issues/55"' "$CACHE_FILE"
|
|
331
|
+
grep -q '"last_seen_state": *"OPEN"' "$CACHE_FILE"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# ── Behavioural: audit-log file is appended after the pass ──────────────────
|
|
335
|
+
|
|
336
|
+
@test "audit-log file is appended with a timestamped pass heading" {
|
|
337
|
+
cat > "$PROBLEMS_DIR/106-corge.open.md" <<'EOF'
|
|
338
|
+
# Problem 106: Corge
|
|
339
|
+
|
|
340
|
+
**Status**: Open
|
|
341
|
+
|
|
342
|
+
## Reported Upstream
|
|
343
|
+
|
|
344
|
+
- **URL**: https://github.com/example/repo/issues/66
|
|
345
|
+
- **Reported**: 2026-05-01
|
|
346
|
+
- **Template used**: structured default
|
|
347
|
+
- **Disclosure path**: public issue
|
|
348
|
+
- **Cross-reference confirmed**: yes
|
|
349
|
+
EOF
|
|
350
|
+
|
|
351
|
+
make_fake_gh
|
|
352
|
+
prime_gh_response "https://github.com/example/repo/issues/66" '{"state":"OPEN","comments":[],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
353
|
+
|
|
354
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
355
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
356
|
+
[ "$status" -eq 0 ]
|
|
357
|
+
[ -f "$AUDIT_LOG" ]
|
|
358
|
+
# Audit-log contains a heading line matching the ISO timestamp pattern + pass summary.
|
|
359
|
+
grep -qE '^## [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' "$AUDIT_LOG"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# ── Behavioural: --ticket filter restricts to one ticket ────────────────────
|
|
363
|
+
|
|
364
|
+
@test "--ticket filter restricts polling to the named ticket only" {
|
|
365
|
+
cat > "$PROBLEMS_DIR/107-grault.open.md" <<'EOF'
|
|
366
|
+
# Problem 107: Grault
|
|
367
|
+
|
|
368
|
+
## Reported Upstream
|
|
369
|
+
|
|
370
|
+
- **URL**: https://github.com/example/repo/issues/107
|
|
371
|
+
- **Reported**: 2026-05-01
|
|
372
|
+
- **Template used**: structured default
|
|
373
|
+
- **Disclosure path**: public issue
|
|
374
|
+
- **Cross-reference confirmed**: yes
|
|
375
|
+
EOF
|
|
376
|
+
|
|
377
|
+
cat > "$PROBLEMS_DIR/108-garply.open.md" <<'EOF'
|
|
378
|
+
# Problem 108: Garply
|
|
379
|
+
|
|
380
|
+
## Reported Upstream
|
|
381
|
+
|
|
382
|
+
- **URL**: https://github.com/example/repo/issues/108
|
|
383
|
+
- **Reported**: 2026-05-01
|
|
384
|
+
- **Template used**: structured default
|
|
385
|
+
- **Disclosure path**: public issue
|
|
386
|
+
- **Cross-reference confirmed**: yes
|
|
387
|
+
EOF
|
|
388
|
+
|
|
389
|
+
make_fake_gh
|
|
390
|
+
prime_gh_response "https://github.com/example/repo/issues/107" '{"state":"OPEN","comments":[],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
391
|
+
prime_gh_response "https://github.com/example/repo/issues/108" '{"state":"OPEN","comments":[],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
392
|
+
|
|
393
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
394
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh" --ticket P107
|
|
395
|
+
[ "$status" -eq 0 ]
|
|
396
|
+
echo "$output" | grep -q "P107"
|
|
397
|
+
! echo "$output" | grep -q "P108"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# ── Behavioural: dual-tolerant subdir layout (RFC-002) ──────────────────────
|
|
401
|
+
|
|
402
|
+
@test "ticket in per-state subdir is discovered and polled" {
|
|
403
|
+
mkdir -p "$PROBLEMS_DIR/open"
|
|
404
|
+
cat > "$PROBLEMS_DIR/open/109-waldo.md" <<'EOF'
|
|
405
|
+
# Problem 109: Waldo
|
|
406
|
+
|
|
407
|
+
## Reported Upstream
|
|
408
|
+
|
|
409
|
+
- **URL**: https://github.com/example/repo/issues/109
|
|
410
|
+
- **Reported**: 2026-05-01
|
|
411
|
+
- **Template used**: structured default
|
|
412
|
+
- **Disclosure path**: public issue
|
|
413
|
+
- **Cross-reference confirmed**: yes
|
|
414
|
+
EOF
|
|
415
|
+
|
|
416
|
+
make_fake_gh
|
|
417
|
+
prime_gh_response "https://github.com/example/repo/issues/109" '{"state":"OPEN","comments":[],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
418
|
+
|
|
419
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
420
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
421
|
+
[ "$status" -eq 0 ]
|
|
422
|
+
echo "$output" | grep -q "P109"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# ── Behavioural: gh failure for one URL doesn't kill the pass ───────────────
|
|
426
|
+
|
|
427
|
+
@test "gh failure on one URL is surfaced as FAIL; pass continues for other tickets" {
|
|
428
|
+
cat > "$PROBLEMS_DIR/110-broken.open.md" <<'EOF'
|
|
429
|
+
# Problem 110: Broken
|
|
430
|
+
|
|
431
|
+
## Reported Upstream
|
|
432
|
+
|
|
433
|
+
- **URL**: https://github.com/example/repo/issues/110
|
|
434
|
+
- **Reported**: 2026-05-01
|
|
435
|
+
- **Template used**: structured default
|
|
436
|
+
- **Disclosure path**: public issue
|
|
437
|
+
- **Cross-reference confirmed**: yes
|
|
438
|
+
EOF
|
|
439
|
+
|
|
440
|
+
cat > "$PROBLEMS_DIR/111-working.open.md" <<'EOF'
|
|
441
|
+
# Problem 111: Working
|
|
442
|
+
|
|
443
|
+
## Reported Upstream
|
|
444
|
+
|
|
445
|
+
- **URL**: https://github.com/example/repo/issues/111
|
|
446
|
+
- **Reported**: 2026-05-01
|
|
447
|
+
- **Template used**: structured default
|
|
448
|
+
- **Disclosure path**: public issue
|
|
449
|
+
- **Cross-reference confirmed**: yes
|
|
450
|
+
EOF
|
|
451
|
+
|
|
452
|
+
make_fake_gh
|
|
453
|
+
# Only prime 111; 110 will fail.
|
|
454
|
+
prime_gh_response "https://github.com/example/repo/issues/111" '{"state":"OPEN","comments":[],"labels":[],"updatedAt":"2026-05-17T08:00:00Z"}'
|
|
455
|
+
|
|
456
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
457
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh"
|
|
458
|
+
# Exit 2 = partial (some polls failed).
|
|
459
|
+
[ "$status" -eq 2 ]
|
|
460
|
+
echo "$output" | grep -qE "^FAIL *P110"
|
|
461
|
+
echo "$output" | grep -q "P111"
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
# ── Behavioural: --force-recheck ignores cache and re-emits NEW ─────────────
|
|
465
|
+
|
|
466
|
+
@test "--force-recheck re-emits ticket as new even when cache matches" {
|
|
467
|
+
cat > "$PROBLEMS_DIR/112-fresh.open.md" <<'EOF'
|
|
468
|
+
# Problem 112: Fresh
|
|
469
|
+
|
|
470
|
+
## Reported Upstream
|
|
471
|
+
|
|
472
|
+
- **URL**: https://github.com/example/repo/issues/112
|
|
473
|
+
- **Reported**: 2026-05-01
|
|
474
|
+
- **Template used**: structured default
|
|
475
|
+
- **Disclosure path**: public issue
|
|
476
|
+
- **Cross-reference confirmed**: yes
|
|
477
|
+
EOF
|
|
478
|
+
|
|
479
|
+
cat > "$CACHE_FILE" <<'EOF'
|
|
480
|
+
{
|
|
481
|
+
"last_checked": "2026-05-17T00:00:00Z",
|
|
482
|
+
"tickets": {
|
|
483
|
+
"P112": {
|
|
484
|
+
"upstream_url": "https://github.com/example/repo/issues/112",
|
|
485
|
+
"last_checked_at": "2026-05-17T00:00:00Z",
|
|
486
|
+
"last_seen_state": "OPEN",
|
|
487
|
+
"last_seen_comment_count": 0,
|
|
488
|
+
"last_seen_labels": [],
|
|
489
|
+
"last_seen_updated_at": "2026-05-17T00:00:00Z"
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
EOF
|
|
494
|
+
|
|
495
|
+
make_fake_gh
|
|
496
|
+
prime_gh_response "https://github.com/example/repo/issues/112" '{"state":"OPEN","comments":[],"labels":[],"updatedAt":"2026-05-17T00:00:00Z"}'
|
|
497
|
+
|
|
498
|
+
export GHFAKE_DATA="$GHFAKE_DIR"
|
|
499
|
+
run "$SCRIPT" --problems-dir "$PROBLEMS_DIR" --cache-file "$CACHE_FILE" --audit-log "$AUDIT_LOG" --gh-bin "$GHFAKE_DIR/gh" --force-recheck
|
|
500
|
+
[ "$status" -eq 0 ]
|
|
501
|
+
# With --force-recheck, the line is emitted as NEW regardless of cache match.
|
|
502
|
+
echo "$output" | grep -qE "^NEW *P112"
|
|
503
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wr-itil:check-upstream-responses
|
|
3
|
+
description: Poll upstream issues we've filed via `/wr-itil:report-upstream` and surface new comments, state changes, or label changes since last check. Reads `## Reported Upstream` back-link sections in local problem tickets, queries `gh issue view` (read-only), diffs against `docs/problems/.outbound-responses-cache.json`, and appends an audit-log entry to `docs/audits/outbound-responses-log.md`. Outbound symmetric counterpart to ADR-062's inbound discovery pipeline (P249 Phase 1).
|
|
4
|
+
allowed-tools: Read, Edit, Write, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Check Upstream Responses — Outbound Response-Check Skill
|
|
8
|
+
|
|
9
|
+
Poll the upstream issues we have filed via `/wr-itil:report-upstream` and surface new responses (comments, state changes, label changes) since the last check. This skill closes the outbound half of the feedback loop that ADR-062's inbound assessment pipeline opens — together they form the bidirectional cross-repo coordination surface JTBD-004 names.
|
|
10
|
+
|
|
11
|
+
This is **Phase 1** of P249. Phase 1 covers the **us-as-upstream-reporter** half (we polling our own filed-upstream reports). Phase 2 — the external-reporter-as-our-reporter half (plugin users polling responses to reports they filed against this repo) — is deferred to a separate iter.
|
|
12
|
+
|
|
13
|
+
## Scope
|
|
14
|
+
|
|
15
|
+
In scope:
|
|
16
|
+
- Scan `docs/problems/**/*.md` for `## Reported Upstream` back-link sections (the contract section written by `/wr-itil:report-upstream` Step 7 — see [ADR-024](../../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) Step 7).
|
|
17
|
+
- For each ticket with a back-link, extract the `- **URL**:` line and poll the upstream issue via `gh issue view <url> --json comments,state,labels,updatedAt`.
|
|
18
|
+
- Diff against `docs/problems/.outbound-responses-cache.json` (cache file mirroring the inbound `.upstream-cache.json` shape per ADR-031 § "Cache files live under docs/problems/").
|
|
19
|
+
- Surface five response classes: NEW (new comments), STATE (state change), LABEL (label change), NONE (no change), FAIL (gh poll error).
|
|
20
|
+
- Update the cache file with the latest seen state.
|
|
21
|
+
- Append a timestamped pass entry to `docs/audits/outbound-responses-log.md` (audit-log mirroring `docs/audits/inbound-discovery-log.md` per ADR-062's audit-log surface contract).
|
|
22
|
+
|
|
23
|
+
Out of scope:
|
|
24
|
+
- Posting comments back to the upstream issue. The skill is **read-only externally** — does not trip ADR-028's external-comms gate.
|
|
25
|
+
- Auto-transitioning local ticket lifecycle based on upstream state change (that is P080's bidirectional update axis, separate).
|
|
26
|
+
- Polling against the inbound-discovery channels (`docs/problems/.upstream-channels.json`). That is the inverse axis, owned by `/wr-itil:review-problems` Step 4.5 per ADR-062.
|
|
27
|
+
- Phase 2 external-reporter-as-our-reporter surface (deferred).
|
|
28
|
+
|
|
29
|
+
## Invocation
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/wr-itil:check-upstream-responses
|
|
33
|
+
[--problems-dir <dir>] default: docs/problems
|
|
34
|
+
[--cache-file <path>] default: <problems-dir>/.outbound-responses-cache.json
|
|
35
|
+
[--audit-log <path>] default: docs/audits/outbound-responses-log.md
|
|
36
|
+
[--ticket P<NNN>] restrict polling to one ticket
|
|
37
|
+
[--force-recheck] ignore cache; treat all as new
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Future iter will wire `/wr-itil:work-problems` Step 0c pre-flight to invoke this skill when the outbound cache is stale (sibling to Step 0b inbound cache check per ADR-062 Confirmation #5). Phase 1 ships manual-invocation only.
|
|
41
|
+
|
|
42
|
+
## AFK behaviour
|
|
43
|
+
|
|
44
|
+
This skill is **AFK-safe by construction**:
|
|
45
|
+
|
|
46
|
+
- Read-only `gh issue view` calls — does NOT fire ADR-028 external-comms gate.
|
|
47
|
+
- No `AskUserQuestion` calls — five flag-based knobs (`--problems-dir`, `--cache-file`, `--audit-log`, `--ticket`, `--force-recheck`) are the user-direction surface per CLAUDE.md `act on obvious, AskUserQuestion for ambiguous, NEVER prose-ask` (P085).
|
|
48
|
+
- Partial-failure exit code (2) lets AFK orchestrators distinguish "some upstream URLs were unreachable" from "everything broke" without halting the loop.
|
|
49
|
+
|
|
50
|
+
## Steps
|
|
51
|
+
|
|
52
|
+
### 1. Run the diagnose+act script
|
|
53
|
+
|
|
54
|
+
Invoke the helper:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
wr-itil-check-upstream-responses
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The `wr-itil-check-upstream-responses` command is a `$PATH`-resolved shim shipped in `packages/itil/bin/` that dispatches the canonical `packages/itil/scripts/check-upstream-responses.sh` body. Per [ADR-049](../../../../docs/decisions/049-plugin-script-resolution-via-bin-on-path.proposed.md) — never invoke the canonical script via repo-relative path; the path does not resolve in adopter trees.
|
|
61
|
+
|
|
62
|
+
The script:
|
|
63
|
+
|
|
64
|
+
1. Walks `<problems-dir>` (both flat layout `<NNN>-*.<state>.md` AND per-state subdir layout `<state>/<NNN>-*.md` per RFC-002 dual-tolerant migration).
|
|
65
|
+
2. For each ticket file, extracts the `## Reported Upstream` URL line. Tickets without that section are silently skipped.
|
|
66
|
+
3. For each URL, calls `gh issue view <url> --json comments,state,labels,updatedAt`.
|
|
67
|
+
4. Compares the response against the cached entry for that ticket and emits one of: NEW / STATE / LABEL / NONE / FAIL.
|
|
68
|
+
5. Updates the cache file and appends an audit-log entry.
|
|
69
|
+
|
|
70
|
+
Exit codes:
|
|
71
|
+
|
|
72
|
+
- `0` — success. Cache and audit-log have been updated. Per-ticket lines printed to stdout.
|
|
73
|
+
- `1` — error (problems-dir missing, malformed cache, malformed CLI args, jq missing).
|
|
74
|
+
- `2` — partial. Some upstream polls failed; the successful ones are still written to cache + audit-log.
|
|
75
|
+
|
|
76
|
+
### 2. Summarise the response classes inline
|
|
77
|
+
|
|
78
|
+
Read the stdout output and summarise the response classes in chat for the user. The audit-log is the durable surface — the agent's inline summary is the in-session affordance. Do NOT re-dump the full stdout; lead with the most-important classes:
|
|
79
|
+
|
|
80
|
+
- STATE changes (upstream state OPEN → CLOSED / REOPENED) — most actionable; usually a verification signal.
|
|
81
|
+
- NEW comments (delta count > 0) — second-most actionable; may carry triage labels, follow-up questions, or fix confirmation.
|
|
82
|
+
- LABEL changes — informational; signals maintainer triage activity.
|
|
83
|
+
- NONE — quiet; only mention the count, not each ticket.
|
|
84
|
+
- FAIL — call out per-ticket reasons so the user can investigate (URL changed, repo renamed, auth issue).
|
|
85
|
+
|
|
86
|
+
### 3. Commit per ADR-014
|
|
87
|
+
|
|
88
|
+
The cache file and audit-log file ride a single commit:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git add docs/problems/.outbound-responses-cache.json docs/audits/outbound-responses-log.md
|
|
92
|
+
git commit -m "chore(problems): check upstream responses — <N> polled, <M> new"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See [ADR-014](../../../../docs/decisions/014-governance-skills-commit-their-own-work.proposed.md) commit-message-convention table for the canonical row.
|
|
96
|
+
|
|
97
|
+
If the cumulative pipeline risk lands above appetite and `AskUserQuestion` is unavailable, apply the [ADR-013](../../../../docs/decisions/013-structured-user-interaction-for-governance-decisions.proposed.md) Rule 6 non-interactive fail-safe: skip the commit and report the uncommitted state.
|
|
98
|
+
|
|
99
|
+
## When invoked
|
|
100
|
+
|
|
101
|
+
Three invocation surfaces:
|
|
102
|
+
|
|
103
|
+
1. **Direct user invocation** — `/wr-itil:check-upstream-responses` (or with flags). The default user-facing surface.
|
|
104
|
+
2. **AFK orchestrator pre-flight** (future iter) — `/wr-itil:work-problems` Step 0c will invoke this skill when the outbound cache is stale, mirroring Step 0b's inbound staleness check per ADR-062 Confirmation #5. This wiring is deferred to a future iter; Phase 1 ships manual only.
|
|
105
|
+
3. **Manual investigation during a problem-management session** — when a maintainer wants to see if any upstream reports moved before transitioning a `verifying` ticket back to `closed`. Foreground synchronous; the maintainer reads the inline summary and decides next steps.
|
|
106
|
+
|
|
107
|
+
## Confirmation
|
|
108
|
+
|
|
109
|
+
This skill's contract holds when:
|
|
110
|
+
|
|
111
|
+
1. The script `packages/itil/scripts/check-upstream-responses.sh` is read-only externally — only `gh issue view` (no `gh issue comment`, no `gh issue create`, no `gh api`).
|
|
112
|
+
2. The script extracts the URL from `## Reported Upstream` sections matching the format `- **URL**: <url>` per ADR-024 Step 7's back-link contract.
|
|
113
|
+
3. After a successful pass, the cache file exists, is valid JSON, and contains a `tickets.<P<NNN>>` entry for every polled ticket.
|
|
114
|
+
4. After a successful pass, the audit-log file exists and has a new `## YYYY-MM-DDTHH:MM:SSZ` heading appended.
|
|
115
|
+
5. The skill is AFK-safe: zero `AskUserQuestion` calls, zero external-comms gate triggers.
|
|
116
|
+
6. The exit code distinguishes success (0), error (1), and partial failure (2) so AFK orchestrators can branch correctly.
|
|
117
|
+
|
|
118
|
+
## ADR alignment
|
|
119
|
+
|
|
120
|
+
- **ADR-014** — governance skills commit their own work. Cache file + audit-log ride a single commit per pass. ADR-014's commit-message-convention table is amended in the same commit as this skill ships to add the canonical row.
|
|
121
|
+
- **ADR-024** — cross-project problem-reporting contract. The `## Reported Upstream` back-link section (Step 7) is the source of truth this skill reads. ADR-024's Confirmation section is amended in the same commit to record that the back-link section's URL field is now a load-bearing contract surface for two skills (one writes, one reads).
|
|
122
|
+
- **ADR-031** — problem-ticket directory layout. Cache file lives under `docs/problems/` per the same precedent that placed `.upstream-cache.json` and `.upstream-channels.json` there for the inbound axis.
|
|
123
|
+
- **ADR-032** — governance skill invocation patterns. Foreground synchronous; no subagent dispatch needed.
|
|
124
|
+
- **ADR-037** — skill testing strategy. Behavioural bats at `packages/itil/scripts/test/check-upstream-responses.bats` (script-level) covers the contract.
|
|
125
|
+
- **ADR-038** — progressive disclosure. Per-row stdout output ≤ 150 bytes; the agent expands per-ticket detail on demand.
|
|
126
|
+
- **ADR-049** — bin shim. Script invoked as `wr-itil-check-upstream-responses`, not via repo-relative `bash <path>`.
|
|
127
|
+
- **ADR-062** — inbound upstream-report discovery + assessment pipeline. This skill is the outbound symmetric counterpart; ADR-062 `## Related` is amended in the same commit to forward-point at this skill.
|
|
128
|
+
|
|
129
|
+
## Related
|
|
130
|
+
|
|
131
|
+
- `packages/itil/scripts/check-upstream-responses.sh` — the diagnose+act script body.
|
|
132
|
+
- `packages/itil/scripts/test/check-upstream-responses.bats` — behavioural bats covering the script contract.
|
|
133
|
+
- `packages/itil/skills/report-upstream/SKILL.md` — the writer of the `## Reported Upstream` section this skill reads.
|
|
134
|
+
- `packages/itil/skills/review-problems/SKILL.md` — the inbound axis sibling (Step 4.5 inbound-discovery pass per ADR-062).
|
|
135
|
+
- `docs/audits/inbound-discovery-log.md` — inbound audit-log; symmetric peer of `docs/audits/outbound-responses-log.md`.
|
|
136
|
+
- `docs/problems/.upstream-cache.json` — inbound cache; symmetric peer of `docs/problems/.outbound-responses-cache.json`.
|
|
137
|
+
- `docs/decisions/024-cross-project-problem-reporting-contract.proposed.md` — outbound contract; back-link section is the source of truth.
|
|
138
|
+
- `docs/decisions/062-inbound-upstream-report-discovery-assessment-pipeline.proposed.md` — inbound discovery; this skill is the outbound counterpart.
|
|
139
|
+
- **P249** (`docs/problems/open/249-no-process-for-reporters-to-check-for-responses-symmetric-to-inbound-discovery.md`) — driver ticket. Phase 1 (us-as-upstream-reporter) ships here; Phase 2 (external-reporter-as-our-reporter) deferred.
|
|
140
|
+
- **P080** (`docs/problems/open/080-no-bidirectional-update-of-upstream-reported-problems.md`) — inverse axis (we push local status BACK to upstream issues we ingested). Composes with this skill.
|
|
141
|
+
- **P229** — inbound discovery ack-comment shape gap (verdict-shaped acks). Inverse-shape sibling on the inbound axis.
|
|
142
|
+
- **JTBD-004** — Connect Agents Across Repos to Collaborate (primary anchor).
|
|
143
|
+
- **JTBD-006** — Progress the Backlog While I'm Away (AFK-safe).
|
|
144
|
+
- **JTBD-001** — Enforce Governance Without Slowing Down (eliminates manual upstream polling).
|
|
145
|
+
- **JTBD-201** — Restore Service Fast with an Audit Trail (audit-log replay).
|
|
146
|
+
- **JTBD-202** — Run Pre-Flight Governance Checks Before Release or Handover (state-change signals before retro / release).
|