@windyroad/itil 0.29.0 → 0.30.0-preview.313

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.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code"
5
5
  }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # Inbound-discovery cache staleness check — Step 0b of /wr-itil:work-problems.
3
+ # ADR-062 § JTBD-006 driver: work-problems should pre-flight
4
+ # /wr-itil:review-problems when the upstream inbound-discovery cache is stale
5
+ # or missing, so AFK loops keep upstream-reported problems visible without
6
+ # the maintainer remembering to invoke review-problems first.
7
+ #
8
+ # The staleness comparison MUST stay symmetric with review-problems Step 4.5b's
9
+ # branches (first-run / TTL-expiry / cache-fresh). Drift here re-opens the
10
+ # inbound-discovery staleness contract — any change to TTL semantics MUST
11
+ # update both this helper and packages/itil/skills/review-problems/SKILL.md
12
+ # Step 4.5b in the same commit.
13
+ # <!-- INBOUND-CACHE-STALENESS-CONTRACT-SOURCE: packages/itil/skills/review-problems/SKILL.md Step 4.5b -->
14
+ #
15
+ # Source this file, then call `should_promote_inbound_discovery_preflight`:
16
+ # . packages/itil/lib/check-upstream-cache-staleness.sh
17
+ # reason="$(should_promote_inbound_discovery_preflight "$PWD")"
18
+ #
19
+ # Output (one of):
20
+ # no-channels-config → channels-config absent; skip silently.
21
+ # Downstream-adopter non-obligation per
22
+ # ADR-062 § Downstream-adopter contract.
23
+ # first-run-cache-absent → channels-config present, cache file absent.
24
+ # Dispatch review-problems.
25
+ # first-run-last-checked-null → cache present but last_checked is null.
26
+ # Dispatch review-problems.
27
+ # fresh-within-ttl → cache within TTL; silent-pass.
28
+ # ttl-expiry age=<N>s ttl=<M>s → cache older than TTL; dispatch.
29
+ #
30
+ # Dependencies: bash 4+, jq, python3 (for ISO-8601 parsing — portable across
31
+ # Linux/BSD date implementations).
32
+
33
+ should_promote_inbound_discovery_preflight() {
34
+ local repo_root="${1:-$PWD}"
35
+ local channels_file="$repo_root/docs/problems/.upstream-channels.json"
36
+ local cache_file="$repo_root/docs/problems/.upstream-cache.json"
37
+
38
+ if [ ! -f "$channels_file" ]; then
39
+ echo "no-channels-config"
40
+ return 0
41
+ fi
42
+
43
+ local ttl_seconds
44
+ ttl_seconds="$(jq -r '.ttl_seconds // 86400' "$channels_file")"
45
+
46
+ if [ ! -f "$cache_file" ]; then
47
+ echo "first-run-cache-absent"
48
+ return 0
49
+ fi
50
+
51
+ local last_checked
52
+ last_checked="$(jq -r '.last_checked // ""' "$cache_file")"
53
+
54
+ if [ -z "$last_checked" ] || [ "$last_checked" = "null" ]; then
55
+ echo "first-run-last-checked-null"
56
+ return 0
57
+ fi
58
+
59
+ local last_checked_epoch now_epoch cache_age
60
+ last_checked_epoch="$(python3 -c "import datetime,sys; ts=sys.argv[1].replace('Z','+00:00'); print(int(datetime.datetime.fromisoformat(ts).timestamp()))" "$last_checked" 2>/dev/null || echo "0")"
61
+ now_epoch="$(date +%s)"
62
+ cache_age=$((now_epoch - last_checked_epoch))
63
+
64
+ if [ "$cache_age" -gt "$ttl_seconds" ]; then
65
+ echo "ttl-expiry age=${cache_age}s ttl=${ttl_seconds}s"
66
+ return 0
67
+ fi
68
+
69
+ echo "fresh-within-ttl"
70
+ return 0
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.29.0",
3
+ "version": "0.30.0-preview.313",
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"
@@ -145,7 +145,46 @@ On a flat-layout adopter repo (first invocation post-update — JTBD-101 plugin-
145
145
 
146
146
  **First-fire signal**: the routine emits a single stderr line on the migrating invocation; silent on no-op re-invocations.
147
147
 
148
- After Step 0a completes (whether no-op or migration), proceed to Step 1 backlog scan. The dual-tolerant glob at Step 1 (RFC-002 transitional window) continues to match both layouts; post-T6 (single-pattern collapse), Step 1 will tighten to per-state only and the migration commit ensures the adopter tree matches.
148
+ After Step 0a completes (whether no-op or migration), proceed to Step 0b's inbound-discovery pre-flight check. The dual-tolerant glob at Step 1 (RFC-002 transitional window) continues to match both layouts; post-T6 (single-pattern collapse), Step 1 will tighten to per-state only and the migration commit ensures the adopter tree matches.
149
+
150
+ ### Step 0b: Upstream inbound-discovery pre-flight (per ADR-062 § JTBD-006 driver)
151
+
152
+ After Step 0a's auto-migrate and before Step 1's backlog scan, check whether the upstream inbound-discovery cache is fresh. ADR-062 § Decision Drivers names `/wr-itil:work-problems` as the surface that should keep inbound reports visible during AFK loops; the TTL self-healing branch inside `/wr-itil:review-problems` Step 4.5b only fires if review-problems is entered. This step closes that gap by pre-flighting `/wr-itil:review-problems` when the cache is stale or missing.
153
+
154
+ **Mechanism:**
155
+
156
+ ```bash
157
+ source packages/itil/lib/check-upstream-cache-staleness.sh
158
+ preflight_reason="$(should_promote_inbound_discovery_preflight "$PWD")"
159
+ ```
160
+
161
+ The helper returns one of five outcomes (contract documented at `packages/itil/lib/check-upstream-cache-staleness.sh` + asserted by `packages/itil/skills/work-problems/test/work-problems-step-0b-cache-staleness-behavioural.bats`):
162
+
163
+ | `preflight_reason` | Action |
164
+ |-----------------------------------|--------------------------------------------------------------------------------------------------------|
165
+ | `no-channels-config` | Silent-pass. Downstream-adopter non-obligation per ADR-062 § Downstream-adopter contract. Proceed to Step 1. |
166
+ | `first-run-cache-absent` | Dispatch `/wr-itil:review-problems` as a pre-flight iter via the standard `claude -p` subprocess wrapper (same shape as Step 5; see Step 5 for the subprocess invocation contract). |
167
+ | `first-run-last-checked-null` | Same as `first-run-cache-absent` — cache schema present but never populated. |
168
+ | `ttl-expiry age=<N>s ttl=<M>s` | Dispatch `/wr-itil:review-problems` as a pre-flight iter. Cache is stale; review-problems' Step 4.5b's TTL-expiry auto-recheck branch fires inside the dispatched subprocess and refreshes the cache + audit-log + README. |
169
+ | `fresh-within-ttl` | Silent-pass per ADR-013 Rule 5 + P132 mechanical-stage carve-out. Proceed to Step 1. |
170
+
171
+ **Pre-flight dispatch shape**: when promoted, dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:review-problems` (per P084 + ADR-032 subprocess isolation). The subprocess runs the full Step 4.5 inbound-discovery + assessment pipeline; the cache + `docs/audits/inbound-discovery-log.md` + `docs/problems/README.md` are refreshed in its own commit per ADR-014 (review-problems' Slice E commit grain). After the subprocess completes, the orchestrator reads the freshly-refreshed README at Step 1.
172
+
173
+ **Iter-summary annotation**:
174
+
175
+ - Channels-config absent: `Step 0b skipped — no upstream-channels.json (downstream-adopter non-obligation)`.
176
+ - Cache fresh: `Step 0b skipped — upstream inbound-discovery cache fresh within TTL`.
177
+ - Pre-flight ran: `Step 0b pre-flighted /wr-itil:review-problems — reason=<preflight_reason>, <N> reports discovered, <M> local tickets created`.
178
+
179
+ The annotation pre-empts the "surprise heavy iter" perception JTBD-006 expects auditability for — a maintainer running multiple short AFK loops within a 24h window will hit `fresh-within-ttl` on subsequent invocations and see the cache-fresh annotation, confirming the system's silent-pass discipline rather than wondering whether the check ran at all.
180
+
181
+ **AFK authorisation per ADR-013 Rule 6**: review-problems' Step 4.5 pipeline is itself AFK-safe — branch decisions are mechanical per P132 / ADR-044 category 4 silent framework action; external-comms gates on verdict/acknowledgement/pushback comments silent-pass on low-risk verdicts per ADR-028 + the `wr-risk-scorer:external-comms` subagent's *"policy-authorised drafts proceed silently"* contract (`packages/risk-scorer/agents/external-comms.md` § PASS Output); gate-denial sub-branches fail-soft and retry on the next discovery pass. No new user-attention surface introduced at the Step 0b promotion point.
182
+
183
+ **Compose-with**: ADR-014 (review-problems' Slice E commit grain holds — the pre-flight subprocess emits its own commit; orchestrator-main-turn does not commit Step 0b), ADR-013 Rule 5/6 (silent-pass + AFK fail-safe — both honored), P084 + P077 (subprocess isolation reuse — same `claude -p` wrapper as Step 5), ADR-019 (preflight surface — Step 0b is the natural extension of "reconcile state before opening the loop"), P132 (mechanical-stage carve-out — no `AskUserQuestion` at the promotion point). Mid-loop ticket creation by Step 4.5e's safe-and-valid branch enters the WSJF queue Step 1 reads on the same invocation — natural absorption, no deadlock; the pre-flight commit lands before Step 1's README read.
184
+
185
+ **Staleness contract drift**: the staleness comparison MUST stay symmetric with `/wr-itil:review-problems` Step 4.5b's branches (first-run / TTL-expiry / cache-fresh). Drift here re-opens the inbound-discovery staleness contract — any change to TTL semantics MUST update both this Step 0b helper and review-problems Step 4.5b in the same commit. <!-- INBOUND-CACHE-STALENESS-CONTRACT-SOURCE: packages/itil/skills/review-problems/SKILL.md Step 4.5b -->
186
+
187
+ After Step 0b completes (whether dispatched or silent-passed), proceed to Step 1.
149
188
 
150
189
  ### Step 1: Scan the backlog
151
190
 
@@ -157,7 +196,7 @@ Exclude:
157
196
  - `.closed.md` files (done)
158
197
  - `.parked.md` files (blocked on upstream)
159
198
  - `.verifying.md` files (Verification Pending — fix released, awaiting user verification per ADR-022; surfaced in the Verification Queue section, never in dev-work ranking)
160
- - Problems with no WSJF score (need a review first — run `/wr-itil:manage-problem review` as the first iteration if scores are missing)
199
+ - Problems with no WSJF score (need a review first — run `/wr-itil:review-problems` as the first iteration if scores are missing)
161
200
 
162
201
  ### Step 2: Check stop conditions
163
202
 
@@ -668,7 +707,7 @@ The orchestrator MUST NOT call `AskUserQuestion` between iterations except at th
668
707
 
669
708
  ## Edge Cases
670
709
 
671
- **Review needed first**: If no problems have WSJF scores, run `/wr-itil:manage-problem review` as the first iteration to score everything, then proceed to the work loop.
710
+ **Review needed first**: If no problems have WSJF scores, run `/wr-itil:review-problems` as the first iteration to score everything, then proceed to the work loop. (Independent of Step 0b's inbound-discovery pre-flight, which fires on cache staleness regardless of WSJF-score state.)
672
711
 
673
712
  **Scope creep during investigation**: If investigating an open problem reveals the scope is larger than expected (effort re-sized from S to L, or L to XL), save findings to the problem file, update the WSJF score, and move to the next problem. Don't sink unlimited effort into one problem during AFK mode — the user can decide when they return.
674
713
 
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Step 0b behavioural fixture per ADR-062 § JTBD-006 driver:
4
+ # work-problems pre-flights /wr-itil:review-problems when the
5
+ # upstream inbound-discovery cache is stale or missing. The
6
+ # staleness decision lives in
7
+ # `packages/itil/lib/check-upstream-cache-staleness.sh::should_promote_inbound_discovery_preflight`
8
+ # so the SKILL.md Step 0b prose is a thin source-and-call wrapper
9
+ # around a behaviorally-testable shell function (P081 / user
10
+ # feedback: prefer behavioural over structural-grep tests).
11
+ #
12
+ # Cases covered:
13
+ # 1. No channels-config file → "no-channels-config" (downstream-adopter non-obligation, ADR-062 § Downstream-adopter contract).
14
+ # 2. Channels-config present, cache file absent → "first-run-cache-absent".
15
+ # 3. Channels-config present, cache present, last_checked null → "first-run-last-checked-null".
16
+ # 4. Channels-config present, cache fresh within TTL → "fresh-within-ttl".
17
+ # 5. Channels-config present, cache older than TTL → "ttl-expiry" (with age + ttl in the reason).
18
+ # 6. Custom ttl_seconds in channels-config is respected (not hardcoded default).
19
+
20
+ setup() {
21
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../../.." && pwd)"
22
+ HELPER="$REPO_ROOT/packages/itil/lib/check-upstream-cache-staleness.sh"
23
+
24
+ FIXTURE="$(mktemp -d)"
25
+ mkdir -p "$FIXTURE/docs/problems"
26
+ }
27
+
28
+ teardown() {
29
+ rm -rf "$FIXTURE"
30
+ }
31
+
32
+ @test "helper exists at the contracted path" {
33
+ [ -f "$HELPER" ]
34
+ }
35
+
36
+ @test "case 1: no channels-config → no-channels-config" {
37
+ # shellcheck disable=SC1090
38
+ source "$HELPER"
39
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
40
+ [ "$status" -eq 0 ]
41
+ [ "$output" = "no-channels-config" ]
42
+ }
43
+
44
+ @test "case 2: channels-config present, cache absent → first-run-cache-absent" {
45
+ cat > "$FIXTURE/docs/problems/.upstream-channels.json" <<'EOF'
46
+ { "channels": [], "ttl_seconds": 86400 }
47
+ EOF
48
+ # shellcheck disable=SC1090
49
+ source "$HELPER"
50
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
51
+ [ "$status" -eq 0 ]
52
+ [ "$output" = "first-run-cache-absent" ]
53
+ }
54
+
55
+ @test "case 3: cache present, last_checked null → first-run-last-checked-null" {
56
+ cat > "$FIXTURE/docs/problems/.upstream-channels.json" <<'EOF'
57
+ { "channels": [], "ttl_seconds": 86400 }
58
+ EOF
59
+ cat > "$FIXTURE/docs/problems/.upstream-cache.json" <<'EOF'
60
+ { "last_checked": null, "channels": [] }
61
+ EOF
62
+ # shellcheck disable=SC1090
63
+ source "$HELPER"
64
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
65
+ [ "$status" -eq 0 ]
66
+ [ "$output" = "first-run-last-checked-null" ]
67
+ }
68
+
69
+ @test "case 4: cache fresh within TTL → fresh-within-ttl (silent-pass)" {
70
+ cat > "$FIXTURE/docs/problems/.upstream-channels.json" <<'EOF'
71
+ { "channels": [], "ttl_seconds": 86400 }
72
+ EOF
73
+ # last_checked 1 hour ago — well within 24h TTL.
74
+ local recent_iso
75
+ recent_iso="$(python3 -c "import datetime; print((datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M:%SZ'))")"
76
+ cat > "$FIXTURE/docs/problems/.upstream-cache.json" <<EOF
77
+ { "last_checked": "$recent_iso", "channels": [] }
78
+ EOF
79
+ # shellcheck disable=SC1090
80
+ source "$HELPER"
81
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
82
+ [ "$status" -eq 0 ]
83
+ [ "$output" = "fresh-within-ttl" ]
84
+ }
85
+
86
+ @test "case 5: cache older than TTL → ttl-expiry with age + ttl in the reason" {
87
+ cat > "$FIXTURE/docs/problems/.upstream-channels.json" <<'EOF'
88
+ { "channels": [], "ttl_seconds": 86400 }
89
+ EOF
90
+ # last_checked 2 days ago — past 24h TTL.
91
+ local stale_iso
92
+ stale_iso="$(python3 -c "import datetime; print((datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=2)).strftime('%Y-%m-%dT%H:%M:%SZ'))")"
93
+ cat > "$FIXTURE/docs/problems/.upstream-cache.json" <<EOF
94
+ { "last_checked": "$stale_iso", "channels": [] }
95
+ EOF
96
+ # shellcheck disable=SC1090
97
+ source "$HELPER"
98
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
99
+ [ "$status" -eq 0 ]
100
+ # Format: "ttl-expiry age=<N>s ttl=<M>s"
101
+ [[ "$output" =~ ^ttl-expiry\ age=[0-9]+s\ ttl=86400s$ ]]
102
+ }
103
+
104
+ @test "case 6: custom ttl_seconds is honored (not hardcoded default)" {
105
+ # 1-hour TTL; last_checked 90 minutes ago → stale under the custom TTL,
106
+ # but would be FRESH under the 86400s default. Confirms the helper reads
107
+ # ttl_seconds from channels-config rather than hardcoding 86400.
108
+ cat > "$FIXTURE/docs/problems/.upstream-channels.json" <<'EOF'
109
+ { "channels": [], "ttl_seconds": 3600 }
110
+ EOF
111
+ local mid_iso
112
+ mid_iso="$(python3 -c "import datetime; print((datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=90)).strftime('%Y-%m-%dT%H:%M:%SZ'))")"
113
+ cat > "$FIXTURE/docs/problems/.upstream-cache.json" <<EOF
114
+ { "last_checked": "$mid_iso", "channels": [] }
115
+ EOF
116
+ # shellcheck disable=SC1090
117
+ source "$HELPER"
118
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
119
+ [ "$status" -eq 0 ]
120
+ [[ "$output" =~ ^ttl-expiry\ age=[0-9]+s\ ttl=3600s$ ]]
121
+ }
122
+
123
+ @test "case 7: missing ttl_seconds field defaults to 86400 (24h)" {
124
+ cat > "$FIXTURE/docs/problems/.upstream-channels.json" <<'EOF'
125
+ { "channels": [] }
126
+ EOF
127
+ # last_checked 1 hour ago — fresh under the default 86400s TTL.
128
+ local recent_iso
129
+ recent_iso="$(python3 -c "import datetime; print((datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M:%SZ'))")"
130
+ cat > "$FIXTURE/docs/problems/.upstream-cache.json" <<EOF
131
+ { "last_checked": "$recent_iso", "channels": [] }
132
+ EOF
133
+ # shellcheck disable=SC1090
134
+ source "$HELPER"
135
+ run should_promote_inbound_discovery_preflight "$FIXTURE"
136
+ [ "$status" -eq 0 ]
137
+ [ "$output" = "fresh-within-ttl" ]
138
+ }