@windyroad/itil 0.16.0-preview.159 → 0.17.0-preview.161

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.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.16.0-preview.159",
3
+ "version": "0.17.0-preview.161",
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"
@@ -92,6 +92,46 @@ WSJF = (8 × 1.0) / 8 = **1.0** — defer until severity climbs or scope shrinks
92
92
 
93
93
  When estimating effort, read the problem's root cause analysis and fix strategy. If effort is unknown, default to M (2). Effort is a **live estimate**, not a set-once label: re-rate it when root cause is confirmed, when architect review narrows or expands scope, and during each `manage-problem review`. A note capturing the reason for any bucket change makes the ranking audit-able (see steps 7 and 9b).
94
94
 
95
+ ### Transitive dependencies (P076)
96
+
97
+ > **Serves**: JTBD-001 (enforce governance without slowing down — queue must not lie), JTBD-006 (progress the backlog while I'm away — AFK orchestrator iterates top-down on a trustworthy rank), JTBD-201 (restore service fast with an audit trail — ranking decisions must be defensible post-hoc).
98
+
99
+ Effort is scored per-ticket as a **marginal** estimate (the work this ticket adds on top of its upstream dependencies). When a ticket has upstream dependencies — other tickets that must close first before this one can reach "done" — the ticket's effective effort for WSJF purposes is the **transitive closure** of its marginal effort plus all blocking upstreams, not the marginal alone.
100
+
101
+ **Rule**:
102
+
103
+ ```
104
+ Effort(T)_transitive = max(
105
+ Effort(T)_marginal,
106
+ max{ Effort(U)_transitive | U ∈ Blocked_by(T) }
107
+ )
108
+
109
+ WSJF(T) = (Severity(T) × StatusMultiplier(T)) / Effort(T)_transitive
110
+ ```
111
+
112
+ A dependent ticket cannot reach its "done" state without the upstream work happening first. Scoring the dependent at its marginal-only effort lies about what it costs to deliver — the **queue** would rank it higher than its blocker even though the blocker's work is strictly contained within it.
113
+
114
+ **Dependency signal**: drive the closure from the ticket's `## Dependencies` section (see the Step 5 template). Only `**Blocked by**` entries propagate effort; `**Composes with**` does NOT propagate — compositional overlap shares surface but neither side strictly blocks.
115
+
116
+ **Upstream status carve-out**: an upstream ticket in `.closed.md`, `.verifying.md`, or `.parked.md` contributes **0** to the transitive closure. Closed upstream work is done; verifying upstream work is user-side (not dev effort and excluded from dev ranking per the WSJF multiplier table); parked upstream work is suspended (excluded from ranking until un-parked). Without this carve-out, a ticket blocked by a closed ticket would inherit XL forever.
117
+
118
+ **Cycle handling**: when two or more tickets mutually block each other (e.g., shared gate-surface tickets that each list the other under `**Blocked by**`), treat the strongly-connected component as a **bundle**. The bundle's effective effort is `max{ marginal | members }`. All bundle members surface the same WSJF in review output — the shared WSJF is a **computed artefact** of the rendering, not written as a field into individual ticket files. Bundle members retain their individual Status suffixes and individual ticket files (ADR-022 suffix-based lifecycle).
119
+
120
+ **Re-rate on upstream status change**: when a dependency transitions to `.closed.md` / `.verifying.md` / `.parked.md`, the dependent ticket's transitive closure shrinks and the effort drops accordingly. Step 9b catches this automatically — no transition-time graph re-walk is required.
121
+
122
+ **Worked example**: P073 has marginal effort S (one surface-row add). P073 is blocked by P038 (XL). Then:
123
+
124
+ ```
125
+ Effort(P073)_transitive = max(S=1, Effort(P038)_transitive) = max(1, 8) = 8
126
+ WSJF(P073) = (Severity(P073) × 1.0) / 8 = 12 / 8 = 1.5
127
+ ```
128
+
129
+ P073's WSJF matches P038's by construction — P073 cannot out-rank the ticket whose work is strictly contained within it. Contrast with the marginal-only (incorrect) computation: `12 / 2 = 6.0`, which would mis-rank P073 as "top of queue" despite being blocked.
130
+
131
+ **Determinism**: the rule is deterministic from the graph — no `AskUserQuestion` branch is required when Step 9b re-rates a ticket. The re-rate fires silently and is logged in the review output per the Step 9b re-rate message format.
132
+
133
+ **Reassessment criteria**: this rule lives inline in manage-problem's SKILL.md (following ADR-022's precedent for inline WSJF additions). If a second skill (e.g., manage-incident or a future cross-plugin `work-backlog` orchestrator) adopts the `## Dependencies` section and the transitive-effort rule, extract to a sibling ADR at that point — wider adoption justifies the ADR cost that today's single-skill scope does not.
134
+
95
135
  ## Working a Problem
96
136
 
97
137
  What "work" means depends on the problem's status:
@@ -286,11 +326,29 @@ Before writing the problem file, perform a concern-boundary analysis on the gath
286
326
  - [ ] Create reproduction test
287
327
  - [ ] Create INVEST story for permanent fix
288
328
 
329
+ ## Dependencies
330
+
331
+ - **Blocks**: <tickets that can't close until this one does — bare IDs, comma-separated; leave empty if none>
332
+ - **Blocked by**: <tickets that must close first — bare IDs, comma-separated; drives the transitive-effort rule; leave empty if none>
333
+ - **Composes with**: <tickets whose work overlaps but neither blocks the other — does NOT propagate effort; leave empty if none>
334
+
289
335
  ## Related
290
336
 
291
337
  <links to related files, problems, ADRs>
292
338
  ```
293
339
 
340
+ The `## Dependencies` section uses **bare ticket IDs** (`P038`, not `[P038](./038-...)` link syntax) — review output renders to links on demand. An empty row is valid and explicit: `- **Blocked by**: (none)` reads better than omitting the row. The transitive-effort rule in the WSJF Prioritisation section consumes this section at review time.
341
+
342
+ **Concrete example** (for P073 referencing two upstreams):
343
+
344
+ ```markdown
345
+ ## Dependencies
346
+
347
+ - **Blocks**: (none)
348
+ - **Blocked by**: P038, P064
349
+ - **Composes with**: (none)
350
+ ```
351
+
294
352
  ### 6. For updates: Edit the existing file
295
353
 
296
354
  Find the file matching the problem ID:
@@ -469,6 +527,27 @@ Parked problems and Verification Pending problems are excluded from WSJF ranking
469
527
  - Update the Status field to "Known Error"
470
528
  - This happens automatically — do not ask the user
471
529
 
