@windyroad/itil 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,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
|
@@ -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
|
|
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:
|
|
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:
|
|
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
|
+
}
|