@windyroad/itil 0.47.9-preview.565 → 0.47.10

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.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.47.9"
500
+ "version": "0.47.10"
501
501
  }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/run-check-deferred-placeholder-staleness.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/run-check-deferred-placeholder-staleness.sh" "$@"
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # Deferred-placeholder + README-cadence staleness check — Step 0c of
3
+ # /wr-itil:work-problems (per P271).
4
+ #
5
+ # P271 driver: /wr-itil:review-problems is the meta-workflow that re-rates
6
+ # the deferred-placeholder (Priority + Effort) WSJF inputs that
7
+ # /wr-itil:capture-problem leaves behind. Without an auto-fire trigger, the
8
+ # placeholders accumulate silently across sessions — 76 → 83 on the
9
+ # 2026-05-24 work-problems session evidenced the gap — and the AFK
10
+ # orchestrator dispatches iters against stale WSJF rankings until the
11
+ # maintainer manually invokes review-problems.
12
+ #
13
+ # Trigger rule — TWO-AXIS AND (load-bearing per architect verdict on the
14
+ # P271 fix shape, Condition 2). Both axes must hold; either alone over-fires:
15
+ #
16
+ # 1. count of deferred-placeholder tickets ≥ 3
17
+ # Signal: there is work to re-rate. A backlog with <3 placeholders is
18
+ # not stale enough to be worth a heavyweight review pass.
19
+ #
20
+ # 2. docs/problems/README.md "Last reviewed" line-3 age > 7 days
21
+ # Signal: cadence has slipped. A README updated yesterday but with
22
+ # 3 fresh captures is the in-spec deferred-placeholder behaviour
23
+ # (today's captures are tomorrow's review). The age axis filters out
24
+ # this false-positive.
25
+ #
26
+ # The intersection (count ≥ 3 AND age > 7 days) is the actual signal —
27
+ # "there is work to do AND the cadence has slipped". Either-axis alone
28
+ # produces false positives that erode trust in the trigger.
29
+ #
30
+ # Mirrors the Step 0b cache-staleness pattern at
31
+ # packages/itil/lib/check-upstream-cache-staleness.sh.
32
+ #
33
+ # <!-- DEFERRED-PLACEHOLDER-STALENESS-CONTRACT-SOURCE: packages/itil/skills/work-problems/SKILL.md Step 0c -->
34
+ #
35
+ # Any change to the threshold constants (3 placeholders, 7 days) MUST
36
+ # update this helper, work-problems SKILL.md Step 0c, manage-problem
37
+ # SKILL.md Step 0.5, and capture-problem SKILL.md Step 7 in the same
38
+ # commit — drift here re-opens P271.
39
+ #
40
+ # Source this file, then call `should_promote_review_problems_dispatch`:
41
+ # . packages/itil/lib/check-deferred-placeholder-staleness.sh
42
+ # reason="$(should_promote_review_problems_dispatch "$PWD")"
43
+ #
44
+ # Output (one of):
45
+ # no-deferred-placeholders → count is 0; silent-pass.
46
+ # below-threshold count=<N> threshold=3 → 0 < count < 3; silent-pass.
47
+ # no-readme count=<N> → README absent OR malformed
48
+ # line 3; first-run dispatch
49
+ # trigger.
50
+ # fresh-readme count=<N> age=<X>s threshold=<Y>s → README age within window;
51
+ # silent-pass per ADR-013
52
+ # Rule 5.
53
+ # stale-readme count=<N> age=<X>s threshold=<Y>s → THE auto-dispatch
54
+ # trigger; both axes met.
55
+ #
56
+ # Glob — DUAL-TOLERANT per ADR-031 RFC-002 migration window. Covers BOTH
57
+ # the flat `docs/problems/<NNN>-<title>.<state>.md` layout AND the per-state
58
+ # subdir `docs/problems/<state>/<NNN>-<title>.md` layout. Closed and
59
+ # verifying tickets are EXCLUDED — they carry no dev-work WSJF per ADR-022
60
+ # and their deferred-placeholders are out-of-scope for re-rate.
61
+ #
62
+ # Dependencies: bash 4+, grep, awk, python3 (for date parsing — portable
63
+ # across Linux/BSD).
64
+
65
+ should_promote_review_problems_dispatch() {
66
+ local repo_root="${1:-$PWD}"
67
+ local problems_dir="$repo_root/docs/problems"
68
+
69
+ # Threshold constants — both axes documented above.
70
+ local count_threshold=3
71
+ local age_threshold_seconds=604800 # 7 days × 86400 seconds.
72
+
73
+ # Axis 1 — count deferred-placeholder tickets across open + known-error
74
+ # via dual-tolerant globs (RFC-002 migration window).
75
+ local count=0
76
+ local marker='deferred — re-rate at next /wr-itil:review-problems'
77
+ local file
78
+ shopt -s nullglob
79
+ for file in \
80
+ "$problems_dir"/open/*.md \
81
+ "$problems_dir"/known-error/*.md \
82
+ "$problems_dir"/*.open.md \
83
+ "$problems_dir"/*.known-error.md; do
84
+ [ -f "$file" ] || continue
85
+ if grep -qF "$marker" "$file" 2>/dev/null; then
86
+ count=$((count + 1))
87
+ fi
88
+ done
89
+ shopt -u nullglob
90
+
91
+ if [ "$count" -eq 0 ]; then
92
+ echo "no-deferred-placeholders"
93
+ return 0
94
+ fi
95
+
96
+ if [ "$count" -lt "$count_threshold" ]; then
97
+ echo "below-threshold count=${count} threshold=${count_threshold}"
98
+ return 0
99
+ fi
100
+
101
+ # Axis 2 — README.md "Last reviewed" line-3 age.
102
+ local readme="$problems_dir/README.md"
103
+
104
+ if [ ! -f "$readme" ]; then
105
+ echo "no-readme count=${count}"
106
+ return 0
107
+ fi
108
+
109
+ # Read line 3 — the canonical "Last reviewed" surface per P134 + the
110
+ # SKILL.md contract. Use awk so the read survives BSD/GNU portability.
111
+ local line3
112
+ line3="$(awk 'NR==3' "$readme" 2>/dev/null || echo "")"
113
+
114
+ # Parse the ISO date from the "Last reviewed: YYYY-MM-DD" prefix.
115
+ # The line shape is `> Last reviewed: YYYY-MM-DD **<event>** — <summary>`
116
+ # per the P134 inline rotation contract. A missing or malformed line is
117
+ # treated as no-readme (defensive — forces a fresh dispatch rather than
118
+ # silently silent-passing on malformed README state).
119
+ local last_reviewed_date
120
+ last_reviewed_date="$(echo "$line3" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)"
121
+
122
+ if [ -z "$last_reviewed_date" ]; then
123
+ echo "no-readme count=${count}"
124
+ return 0
125
+ fi
126
+
127
+ local last_reviewed_epoch now_epoch age_seconds
128
+ last_reviewed_epoch="$(python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], '%Y-%m-%d').replace(tzinfo=datetime.timezone.utc).timestamp()))" "$last_reviewed_date" 2>/dev/null || echo "0")"
129
+ now_epoch="$(date +%s)"
130
+ age_seconds=$((now_epoch - last_reviewed_epoch))
131
+
132
+ if [ "$age_seconds" -gt "$age_threshold_seconds" ]; then
133
+ echo "stale-readme count=${count} age=${age_seconds}s threshold=${age_threshold_seconds}s"
134
+ return 0
135
+ fi
136
+
137
+ echo "fresh-readme count=${count} age=${age_seconds}s threshold=${age_threshold_seconds}s"
138
+ return 0
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.47.9-preview.565",
3
+ "version": "0.47.10",
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,23 @@
1
+ #!/usr/bin/env bash
2
+ # wr-itil — echo the deferred-placeholder + README-cadence promotion reason,
3
+ # or empty if promotion is not warranted (P271 / P317 RFC-009 adopter-safe).
4
+ #
5
+ # Adopter-safe wrapper: sources the canonical lib RELATIVE TO THIS SCRIPT
6
+ # (`$(dirname)/../lib`), then echoes the function's result on stdout (the SKILL
7
+ # captures it via `$(...)`). Mirrors the
8
+ # `run-check-upstream-cache-staleness.sh` precedent — never `source
9
+ # packages/...` repo-relative from a SKILL; those paths only resolve in the
10
+ # source monorepo, not adopter installs.
11
+ #
12
+ # SKILLs invoke this by name via the
13
+ # `wr-itil-check-deferred-placeholder-staleness` PATH shim (ADR-049 +
14
+ # ADR-080). Operates on the directory given as $1 (defaults to $PWD).
15
+ set -uo pipefail
16
+
17
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
18
+ echo "wr-itil-check-deferred-placeholder-staleness: cannot locate lib next to the script" >&2
19
+ exit 2
20
+ }
21
+ # shellcheck source=/dev/null
22
+ source "$LIB/check-deferred-placeholder-staleness.sh"
23
+ should_promote_review_problems_dispatch "${1:-$PWD}"
@@ -10,6 +10,12 @@ Capture a problem ticket quickly during foreground work. Lightweight aside-invoc
10
10
 
11
11
  This skill is the foreground-lightweight-capture variant of `/wr-itil:manage-problem`'s new-problem path per ADR-032 (P155 amendment, 2026-05-03). The deferred background-capture variant named in ADR-032's original taxonomy remains deferred per P088 settlement.
12
12
 
13
+ ## Output Formatting
14
+
15
+ When referencing problem, JTBD, ADR, or RFC IDs in prose output (stderr advisories, capture-report messages), always include the human-readable title or substance on first mention. Use the format `JTBD-001 (Enforce Governance Without Slowing Down)`, not bare `JTBD-001`.
16
+
17
+ **Brief-before-ID discipline at the Step 1.5b derive-then-ratify `AskUserQuestion` surface (P350).** When the derive-then-ratify dispatch proposes persona/JTBD candidates to the user, the question text MUST inline what each proposed JTBD/persona asserts — the job statement, the user-need, the persona's key constraint — BEFORE naming it by `JTBD-NNN` / `persona-slug`. The user reads the prompt without project filesystem access (mobile clients, accessibility tooling, notification surfaces) and cannot follow links into `docs/jtbd/`. Acceptable option label: *"Developer persona — enforce governance automatically so manual-review safety comes without overhead."* Unacceptable: *"developer + JTBD-001"*. IDs may appear ONLY after a self-contained explanation. Mirrors the canonical `/wr-architect:create-adr` Step 5 § 5a Rule 3 ("No IDs as explainers"). See also session memory `feedback_brief_before_id.md`.
18
+
13
19
  ## When to invoke
14
20
 
15
21
  - **Mid-iter sibling-finding**: agent observes a tangential ticket-worthy issue while working on a different problem and cannot afford the 10-turn `/wr-itil:manage-problem` ceremony.
@@ -315,9 +321,27 @@ After the commit, report:
315
321
 
316
322
  - The new ticket file path and ID.
317
323
  - The list of duplicate matches found (if any). If matches found, name them and remind the user to merge at next `/wr-itil:review-problems` if appropriate.
318
- - Trailing pointer: `Run /wr-itil:review-problems next to fold P<NNN> into the WSJF rankings, re-rate the deferred placeholders, and refresh docs/problems/README.md.`
324
+ - **Trailing pointer (conditional shape per P271)**: invoke `wr-itil-check-deferred-placeholder-staleness "$PWD"` to read the deferred-placeholder + README-cadence staleness signal. The pointer shape switches on the helper's five-outcome enum:
325
+
326
+ ```bash
327
+ preflight_reason="$(wr-itil-check-deferred-placeholder-staleness "$PWD")"
328
+ ```
329
+
330
+ See `/wr-itil:work-problems` SKILL.md Step 0c for the canonical contract on the two-axis trigger (count ≥ 3 deferred placeholders AND README age > 7 days); the threshold constants live in the helper. <!-- DEFERRED-PLACEHOLDER-STALENESS-CONTRACT-SOURCE: packages/itil/lib/check-deferred-placeholder-staleness.sh -->
331
+
332
+ | `preflight_reason` | Trailing-pointer shape |
333
+ |---------------------------------------------------|--------------------------------------------------------------------------------------------------------|
334
+ | `no-deferred-placeholders` / `below-threshold ...` / `fresh-readme ...` | Default (low-priority) pointer: *"Run `/wr-itil:review-problems` next to fold P\<NNN\> into the WSJF rankings, re-rate the deferred placeholders, and refresh `docs/problems/README.md`."* |
335
+ | `no-readme count=<N>` | **Highlighted (actionable) pointer**: *"⚠ `<N>` deferred-placeholder ticket(s) have accumulated AND `docs/problems/README.md` is missing/malformed — run `/wr-itil:review-problems` NOW to rebuild the README and re-rate placeholders."* |
336
+ | `stale-readme count=<N> age=<X>s threshold=<Y>s` | **Highlighted (actionable) pointer**: *"⚠ `<N>` deferred-placeholder ticket(s) have accumulated AND the WSJF Rankings cadence is `<X>` days stale (> 7-day threshold) — run `/wr-itil:review-problems` NOW to re-rate placeholders and refresh `docs/problems/README.md`."* |
337
+
338
+ **Why conditional, not auto-dispatch** (ADR-032 + JTBD-001): capture-problem is intentionally lightweight per ADR-032 P155 amendment. Auto-dispatching review-problems from capture would re-introduce the ~10-turn ceremony the lightweight aside is engineered to avoid. The conditional pointer preserves the speed-of-capture contract while surfacing the signal "the README is now MORE than transiently stale" when both axes hit threshold. The user picks when to absorb the re-rate cost.
339
+
340
+ **Fail-soft**: any error in the helper invocation MUST NOT block the report — fall back to the default pointer shape.
341
+
342
+ The trailing pointer is **not optional** — it is the user-visible signal that the README is transiently stale and how to reconcile it. The conditional highlight (P271) escalates the signal when the deferred-placeholder backlog AND the cadence axis both hit threshold. Drift here re-opens P271.
319
343
 
320
- The trailing pointer is **not optional** it is the user-visible signal that the README is transiently stale and how to reconcile it. Drift here re-opens the deferred-README-refresh contract gap.
344
+ <!-- @jtbd JTBD-001 (Enforce Governance Without Slowing Down conditional highlight escalates the signal without forcing a flow break) -->
321
345
 
322
346
  ## Composition with manage-problem
323
347
 
@@ -12,6 +12,12 @@ This skill is one half of the capture-then-manage RFC framework introduced by AD
12
12
 
13
13
  **Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; this skill IS the capture-time decomposition surface), JTBD-001 (extended scope — change-set-level governance), JTBD-101 (atomic-fix-adopter — every fix goes through an RFC per ADR-071; capture-rfc is invoked deliberately, not auto-fired, because RFC scope is direction-setting per ADR-073 — NOT because atomic fixes skip ceremony).
14
14
 
15
+ ## Output Formatting
16
+
17
+ When referencing RFC IDs, problem IDs, ADR IDs, JTBD IDs, or story IDs in prose output (stderr advisories, capture-report messages), always include the human-readable title or substance on first mention. Use the format `RFC-006 (Decision homing implementation)`, not bare `RFC-006`.
18
+
19
+ **Brief-before-ID discipline at any `AskUserQuestion` surface this skill emits (P350).** When this skill surfaces an interactive question (e.g. ambiguous problem-trace selection, forward-referenced story confirmation), the question/option/description text MUST inline what each referenced artefact is and what is at stake BEFORE naming it by ID. `RFC-NNN` / `P-NNN` / `STORY-NNN` / `ADR-NNN` / `JTBD-NNN` references are audit-trail annotations, NEVER carriers of meaning — the user reads the prompt without project filesystem access (mobile clients, accessibility tooling, notification surfaces) and cannot follow links. Every option's substance MUST be self-contained in the briefing prose + option `label` and `description`; IDs may appear ONLY after a self-contained explanation. Mirrors the canonical `/wr-architect:create-adr` Step 5 § 5a Rule 3 ("No IDs as explainers"). See also session memory `feedback_brief_before_id.md`.
20
+
15
21
  ## When to invoke
16
22
 
17
23
  - **Multi-commit fix at the start of work**: agent / user observes that a problem fix decomposes into multiple coordinated changes (a refactor across packages, a phased migration, a framework evolution). Capture an RFC scoping the work *before* the first commit lands so each phase competes for WSJF attention as a first-class entity (per JTBD-008).
@@ -13,6 +13,8 @@ Create, update, or transition problem tickets following an ITIL-aligned problem
13
13
 
14
14
  When referencing problem IDs, ADR IDs, or JTBD IDs in prose output, always include the human-readable title on first mention. Use the format `P029 (Edit gate overhead for governance docs)`, not bare `P029`. Tables with separate ID and Title columns are fine as-is.
15
15
 
16
+ **Brief-before-ID discipline at `AskUserQuestion` surfaces (P350).** Title-on-first-mention is the minimum for prose output. For `AskUserQuestion` question/option/description text the rule is stricter: brief the artefact's purpose and substance BEFORE naming it by ID. `P-NNN` / `ADR-NNN` / `JTBD-NNN` / `RFC-NNN` references are audit-trail annotations, NEVER carriers of meaning — the user reads the prompt without project filesystem access (mobile clients, accessibility tooling, notification surfaces) and cannot follow links. Every option's substance MUST be self-contained in the briefing prose + the option `label` and `description`; IDs may appear ONLY after a self-contained explanation. Mirrors the canonical `/wr-architect:create-adr` Step 5 § 5a Rule 3 ("No IDs as explainers"). Applies to Step 4b's fix-strategy and verification dispatches and any other `AskUserQuestion` site emitted by this skill. See also session memory `feedback_brief_before_id.md`.
17
+
16
18
  ## First-run intake-scaffold pointer (P065 / ADR-036)
17
19
 
18
20
  This skill is one of the two host skills wired to surface the [`/wr-itil:scaffold-intake`](../scaffold-intake/SKILL.md) skill on first invocation in a project that has not yet adopted the OSS intake surface. The contract is documented in [ADR-036](../../../../docs/decisions/036-scaffold-downstream-oss-intake.proposed.md) (Scaffold downstream OSS intake — skill + layered triggers).
@@ -274,6 +276,41 @@ This is a **preflight CHECK only** — manage-problem does NOT itself apply edit
274
276
 
275
277
  This step is a robustness layer ON TOP of P094 + P062, not a supersession of either — both per-operation contracts remain in force at Step 5 (creation refresh) and Step 7 (transition refresh).
276
278
 
279
+ ### 0.5. Deferred-placeholder + README-cadence advisory (per P271)
280
+
281
+ After Step 0's README reconciliation preflight and before Step 1's request parsing, check whether the deferred-placeholder backlog has accumulated past threshold AND the `docs/problems/README.md` "Last reviewed" cadence has slipped. This is the **interactive** sibling of `/wr-itil:work-problems` Step 0c. At an interactive `manage-problem` surface the right shape per ADR-013 Rule 1 is **advisory not auto-dispatch** — the user is at the keyboard; the advisory is the surface; the user invokes `/wr-itil:review-problems` directly if they want the heavyweight re-rate pass. Auto-dispatching a heavyweight skill mid-interactive-session would break JTBD-001's "Reviews complete in under 60 seconds so they don't break flow" outcome.
282
+
283
+ **Mechanism:**
284
+
285
+ ```bash
286
+ preflight_reason="$(wr-itil-check-deferred-placeholder-staleness "$PWD")"
287
+ ```
288
+
289
+ The helper is the same one Step 0c (work-problems) uses — single source of truth for the two-axis trigger rule (count ≥ 3 deferred placeholders AND README age > 7 days). See `/wr-itil:work-problems` SKILL.md Step 0c for the canonical contract; the threshold constants live in the helper. <!-- DEFERRED-PLACEHOLDER-STALENESS-CONTRACT-SOURCE: packages/itil/lib/check-deferred-placeholder-staleness.sh -->
290
+
291
+ Routing on the helper's five-outcome enum:
292
+
293
+ | `preflight_reason` | Action |
294
+ |----------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
295
+ | `no-deferred-placeholders` | Silent-pass. Proceed to Step 1. |
296
+ | `below-threshold count=<N> threshold=3` | Silent-pass. Proceed to Step 1. |
297
+ | `fresh-readme count=<N> age=<X>s threshold=<Y>s` | Silent-pass per ADR-013 Rule 5 — the cadence is in spec. |
298
+ | `no-readme count=<N>` | Emit an **advisory note** to stdout (visible to the interactive maintainer) naming the placeholder count + a directive: *"`<N>` deferred-placeholder ticket(s) accumulated AND `docs/problems/README.md` is missing/malformed; run `/wr-itil:review-problems` to re-rate and rebuild the README."* Then proceed to Step 1. |
299
+ | `stale-readme count=<N> age=<X>s threshold=<Y>s` | Emit an **advisory note** naming the placeholder count + readable age: *"`<N>` deferred-placeholder ticket(s) accumulated AND the WSJF Rankings cadence is `<X>` days stale (> 7-day threshold); run `/wr-itil:review-problems` to re-rate and refresh."* Then proceed to Step 1. |
300
+
301
+ **Why advisory not auto-dispatch at this surface** (ADR-013 Rule 1 + JTBD-001):
302
+ - The interactive user is at the keyboard. The advisory IS the surface; auto-dispatching a heavyweight skill mid-interactive-session would force a flow break that JTBD-001 explicitly proscribes ("Reviews complete in under 60 seconds so they don't break flow").
303
+ - The advisory preserves the user's authority to pick when to absorb the re-rate cost — at the next natural break, after a release, before the next session, etc.
304
+ - `AskUserQuestion` would also be valid here (ADR-013 Rule 1 interactive surface), but for a single optional pre-flight directive the advisory shape carries less round-trip friction than a structured question. The advisory is a **directive**, not a decision — the user already knows to run review-problems if they want to.
305
+
306
+ **ADR-079 composition note**: when the user follows the advisory and invokes `/wr-itil:review-problems`, that skill includes Step 4.6 relevance-close per ADR-079 — relevance-close fires as a side-effect of any review pass.
307
+
308
+ **Fail-soft**: any error in this step (helper missing, malformed output) MUST NOT block manage-problem — log an advisory note and proceed to Step 1.
309
+
310
+ <!-- @jtbd JTBD-001 (Enforce Governance Without Slowing Down — interactive advisory keeps the signal surfaced without forcing a flow break) -->
311
+
312
+ After Step 0.5 completes (whether silent-pass or advisory emitted), proceed to Step 1.
313
+
277
314
  ### 1. Parse the request
278
315
 
279
316
  Determine the operation from `$ARGUMENTS`:
@@ -10,6 +10,12 @@ Create, update, or transition RFC tickets following the Problem-RFC-Story framew
10
10
 
11
11
  **Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; this skill governs the lifecycle of decomposed work), JTBD-001 (extended scope — change-set-level governance), JTBD-101 (atomic-fix-adopter — every fix goes through an RFC per ADR-071; the RFC skills are invoked deliberately, not auto-fired, because RFC scope is direction-setting per ADR-073 — NOT because atomic fixes skip ceremony).
12
12
 
13
+ ## Output Formatting
14
+
15
+ When referencing RFC IDs, problem IDs, ADR IDs, or JTBD IDs in prose output, always include the human-readable title on first mention. Use the format `RFC-006 (Decision homing implementation)`, not bare `RFC-006`. Tables with separate ID and Title columns are fine as-is.
16
+
17
+ **Brief-before-ID discipline at `AskUserQuestion` surfaces (P350).** When this skill emits an `AskUserQuestion` (transition-trigger ambiguity, scope-expansion approval, problem-trace ratification), the question/option/description text MUST inline what each referenced artefact is and what is at stake BEFORE naming it by ID. `RFC-NNN` / `P-NNN` / `ADR-NNN` / `JTBD-NNN` references are audit-trail annotations, NEVER carriers of meaning — the user reads the prompt without project filesystem access (mobile clients, accessibility tooling, notification surfaces) and cannot follow links. Every option's substance MUST be self-contained in the briefing prose + option `label` and `description`; IDs may appear ONLY after a self-contained explanation. Mirrors the canonical `/wr-architect:create-adr` Step 5 § 5a Rule 3 ("No IDs as explainers"). See also session memory `feedback_brief_before_id.md`.
18
+
13
19
  ## RFC Lifecycle
14
20
 
15
21
  | Status | File suffix | Meaning | Entry criteria |
@@ -10,6 +10,12 @@ Re-assess the problem backlog. This skill is a **batch operation** that reads ev
10
10
 
11
11
  This skill is the P071 phased-landing split of `/wr-itil:manage-problem review` per ADR-010 amended Skill Granularity rule: one skill per distinct user intent. The original `/wr-itil:manage-problem review` subcommand route remains as a thin-router forwarder during the deprecation window but is scheduled for removal in `@windyroad/itil`'s next major version.
12
12
 
13
+ ## Output Formatting
14
+
15
+ When referencing problem IDs, ADR IDs, JTBD IDs, or RFC IDs in prose output (the Verification Queue summary, the WSJF re-rank report, stderr advisories), always include the human-readable title on first mention. Use the format `P350 (Empathy gap on opaque IDs)`, not bare `P350`. Tables with separate ID and Title columns are fine as-is.
16
+
17
+ **Brief-before-ID discipline at `AskUserQuestion` surfaces (P350).** When this skill emits an `AskUserQuestion` — most notably the Step 4 verification prompts that propose `.verifying.md` tickets for Closed transition and any priority-rerate-confirmation surface — the question/option/description text MUST inline what each referenced ticket is about, the relevant verification evidence, and what is at stake BEFORE naming it by `P-NNN`. The user reads the prompt without project filesystem access (mobile clients, accessibility tooling, notification surfaces) and cannot open the ticket file to recall the substance. Acceptable: *"Verify and close: the Excel sheet-name reading-order ticket. Evidence cited: the fix landed in `@windyroad/document-a11y@1.4.2` two weeks ago and no regressions reported. Close as Verified?"* Unacceptable: *"Verify and close P192?"*. Every option's substance MUST be self-contained in the briefing prose + option `label` and `description`; IDs may appear ONLY after a self-contained explanation. Mirrors the canonical `/wr-architect:create-adr` Step 5 § 5a Rule 3 ("No IDs as explainers"). See also session memory `feedback_brief_before_id.md`.
18
+
13
19
  ## Scope
14
20
 
15
21
  **In scope** (RFC-002 migration window — each glob is dual-tolerant, covering BOTH the flat `docs/problems/<NNN>-<title>.<state>.md` filename-suffix layout AND the per-state subdir `docs/problems/<state>/<NNN>-<title>.md` layout):
@@ -186,7 +186,57 @@ The annotation pre-empts the "surprise heavy iter" perception JTBD-006 expects a
186
186
 
187
187
  **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 -->
188
188
 
189
- After Step 0b completes (whether dispatched or silent-passed), proceed to Step 1.
189
+ After Step 0b completes (whether dispatched or silent-passed), proceed to Step 0c.
190
+
191
+ ### Step 0c: Deferred-placeholder + README-cadence pre-flight (per P271)
192
+
193
+ After Step 0b's inbound-discovery pre-flight and before Step 1's backlog scan, check whether the deferred-placeholder backlog has accumulated past threshold AND the `docs/problems/README.md` "Last reviewed" cadence has slipped. This step closes the load-bearing gap P271 names: `/wr-itil:capture-problem` leaves deferred-placeholder Priority + Effort lines that `/wr-itil:review-problems` is the only authoritative re-rate path for; without an auto-fire trigger, placeholders accumulate silently across sessions (76 → 83 evidenced on the 2026-05-24 work-problems session) and the orchestrator dispatches iters against stale WSJF rankings.
194
+
195
+ **Mechanism:**
196
+
197
+ ```bash
198
+ preflight_reason="$(wr-itil-check-deferred-placeholder-staleness "$PWD")"
199
+ ```
200
+
201
+ `wr-itil-check-deferred-placeholder-staleness` is the ADR-049 + ADR-080 `$PATH` shim (adopter-safe — resolves `lib/check-deferred-placeholder-staleness.sh` relative to the script, NOT cwd; P317/RFC-009) that internalises `should_promote_review_problems_dispatch "$PWD"` and echoes the result. NEVER `source packages/...` repo-relative from a SKILL — those paths only resolve in the source monorepo, not adopter installs.
202
+
203
+ The helper returns one of five outcomes (contract documented at `packages/itil/lib/check-deferred-placeholder-staleness.sh` + asserted by `packages/itil/skills/work-problems/test/work-problems-step-0c-deferred-placeholder-staleness-behavioural.bats`):
204
+
205
+ | `preflight_reason` | Action |
206
+ |----------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
207
+ | `no-deferred-placeholders` | Silent-pass per ADR-013 Rule 5 + P132 mechanical-stage carve-out. Proceed to Step 1. |
208
+ | `below-threshold count=<N> threshold=3` | Silent-pass — there is work to re-rate but not enough to be worth a heavyweight pass. Proceed to Step 1. |
209
+ | `no-readme count=<N>` | Dispatch `/wr-itil:review-problems` as a pre-flight iter via the standard `claude -p` subprocess wrapper (same shape as Step 0b / Step 5). README absent OR malformed line 3 → first-run dispatch. |
210
+ | `fresh-readme count=<N> age=<X>s threshold=<Y>s` | Silent-pass per ADR-013 Rule 5 — the cadence is in spec; today's captures are tomorrow's review. |
211
+ | `stale-readme count=<N> age=<X>s threshold=<Y>s` | Dispatch `/wr-itil:review-problems` as a pre-flight iter. Both axes met — there is work AND the cadence has slipped. |
212
+
213
+ **Two-axis AND rule (load-bearing per architect verdict on the P271 fix shape).** Both axes — count ≥ 3 AND README age > 7 days — must hold. Either axis alone over-fires:
214
+ - Count ≥ 3 alone fires on a backlog where review-problems was run yesterday and 3 captures came in today (that's the in-spec deferred-placeholder behaviour, not a staleness signal).
215
+ - Age > 7 days alone fires on quiet weeks where no captures occurred and there is nothing to re-rate.
216
+
217
+ The intersection is the actual signal: "there is work to do AND the cadence has slipped".
218
+
219
+ **Pre-flight dispatch shape**: when promoted (`no-readme` or `stale-readme`), dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:review-problems` (per P084 + ADR-032 subprocess isolation). Reuse the Step 5 subprocess wrapper verbatim — same flag set, same idle-timeout SIGTERM poll loop, same retro-on-exit contract. The subprocess runs the full Step 2 + Step 2.5 + Step 4 + Step 5 re-rate + README refresh + commit; the orchestrator reads the freshly-refreshed README at Step 1.
220
+
221
+ **ADR-079 composition note**: Step 0c dispatches `/wr-itil:review-problems` which includes Step 4.6 relevance-close per ADR-079 — relevance-close fires as a side-effect of the auto-dispatch. This is desirable: relevance closes accumulate the same way deferred placeholders do, and the AND-trigger reasonably gates both pieces of work.
222
+
223
+ **Iter-summary annotation**:
224
+
225
+ - No placeholders / below threshold: `Step 0c skipped — <N> deferred placeholders below threshold (3)`.
226
+ - Fresh README cadence: `Step 0c skipped — README cadence fresh (age=<X>s within 7-day window)`.
227
+ - Pre-flight ran: `Step 0c pre-flighted /wr-itil:review-problems — reason=<preflight_reason>, <N> placeholders re-rated, <M> tickets auto-transitioned, <K> tickets relevance-closed`.
228
+
229
+ The annotation pre-empts the "surprise heavy iter" perception JTBD-006 expects auditability for — a maintainer running multiple short AFK loops with fresh-cache will see the silent-pass annotation, confirming the system's silent-pass discipline rather than wondering whether the check ran at all.
230
+
231
+ **AFK authorisation per ADR-013 Rule 6**: review-problems is itself AFK-safe — branch decisions are mechanical per P132 / ADR-044 category 4 silent framework action; Step 4 verification prompts skip silently when `AskUserQuestion` is unavailable per the review-problems Step 4 AFK branch. No new user-attention surface introduced at the Step 0c promotion point.
232
+
233
+ **Compose-with**: ADR-013 Rule 5/6 (silent-pass + AFK fail-safe), ADR-044 category 4 (silent-framework — the trigger is policy + observable evidence), ADR-014 (review-problems' commit grain holds — the pre-flight subprocess emits its own commit), ADR-049 / ADR-080 (PATH shim grammar + highest-version-wins wrapper), ADR-062 § Step 0b (precedent staleness-pre-flight shape), ADR-079 § Step 4.6 (relevance-close composition), P084 + P077 (subprocess isolation reuse — same `claude -p` wrapper as Step 5), P132 (mechanical-stage carve-out — no `AskUserQuestion` at the promotion point), P170 / RFC-002 (dual-tolerant glob — the helper handles both layouts), P317 / RFC-009 (adopter-safe PATH shim).
234
+
235
+ **Staleness contract drift**: the two-axis trigger (count ≥ 3 AND age > 7 days) MUST stay symmetric across the four SKILL surfaces that read it — this Step 0c, `/wr-itil:manage-problem` Step 0.5 (advisory), `/wr-itil:capture-problem` Step 7 (conditional trailing pointer), AND the helper's threshold constants. Drift here re-opens P271. <!-- DEFERRED-PLACEHOLDER-STALENESS-CONTRACT-SOURCE: packages/itil/lib/check-deferred-placeholder-staleness.sh -->
236
+
237
+ <!-- @jtbd JTBD-006 (Progress the Backlog While I'm Away — AFK orchestrator pre-flights review-problems so iters dispatch against fresh WSJF rankings) -->
238
+
239
+ After Step 0c completes (whether dispatched or silent-passed), proceed to Step 1.
190
240
 
191
241
  ### Step 1: Scan the backlog
192
242
 
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Step 0c behavioural fixture per P271:
4
+ # work-problems pre-flights /wr-itil:review-problems when deferred-placeholder
5
+ # tickets accumulate AND the docs/problems/README.md "Last reviewed" line is
6
+ # stale. Mirrors the Step 0b cache-staleness shape (helper-in-lib + 5-outcome
7
+ # enum + behavioural bats) so the SKILL.md Step 0c prose stays a thin
8
+ # source-and-call wrapper around a behaviorally-testable shell function.
9
+ #
10
+ # Trigger rule — two-axis AND per P271 § Recommended fix shape (architect
11
+ # Condition 2 — both axes are load-bearing; either alone over-fires):
12
+ # - count of deferred-placeholder tickets ≥ 3 (signal: there is work to
13
+ # re-rate), AND
14
+ # - README.md "Last reviewed" line-3 age > 7 days (signal: cadence has
15
+ # slipped).
16
+ #
17
+ # Cases covered:
18
+ # 1. No deferred placeholders → "no-deferred-placeholders"
19
+ # 2. Count below threshold (1 or 2) → "below-threshold count=<N> threshold=3"
20
+ # 3. Count ≥ 3 + README absent → "no-readme count=<N>"
21
+ # 4. Count ≥ 3 + README fresh (< 7 days) → "fresh-readme count=<N> age=<X>s threshold=<Y>s"
22
+ # 5. Count ≥ 3 + README stale (> 7 days) → "stale-readme count=<N> age=<X>s threshold=<Y>s"
23
+ # 6. Dual-tolerant glob (per ADR-031 RFC-002 migration window — both flat
24
+ # docs/problems/<NNN>-*.<state>.md AND per-state docs/problems/<state>/<NNN>-*.md
25
+ # contribute to count)
26
+
27
+ setup() {
28
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../../.." && pwd)"
29
+ HELPER="$REPO_ROOT/packages/itil/lib/check-deferred-placeholder-staleness.sh"
30
+
31
+ FIXTURE="$(mktemp -d)"
32
+ mkdir -p "$FIXTURE/docs/problems/open"
33
+ mkdir -p "$FIXTURE/docs/problems/known-error"
34
+ }
35
+
36
+ teardown() {
37
+ rm -rf "$FIXTURE"
38
+ }
39
+
40
+ # Helper — write a problem ticket with the deferred-placeholder marker.
41
+ write_deferred_ticket() {
42
+ local path="$1"
43
+ cat > "$path" <<'EOF'
44
+ # Problem 999: test
45
+
46
+ **Status**: Open
47
+ **Priority**: 3 (Medium) — Impact: 3 x Likelihood: 1 (deferred — re-rate at next /wr-itil:review-problems)
48
+ **Effort**: M (deferred — re-rate at next /wr-itil:review-problems)
49
+
50
+ ## Description
51
+
52
+ test fixture
53
+ EOF
54
+ }
55
+
56
+ # Helper — write a non-deferred (already-rated) ticket.
57
+ write_rated_ticket() {
58
+ local path="$1"
59
+ cat > "$path" <<'EOF'
60
+ # Problem 998: test
61
+
62
+ **Status**: Open
63
+ **Priority**: 6 (Medium) — Impact: 3 x Likelihood: 2
64
+ **Effort**: S
65
+
66
+ ## Description
67
+
68
+ test fixture
69
+ EOF
70
+ }
71
+
72
+ # Helper — write a README.md with a known "Last reviewed" date on line 3.
73
+ write_readme_with_date() {
74
+ local path="$1"
75
+ local iso_date="$2"
76
+ cat > "$path" <<EOF
77
+ # Problem Backlog
78
+
79
+ > Last reviewed: $iso_date **review** — test fixture
80
+ > Run \`/wr-itil:review-problems\` to refresh WSJF rankings.
81
+
82
+ ## WSJF Rankings
83
+ EOF
84
+ }
85
+
86
+ @test "helper exists at the contracted path" {
87
+ [ -f "$HELPER" ]
88
+ }
89
+
90
+ @test "case 1: no deferred placeholders → no-deferred-placeholders" {
91
+ write_rated_ticket "$FIXTURE/docs/problems/open/001-foo.md"
92
+ write_rated_ticket "$FIXTURE/docs/problems/open/002-bar.md"
93
+ # shellcheck disable=SC1090
94
+ source "$HELPER"
95
+ run should_promote_review_problems_dispatch "$FIXTURE"
96
+ [ "$status" -eq 0 ]
97
+ [ "$output" = "no-deferred-placeholders" ]
98
+ }
99
+
100
+ @test "case 2: count below threshold (1) → below-threshold" {
101
+ write_deferred_ticket "$FIXTURE/docs/problems/open/001-foo.md"
102
+ write_rated_ticket "$FIXTURE/docs/problems/open/002-bar.md"
103
+ # shellcheck disable=SC1090
104
+ source "$HELPER"
105
+ run should_promote_review_problems_dispatch "$FIXTURE"
106
+ [ "$status" -eq 0 ]
107
+ [[ "$output" =~ ^below-threshold\ count=1\ threshold=3$ ]]
108
+ }
109
+
110
+ @test "case 2b: count below threshold (2) → below-threshold" {
111
+ write_deferred_ticket "$FIXTURE/docs/problems/open/001-foo.md"
112
+ write_deferred_ticket "$FIXTURE/docs/problems/open/002-bar.md"
113
+ # shellcheck disable=SC1090
114
+ source "$HELPER"
115
+ run should_promote_review_problems_dispatch "$FIXTURE"
116
+ [ "$status" -eq 0 ]
117
+ [[ "$output" =~ ^below-threshold\ count=2\ threshold=3$ ]]
118
+ }
119
+
120
+ @test "case 3: count ≥ 3 + README absent → no-readme (first-run dispatch trigger)" {
121
+ write_deferred_ticket "$FIXTURE/docs/problems/open/001-foo.md"
122
+ write_deferred_ticket "$FIXTURE/docs/problems/open/002-bar.md"
123
+ write_deferred_ticket "$FIXTURE/docs/problems/open/003-baz.md"
124
+ # No README written.
125
+ # shellcheck disable=SC1090
126
+ source "$HELPER"
127
+ run should_promote_review_problems_dispatch "$FIXTURE"
128
+ [ "$status" -eq 0 ]
129
+ [[ "$output" =~ ^no-readme\ count=3$ ]]
130
+ }
131
+
132
+ @test "case 4: count ≥ 3 + README fresh (< 7 days) → fresh-readme silent-pass" {
133
+ write_deferred_ticket "$FIXTURE/docs/problems/open/001-foo.md"
134
+ write_deferred_ticket "$FIXTURE/docs/problems/open/002-bar.md"
135
+ write_deferred_ticket "$FIXTURE/docs/problems/open/003-baz.md"
136
+ # README dated 3 days ago — fresh under 7-day threshold.
137
+ local recent_iso
138
+ recent_iso="$(python3 -c "import datetime; print((datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=3)).strftime('%Y-%m-%d'))")"
139
+ write_readme_with_date "$FIXTURE/docs/problems/README.md" "$recent_iso"
140
+ # shellcheck disable=SC1090
141
+ source "$HELPER"
142
+ run should_promote_review_problems_dispatch "$FIXTURE"
143
+ [ "$status" -eq 0 ]
144
+ [[ "$output" =~ ^fresh-readme\ count=3\ age=[0-9]+s\ threshold=604800s$ ]]
145
+ }
146
+
147
+ @test "case 5: count ≥ 3 + README stale (> 7 days) → stale-readme (dispatch trigger)" {
148
+ write_deferred_ticket "$FIXTURE/docs/problems/open/001-foo.md"
149
+ write_deferred_ticket "$FIXTURE/docs/problems/open/002-bar.md"
150
+ write_deferred_ticket "$FIXTURE/docs/problems/open/003-baz.md"
151
+ # README dated 14 days ago — stale under 7-day threshold.
152
+ local stale_iso
153
+ stale_iso="$(python3 -c "import datetime; print((datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=14)).strftime('%Y-%m-%d'))")"
154
+ write_readme_with_date "$FIXTURE/docs/problems/README.md" "$stale_iso"
155
+ # shellcheck disable=SC1090
156
+ source "$HELPER"
157
+ run should_promote_review_problems_dispatch "$FIXTURE"
158
+ [ "$status" -eq 0 ]
159
+ [[ "$output" =~ ^stale-readme\ count=3\ age=[0-9]+s\ threshold=604800s$ ]]
160
+ }
161
+
162
+ @test "case 6: dual-tolerant glob — per-state subdir layout (ADR-031 post-migration)" {
163
+ # Per-state subdir tickets (post-RFC-002-migration shape — the dominant
164
+ # shape in this monorepo).
165
+ write_deferred_ticket "$FIXTURE/docs/problems/open/100-foo.md"
166
+ write_deferred_ticket "$FIXTURE/docs/problems/open/101-bar.md"
167
+ write_deferred_ticket "$FIXTURE/docs/problems/known-error/102-baz.md"
168
+ # No README → first-run dispatch.
169
+ # shellcheck disable=SC1090
170
+ source "$HELPER"
171
+ run should_promote_review_problems_dispatch "$FIXTURE"
172
+ [ "$status" -eq 0 ]
173
+ [[ "$output" =~ ^no-readme\ count=3$ ]]
174
+ }
175
+
176
+ @test "case 6b: dual-tolerant glob — flat layout (ADR-031 pre-migration window)" {
177
+ # Flat-layout tickets (pre-RFC-002-migration shape — the legacy shape).
178
+ write_deferred_ticket "$FIXTURE/docs/problems/200-foo.open.md"
179
+ write_deferred_ticket "$FIXTURE/docs/problems/201-bar.open.md"
180
+ write_deferred_ticket "$FIXTURE/docs/problems/202-baz.known-error.md"
181
+ # No README → first-run dispatch.
182
+ # shellcheck disable=SC1090
183
+ source "$HELPER"
184
+ run should_promote_review_problems_dispatch "$FIXTURE"
185
+ [ "$status" -eq 0 ]
186
+ [[ "$output" =~ ^no-readme\ count=3$ ]]
187
+ }
188
+
189
+ @test "case 6c: dual-tolerant glob — mixed layout (RFC-002 migration in progress)" {
190
+ # Mixed-layout tickets — both shapes contribute to the count.
191
+ write_deferred_ticket "$FIXTURE/docs/problems/open/100-foo.md"
192
+ write_deferred_ticket "$FIXTURE/docs/problems/200-bar.open.md"
193
+ write_deferred_ticket "$FIXTURE/docs/problems/known-error/300-baz.md"
194
+ # shellcheck disable=SC1090
195
+ source "$HELPER"
196
+ run should_promote_review_problems_dispatch "$FIXTURE"
197
+ [ "$status" -eq 0 ]
198
+ [[ "$output" =~ ^no-readme\ count=3$ ]]
199
+ }
200
+
201
+ @test "case 7: README missing 'Last reviewed' line → no-readme (defensive fallback)" {
202
+ write_deferred_ticket "$FIXTURE/docs/problems/open/001-foo.md"
203
+ write_deferred_ticket "$FIXTURE/docs/problems/open/002-bar.md"
204
+ write_deferred_ticket "$FIXTURE/docs/problems/open/003-baz.md"
205
+ # README exists but no "Last reviewed:" marker on line 3.
206
+ cat > "$FIXTURE/docs/problems/README.md" <<'EOF'
207
+ # Problem Backlog
208
+
209
+ Some other line that does not have the Last reviewed marker.
210
+
211
+ ## WSJF Rankings
212
+ EOF
213
+ # shellcheck disable=SC1090
214
+ source "$HELPER"
215
+ run should_promote_review_problems_dispatch "$FIXTURE"
216
+ [ "$status" -eq 0 ]
217
+ # Defensive: parse failure routes to no-readme (forces a fresh dispatch
218
+ # rather than silently silent-passing on malformed README).
219
+ [[ "$output" =~ ^no-readme\ count=3$ ]]
220
+ }
221
+
222
+ @test "case 8: closed and verifying tickets do NOT contribute to count" {
223
+ # Only open + known-error tickets are eligible re-rate candidates.
224
+ # .verifying.md and .closed.md tickets carry no WSJF (excluded from
225
+ # dev-work ranking per ADR-022); their deferred-placeholders must not
226
+ # influence the dispatch.
227
+ mkdir -p "$FIXTURE/docs/problems/closed"
228
+ mkdir -p "$FIXTURE/docs/problems/verifying"
229
+ write_deferred_ticket "$FIXTURE/docs/problems/closed/100-foo.md"
230
+ write_deferred_ticket "$FIXTURE/docs/problems/verifying/101-bar.md"
231
+ write_deferred_ticket "$FIXTURE/docs/problems/open/102-baz.md"
232
+ # shellcheck disable=SC1090
233
+ source "$HELPER"
234
+ run should_promote_review_problems_dispatch "$FIXTURE"
235
+ [ "$status" -eq 0 ]
236
+ # Only the open/ ticket counts → below threshold.
237
+ [[ "$output" =~ ^below-threshold\ count=1\ threshold=3$ ]]
238
+ }