530
+ **Step 9b.1: Dependency-graph traversal — propagate transitive effort (P076)**
531
+
532
+ After every `.open.md` / `.known-error.md` ticket has a marginal effort, run a **second pass** that walks the dependency graph and propagates effort up per the transitive-dependency rule (see the WSJF Prioritisation section's "Transitive dependencies" subsection). This is a deterministic re-rate — no `AskUserQuestion` required.
533
+
534
+ 1. **Build the graph**: for each `.open.md` / `.known-error.md` ticket, parse the `## Dependencies` section. Record `Blocked by` edges (bare IDs) into an adjacency map. Ignore `Composes with` (does not propagate) and `Blocks` (derivable from inverse).
535
+ 2. **Classify upstream status**: for each upstream ID referenced in any `Blocked by` edge, resolve the file suffix. Upstreams in `.closed.md`, `.verifying.md`, or `.parked.md` contribute **0** to the closure per the carve-out. Upstreams in `.open.md` or `.known-error.md` contribute their own transitive effort.
536
+ 3. **Topologically sort** the open/known-error subgraph so upstream tickets are scored before their dependents. If a cycle is detected (two or more tickets mutually `Blocked by` each other), treat the strongly-connected component as a **bundle** with effort = `max{ marginal | members }`.
537
+ 4. **Compute transitive effort** for each ticket in topological order using `Effort_transitive = max(marginal, max{ upstream transitive })`. Cycle-bundle members all receive the bundle's effort.
538
+ 5. **Update Effort and WSJF lines**: if a ticket's transitive effort differs from its marginal, edit the Effort line to the transitive bucket (S → M / L / XL as needed), recompute WSJF, and update the Priority and WSJF lines. Write a short audit trail in a `<!-- transitive: <bucket> via <UPSTREAM> -->` HTML comment on the Effort line so the next review can distinguish a manually-set marginal from a propagated transitive.
539
+ 6. **Report each re-rate** in the review summary using the concrete format:
540
+
541
+ ```
542
+ P<NNN>: Effort <OLD> → <NEW> (transitive via <UPSTREAM>)
543
+ ```
544
+
545
+ Example: `P073: Effort S → XL (transitive via P038)`. The shape is fixed so downstream audit tools can grep it deterministically.
546
+
547
+ 7. **Cycle-bundle output**: for cycle bundles, surface a shared WSJF line covering all members, e.g. `Bundle [P038, P064]: effort XL (cycle), WSJF 3.0 (shared)`. The shared WSJF is a computed artefact of the review rendering — do NOT write a shared-bundle field into the individual ticket files.
548
+
549
+ The re-rate pass is part of Step 9b's output — a re-rate row appears in the step 9c ranked table with the transitive effort (not the marginal). Hide the marginal from the main table but preserve it in the ticket's HTML-comment audit trail so a future review knows where the propagation came from.
550
+
472
551
  **Step 9c: Present summary and select problem to work**
473
552
 
474
553
  After reviewing all problems, present a WSJF-ranked table for open/known-error problems (the main dev-work queue):
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env bats
2
+ # Contract + behavioural tests: manage-problem SKILL.md must define a
3
+ # transitive-dependency rule for WSJF effort, extend Step 9b with
4
+ # dependency-graph traversal, and the problem-ticket template must carry
5
+ # a `## Dependencies` section.
6
+ #
7
+ # Mixed shape — structural assertions (ADR-037 Permitted Exception) for
8
+ # the contract prose Claude interprets at invocation time, AND one
9
+ # behavioural fixture test that exercises the transitive-closure
10
+ # algorithm directly so the rule is not merely "keyword present" (P081
11
+ # pressure — behavioural where feasible).
12
+ #
13
+ # Cross-reference:
14
+ # @problem P076 — docs/problems/076-wsjf-does-not-model-transitive-dependencies.open.md
15
+ # ADR-022: verification-pending status carve-out from the closure
16
+ # ADR-014: governance-skills commit their own work
17
+ # ADR-037: contract-assertion bats pattern for SKILL.md prose contracts
18
+ # @jtbd JTBD-001 (solo-developer — enforce governance without slowing down)
19
+ # @jtbd JTBD-006 (solo-developer — progress the backlog while I'm away)
20
+ # @jtbd JTBD-201 (tech-lead — restore service fast with an audit trail)
21
+
22
+ setup() {
23
+ SKILL_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
24
+ SKILL_FILE="${SKILL_DIR}/SKILL.md"
25
+ FIXTURE_DIR="$(mktemp -d)"
26
+ }
27
+
28
+ teardown() {
29
+ [ -n "${FIXTURE_DIR:-}" ] && [ -d "$FIXTURE_DIR" ] && rm -rf "$FIXTURE_DIR"
30
+ }
31
+
32
+ # ──────────────────────────────────────────────────────────────────────────────
33
+ # Contract: WSJF section defines the transitive-dependency rule
34
+ # ──────────────────────────────────────────────────────────────────────────────
35
+
36
+ @test "SKILL.md WSJF section has a Transitive dependencies subsection (P076)" {
37
+ # The rule must live in a discoverable subsection so Claude finds it
38
+ # when interpreting the WSJF scoring contract.
39
+ run grep -nE "^### .*[Tt]ransitive [Dd]ependenc" "$SKILL_FILE"
40
+ [ "$status" -eq 0 ]
41
+ }
42
+
43
+ @test "SKILL.md transitive rule defines effort as max(marginal, blocked-by closure) (P076)" {
44
+ # The core rule: Effort(T)_transitive = max(marginal, max{blocked-by})
45
+ # The prose must make the max-of relationship explicit so a dependent
46
+ # ticket inherits upstream effort.
47
+ run grep -inE "max.*marginal|marginal.*max|transitive.*effort|effort.*transitive" "$SKILL_FILE"
48
+ [ "$status" -eq 0 ]
49
+ }
50
+
51
+ @test "SKILL.md transitive rule cites Blocked by as the dependency signal (P076)" {
52
+ # Blocked_by is the formally-scoped edge in the ticket-dependency graph.
53
+ # The rule must name it (not just generic "dependencies") so Claude knows
54
+ # which `## Dependencies` row drives the effort propagation.
55
+ run grep -inE "Blocked[[:space:]-]?by" "$SKILL_FILE"
56
+ [ "$status" -eq 0 ]
57
+ }
58
+
59
+ @test "SKILL.md transitive rule carves Composes with OUT of the closure (P076)" {
60
+ # Compositional overlap does NOT strictly block — sibling work that
61
+ # shares surface should not inflate the dependent's effort.
62
+ run grep -inE "[Cc]omposes[[:space:]]with.*not|not.*[Cc]omposes|[Cc]omposes[[:space:]]with.*does NOT|[Cc]omposes[[:space:]]with.*excluded" "$SKILL_FILE"
63
+ [ "$status" -eq 0 ]
64
+ }
65
+
66
+ @test "SKILL.md transitive rule carves .closed / .verifying / .parked OUT of the closure (architect note)" {
67
+ # Architect correction: already-done or user-blocked upstreams contribute
68
+ # zero marginal dev effort — otherwise a ticket blocked by a closed
69
+ # ticket would inherit XL forever.
70
+ run grep -inE "(\.closed\.md|\.verifying\.md|\.parked\.md).*(contribut|0|zero|exclud)|contribut.*0.*(\.closed|\.verifying|\.parked)" "$SKILL_FILE"
71
+ [ "$status" -eq 0 ]
72
+ }
73
+
74
+ @test "SKILL.md transitive rule documents cycle bundling (shared WSJF as computed artefact) (P076)" {
75
+ # Cycles (e.g. P038/P064 mutual composition) must be handled — bundle's
76
+ # effort = max of members' marginals; shared WSJF surfaced in review
77
+ # output, not written as a field per-ticket (ADR-022 suffix-based pattern).
78
+ run grep -inE "cycle|bundle" "$SKILL_FILE"
79
+ [ "$status" -eq 0 ]
80
+ }
81
+
82
+ @test "SKILL.md transitive rule carries a worked example (P076)" {
83
+ # Worked example: P073 marginal S blocked by P038 XL → transitive XL →
84
+ # WSJF drops to match P038. The number-grounded example keeps the rule
85
+ # out of hand-wavy territory (ADR-026 grounded output).
86
+ run grep -inE "[Ww]orked[[:space:]]example|example.*transitive|transitive.*example" "$SKILL_FILE"
87
+ [ "$status" -eq 0 ]
88
+ }
89
+
90
+ @test "SKILL.md transitive rule notes reassessment-criteria for future ADR extraction (architect note)" {
91
+ # Architect guidance: keep a pointer for when another skill adopts the
92
+ # `## Dependencies` convention — at that point, extract to a sibling ADR.
93
+ # Inline note now avoids speculative ADR authoring today.
94
+ run grep -inE "[Rr]eassessment|[Ee]xtract.*ADR|sibling ADR|future ADR" "$SKILL_FILE"
95
+ [ "$status" -eq 0 ]
96
+ }
97
+
98
+ # ──────────────────────────────────────────────────────────────────────────────
99
+ # Contract: Step 9b review extends to dependency-graph traversal
100
+ # ──────────────────────────────────────────────────────────────────────────────
101
+
102
+ @test "SKILL.md Step 9b describes a dependency-graph traversal pass (P076)" {
103
+ # After per-ticket marginal scoring, Step 9b must walk the graph and
104
+ # propagate effort up. The prose must name the traversal explicitly so
105
+ # Claude performs the second pass, not just the first.
106
+ run grep -inE "dependency[[:space:]-]?graph|graph traversal|topological.*sort|propagate.*effort|effort.*propagat" "$SKILL_FILE"
107
+ [ "$status" -eq 0 ]
108
+ }
109
+
110
+ @test "SKILL.md Step 9b reports transitive re-rates in the review summary (P076 + JTBD-006)" {
111
+ # The AFK orchestrator (JTBD-006) and the audit-trail persona (JTBD-201)
112
+ # depend on Step 9b emitting a visible re-rate line — not a silent
113
+ # update.
114
+ run grep -inE "transitive.*(re-?rate|rerat|re-?estimat)|report.*transitive|re-?rate.*transitive" "$SKILL_FILE"
115
+ [ "$status" -eq 0 ]
116
+ }
117
+
118
+ @test "SKILL.md Step 9b re-rate message format is concrete (architect note — bats-assertable shape)" {
119
+ # Architect correction 4: specify a concrete message shape so the
120
+ # contract can be grepped. Format: P<NNN>: Effort <OLD> → <NEW>
121
+ # (transitive via <UPSTREAM>).
122
+ run grep -inE "Effort.*→.*transitive via|transitive via.*P[0-9]" "$SKILL_FILE"
123
+ [ "$status" -eq 0 ]
124
+ }
125
+
126
+ # ──────────────────────────────────────────────────────────────────────────────
127
+ # Contract: Step 5 problem-ticket template includes `## Dependencies`
128
+ # ──────────────────────────────────────────────────────────────────────────────
129
+
130
+ @test "SKILL.md Step 5 template includes a ## Dependencies section (P076)" {
131
+ # New tickets must carry an explicit dependency list so the graph is
132
+ # legible. Empty lists are allowed (default: no deps).
133
+ run grep -nE "^## Dependencies" "$SKILL_FILE"
134
+ [ "$status" -eq 0 ]
135
+ }
136
+
137
+ @test "SKILL.md Dependencies template lists Blocks / Blocked by / Composes with rows (P076)" {
138
+ # The three row labels must all be present in the template so the
139
+ # author knows which semantics apply. Blocks = downstream; Blocked by =
140
+ # drives effort propagation; Composes with = overlap without blocking.
141
+ run grep -inE "\*\*Blocks\*\*|\*\*Blocked by\*\*|\*\*Composes with\*\*" "$SKILL_FILE"
142
+ [ "$status" -eq 0 ]
143
+ # All three labels must be present.
144
+ blocks_hits=$(grep -cE "\*\*Blocks\*\*" "$SKILL_FILE" || true)
145
+ blocked_hits=$(grep -cE "\*\*Blocked by\*\*" "$SKILL_FILE" || true)
146
+ composes_hits=$(grep -cE "\*\*Composes with\*\*" "$SKILL_FILE" || true)
147
+ [ "$blocks_hits" -ge 1 ]
148
+ [ "$blocked_hits" -ge 1 ]
149
+ [ "$composes_hits" -ge 1 ]
150
+ }
151
+
152
+ @test "SKILL.md Dependencies rows use bare ticket IDs (architect note Q1)" {
153
+ # Bare IDs (P038) beat link syntax (`[P038](./038-...)`) — less
154
+ # maintenance; review output renders to links on demand. The template
155
+ # example must show bare-ID form (not `[P038](./038-...)`).
156
+ # Canonical shape: `- **Blocked by**: P<NNN>, P<NNN>` — match either
157
+ # bold-markdown label form or plain-label form, asserting the ID is
158
+ # bare (not bracketed).
159
+ run grep -inE "Blocked by\*{0,2}:[[:space:]]*P[0-9]" "$SKILL_FILE"
160
+ [ "$status" -eq 0 ]
161
+ # Assert the example does NOT use link syntax `[PNNN](./...)`.
162
+ ! grep -E "Blocked by\*{0,2}:[[:space:]]*\[P[0-9]" "$SKILL_FILE"
163
+ }
164
+
165
+ # ──────────────────────────────────────────────────────────────────────────────
166
+ # Traceability: P076 cited
167
+ # ──────────────────────────────────────────────────────────────────────────────
168
+
169
+ @test "SKILL.md cites P076 in the transitive-dependencies prose (traceability)" {
170
+ # Every governance-contract change must cite the problem ticket that
171
+ # motivated it — audit trail per ADR-014.
172
+ run grep -n "P076" "$SKILL_FILE"
173
+ [ "$status" -eq 0 ]
174
+ }
175
+
176
+ # ──────────────────────────────────────────────────────────────────────────────
177
+ # Behavioural: exercise the transitive-closure rule with fixture tickets
178
+ # ──────────────────────────────────────────────────────────────────────────────
179
+ # These tests build a minimal fixture backlog and run a bash transcription
180
+ # of the transitive-closure rule against it. The rule's executable form is
181
+ # small enough to bash-implement; we assert the output matches the rule's
182
+ # spec so a prose drift (e.g. accidentally documenting `min` instead of
183
+ # `max`) would be caught at test time.
184
+
185
+ # Bash transcription of the transitive-closure rule — effort map is
186
+ # keyed by ticket ID, value is the integer divisor from the SKILL.md
187
+ # effort table (S=1, M=2, L=4, XL=8).
188
+ transitive_effort() {
189
+ local ticket="$1"
190
+ local backlog_dir="$2"
191
+ local file
192
+ file=$(ls "$backlog_dir"/${ticket}-*.md 2>/dev/null | head -1)
193
+ [ -z "$file" ] && { echo 0; return; }
194
+
195
+ # Extract marginal effort from the Effort line. The line shape matches
196
+ # the SKILL.md Priority/Effort convention: "**Effort**: <bucket> — ..."
197
+ local marginal_bucket
198
+ marginal_bucket=$(grep -oE '\*\*Effort\*\*: [SMLXL]+' "$file" | head -1 | grep -oE '[SMLXL]+' | head -1)
199
+ local marginal_divisor
200
+ case "$marginal_bucket" in
201
+ S) marginal_divisor=1 ;;
202
+ M) marginal_divisor=2 ;;
203
+ L) marginal_divisor=4 ;;
204
+ XL) marginal_divisor=8 ;;
205
+ *) marginal_divisor=2 ;; # default M
206
+ esac
207
+
208
+ # .closed.md / .verifying.md / .parked.md upstreams contribute 0 (architect carve-out)
209
+ case "$file" in
210
+ *.closed.md|*.verifying.md|*.parked.md)
211
+ echo 0
212
+ return
213
+ ;;
214
+ esac
215
+
216
+ # Extract `Blocked by:` dependency IDs (bare, comma-separated).
217
+ # The template shape is `- **Blocked by**: P<NNN>, P<NNN>` (markdown
218
+ # list item under `## Dependencies`) — match the label with optional
219
+ # leading list marker.
220
+ local blocked_by_line
221
+ blocked_by_line=$(grep -E "^[[:space:]]*-?[[:space:]]*\*\*Blocked by\*\*:" "$file" | head -1 | sed 's/.*\*\*Blocked by\*\*://')
222
+ local max_upstream=0
223
+ local dep
224
+ for dep in $(echo "$blocked_by_line" | grep -oE 'P[0-9]+'); do
225
+ local upstream_effort
226
+ upstream_effort=$(transitive_effort "$dep" "$backlog_dir")
227
+ [ "$upstream_effort" -gt "$max_upstream" ] && max_upstream="$upstream_effort"
228
+ done
229
+
230
+ # Transitive = max(marginal, upstream closure)
231
+ if [ "$max_upstream" -gt "$marginal_divisor" ]; then
232
+ echo "$max_upstream"
233
+ else
234
+ echo "$marginal_divisor"
235
+ fi
236
+ }
237
+
238
+ @test "behavioural: dependent ticket inherits upstream XL when blocked by XL ticket (P076 worked example)" {
239
+ # Fixture: P200 (marginal S) Blocked by: P201 (XL)
240
+ # Expected: transitive effort for P200 = XL divisor = 8
241
+ cat > "$FIXTURE_DIR/P201-upstream-xl.open.md" <<'EOF'
242
+ # Problem 201: upstream XL ticket
243
+ **Status**: Open
244
+ **Effort**: XL — multi-day cross-package work
245
+
246
+ ## Dependencies
247
+ - **Blocked by**: (none)
248
+ EOF
249
+ cat > "$FIXTURE_DIR/P200-dependent-small.open.md" <<'EOF'
250
+ # Problem 200: dependent with small marginal effort
251
+ **Status**: Open
252
+ **Effort**: S — one-line surface add
253
+
254
+ ## Dependencies
255
+ - **Blocked by**: P201
256
+ EOF
257
+ result=$(transitive_effort "P200" "$FIXTURE_DIR")
258
+ [ "$result" = "8" ]
259
+ }
260
+
261
+ @test "behavioural: self-contained ticket keeps marginal effort (P076 no-op path)" {
262
+ # Fixture: P210 (marginal M) with empty Blocked by.
263
+ # Expected: transitive = marginal M divisor = 2.
264
+ cat > "$FIXTURE_DIR/P210-solo-m.open.md" <<'EOF'
265
+ # Problem 210: self-contained
266
+ **Status**: Open
267
+ **Effort**: M — couple of files
268
+
269
+ ## Dependencies
270
+ - **Blocked by**: (none)
271
+ EOF
272
+ result=$(transitive_effort "P210" "$FIXTURE_DIR")
273
+ [ "$result" = "2" ]
274
+ }
275
+
276
+ @test "behavioural: closed upstream contributes 0 to the closure (architect carve-out)" {
277
+ # Fixture: P220 (marginal S) Blocked by: P221 (was XL, now closed).
278
+ # Expected: transitive = marginal S = 1 (closed upstream contributes 0;
279
+ # otherwise P220 would inherit XL forever).
280
+ cat > "$FIXTURE_DIR/P221-closed-xl.closed.md" <<'EOF'
281
+ # Problem 221: closed upstream (was XL)
282
+ **Status**: Closed
283
+ **Effort**: XL
284
+ EOF
285
+ cat > "$FIXTURE_DIR/P220-dependent-on-closed.open.md" <<'EOF'
286
+ # Problem 220: dependent on closed
287
+ **Status**: Open
288
+ **Effort**: S
289
+
290
+ ## Dependencies
291
+ - **Blocked by**: P221
292
+ EOF
293
+ result=$(transitive_effort "P220" "$FIXTURE_DIR")
294
+ [ "$result" = "1" ]
295
+ }
296
+
297
+ @test "behavioural: marginal effort wins when upstream is smaller (P076 max-of semantics)" {
298
+ # Fixture: P230 (marginal L = 4) Blocked by: P231 (S = 1)
299
+ # Expected: transitive = max(L, S) = L divisor = 4.
300
+ # (Contrast: a buggy implementation using min or sum would produce 1 or 5.)
301
+ cat > "$FIXTURE_DIR/P231-small-upstream.open.md" <<'EOF'
302
+ # Problem 231: small upstream
303
+ **Status**: Open
304
+ **Effort**: S
305
+ EOF
306
+ cat > "$FIXTURE_DIR/P230-big-dependent.open.md" <<'EOF'
307
+ # Problem 230: bigger dependent
308
+ **Status**: Open
309
+ **Effort**: L
310
+
311
+ ## Dependencies
312
+ - **Blocked by**: P231
313
+ EOF
314
+ result=$(transitive_effort "P230" "$FIXTURE_DIR")
315
+ [ "$result" = "4" ]
316
+ }
317
+
318
+ @test "behavioural: transitive propagates across two hops (P076 closure semantics)" {
319
+ # Fixture: P240 (S) → blocked by P241 (S) → blocked by P242 (XL)
320
+ # Expected: transitive(P240) = max(S, transitive(P241)) = max(S, max(S, XL)) = XL = 8.
321
+ cat > "$FIXTURE_DIR/P242-deepest-xl.open.md" <<'EOF'
322
+ # Problem 242: deepest XL
323
+ **Status**: Open
324
+ **Effort**: XL
325
+ EOF
326
+ cat > "$FIXTURE_DIR/P241-middle-s.open.md" <<'EOF'
327
+ # Problem 241: middle S
328
+ **Status**: Open
329
+ **Effort**: S
330
+
331
+ ## Dependencies
332
+ - **Blocked by**: P242
333
+ EOF
334
+ cat > "$FIXTURE_DIR/P240-top-s.open.md" <<'EOF'
335
+ # Problem 240: top S
336
+ **Status**: Open
337
+ **Effort**: S
338
+
339
+ ## Dependencies
340
+ - **Blocked by**: P241
341
+ EOF
342
+ result=$(transitive_effort "P240" "$FIXTURE_DIR")
343
+ [ "$result" = "8" ]
344
+ }
345
+
346
+ @test "behavioural: verification-pending upstream contributes 0 (architect carve-out for .verifying.md)" {
347
+ # Fixture: P250 (marginal S) Blocked by: P251 (was XL, now verifying).
348
+ # Expected: transitive = marginal S = 1 (verifying contributes 0 — the
349
+ # remaining work is user-side verification, not dev effort).
350
+ cat > "$FIXTURE_DIR/P251-verifying-xl.verifying.md" <<'EOF'
351
+ # Problem 251: verification pending (was XL)
352
+ **Status**: Verification Pending
353
+ **Effort**: XL
354
+ EOF
355
+ cat > "$FIXTURE_DIR/P250-dependent-on-verifying.open.md" <<'EOF'
356
+ # Problem 250: dependent on verifying
357
+ **Status**: Open
358
+ **Effort**: S
359
+
360
+ ## Dependencies
361
+ - **Blocked by**: P251
362
+ EOF
363
+ result=$(transitive_effort "P250" "$FIXTURE_DIR")
364
+ [ "$result" = "1" ]
365
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wr-itil:report-upstream
3
- description: Report a local problem ticket as a structured issue against an upstream repository, with bidirectional cross-references and SECURITY.md-aware routing for security-classified tickets. Implements the contract in ADR-024.
3
+ description: Report a local problem ticket as a structured issue against an upstream repository, with bidirectional cross-references and SECURITY.md-aware routing for security-classified tickets. Implements the contract in ADR-024, with ADR-033 governing problem-first classifier + default body shape.
4
4
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
5
5
  ---
6
6
 
@@ -10,6 +10,8 @@ File a local `docs/problems/<NNN>` ticket as an issue (or private security advis
10
10
 
11
11
  This skill implements the contract documented in [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) (Cross-project problem-reporting contract). All step numbering below maps 1:1 to ADR-024 Decision Outcome.
12
12
 
13
+ [ADR-033](../../../docs/decisions/033-report-upstream-classifier-problem-first.proposed.md) (Report-upstream classifier is problem-first) partially supersedes ADR-024 Decision Outcome **Steps 3 and 5 only** — the classifier is problem-first with best-fit backward-compat fallback (per Step 3 below), and the structured default body is problem-shaped (per Step 5 below). ADR-024 Steps 1, 2, 4, 6, 7, 8 and all Consequences / Confirmation clauses remain in force unchanged.
14
+
13
15
  ## Invocation
14
16
 
15
17
  ```
@@ -77,18 +79,28 @@ For each `.yml` template found, fetch the file via `gh api repos/<owner>/<repo>/
77
79
 
78
80
  ### 3. Classify the local ticket and pick the best-matching template
79
81
 
80
- Heuristic:
81
- - Local ticket title contains `bug`, `defect`, `crash`, `error`, `fails to`, `broken`, `regression` → classify as `bug`.
82
- - Local ticket title contains `feature`, `add`, `support for`, `would be nice`, `enhancement` → classify as `feature`.
83
- - Local ticket title is a question → classify as `question`.
84
- - The CLI `--classification` argument overrides the heuristic.
82
+ This step is governed by [ADR-033](../../../docs/decisions/033-report-upstream-classifier-problem-first.proposed.md) (Report-upstream classifier is problem-first), which partially supersedes ADR-024 Decision Outcome Step 3. Classification is **problem-first with best-fit backward-compat fallback** — upstream repos that have adopted the problem-first intake shape (per `@windyroad/itil`) are targeted first; older repos that still ship bug/feature/question templates are served via a fallback.
83
+
84
+ **Preference order** (first match wins):
85
+
86
+ 1. **`problem` shape (primary)** — any of the tokens `problem`, `issue`, `concern`, `defect`, `gap` appear in the local ticket title or body; or the body contains a scoped-npm package reference (`@scope/name`); or the body contains any of `root cause`, `reproduction`, `workaround`. This is the default for tickets authored via `/wr-itil:manage-problem`.
87
+ 2. **`bug` shape (backward-compat fallback)** — no primary tokens match, and the prose is defect-like (contains `broken`, `fails`, `error`, `bug`, `regression`, or a specific observed-vs-expected contrast). Produces a bug-shaped body only when the upstream has no `problem-report.yml`.
88
+ 3. **`feature` shape (backward-compat fallback)** — no primary tokens match, and the prose is proposal-like (contains `would be nice`, `enhancement`, `feature request`, `could we`, `wish`).
89
+ 4. **`question` shape (backward-compat fallback)** — trailing fallback when the prose is a genuine question (ends in `?`, contains `how do I`, `is there a way`).
90
+
91
+ The CLI `--classification` argument overrides the heuristic. The security-path check in Step 4 fires **before** this classifier — security-classified tickets bypass the classifier entirely.
92
+
93
+ **Template-discovery preference order** (extends ADR-024 Step 1; search the upstream `.github/ISSUE_TEMPLATE/` directory in this order, first match wins):
85
94
 
86
- Pick the upstream template whose `name:` (or filename) most closely matches the classification:
87
- - For `bug`: prefer `bug-report.yml`, `bug.yml`, `bug-report.md`, `bug.md`.
88
- - For `feature`: prefer `feature-request.yml`, `feature.yml`, `feature-request.md`.
89
- - For `question`: prefer `question.yml`, `question.md`. If absent, the upstream's `config.yml` likely routes questions elsewhere (Discussions); halt and surface the routing target.
95
+ 1. `problem-report.yml` preferred; the Windy-Road problem-first shape.
96
+ 2. `problem.yml` — alternate naming for problem-shaped templates.
97
+ 3. `problem-report.md` / `problem.md` — legacy markdown variants of the problem-shaped template.
98
+ 4. `bug-report.yml` / `bug.yml` / `bug-report.md` / `bug.md` if primary classifier picked `bug` shape OR no problem template exists and fallback is `bug`.
99
+ 5. `feature-request.yml` / `feature.yml` / `feature-request.md` — `feature` shape fallback.
100
+ 6. `question.yml` / `question.md` — `question` shape fallback. If absent, the upstream's `config.yml` likely routes questions elsewhere (Discussions); halt and surface the routing target.
101
+ 7. Structured default body per Step 5 below — if no template matches.
90
102
 
91
- Log the matched template name in the Step 7 back-write. If no template matches the classification, fall through to the structured default in Step 5.
103
+ Log the matched template name (or `structured default`) in the Step 7 back-write. If no template matches the classification, fall through to the structured default in Step 5.
92
104
 
93
105
  ### 4. Security-path routing check
94
106
 
@@ -102,19 +114,72 @@ If security-classified, route to Step 6. Otherwise, route to Step 5 (public-issu
102
114
 
103
115
  ### 5. Public-issue path
104
116
 
105
- If the upstream had a matching template (Step 3), fill its required fields from the local ticket:
117
+ This step is governed by [ADR-033](../../../docs/decisions/033-report-upstream-classifier-problem-first.proposed.md) (Report-upstream classifier is problem-first), which partially supersedes ADR-024 Decision Outcome Step 5. The primary structured default body is **problem-shaped** and mirrors the `/wr-itil:manage-problem` ticket shape; the bug-shaped / feature-shaped / question-shaped bodies are retained as fallback-only templates for the backward-compat branches of the Step 3 classifier.
118
+
119
+ If the upstream had a matching template (Step 3), fill its required fields from the local ticket. Field-mapping table for the problem-first case (problem-report.yml template):
106
120
 
107
121
  | Upstream template field (typical) | Local ticket source |
108
122
  |---|---|
109
- | `plugin` / `package` / `module` | Inferred from upstream repo name or local ticket's "Affected plugin" section |
123
+ | `plugin` / `package` / `module` | Inferred from upstream repo name or local ticket's "Affected plugin / component" |
110
124
  | `version` | Local ticket's environment notes; or `npm view <pkg> version` for the latest if ambiguous |
111
125
  | `claude-code-version` | `claude --version` if the report originates from a Claude Code session |
112
126
  | `os` | Local ticket's environment notes; or `uname -srm` of the reporting host |
113
- | `reproduction` | Local ticket's `## Symptoms` section |
114
- | `expected` | Local ticket's "expected behaviour" line under `## Description` |
115
- | `actual` | Local ticket's "actual behaviour" line under `## Description` |
127
+ | `description` | Local ticket's `## Description` section |
128
+ | `symptoms` | Local ticket's `## Symptoms` section |
129
+ | `workaround` | Local ticket's `## Workaround` section (or "None identified yet.") |
130
+ | `frequency` | Local ticket's `## Impact Assessment` Frequency line |
131
+ | `evidence` | Commit SHAs, test output, transcript excerpts from Investigation Tasks |
132
+
133
+ For upstream repos whose matched template is `bug-report.yml` / `feature-request.yml` / `question.yml` (Step 3 backward-compat fallback), the skill fills the corresponding field set: `reproduction` ← `## Symptoms`; `expected` / `actual` ← observed-vs-expected contrast lines under `## Description`; `proposal` (for features) ← `## Description`.
134
+
135
+ #### Structured default body — problem-shaped (primary, per ADR-033)
136
+
137
+ Use this body when the Step 3 classifier picked `problem` shape AND the upstream has no `problem-report.yml` / `problem.yml` / `problem-report.md` / `problem.md`:
138
+
139
+ ```markdown
140
+ ## Description
141
+
142
+ <one-paragraph synthesis of the local ticket's Description>
143
+
144
+ ## Symptoms
145
+
146
+ <bullet list from local ticket's Symptoms>
147
+
148
+ ## Workaround
149
+
150
+ <from local ticket's Workaround section; "None identified yet." if absent>
151
+
152
+ ## Affected plugin / component
153
+
154
+ <inferred from the local ticket's Impact Assessment or inferred from context>
155
+
156
+ ## Frequency
157
+
158
+ <from the local ticket's Impact Assessment "Frequency" line>
159
+
160
+ ## Environment
161
+
162
+ - Package: <inferred from upstream repo>
163
+ - Version: <detected via npm ls or local ticket's notes>
164
+ - Claude Code version: <claude --version>
165
+ - OS: <uname -srm>
166
+
167
+ ## Evidence
168
+
169
+ <commit SHAs, test output, transcript excerpts — drawn from the local ticket's Investigation Tasks>
170
+
171
+ ## Cross-reference
172
+
173
+ Reported from <downstream-repo-url>/<local-ticket-relative-path>
174
+
175
+ This issue is tracked locally as P<NNN> in the downstream project's `docs/problems/` directory.
176
+ ```
177
+
178
+ The body MUST include the `## Cross-reference` section so Step 7's back-write contract works (the downstream ticket's `## Reported Upstream` section records the upstream URL; the upstream issue body records the downstream reference).
116
179
 
117
- If no template matches, emit the **structured default** body:
180
+ #### Structured default body bug-shaped (fallback-only)
181
+
182
+ Use this body only when the Step 3 classifier picked `bug` shape as backward-compat fallback (no primary `problem` tokens matched) AND the upstream has no matching template:
118
183
 
119
184
  ```markdown
120
185
  ## Summary
@@ -147,6 +212,50 @@ Reported from <downstream-repo-url>/<local-ticket-relative-path>
147
212
  This issue is tracked locally as P<NNN> in the downstream project's `docs/problems/` directory.
148
213
  ```
149
214
 
215
+ #### Structured default body — feature-shaped (fallback-only)
216
+
217
+ Use this body only when the Step 3 classifier picked `feature` shape as backward-compat fallback AND the upstream has no matching template:
218
+
219
+ ```markdown
220
+ ## Proposal
221
+
222
+ <one-paragraph synthesis of the local ticket's Description>
223
+
224
+ ## Motivation
225
+
226
+ <why this matters, from local ticket's Impact Assessment>
227
+
228
+ ## Alternatives considered
229
+
230
+ <from local ticket's Root Cause Analysis or Candidate fix options>
231
+
232
+ ## Cross-reference
233
+
234
+ Reported from <downstream-repo-url>/<local-ticket-relative-path>
235
+
236
+ This issue is tracked locally as P<NNN> in the downstream project's `docs/problems/` directory.
237
+ ```
238
+
239
+ #### Structured default body — question-shaped (fallback-only)
240
+
241
+ Use this body only when the Step 3 classifier picked `question` shape as backward-compat fallback AND the upstream has no matching template (and no `config.yml` re-routing to Discussions):
242
+
243
+ ```markdown
244
+ ## Question
245
+
246
+ <the question itself, from the local ticket's title or Description>
247
+
248
+ ## Context
249
+
250
+ <what prompted the question, from local ticket's Description or Symptoms>
251
+
252
+ ## Cross-reference
253
+
254
+ Reported from <downstream-repo-url>/<local-ticket-relative-path>
255
+
256
+ This issue is tracked locally as P<NNN> in the downstream project's `docs/problems/` directory.
257
+ ```
258
+
150
259
  Open the issue:
151
260
 
152
261
  ```bash
@@ -236,13 +345,16 @@ Three distinct AFK branches per the architect review of ADR-024 + ADR-013 Rule 6
236
345
 
237
346
  ## References
238
347
 
239
- - [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) — primary contract this skill implements.
348
+ - [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) — primary contract this skill implements. Steps 1, 2, 4, 6, 7, 8 and all Consequences remain authoritative.
349
+ - [ADR-033](../../../docs/decisions/033-report-upstream-classifier-problem-first.proposed.md) — partially supersedes ADR-024 Decision Outcome Steps 3 + 5; governs the problem-first classifier and problem-shaped structured default body.
240
350
  - [ADR-027](../../../docs/decisions/027-governance-skill-auto-delegation.proposed.md) — Step-0 deferral rationale (held for reassessment).
241
351
  - [ADR-028](../../../docs/decisions/028-voice-tone-gate-external-comms.proposed.md) — voice-tone gate on `gh issue create` and `gh api .../security-advisories`.
242
352
  - [ADR-013](../../../docs/decisions/013-structured-user-interaction-for-governance-decisions.proposed.md) — interaction policy; Rule 1 governs Step 6 missing-SECURITY.md `AskUserQuestion`; Rule 6 governs the commit-gate AFK branch.
243
353
  - [ADR-014](../../../docs/decisions/014-governance-skills-commit-their-own-work.proposed.md) — work → score → commit ordering.
244
354
  - [ADR-015](../../../docs/decisions/015-on-demand-assessment-skills.proposed.md) — fallback path for `wr-risk-scorer:assess-release`.
245
355
  - [P055](../../../docs/problems/055-no-standard-problem-reporting-channel.open.md) — upstream problem ticket (Part B).
356
+ - **P066** — intake templates in this repo adopted the problem-first shape (must ship before P067 so the skill's preference order matches the reference shape).
357
+ - **P067** — driver ticket for the problem-first classifier reform implemented via ADR-033.
246
358
  - `packages/itil/skills/manage-problem/SKILL.md` — names the optional `## Reported Upstream` section as an allowed appendage to a problem ticket.
247
359
 
248
360
  $ARGUMENTS
@@ -73,3 +73,88 @@ setup() {
73
73
  run grep -F 'AFK behaviour summary' "$SKILL_MD"
74
74
  [ "$status" -eq 0 ]
75
75
  }
76
+
77
+ # ─── ADR-033 problem-first classifier contract (P067) ──────────────────────────
78
+ #
79
+ # ADR-033 partially supersedes ADR-024 Decision Outcome Steps 3 + 5 with a
80
+ # problem-first classifier and a problem-shaped structured default body. The
81
+ # following assertions pin the SKILL.md to ADR-033's Confirmation clauses
82
+ # (lines 157-164 of the ADR).
83
+
84
+ @test "report-upstream: SKILL.md cross-references ADR-033 (ADR-033 Confirmation)" {
85
+ run grep -F 'ADR-033' "$SKILL_MD"
86
+ [ "$status" -eq 0 ]
87
+ }
88
+
89
+ @test "report-upstream: SKILL.md Step 3 classifier lists problem-first tokens (ADR-033 Step 3)" {
90
+ # Primary classifier tokens per ADR-033: problem / issue / concern / defect / gap.
91
+ # All five must appear in the SKILL.md classifier narrative.
92
+ for token in problem issue concern defect gap; do
93
+ run grep -iE "\`${token}\`|\"${token}\"|'${token}'|${token} shape|${token}," "$SKILL_MD"
94
+ [ "$status" -eq 0 ] || {
95
+ echo "missing classifier token: $token"
96
+ return 1
97
+ }
98
+ done
99
+ }
100
+
101
+ @test "report-upstream: SKILL.md template-discovery cites problem-report.yml first (ADR-033 Step 3)" {
102
+ # Preference order: problem-report.yml / problem.yml BEFORE bug-report.yml.
103
+ run grep -n 'problem-report.yml' "$SKILL_MD"
104
+ [ "$status" -eq 0 ]
105
+ problem_line=$(grep -n 'problem-report.yml' "$SKILL_MD" | head -1 | cut -d: -f1)
106
+ bug_line=$(grep -n 'bug-report.yml' "$SKILL_MD" | head -1 | cut -d: -f1)
107
+ [ -n "$problem_line" ] && [ -n "$bug_line" ]
108
+ [ "$problem_line" -lt "$bug_line" ]
109
+ }
110
+
111
+ @test "report-upstream: SKILL.md retains bug/feature/question fallbacks (ADR-033 Step 3 backward compat)" {
112
+ # Backward-compat fallbacks must still be documented for upstreams that
113
+ # have not adopted problem-first templates.
114
+ run grep -F 'bug-report.yml' "$SKILL_MD"
115
+ [ "$status" -eq 0 ]
116
+ run grep -F 'feature-request.yml' "$SKILL_MD"
117
+ [ "$status" -eq 0 ]
118
+ run grep -F 'question.yml' "$SKILL_MD"
119
+ [ "$status" -eq 0 ]
120
+ }
121
+
122
+ @test "report-upstream: SKILL.md structured default uses problem-shaped section order (ADR-033 Step 5)" {
123
+ # Problem-shaped default body per ADR-033: Description -> Symptoms ->
124
+ # Workaround -> Affected plugin / component -> Frequency -> Environment
125
+ # -> Evidence -> Cross-reference. Assert section order in the primary
126
+ # default block.
127
+ desc_line=$(grep -n '^## Description$' "$SKILL_MD" | head -1 | cut -d: -f1)
128
+ symptoms_line=$(grep -n '^## Symptoms$' "$SKILL_MD" | head -1 | cut -d: -f1)
129
+ workaround_line=$(grep -n '^## Workaround$' "$SKILL_MD" | head -1 | cut -d: -f1)
130
+ affected_line=$(grep -nE '^## Affected plugin' "$SKILL_MD" | head -1 | cut -d: -f1)
131
+ freq_line=$(grep -n '^## Frequency$' "$SKILL_MD" | head -1 | cut -d: -f1)
132
+ env_line=$(grep -n '^## Environment$' "$SKILL_MD" | head -1 | cut -d: -f1)
133
+ evidence_line=$(grep -n '^## Evidence$' "$SKILL_MD" | head -1 | cut -d: -f1)
134
+ xref_line=$(grep -n '^## Cross-reference$' "$SKILL_MD" | head -1 | cut -d: -f1)
135
+
136
+ [ -n "$desc_line" ] || { echo "missing ## Description"; return 1; }
137
+ [ -n "$symptoms_line" ] || { echo "missing ## Symptoms"; return 1; }
138
+ [ -n "$workaround_line" ] || { echo "missing ## Workaround"; return 1; }
139
+ [ -n "$affected_line" ] || { echo "missing ## Affected plugin / component"; return 1; }
140
+ [ -n "$freq_line" ] || { echo "missing ## Frequency"; return 1; }
141
+ [ -n "$env_line" ] || { echo "missing ## Environment"; return 1; }
142
+ [ -n "$evidence_line" ] || { echo "missing ## Evidence"; return 1; }
143
+ [ -n "$xref_line" ] || { echo "missing ## Cross-reference"; return 1; }
144
+
145
+ [ "$desc_line" -lt "$symptoms_line" ]
146
+ [ "$symptoms_line" -lt "$workaround_line" ]
147
+ [ "$workaround_line" -lt "$affected_line" ]
148
+ [ "$affected_line" -lt "$freq_line" ]
149
+ [ "$freq_line" -lt "$env_line" ]
150
+ [ "$env_line" -lt "$evidence_line" ]
151
+ [ "$evidence_line" -lt "$xref_line" ]
152
+ }
153
+
154
+ @test "report-upstream: SKILL.md cites ADR-033 as authority for Steps 3 + 5 (ADR-033 Confirmation)" {
155
+ # ADR-033 must be cited near the Step 3 / Step 5 headings, not only in the
156
+ # References section, so future maintainers see the authority inline.
157
+ run grep -niE 'adr-033|033.*problem-first' "$SKILL_MD"
158
+ [ "$status" -eq 0 ]
159
+ [ "${#lines[@]}" -ge 2 ]
160
+ }
@@ -49,6 +49,18 @@ For each `docs/problems/*.open.md` and `docs/problems/*.known-error.md` file (sk
49
49
  - Re-stage explicitly per the P057 staging trap: `git add <new-path>` after the Edit.
50
50
  - This happens automatically — do not ask the user. The transition's fix-strategy is documented; only the shipping is outstanding.
51
51
 
52
+ ### 2.5. Dependency-graph traversal — propagate transitive effort (P076)
53
+
54
+ After Step 2 assigns each ticket its **marginal** effort, run a second pass that walks the `## Dependencies` graph and propagates effort up per the transitive-dependency rule defined in `/wr-itil:manage-problem`'s WSJF Prioritisation section (the canonical location). This is a deterministic re-rate — no `AskUserQuestion` required.
55
+
56
+ 1. **Build the graph**: for each `.open.md` / `.known-error.md` ticket, parse the `## Dependencies` section. Record `**Blocked by**` edges (bare IDs) into an adjacency map. Ignore `**Composes with**` (does not propagate) and `**Blocks**` (derivable from inverse).
57
+ 2. **Classify upstream status**: upstreams in `.closed.md`, `.verifying.md`, or `.parked.md` contribute **0** to the closure (architect carve-out per P076). Upstreams in `.open.md` or `.known-error.md` contribute their own transitive effort.
58
+ 3. **Topologically sort** and compute `Effort_transitive = max(marginal, max{ upstream transitive })`. Cycle-bundle members all receive the bundle's effort = `max{ marginal | members }`.
59
+ 4. **Update Effort and WSJF lines** when the transitive effort differs from the marginal. Add a `<!-- transitive: <bucket> via <UPSTREAM> -->` HTML comment on the Effort line so the next review can distinguish a manually-set marginal from a propagated transitive.
60
+ 5. **Report each re-rate** in the review summary using the concrete format `P<NNN>: Effort <OLD> → <NEW> (transitive via <UPSTREAM>)`, e.g. `P073: Effort S → XL (transitive via P038)`. Cycle bundles surface a shared line: `Bundle [P038, P064]: effort XL (cycle), WSJF 3.0 (shared)`.
61
+
62
+ Re-read the WSJF Prioritisation → "Transitive dependencies (P076)" subsection in `packages/itil/skills/manage-problem/SKILL.md` if unsure — that is the canonical rule definition.
63
+
52
64
  ### 3. Present the refreshed ranking
53
65
 
54
66
  After re-scoring, present three sections matching the README.md format (same rendering used by `/wr-itil:list-problems` and by the README cache — Step 5 writes the same layout):
@@ -175,3 +175,33 @@ setup() {
175
175
  run grep -inE "If arguments start with|If arguments contain" "$SKILL_FILE"
176
176
  [ "$status" -ne 0 ]
177
177
  }
178
+
179
+ @test "SKILL.md has a dependency-graph-traversal pass for transitive effort (P076)" {
180
+ # P076: review-problems is the executor for the WSJF transitive-effort
181
+ # rule. Step 2 assigns marginal effort; a second pass must walk
182
+ # `## Dependencies` edges and propagate effort per the canonical rule
183
+ # in /wr-itil:manage-problem's WSJF Prioritisation section. Without
184
+ # this pass, the marginal-only ranking mis-ranks dependent tickets
185
+ # above their blockers (the exact failure P076 documents).
186
+ run grep -inE "transitive[[:space:]]?effort|graph traversal|propagate.*effort|dependency[[:space:]-]?graph" "$SKILL_FILE"
187
+ [ "$status" -eq 0 ]
188
+ }
189
+
190
+ @test "SKILL.md transitive-effort pass cites P076 and the canonical rule location (traceability)" {
191
+ # Audit trail: the Step 2.5 block must cite P076 (motivation) and
192
+ # point to /wr-itil:manage-problem's WSJF section as the canonical
193
+ # definition — duplication would fork the contract.
194
+ run grep -n "P076" "$SKILL_FILE"
195
+ [ "$status" -eq 0 ]
196
+ run grep -inE "manage-problem.*WSJF|WSJF.*manage-problem|canonical" "$SKILL_FILE"
197
+ [ "$status" -eq 0 ]
198
+ }
199
+
200
+ @test "SKILL.md transitive re-rate message format matches the canonical shape (P076)" {
201
+ # Architect correction 4: the re-rate message must be greppable and
202
+ # consistent across skills. Shape: `P<NNN>: Effort <OLD> → <NEW>
203
+ # (transitive via <UPSTREAM>)`. review-problems emits this in step 3's
204
+ # summary output so downstream audit tools can grep the review log.
205
+ run grep -inE "Effort.*→.*transitive via|transitive via.*P[0-9]" "$SKILL_FILE"
206
+ [ "$status" -eq 0 ]
207
+ }