@windyroad/itil 0.33.0 → 0.34.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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-itil",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code"
5
5
  }
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) |
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/../scripts/check-upstream-responses.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -0,0 +1,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).