@windyroad/itil 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.45.0"
500
+ "version": "0.46.0"
501
501
  }
@@ -36,10 +36,17 @@
36
36
  # Exported functions:
37
37
  # emit_stderr_advisory <skill> <field> <value> <source> [reversibility]
38
38
  # derive_kebab_slug <description> [max_tokens=8]
39
- # lexical_classify_two_sided <text> <side_a_patterns_var> <side_b_patterns_var>
40
39
  # risk_policy_matrix_lookup <text> <impact_high> <impact_mod> <impact_low>
41
40
  # <likelihood_high> <likelihood_med> <likelihood_low>
42
41
  #
42
+ # RETIRED 2026-06-02 (P287): lexical_classify_two_sided was the two-sided
43
+ # binary classifier used exclusively by capture-problem Step 1.5 Type
44
+ # classification (technical vs user-business). With the type axis retired
45
+ # per twice-confirmed user direction, the function has no remaining
46
+ # consumer and was removed. The slug + advisory + matrix helpers stay —
47
+ # they serve manage-incident severity, manage-problem priority, and
48
+ # create-adr title derivation.
49
+ #
43
50
  # @adr ADR-002 (Monorepo per-plugin packages — architecture context for ADR-017)
44
51
  # @adr ADR-017 (Shared code duplicated into per-package lib/ kept in sync)
45
52
  # @adr ADR-044 (Decision-Delegation Contract — derive-first framework boundary)
@@ -105,60 +112,6 @@ derive_kebab_slug() {
105
112
  | paste -sd '-' -
106
113
  }
107
114
 
108
- # ---------------------------------------------------------------------------
109
- # lexical_classify_two_sided — two-sided binary lexical classifier.
110
- #
111
- # Used by capture-problem Step 1.5 Type classification (technical vs
112
- # user-business). Callers pass description text plus two regex pattern
113
- # arrays (by name); helper counts hits per side and echoes one of:
114
- #
115
- # SIDE_A_UNAMBIGUOUS|<matched signals (comma-separated)>
116
- # ≥1 side-A signal hit AND 0 side-B signals hit.
117
- # SIDE_B_UNAMBIGUOUS|<matched signals (comma-separated)>
118
- # 0 side-A signals hit AND ≥1 side-B signal hit.
119
- # AMBIGUOUS|<a=N b=N>
120
- # Mixed (both sides matched) OR zero (neither side matched).
121
- #
122
- # Caller is responsible for:
123
- # - Mapping SIDE_A/SIDE_B to its domain values (e.g. technical / user-business).
124
- # - Calling emit_stderr_advisory on the unambiguous path.
125
- # - Firing AskUserQuestion on the AMBIGUOUS path (ADR-044 category-5 taste fallback).
126
- # ---------------------------------------------------------------------------
127
- lexical_classify_two_sided() {
128
- local description="$1"
129
- local -n _side_a_patterns_ref="$2"
130
- local -n _side_b_patterns_ref="$3"
131
- local a_hits=()
132
- local b_hits=()
133
- local pattern
134
-
135
- for pattern in "${_side_a_patterns_ref[@]}"; do
136
- if printf '%s' "$description" | grep -qiE "$pattern" 2>/dev/null; then
137
- a_hits+=("$pattern")
138
- fi
139
- done
140
- for pattern in "${_side_b_patterns_ref[@]}"; do
141
- if printf '%s' "$description" | grep -qiE "$pattern" 2>/dev/null; then
142
- b_hits+=("$pattern")
143
- fi
144
- done
145
-
146
- local a_count="${#a_hits[@]}"
147
- local b_count="${#b_hits[@]}"
148
-
149
- if (( a_count >= 1 && b_count == 0 )); then
150
- local joined
151
- joined=$(IFS=,; echo "${a_hits[*]}")
152
- printf 'SIDE_A_UNAMBIGUOUS|%s\n' "$joined"
153
- elif (( a_count == 0 && b_count >= 1 )); then
154
- local joined
155
- joined=$(IFS=,; echo "${b_hits[*]}")
156
- printf 'SIDE_B_UNAMBIGUOUS|%s\n' "$joined"
157
- else
158
- printf 'AMBIGUOUS|a=%d b=%d\n' "$a_count" "$b_count"
159
- fi
160
- }
161
-
162
115
  # ---------------------------------------------------------------------------
163
116
  # risk_policy_matrix_lookup — RISK-POLICY.md Impact × Likelihood lookup.
164
117
  #
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -6,18 +6,24 @@ bats_require_minimum_version 1.5.0
6
6
  # the shared derive-first dispatch helper extracted in P132 Phase 2a-iii-A.
7
7
  #
8
8
  # The helper centralises the dispatch mechanism shipped across three
9
- # declaration-skill surfaces (capture-problem Step 1.5, manage-incident
10
- # Step 4, manage-problem Step 4). Each caller passes surface-specific
11
- # signal definitions; the helper owns:
9
+ # declaration-skill surfaces (manage-incident Step 4, manage-problem
10
+ # Step 4, create-adr Step 2 — Phase 2a-iii-B 4th adopter). Each caller
11
+ # passes surface-specific signal definitions; the helper owns:
12
12
  #
13
13
  # - Slug derivation (Title) from prose
14
- # - Two-sided lexical classifier (Type for capture-problem)
15
14
  # - RISK-POLICY matrix lookup (Severity / Priority)
16
15
  # - I2-isomorphic stderr advisory format
17
16
  #
17
+ # P287 (2026-06-02) retired the two-sided lexical classifier (formerly
18
+ # used by capture-problem Step 1.5 Type classification — technical vs
19
+ # user-business). The type axis was removed as redundant with RFC/Story
20
+ # persona-anchoring per ADR-060 Phase 4; the function had no remaining
21
+ # consumer.
22
+ #
18
23
  # @problem P132 (agents over-ask in interactive sessions — Phase 2a-iii-A
19
24
  # shared helper extraction)
20
- # @problem P185 (capture-problem Step 1.5 worked-example precedent)
25
+ # @problem P185 (capture-problem Step 1.5 worked-example precedent — retired by P287)
26
+ # @problem P287 (type-classification retirement — lexical_classify_two_sided removed)
21
27
  # @adr ADR-044 (Decision-Delegation Contract — derive-first framework
22
28
  # resolution boundary)
23
29
  # @adr ADR-026 (cost-source grounding — stderr advisory shape)
@@ -118,51 +124,22 @@ setup() {
118
124
  }
119
125
 
120
126
  # ----------------------------------------------------------------------
121
- # Two-sided lexical classifier (capture-problem Step 1.5 mechanism).
122
- # Returns:
123
- # SIDE_A_UNAMBIGUOUS|<matched signals> — ≥1 A hit AND 0 B hits
124
- # SIDE_B_UNAMBIGUOUS|<matched signals> — 0 A hits AND ≥1 B hit
125
- # AMBIGUOUS|<reason> — mixed (both sides) OR zero
127
+ # P287 (2026-06-02) retirement: lexical_classify_two_sided removed.
128
+ # Regression guard — the function must NOT exist in the helper.
126
129
  # ----------------------------------------------------------------------
127
130
 
128
- @test "lexical_classify_two_sided returns SIDE_A_UNAMBIGUOUS on technical-only signals" {
129
- run -0 bash -c '
130
- source "'"$HELPER"'"
131
- side_a=("\\b(hook|gate|regex|stderr|stdout|drift|TTL|cache)\\b")
132
- side_b=("\\b(adopter|UX|friction|JTBD-[0-9]+)\\b")
133
- lexical_classify_two_sided "the hook fires on stderr and the cache invalidates" side_a side_b
134
- '
135
- [[ "$output" == "SIDE_A_UNAMBIGUOUS|"* ]]
131
+ @test "P287: lexical_classify_two_sided is removed from the helper (regression guard)" {
132
+ # The function name must not appear as a function definition.
133
+ ! grep -E '^lexical_classify_two_sided\(\)' "$HELPER"
136
134
  }
137
135
 
138
- @test "lexical_classify_two_sided returns SIDE_B_UNAMBIGUOUS on user-business-only signals" {
139
- run -0 bash -c '
140
- source "'"$HELPER"'"
141
- side_a=("\\b(hook|gate|regex|stderr|stdout|drift|TTL|cache)\\b")
142
- side_b=("\\b(adopter|UX|friction|JTBD-[0-9]+)\\b")
143
- lexical_classify_two_sided "the adopter friction makes JTBD-101 hard to complete" side_a side_b
144
- '
145
- [[ "$output" == "SIDE_B_UNAMBIGUOUS|"* ]]
146
- }
147
-
148
- @test "lexical_classify_two_sided returns AMBIGUOUS on mixed signals" {
149
- run -0 bash -c '
150
- source "'"$HELPER"'"
151
- side_a=("\\b(hook|gate|regex|stderr)\\b")
152
- side_b=("\\b(adopter|UX|friction)\\b")
153
- lexical_classify_two_sided "the hook causes adopter friction" side_a side_b
154
- '
155
- [[ "$output" == "AMBIGUOUS|"* ]]
156
- }
157
-
158
- @test "lexical_classify_two_sided returns AMBIGUOUS on zero signals" {
159
- run -0 bash -c '
160
- source "'"$HELPER"'"
161
- side_a=("\\b(hook|gate)\\b")
162
- side_b=("\\b(adopter|UX)\\b")
163
- lexical_classify_two_sided "totally bland text with no signals at all" side_a side_b
164
- '
165
- [[ "$output" == "AMBIGUOUS|"* ]]
136
+ @test "P287: helper still exports the three surviving functions" {
137
+ # emit_stderr_advisory, derive_kebab_slug, risk_policy_matrix_lookup
138
+ # serve manage-incident severity, manage-problem priority, and
139
+ # create-adr title derivation — they must stay.
140
+ grep -E '^emit_stderr_advisory\(\)' "$HELPER"
141
+ grep -E '^derive_kebab_slug\(\)' "$HELPER"
142
+ grep -E '^risk_policy_matrix_lookup\(\)' "$HELPER"
166
143
  }
167
144
 
168
145
  # ----------------------------------------------------------------------
@@ -243,13 +220,6 @@ setup() {
243
220
  # packages/architect/lib/ stay byte-identical via scripts/sync-derive-first-dispatch.sh.
244
221
  # ----------------------------------------------------------------------
245
222
 
246
- @test "capture-problem Step 1.5 cross-references derive-first-dispatch.sh helper" {
247
- run grep -c "derive-first-dispatch\\.sh\\|packages/itil/lib/derive-first-dispatch" \
248
- "${PKG_ROOT}/skills/capture-problem/SKILL.md"
249
- [ "$status" -eq 0 ]
250
- [ "$output" -ge 1 ]
251
- }
252
-
253
223
  @test "manage-incident Step 4 cross-references derive-first-dispatch.sh helper" {
254
224
  run grep -c "derive-first-dispatch\\.sh\\|packages/itil/lib/derive-first-dispatch" \
255
225
  "${PKG_ROOT}/skills/manage-incident/SKILL.md"
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P287 regression guard — replaces the historical i2-no-type-branching.bats
4
+ # (which asserted no-control-flow-branch-on-type as an I2 invariant).
5
+ #
6
+ # With the type axis retired per twice-confirmed user direction
7
+ # (2026-05-25 + 2026-06-02), the I2 branch-protection invariant is
8
+ # vacuous (no `type` field to branch on). This regression guard
9
+ # preserves the audit-trail compliance evidence per architect-review
10
+ # verdict 2026-06-02 by asserting the POSITIVE state:
11
+ #
12
+ # - The `**Type**:` body field is GONE from the capture-problem
13
+ # skeleton template and from every committed problem ticket.
14
+ # - The `lexical_classify_two_sided` helper function is removed.
15
+ # - capture-problem SKILL.md no longer carries the Step 1.5 Type
16
+ # classification section or its dispatch flags.
17
+ #
18
+ # Drift here = silent reintroduction of the type axis = P287 regression.
19
+ #
20
+ # @problem P287 (type-classification retirement)
21
+ # @problem P176 (agent-side I2 coverage gap — historical; with the
22
+ # type axis retired the gap is vacuous on that axis)
23
+ # @adr ADR-052 (behavioural-by-default; this is a regression-guard
24
+ # structural assertion per Surface 2 escape-hatch contract — the
25
+ # contract surface IS the on-disk artefact state)
26
+ # @adr ADR-014 (single-purpose; one mechanical regression invariant)
27
+ # @jtbd JTBD-001 (enforce governance without slowing down — primary)
28
+
29
+ setup() {
30
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
31
+ PKG_ROOT="$REPO_ROOT/packages/itil"
32
+ SKILL_FILE="$PKG_ROOT/skills/capture-problem/SKILL.md"
33
+ HELPER="$REPO_ROOT/packages/shared/derive-first-dispatch.sh"
34
+ [ -f "$SKILL_FILE" ]
35
+ [ -f "$HELPER" ]
36
+ }
37
+
38
+ @test "P287: no committed problem ticket carries a **Type**: body field" {
39
+ # Find any docs/problems/**/*.md ticket that still carries the
40
+ # retired type field on a body line. Any match = P287 regression.
41
+ result=$(grep -r --include='*.md' -l '^\*\*Type\*\*: ' "$REPO_ROOT/docs/problems/" 2>/dev/null || true)
42
+ [ -z "$result" ]
43
+ }
44
+
45
+ @test "P287: capture-problem skeleton template does not include a **Type**: line" {
46
+ ! grep -F '**Type**: <type_value>' "$SKILL_FILE"
47
+ ! grep -F '**Type**: technical' "$SKILL_FILE"
48
+ ! grep -F '**Type**: user-business' "$SKILL_FILE"
49
+ }
50
+
51
+ @test "P287: capture-problem SKILL.md does not carry a Step 1.5 Type classification header" {
52
+ # The retired Step 1.5 section was titled "### 1.5 Type classification".
53
+ ! grep -E '^### 1\.5 Type classification' "$SKILL_FILE"
54
+ }
55
+
56
+ @test "P287: shared dispatch helper does not export lexical_classify_two_sided" {
57
+ ! grep -E '^lexical_classify_two_sided\(\)' "$HELPER"
58
+ }
59
+
60
+ @test "P287: per-package lib/ copies are byte-identical to the canonical shared/ source" {
61
+ # ADR-017 sync compliance — the strip removed lexical_classify_two_sided
62
+ # from the canonical packages/shared/derive-first-dispatch.sh; the
63
+ # sync script must propagate the removal to per-package copies.
64
+ diff -q "$HELPER" "$REPO_ROOT/packages/itil/lib/derive-first-dispatch.sh"
65
+ diff -q "$HELPER" "$REPO_ROOT/packages/architect/lib/derive-first-dispatch.sh"
66
+ }
67
+
68
+ @test "P287: capture-problem SKILL.md does not carry --type= or --no-prompt flag rows" {
69
+ # The retired Step 1.5 dispatch flags must not appear in the flag table.
70
+ ! grep -E '^\| `--type=technical`' "$SKILL_FILE"
71
+ ! grep -E '^\| `--type=user-business`' "$SKILL_FILE"
72
+ ! grep -E '^\| `--no-prompt`' "$SKILL_FILE"
73
+ }
@@ -23,7 +23,7 @@ This skill is the foreground-lightweight-capture variant of `/wr-itil:manage-pro
23
23
 
24
24
  ## Rule 6 audit (per ADR-032 + ADR-013)
25
25
 
26
- This skill has **at most one classification-only AskUserQuestion (type-tag, ambiguous-signal fallback only) and zero control-flow branches keyed on the answer**. Each potentially-interactive decision is framework-mediated per ADR-044:
26
+ This skill has **at most one classification-only AskUserQuestion (persona, on JTBD-cited descriptions with disagreeing personas only) and zero control-flow branches keyed on the answer**. Each potentially-interactive decision is framework-mediated per ADR-044:
27
27
 
28
28
  | Decision | Resolution |
29
29
  |----------|-----------|
@@ -33,9 +33,12 @@ This skill has **at most one classification-only AskUserQuestion (type-tag, ambi
33
33
  | Effort default | Framework-policy: `M` flagged "deferred — re-rate at next /wr-itil:review-problems". |
34
34
  | Multi-concern split | Out of scope: capture-problem creates one ticket per invocation. Multi-concern observations route to `/wr-itil:manage-problem` (its Step 4b owns the split). |
35
35
  | Empty `$ARGUMENTS` | Halt-with-stderr-directive: print "capture-problem requires a description in $ARGUMENTS — invoke /wr-itil:manage-problem instead for the full intake flow" and exit. AFK orchestrators MUST NOT invoke capture-problem with empty arguments — caller-side contract. |
36
- | Type classification (P170 / ADR-060 item 8c; P185 derive-first refactor) | **Derive-first; silent-framework per ADR-044 category 4 on unambiguous-signal descriptions; taste fallback per category 5 on ambiguous descriptions only.** Step 1.5 runs a lexical-signal classifier against the description text. Unambiguous one-sided signal classify silently + emit stderr advisory. Mixed or zero signals → AskUserQuestion. `--type=<value>` pre-resolves silently (highest priority). `--no-prompt` defaults to `technical` silently (AFK contract). Maintainer-side ONLY: this dispatch is paired with JTBD-301 protection `.github/ISSUE_TEMPLATE/problem-report.yml` (plugin-user-side intake) MUST NOT carry an equivalent type selector; triage assigns the type during `/wr-itil:manage-problem` ingestion of user-reported issues. **The classifier is also NOT invoked from `/wr-itil:manage-problem`'s ingestion-of-plugin-user-reports path** — plugin-user descriptions do not carry the same authorial intent as maintainer-internal captures, so triage stays user-judgement per JTBD-301. **I2 invariant** (ADR-060 line 98): the prompt is a classification facet, not a workflow split — Steps 0-7 control-flow is identical regardless of the chosen `type_value`; only the substituted value in the Step 4 skeleton template differs. The stderr advisory text shape is also I2-isomorphic: identical sentence structure across `technical` vs `user-business` classifications beyond the substituted values + signal names. |
36
+ | JTBD-trace derivation (P287 retained surface) | **Derive-first; silent-framework per ADR-044 category 4 on lexical citations or `--jtbd=` flag pre-resolution.** Step 1.5b runs a lexical detector (`\bJTBD-[0-9]+\b`) against the description. Any cited JTBD IDs are recorded silently. No AskUserQuestion in this dispatch — capture-time JTBD anchoring is optional; the next reviewer can refine at `/wr-itil:review-problems` or `/wr-itil:manage-problem` ingestion. |
37
+ | Persona derivation (P287 retained surface, decoupled from type) | **Derive-first; silent-framework per ADR-044 category 4 when JTBDs cited and agree.** If JTBDs were cited at Step 1.5b, persona derives from the cited JTBDs' frontmatter. Disagreement across cited JTBDs falls back to AskUserQuestion (genuine taste). Empty persona is legal — no hard-block. |
37
38
 
38
- Per ADR-013 Rule 6 fail-safe: every decision above resolves without interactive user input in non-interactive contexts (the type-tag carve-out resolves to `technical` via `--no-prompt` or `--type=` caller-side pre-resolution, or via the derive-first classifier on unambiguous descriptions). AFK orchestrators MUST pass `--no-prompt` or `--type=<value>` per JTBD-006 § Persona Constraints; AFK callers that omit both flags violate the caller-side contract. Interactive (pre-resolved, derive-classified, or ambiguous-fallback) and pre-resolved AFK paths produce identical observable outputs except for the `**Type**:` field value, satisfying the I2 invariant by construction.
39
+ **P287 amendment 2026-06-02 type-classification retired**: the maintainer-side type-classification dispatch (technical vs user-business) was REMOVED per twice-confirmed user direction (2026-05-25 + 2026-06-02). The redundant axis was already covered by RFC/Story persona-anchoring per ADR-060 Phase 4. JTBD-trace + persona dispatch survive as the JTBD-as-source-of-truth surface; the I12 hard-block (type-keyed JTBD-required halt) is also retired pending ADR-060 amendment substance ratification.
40
+
41
+ Per ADR-013 Rule 6 fail-safe: every decision above resolves without interactive user input in non-interactive contexts. The Persona fallback AskUserQuestion fires only on cited-JTBD-disagreement (genuine ADR-044 category 5 taste); AFK orchestrators avoid this branch by passing `--persona=<value>` or by capturing without JTBD citations.
39
42
 
40
43
  ## Steps
41
44
 
@@ -60,84 +63,36 @@ fi
60
63
 
61
64
  `$ARGUMENTS` may carry up to two leading flags before the free-text description (caller-side pre-resolution per ADR-044 silent-proceed shape):
62
65
 
63
- | Flag | Effect on Step 1.5 |
66
+ | Flag | Effect on Step 1.5b |
64
67
  |------|-------------------|
65
- | `--type=technical` | Pre-resolves type to `technical`; Step 1.5 skips the classifier and the AskUserQuestion. |
66
- | `--type=user-business` | Pre-resolves type to `user-business`; Step 1.5 skips the classifier and the AskUserQuestion. |
67
- | `--no-prompt` | Pre-resolves type to `technical` (default); Step 1.5 skips the classifier and the AskUserQuestion. |
68
- | `--jtbd=JTBD-NNN[,JTBD-NNN...]` | Pre-resolves the JTBD-trace value (Phase 4 P3.1 + I12 invariant per ADR-060 § Phase 3 + Phase 4 in-scope amendment 2026-05-13). Step 1.5b skips the JTBD-trace lexical dispatch and the I12 hard-block. Comma-separated list of JTBD IDs (no spaces). |
69
- | `--persona=<value>` | Pre-resolves the persona value (Phase 4 P4.2). Step 1.5b skips persona derivation. Value MUST be one of: `developer`, `tech-lead`, `plugin-developer`, `plugin-user`. |
68
+ | `--jtbd=JTBD-NNN[,JTBD-NNN...]` | Pre-resolves the JTBD-trace value. Step 1.5b skips the JTBD-trace lexical dispatch. Comma-separated list of JTBD IDs (no spaces). |
69
+ | `--persona=<value>` | Pre-resolves the persona value. Step 1.5b skips persona derivation. Value MUST be one of: `developer`, `tech-lead`, `plugin-developer`, `plugin-user`. |
70
+
71
+ Strip recognised leading flags from `$ARGUMENTS`; the remainder (after flags) is the free-text description. Unknown leading flags halt-with-stderr-directive: print "capture-problem: unknown flag '<flag>' recognised flags: --jtbd=JTBD-NNN, --persona=<value>" and exit.
70
72
 
71
- Strip recognised leading flags from `$ARGUMENTS`; the remainder (after flags) is the free-text description. If both `--type=<value>` and `--no-prompt` are present, `--type=<value>` wins (more specific). Unknown leading flags halt-with-stderr-directive: print "capture-problem: unknown flag '<flag>' recognised flags: --type=technical, --type=user-business, --no-prompt, --jtbd=JTBD-NNN, --persona=<value>" and exit.
73
+ **P287 retirement note**: `--type=technical`, `--type=user-business`, and `--no-prompt` are RETIRED (P287, 2026-06-02). The type-classification axis was removed per twice-confirmed user direction; the AFK-default flag `--no-prompt` is obsolete since the only AskUserQuestion it suppressed is gone. AFK orchestrators that previously passed `--no-prompt` should drop the flag; capture-problem is now silent-by-default.
72
74
 
73
75
  Empty description (post-flag-strip) halts per the Rule 6 audit above.
74
76
 
75
77
  Derive a kebab-case title slug from the first 8-10 non-stopword tokens of the description (matching the existing `manage-problem` slug derivation pattern).
76
78
 
77
- ### 1.5 Type classification (derive-first; silent-framework per ADR-044 category 4; taste fallback per category 5 on ambiguity)
78
-
79
- **Shared dispatch helper**: this surface invokes `packages/itil/lib/derive-first-dispatch.sh` for the canonical lexical-classifier mechanism + I2-isomorphic stderr advisory format. The helper is sourced by `/wr-itil:capture-problem`, `/wr-itil:manage-incident`, and `/wr-itil:manage-problem`; drift in the advisory shape re-opens P132. Surface-specific signal definitions (technical-vs-user-business regex lists) stay inline below — the helper owns the mechanism, not the per-surface signals (architect verdict 2026-05-15 P132 Phase 2a-iii-A: "Helper must preserve per-surface signal definitions; only the dispatch mechanism is shared").
80
-
81
- Resolve `type_value` ∈ {`technical`, `user-business`} per the following framework-mediated dispatch. **The dispatch order is load-bearing** — pre-resolution flags short-circuit BEFORE the classifier runs, and the AskUserQuestion fires ONLY on genuinely-ambiguous descriptions.
82
-
83
- 1. **If `--type=<value>` was set in Step 1**: use that value; do NOT run the classifier; do NOT fire AskUserQuestion (silent-proceed per ADR-013 Rule 5).
84
- 2. **Else if `--no-prompt` was set in Step 1**: default `type_value = technical`; do NOT run the classifier; do NOT fire AskUserQuestion. JTBD-006 protection: AFK orchestrators MUST pass this flag (or `--type=<value>`).
85
- 3. **Else** (interactive context, no caller-side pre-resolution): run the **lexical-signal classifier** against the description text:
86
-
87
- **Technical signals** (regex; matched against the post-flag-strip description, case-insensitive unless noted):
88
- - Code identifiers: `[a-z]+[A-Z][a-zA-Z]+` (camelCase), `[a-z]+-[a-z][a-z-]+` (kebab-case identifiers with ≥2 hyphens), `[a-z]+_[a-z][a-z_]+` (snake_case).
89
- - File paths: `\.(md|sh|bats|ts|js|json|yaml|yml|py|rb|go|css|html)\b`, `packages/[a-z-]+/`, `docs/[a-z-]+/`, `\.github/`, `\.claude/`, `/tmp/`.
90
- - Command-name patterns: `/wr-[a-z-]+:[a-z-]+\b`, `\bgit (commit|push|mv|add|rebase|merge)\b`, `\bnpm (run|install|publish)\b`, `\bbash\b`, `\bbats\b`, `\bgrep\b`, `\bsed\b`, `\bjq\b`.
91
- - Mechanism words: `\b(drift|regression|hook|marker|gate|refresh|idempotent|exit code|stderr|stdout|regex|formula|dispatch|frontmatter|substring|escape|sentinel|bypass|TTL|cache|invalidate|deduplicate|race|deadlock|timeout|preflight)\b`.
92
- - Error-message patterns: `\b(error|failure|exception|panic|stack trace|segfault|null pointer|undefined|not found|permission denied|EACCES|ENOENT|exit \d+)\b`.
93
-
94
- **User-business signals** (regex; same casing):
95
- - Persona names: `\b(adopter|adopters|plugin-user|plugin-users|solo[-_ ]?developer|maintainer-persona|end[-_ ]?user|customer|stakeholder)\b`.
96
- - Journey words: `\b(workflow|journey|onboarding|friction|UX|experience|usability|discoverability|cognitive load|attention|interrupt|context-switch)\b`.
97
- - JTBD-shaped need words: `\bJTBD-\d+\b`, `\bjob-to-be-done\b`, `\b(want|need|can't|cannot|blocked from|unable to|hard to|painful to)\b\s+(use|access|find|discover|complete)`, `\bdesired outcome\b`, `\bunmet need\b`.
98
-
99
- **Decision rule**:
100
-
101
- - **Unambiguous technical** (≥1 technical signal AND 0 user-business signals): set `type_value = technical`; emit stderr advisory and proceed. Do NOT fire AskUserQuestion.
102
- - **Unambiguous user-business** (0 technical signals AND ≥1 user-business signal): set `type_value = user-business`; emit stderr advisory and proceed. Do NOT fire AskUserQuestion.
103
- - **Ambiguous** (≥1 signal each side, OR 0 signals on both sides): fire AskUserQuestion with options `technical` (default) and `user-business`. Question text: *"What type of problem is this?"* Per-option descriptions:
104
- - `technical` — *"Bug, defect, broken behaviour, framework drift — root cause sits in code or process."*
105
- - `user-business` — *"Missing capability, UX gap, adopter friction, JTBD-shaped need — root cause sits in unmet user need."*
79
+ ### 1.5b JTBD-trace + persona dispatch (P287 decoupled from type)
106
80
 
107
- **Stderr advisory contract** (silent-classification path only): emit a SINGLE line to stderr (NOT stdout, NOT in the ticket body) via the shared helper's `emit_stderr_advisory` function in `packages/itil/lib/derive-first-dispatch.sh`. The canonical format produced by the helper:
108
-
109
- ```
110
- capture-problem: derived type=<value> from description signals: <signal1>, <signal2>[, ...]; re-invoke with --type=<other-value> to override
111
- ```
112
-
113
- The advisory text shape is I2-isomorphic — same sentence structure (`<skill>: derived <field>=<value> from <source>; <reversibility>`) across all three derive-first declaration-skill surfaces. The helper is the single source-of-truth for this format; drift here re-opens P132. Embedding the advisory in stdout would risk machine-readers parsing it as a ticket-body line; embedding it in the ticket body would violate ADR-060's frontmatter / body-bullet schema. Stderr is the correct channel — visible to interactive maintainers in the terminal; invisible to ticket consumers; loggable by AFK orchestrators that capture subprocess stderr.
114
-
115
- **I2 invariant guard (ADR-060 line 98)**: the resolved `type_value` is used at Step 4 ONLY as a substituted string in the skeleton template's `**Type**:` body field. Steps 2, 3, 4 (other than the `**Type**:` substitution), 5, 6, 7 execute identically regardless of `type_value`. The skill carries NO control-flow branch keyed on `type` — that would convert classification into a workflow split and violate I2. The lexical-signal classifier is UPSTREAM of the value's substitution (it resolves WHICH value to substitute, not WHICH workflow to execute); the substitution and all downstream steps remain uniform. Pure-bash supporting-script enforcement of this invariant lives in `packages/itil/scripts/test/i2-no-type-branching.bats`; the SKILL.md surface coverage gap is named at P176 (descendant of P012 master harness).
116
-
117
- **JTBD-301 scope guard**: this dispatch fires on the maintainer-side `/wr-itil:capture-problem` skill only. The plugin-user-side intake (`.github/ISSUE_TEMPLATE/problem-report.yml`) MUST NOT carry an equivalent type selector — plugin-user persona constraint is "no pre-classification". Triage assigns `type` during `/wr-itil:manage-problem` ingestion of user-reported issues, not at user-report time. **The lexical-signal classifier is ALSO NOT invoked from `/wr-itil:manage-problem`'s ingestion-of-plugin-user-reports path** — plugin-user descriptions do not carry the same authorial intent as maintainer-internal captures (a plugin-user describing their friction in maintainer-vocabulary terms would mis-classify); triage stays user-judgement, not lexical-classifier inference.
118
-
119
- ### 1.5b JTBD-trace + persona dispatch (Phase 3 P3.1 + Phase 4 P4.2 + I12 invariant)
120
-
121
- Per ADR-060 § Phase 3 + Phase 4 in-scope amendment (2026-05-13). Fires ONLY when `type_value` resolved to `user-business` (whether via `--type=user-business` flag, `--no-prompt` default override is impossible since `--no-prompt` defaults to `technical`, the classifier silent-resolve to `user-business`, or the ambiguous-fallback AskUserQuestion). For `type_value = technical`, Steps 1.5b and the I12 hard-block do NOT fire (technical problems may carry empty `jtbd:` array; persona is optional). The whole dispatch keys on **nullable-field-conditional** shape per ADR-060 line 536 — NEVER on `type` value as a control-flow branch (preserves I2 invariant; the type co-incidence is upstream input, not control-flow key).
81
+ Per ADR-060 § Phase 3 + Phase 4 in-scope amendment (2026-05-13), as amended by P287 (2026-06-02 type-classification retired). Fires UNCONDITIONALLY (no longer keyed on `type_value = user-business`; the type axis was removed). Both `jtbd_trace_value` and `persona_value` are OPTIONAL — capture-time anchoring is best-effort; the next reviewer can refine at `/wr-itil:review-problems` or `/wr-itil:manage-problem` ingestion.
122
82
 
123
83
  **Resolve `jtbd_trace_value`** (an ORDERED list of JTBD IDs, possibly empty) via the following dispatch:
124
84
 
125
85
  1. **If `--jtbd=JTBD-NNN[,JTBD-NNN...]` was set in Step 1**: parse comma-separated list; assign to `jtbd_trace_value`; do NOT run the lexical detector; do NOT fire AskUserQuestion (silent-proceed per ADR-013 Rule 5).
126
86
  2. **Else** run the **lexical JTBD-trace detector** against the description: `grep -oE '\bJTBD-[0-9]+\b' | sort -u`. If matches found, set `jtbd_trace_value` to the matched IDs (de-duplicated, sorted ascending) and emit stderr advisory: `capture-problem: derived jtbd-trace=<id-list> from description JTBD-NNN citations; re-invoke with --jtbd= to override`. Do NOT fire AskUserQuestion.
127
- 3. **Else (no flag, no lexical detection, type=user-business)**: **I12 hard-block** per ADR-060 Confirmation criterion 10. Halt-with-stderr-directive: print `capture-problem: I12 invariant type: user-business requires ≥1 JTBD trace. Re-invoke with --jtbd=JTBD-NNN, OR edit the description to cite a JTBD-NNN ID, OR re-classify as technical via --type=technical.` and exit. This branch is the load-bearing enforcement of the new I12 invariant JTBD-as-source-of-truth for persona-anchored unmet need; user-business problems MUST cite ≥1 JTBD.
87
+ 3. **Else (no flag, no lexical detection)**: leave `jtbd_trace_value` empty. The `**JTBD**:` line is omitted from the Step 4 skeleton template. No hard-block capture-time JTBD anchoring is optional under P287; the I12 hard-block (type-keyed JTBD-required halt) was retired alongside the type axis. ADR-060 amendment substance (whether JTBD anchoring should become a nullable-field-conditional gate keyed on some other discriminator) is queued for user re-confirmation per ADR-074.
128
88
 
129
89
  **Resolve `persona_value`** (a scalar enum value OR empty) via the following dispatch:
130
90
 
131
91
  1. **If `--persona=<value>` was set in Step 1**: validate `<value>` ∈ `{developer, tech-lead, plugin-developer, plugin-user}`; halt with directive if invalid; otherwise assign and proceed silently.
132
- 2. **Else if `jtbd_trace_value` is non-empty**: derive persona from cited JTBDs' frontmatter. Read each cited `docs/jtbd/<persona>/JTBD-<NNN>-*.md` file; extract its `persona:` (and optionally `secondary-persona:`) frontmatter values; if all cited JTBDs agree on a single persona, set `persona_value` to that persona silently and emit stderr advisory: `capture-problem: derived persona=<value> from cited JTBD <id> frontmatter`. If cited JTBDs disagree, fire AskUserQuestion with the union-of-derived-personas as options.
133
- 3. **Else if `type_value = user-business`**: AskUserQuestion fires with the closed enum as options. Per ADR-060 P4.2: `developer | tech-lead | plugin-developer | plugin-user`. Question text: *"What persona does this user-business problem serve?"*
134
- 4. **Else (`type_value = technical`)**: leave `persona_value` empty. `persona:` frontmatter is OPTIONAL on technical problems.
135
-
136
- **I12 hard-block escape hatch (none)**: there is no `BYPASS_I12=1` env override at the SKILL surface. The block is a load-bearing schema constraint per ADR-060 Confirmation criterion 10; if a maintainer captures a user-business problem without yet knowing the JTBD trace, they must EITHER capture as `--type=technical` and re-classify during `/wr-itil:manage-problem` ingestion (when the JTBD becomes clear), OR fast-capture a placeholder JTBD via `/wr-itil:capture-jtbd` (when it ships under Phase 4 follow-on) and reference it.
137
-
138
- **JTBD-301 scope preservation**: this dispatch ALSO fires on the maintainer-side `/wr-itil:capture-problem` only. Plugin-user-side `.github/ISSUE_TEMPLATE/problem-report.yml` MUST NOT prompt for JTBD trace or persona — preserves the JTBD-301 firewall per ADR-060 P4.3 maintainer-side / plugin-user-side asymmetry clarifier. Triage during `/wr-itil:manage-problem` ingestion assigns both fields from the reporter's symptom signals (per the JTBD-301 maintainer-side-complement extension landed 2026-05-13).
92
+ 2. **Else if `jtbd_trace_value` is non-empty**: derive persona from cited JTBDs' frontmatter. Read each cited `docs/jtbd/<persona>/JTBD-<NNN>-*.md` file; extract its `persona:` (and optionally `secondary-persona:`) frontmatter values; if all cited JTBDs agree on a single persona, set `persona_value` to that persona silently and emit stderr advisory: `capture-problem: derived persona=<value> from cited JTBD <id> frontmatter`. If cited JTBDs disagree, fire AskUserQuestion with the union-of-derived-personas as options (genuine taste per ADR-044 category 5 — cited JTBDs have ratified-coherent contradictory persona constraints, only the user can resolve which applies to THIS problem).
93
+ 3. **Else (no JTBDs cited, no `--persona=` flag)**: leave `persona_value` empty. `persona:` frontmatter is OPTIONAL capture-time persona anchoring is best-effort.
139
94
 
140
- **Phase 3 P3.1 nullable-field-conditional shape**: the JTBD-trace prompt + I12 hard-block fire on `jtbd_trace_value` nullability (absent vs present), NOT on the `type` field's value. The composite gate (`type == user-business AND jtbd_trace_value == empty`) treats `type` as upstream-determined co-incident input exactly the carve-out permitted by ADR-060 line 536. Steps 2-7 below execute identically regardless of `type_value`, `jtbd_trace_value`, or `persona_value`; only the values substituted into the Step 4 skeleton template differ. This preserves I2 control-flow uniformity AND extends the I2 behavioural test (per ADR-060 Confirmation criterion 11) to assert no control-flow branch on `persona:` field presence.
95
+ **JTBD-301 scope preservation**: this dispatch fires on the maintainer-side `/wr-itil:capture-problem` only. Plugin-user-side `.github/ISSUE_TEMPLATE/problem-report.yml` MUST NOT prompt for JTBD trace or personapreserves the JTBD-301 firewall per ADR-060 P4.3 maintainer-side / plugin-user-side asymmetry clarifier. Triage during `/wr-itil:manage-problem` ingestion assigns both fields from the reporter's symptom signals (per the JTBD-301 maintainer-side-complement extension landed 2026-05-13).
141
96
 
142
97
  ### 2. Minimal-grep duplicate check (3-keyword title-only) + hang-off-check subagent dispatch (P346 Phase 3 amendment, 2026-05-31)
143
98
 
@@ -196,9 +151,9 @@ fi
196
151
 
197
152
  **Empty-candidates short-circuit**: if `${#candidates[@]} -eq 0` (no shared signals), skip the dispatch and proceed to the marker step. The mechanical pre-filter found nothing to arbitrate.
198
153
 
199
- **JTBD-301 firewall** — sub-step 2b fires on maintainer-side `/wr-itil:capture-problem` invocations ONLY. Plugin-user-side `.github/ISSUE_TEMPLATE/problem-report.yml` MUST NOT carry an equivalent dispatch (plugin-user descriptions do not carry the same authorial intent; a plugin-user describing their friction in maintainer vocabulary could plausibly trigger a wrong-parent HANG_OFF). Triage during `/wr-itil:manage-problem` ingestion stays user-judgement per JTBD-301. Mirrors the existing Step 1.5 firewall at line 116.
154
+ **JTBD-301 firewall** — sub-step 2b fires on maintainer-side `/wr-itil:capture-problem` invocations ONLY. Plugin-user-side `.github/ISSUE_TEMPLATE/problem-report.yml` MUST NOT carry an equivalent dispatch (plugin-user descriptions do not carry the same authorial intent; a plugin-user describing their friction in maintainer vocabulary could plausibly trigger a wrong-parent HANG_OFF). Triage during `/wr-itil:manage-problem` ingestion stays user-judgement per JTBD-301. Mirrors the Step 1.5b JTBD-trace firewall above.
200
155
 
201
- **AFK safe-default (--no-prompt / AFK propagation)**: when `--no-prompt` is set, the dispatch still fires (the subagent verdict is non-interactive by construction no `AskUserQuestion`), and ambiguous-multi-parent cases collapse to `PROCEED_NEW` per the subagent's Rule 6 contract. This satisfies JTBD-006's "Decisions normally requiring my input are resolved using safe defaults."
156
+ **AFK safe-default**: the hang-off-check subagent verdict is non-interactive by construction (no `AskUserQuestion`), and ambiguous-multi-parent cases collapse to `PROCEED_NEW` per the subagent's Rule 6 contract. This satisfies JTBD-006's "Decisions normally requiring my input are resolved using safe defaults" without dependency on the retired `--no-prompt` flag.
202
157
 
203
158
  **Dispatch** — when the candidate set is non-empty and ≤5, delegate to `wr-itil:hang-off-check` via the Agent tool with a structured input payload:
204
159
 
@@ -271,7 +226,6 @@ Log the renumber decision in the operation report if origin and local diverged.
271
226
  **Priority**: 3 (Medium) — Impact: 3 x Likelihood: 1 (deferred — re-rate at next /wr-itil:review-problems)
272
227
  **Origin**: internal
273
228
  **Effort**: M (deferred — re-rate at next /wr-itil:review-problems)
274
- **Type**: <type_value>
275
229
  **JTBD**: <jtbd_trace_value_as_comma_separated_list_OR_omit_line_when_empty>
276
230
  **Persona**: <persona_value_OR_omit_line_when_empty>
277
231
 
@@ -366,9 +320,10 @@ The trailing pointer is **not optional** — it is the user-visible signal that
366
320
  |---------|----------------|-----------------|
367
321
  | Duplicate-check | Wide-net grep + AskUserQuestion branch on matches | 3-keyword title-only grep, list-only (no branch) |
368
322
  | Multi-concern split | Step 4b AskUserQuestion | Out of scope (one ticket per invocation) |
369
- | Skeleton-fill | Full-intake; AskUserQuestion for missing fields | Deferred-placeholder pattern + derive-first type classification (AskUserQuestion fires only on ambiguous descriptions) |
370
- | Type-tag prompt | Step 4-equivalent AskUserQuestion fires alongside other intake fields | Step 1.5 derive-first dispatch — lexical-signal classifier silently resolves unambiguous descriptions (with stderr advisory); ambiguous descriptions fall back to classification-only AskUserQuestion. `--type=` and `--no-prompt` flags pre-resolve for non-interactive callers. I2 invariant: no control-flow branch keyed on type |
371
- | AskUserQuestion authority | Multiple branches (deviation-approval / direction-setting / taste / mechanical) | Zero unconditional AskUserQuestion fires; ambiguous-signal fallback only (silent-framework per ADR-044 cat. 4 on unambiguous; taste per cat. 5 on ambiguous); zero control-flow branches |
323
+ | Skeleton-fill | Full-intake; AskUserQuestion for missing fields | Deferred-placeholder pattern; no classification AskUserQuestion (P287 retired the type prompt) |
324
+ | Type-tag prompt | RETIRED (P287, 2026-06-02) | RETIRED (P287, 2026-06-02) the technical/user-business axis was removed as redundant with RFC/Story persona-anchoring per ADR-060 Phase 4 |
325
+ | JTBD-trace + persona | Step 4-equivalent ingestion path | Step 1.5b derive-first dispatch lexical citations + `--jtbd=` flag pre-resolve silently; persona derives from cited JTBDs' frontmatter; persona-disagreement AskUserQuestion is the only taste fallback |
326
+ | AskUserQuestion authority | Multiple branches (deviation-approval / direction-setting / taste / mechanical) | Zero unconditional AskUserQuestion fires; persona-disagreement fallback only (silent-framework per ADR-044 cat. 4 by default; cat. 5 taste on JTBD-persona disagreement); zero control-flow branches |
372
327
  | README refresh | P094 inline (regenerate + stage in same commit) | Deferred to next `/wr-itil:review-problems` |
373
328
  | Status transitions | Step 7 owns Open → Known Error → Verifying → Closed | Out of scope (creation only) |
374
329
  | Commit grain | One commit per intake (or per split-concern set) | One commit per capture |
@@ -384,21 +339,22 @@ The two skills share the `/tmp/manage-problem-grep-${SESSION_ID}` create-gate ma
384
339
  - **P119** — manage-problem create-gate hook; capture-problem composes with the same marker.
385
340
  - **P262** — the P165 README-refresh-discipline hook conflicted with this skill's deferred-README-refresh contract (Step 6 "do NOT stage README" was denied by the hook on every capture commit). Resolved by the `RISK_BYPASS: capture-deferred-readme` allow-list token (Step 6 trailer above); clears the README-refresh gate only, not the risk-score gate.
386
341
  - **P265** — the RISK_BYPASS-trailer allow-list mechanism in `readme-refresh-detect.sh` that P262's `capture-deferred-readme` token registers into.
387
- - **P170** (`docs/problems/known-error/170-problem-tickets-strain-as-fixes-decompose-into-multiple-coordinated-changes-need-rfc-framework.md`) — RFC framework driver; Slice 4 B7.T3 / item 8c authored the type-classification prompt at Step 1.5.
388
- - **P176** — agent-side I2 (no type-branching) coverage gap on the SKILL.md surface (this file's surface); descendant of P012 master harness ticket. The Step 1.5 I2 invariant guard is enforced by audit-trailed prose here per ADR-052 § Surface 2 escape-hatch contract; behavioural enforcement awaits the master harness.
342
+ - **P170** (`docs/problems/known-error/170-problem-tickets-strain-as-fixes-decompose-into-multiple-coordinated-changes-need-rfc-framework.md`) — RFC framework driver; Slice 4 B7.T3 / item 8c historically authored the type-classification prompt at Step 1.5 (RETIRED by P287, 2026-06-02).
343
+ - **P176** — agent-side I2 (no type-branching) coverage gap on the SKILL.md surface. P287 retires the type axis altogether; the regression guard is preserved under `packages/itil/scripts/test/no-type-regression-guard.bats` (asserting the `**Type**:` field is GONE from skeleton templates).
344
+ - **P287** (`docs/problems/.../287-remove-technical-user-business-type-classification-from-problems-redundant-with-rfc-persona-anchoring.md`) — the user direction (twice-confirmed 2026-05-25 + 2026-06-02) that retired Step 1.5 Type classification; ADR-060 amendment substance (I12 replacement, Phase-4 rework) queued for user re-confirmation per ADR-074.
389
345
  - **ADR-032** (`docs/decisions/032-governance-skill-invocation-patterns.proposed.md`) — foreground-lightweight-capture variant amendment (P155); 5th invocation pattern amendment (P346 Phase 3, 2026-05-31) codifies the hang-off-check sub-step 2b dispatch as the canonical fresh-context-subagent-as-decision-arbiter shape.
390
346
  - **P346** (`docs/problems/.../346-...md`) — backlog-flow-control master ticket; Phase 3 deliverable lands the hang-off-check dispatch at sub-step 2b above.
391
347
  - **RFC-013** (`docs/rfcs/RFC-013-...proposed.md`) — traces P346 Phases 1+2+3 per ADR-071 unconditional Problem→RFC trace.
392
348
  - **`packages/itil/agents/hang-off-check.md`** — the fresh-context subagent invoked by sub-step 2b; reads only the structured input payload; emits HANG_OFF: P<NNN> or PROCEED_NEW with rationale + signals + absorb directive.
393
349
  - **ADR-038** — progressive-disclosure pattern (SKILL.md + REFERENCE.md split).
394
- - **ADR-044** — decision-delegation contract; type classification is **derive-first**: silent-framework per category 4 on unambiguous-signal descriptions (the classifier IS the framework resolving the answer from observable evidence per ADR-026 grounding); taste per category 5 fallback on genuinely-ambiguous descriptions only. `--no-prompt` / `--type=<value>` are policy-authorised silent-proceed shapes per category 4 (caller-side pre-resolution). P185 re-classified Step 1.5's taxonomy position from "cat 5 unconditional ask" to "cat 4 derive-first with cat 5 fallback".
395
- - **P185** — `/wr-itil:capture-problem` asks a classification question it can answer itself from the description's observable evidence — inverse-P078 / P132 trap at a SKILL contract surface. The Step 1.5 derive-first refactor (lexical-signal classifier + stderr advisory) ships this fix.
350
+ - **ADR-044** — decision-delegation contract. Persona derivation (Step 1.5b) is **derive-first**: silent-framework per category 4 when cited JTBDs agree on persona; taste per category 5 fallback only on cited-JTBD-persona disagreement. JTBD-trace itself is purely category 4 (lexical detection or `--jtbd=` flag pre-resolution). The retired Step 1.5 Type classification was a derive-first dispatch too (RETIRED by P287, 2026-06-02).
351
+ - **P185** — `/wr-itil:capture-problem` historical: asked a classification question (type) it could answer itself; the Step 1.5 derive-first refactor (lexical-signal classifier + stderr advisory) shipped the fix in 2026-05-15. P287 then retired the entire surface in 2026-06-02 as the classification axis itself was redundant with RFC/Story persona-anchoring.
396
352
  - **ADR-049** — bin/ on PATH; capture-problem reuses the existing `wr-itil-reconcile-readme` shim.
397
353
  - **ADR-052** — behavioural-tests-default for skill testing; SKILL.md I2 surface coverage gap is named, not silent (P176 + ADR-052 § Surface 2).
398
- - **ADR-060** (`docs/decisions/060-...accepted.md`) — Phase 1 item 8c authored Step 1.5 here; I2 invariant (line 98) governs the no-control-flow-branch contract; line 132 names the maintainer-side-only / JTBD-301-protection scope; line 160 (Confirmation criterion 4) gates the type-prompt placement.
399
- - **JTBD-301** (`docs/jtbd/plugin-user/JTBD-301-...md`) — plugin-user no-pre-classification persona constraint; protected by the Step 1.5 maintainer-side scope guard.
354
+ - **ADR-060** (`docs/decisions/060-...accepted.md`) — body currently encodes the type-tag schema (Decision Outcome item 1, I2 type-uniformity, I12 hard-block, Phase-4 persona+jtbd machinery keyed on `type:user-business`). P287 retires the SKILL implementation of these clauses unilaterally per twice-confirmed user direction; the ADR body amendment substance (I12 replacement shape, Phase-4 rework) is queued for user re-confirmation per ADR-074. Until the amendment lands, ADR-060 body and SKILL implementation are intentionally inconsistent — this is the P287 trade-off the user accepted.
355
+ - **JTBD-301** (`docs/jtbd/plugin-user/JTBD-301-...md`) — plugin-user no-pre-classification persona constraint; the Step 1.5b maintainer-side scope guard preserves the firewall on the JTBD-trace + persona axis. The type-axis firewall is moot (axis retired).
400
356
  - `packages/itil/skills/manage-problem/SKILL.md` — heavyweight intake counterpart.
401
357
  - `packages/itil/skills/review-problems/SKILL.md` — re-rates the deferred placeholders + refreshes README.md.
402
- - `packages/itil/scripts/test/i2-no-type-branching.bats` — pure-bash supporting-script enforcement of the I2 invariant; this SKILL.md change does not affect any pure-bash script and so does not change the bats outcome (still green).
358
+ - `packages/itil/scripts/test/no-type-regression-guard.bats` — pure-bash regression guard that the `**Type**:` field stays GONE from skeleton templates + ticket bodies. Replaces the historical `i2-no-type-branching.bats` (which asserted no-control-flow-branch-on-type; with the type axis retired, the branch-protection invariant is vacuous, but the field-absence regression guard preserves the audit trail per architect-review verdict 2026-06-02).
403
359
 
404
360
  $ARGUMENTS
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env bats
2
2
 
3
- # P170 / Phase 3 P3.1 + Phase 4 P4.2 + I12 — behavioural fixture for
3
+ # P170 / Phase 3 P3.1 + Phase 4 P4.2 — behavioural fixture for
4
4
  # capture-problem Step 1.5b JTBD-trace + persona dispatch. Per ADR-060
5
- # § Phase 3 + Phase 4 in-scope amendment (2026-05-13):
5
+ # § Phase 3 + Phase 4 in-scope amendment (2026-05-13), as amended by
6
+ # P287 (2026-06-02 — type-classification + I12 hard-block retired):
6
7
  #
7
8
  # - Lexical JTBD-trace detection: description-contains-JTBD-NNN-ID →
8
9
  # silent-resolve jtbd_trace_value to the matched IDs.
9
- # - I12 hard-block: type=user-business AND jtbd_trace_value empty AND
10
- # no --jtbd flag → halt-with-stderr-directive.
11
10
  # - --jtbd=JTBD-NNN[,...] flag pre-resolves jtbd_trace_value silently.
12
11
  # - --persona=<value> flag pre-resolves persona_value silently.
13
12
  # - Skeleton template carries **JTBD**: and **Persona**: body fields
14
- # (matches existing **Status**: / **Type**: convention; frontmatter
15
- # migration deferred to follow-on slice).
13
+ # (frontmatter migration deferred to follow-on slice).
14
+ #
15
+ # P287 retirement: the I12 hard-block (type=user-business + empty jtbd
16
+ # → halt) was retired alongside the type axis. JTBD-trace is now purely
17
+ # best-effort capture-time anchoring; the I12 reference-impl predicate
18
+ # below is preserved as a NEGATIVE assertion (never blocks) for
19
+ # regression-guard purposes per architect-review verdict 2026-06-02.
16
20
  #
17
21
  # Reference-impl pattern: this fixture exercises the algorithm directly
18
22
  # via shell helpers; the SKILL.md prose at runtime executes the same
@@ -32,15 +36,12 @@ detect_jtbd_trace() {
32
36
  echo "$desc" | grep -oE '\bJTBD-[0-9]+\b' | sort -u | tr '\n' ' ' | sed 's/[[:space:]]*$//'
33
37
  }
34
38
 
35
- # Reference implementation of the I12 hard-block predicate. Returns
36
- # 0 (true block) when type=user-business AND jtbd_trace empty AND
37
- # no --jtbd flag was provided. Returns 1 otherwise.
39
+ # P287 retirement: the I12 hard-block was retired alongside the type
40
+ # axis. This predicate now ALWAYS returns 1 (never blocks) — preserved
41
+ # as a regression-guard so future drift that re-introduces a type-keyed
42
+ # hard-block surfaces as a test failure.
38
43
  i12_should_block() {
39
- local type_value="$1" jtbd_trace="$2" had_jtbd_flag="$3"
40
- [ "$type_value" = "user-business" ] || return 1
41
- [ -z "$jtbd_trace" ] || return 1
42
- [ "$had_jtbd_flag" = "0" ] || return 1
43
- return 0
44
+ return 1
44
45
  }
45
46
 
46
47
  # Reference implementation of --jtbd= flag parser. Accepts CSV; returns
@@ -89,20 +90,16 @@ validate_persona() {
89
90
  [ "$result" = "JTBD-001" ]
90
91
  }
91
92
 
92
- @test "I12 i12_should_block: user-business + empty jtbd + no flag blocks" {
93
- i12_should_block "user-business" "" "0"
94
- }
95
-
96
- @test "I12 i12_should_block: user-business + non-empty jtbd does NOT block" {
93
+ @test "P287 i12 hard-block retired: never blocks regardless of inputs (regression guard)" {
94
+ # After P287, the I12 hard-block is retired. The predicate must never
95
+ # return 0 (block) for any input combination — capture-time JTBD
96
+ # anchoring is best-effort, not hard-required. If a future maintainer
97
+ # re-introduces a type-keyed hard-block, this test catches it.
98
+ ! i12_should_block "user-business" "" "0"
97
99
  ! i12_should_block "user-business" "JTBD-001" "0"
98
- }
99
-
100
- @test "I12 i12_should_block: user-business + empty jtbd + --jtbd flag set → does NOT block" {
101
100
  ! i12_should_block "user-business" "" "1"
102
- }
103
-
104
- @test "I12 i12_should_block: technical + empty jtbd → does NOT block (technical has no JTBD requirement)" {
105
101
  ! i12_should_block "technical" "" "0"
102
+ ! i12_should_block "anything" "anything" "anything"
106
103
  }
107
104
 
108
105
  @test "P3.1 parse_jtbd_flag: --jtbd=JTBD-NNN parses single ID" {
@@ -116,7 +113,7 @@ validate_persona() {
116
113
  }
117
114
 
118
115
  @test "P3.1 parse_jtbd_flag: non-jtbd-flag arg returns empty" {
119
- result=$(parse_jtbd_flag "--type=user-business")
116
+ result=$(parse_jtbd_flag "--persona=plugin-user")
120
117
  [ -z "$result" ]
121
118
  }
122
119
 
@@ -324,271 +324,24 @@ EOF
324
324
  }
325
325
 
326
326
  # ---------------------------------------------------------------------------
327
- # P185 — Step 1.5 derive-first classifier behavioural tests.
327
+ # P287 (2026-06-02) retirement of P185 Step 1.5 derive-first classifier.
328
328
  #
329
- # The classifier is an agent-driven SKILL.md instruction (not a pure-bash
330
- # script), so these tests mirror the lexical-signal regex sets from
331
- # SKILL.md and assert classification outcomes on fixture descriptions —
332
- # same shape as the existing next-ID-formula and duplicate-grep tests
333
- # that mirror manage-problem Step 3 / capture-problem Step 2 formulas.
329
+ # The type-classification axis was REMOVED per twice-confirmed user
330
+ # direction (2026-05-25 + 2026-06-02). The lexical-signal classifier,
331
+ # stderr-advisory contract, flag pre-resolution dispatch, and
332
+ # meta-recursive corpus assertions that lived here have been removed —
333
+ # they exercise behaviour that no longer exists in capture-problem.
334
334
  #
335
- # Per ADR-052 (behavioural-tests-default): observable input output
336
- # assertions on the classifier's resolution function. Per P081: NOT
337
- # grepping SKILL.md prose for signal-token references that would be
338
- # the structural-test-disguised-as-behavioural anti-pattern.
339
- #
340
- # I2 protection: each test exercises both `technical` and `user-business`
341
- # classification paths and asserts the stderr-advisory shape is
342
- # isomorphic beyond the substituted type-value + signal names.
343
- # ---------------------------------------------------------------------------
344
-
345
- # Mirror of the SKILL.md Step 1.5 classifier — emits "technical",
346
- # "user-business", or "ambiguous" on stdout. Always returns 0 so the
347
- # `result=$(classify_description ...)` substitution in tests below
348
- # never trips bats' default-fail-on-nonzero-substitution behaviour;
349
- # tests assert on the string verdict, not the exit code.
350
- #
351
- # This helper mirrors the SKILL.md regex set so the test exercises the
352
- # classifier's load-bearing pattern set; drift between this helper and
353
- # SKILL.md is the failure mode this test corpus catches.
354
- classify_description() {
355
- local desc="$1"
356
- local tech_signals=()
357
- local ub_signals=()
358
-
359
- # Technical signals — case-insensitive matching via grep -E -o -i.
360
- # Each match contributes the matched token to the signal list.
361
- local tech_patterns=(
362
- '[a-z]+[A-Z][a-zA-Z]+' # camelCase (case-SENSITIVE - the case shape is the signal)
363
- '[a-z]+-[a-z][a-z-]+-[a-z][a-z-]+' # kebab-case with >=2 hyphens
364
- '[a-z]+_[a-z][a-z_]+' # snake_case
365
- '\.(md|sh|bats|ts|js|json|yaml|yml|py|rb|go|css|html)\b'
366
- 'packages/[a-z-]+/'
367
- 'docs/[a-z-]+/'
368
- '\.github/'
369
- '/tmp/'
370
- '/wr-[a-z-]+:[a-z-]+'
371
- '\bgit (commit|push|mv|add|rebase|merge)\b'
372
- '\bnpm (run|install|publish)\b'
373
- '\b(bash|bats|grep|sed|jq)\b'
374
- '\b(drift|regression|hook|marker|gate|refresh|idempotent|stderr|stdout|regex|formula|dispatch|frontmatter|substring|escape|sentinel|bypass|TTL|cache|invalidate|deduplicate|race|deadlock|timeout|preflight)\b'
375
- '\b(error|failure|exception|panic|segfault|undefined|EACCES|ENOENT)\b'
376
- )
377
-
378
- # User-business signals — case-insensitive.
379
- local ub_patterns=(
380
- '\b(adopter|adopters|plugin-user|plugin-users|solo[-_ ]?developer|maintainer-persona|end[-_ ]?user|customer|stakeholder)\b'
381
- '\b(workflow|journey|onboarding|friction|UX|experience|usability|discoverability)\b'
382
- '\bJTBD-[0-9]+\b'
383
- '\bjob-to-be-done\b'
384
- '\b(want|need|cannot|unable to)\b[[:space:]]+(use|access|find|discover|complete)'
385
- '\bdesired outcome\b'
386
- '\bunmet need\b'
387
- )
388
-
389
- for p in "${tech_patterns[@]}"; do
390
- if echo "$desc" | grep -qE "$p" 2>/dev/null; then
391
- tech_signals+=("$p")
392
- fi
393
- done
394
-
395
- for p in "${ub_patterns[@]}"; do
396
- if echo "$desc" | grep -qiE "$p" 2>/dev/null; then
397
- ub_signals+=("$p")
398
- fi
399
- done
400
-
401
- local tech_n="${#tech_signals[@]}"
402
- local ub_n="${#ub_signals[@]}"
403
-
404
- if [ "$tech_n" -ge 1 ] && [ "$ub_n" -eq 0 ]; then
405
- echo "technical"
406
- elif [ "$tech_n" -eq 0 ] && [ "$ub_n" -ge 1 ]; then
407
- echo "user-business"
408
- else
409
- echo "ambiguous"
410
- fi
411
- return 0
412
- }
413
-
414
- @test "P185: classifier resolves pure-technical description as technical" {
415
- # camelCase identifier + file path + mechanism word — all technical signals.
416
- result=$(classify_description "The captureProblem hook in packages/itil/hooks/lib/detectors.sh has a regex drift")
417
- [ "$result" = "technical" ]
418
- }
419
-
420
- @test "P185: classifier resolves pure-technical description (error-message variant)" {
421
- result=$(classify_description "The bats test exits with exception ENOENT on /tmp/manage-problem-grep")
422
- [ "$result" = "technical" ]
423
- }
424
-
425
- @test "P185: classifier resolves pure-technical description (command-name variant)" {
426
- result=$(classify_description "git mv to docs/problems/<NNN>.verifying.md fails on the rebase merge")
427
- [ "$result" = "technical" ]
428
- }
429
-
430
- @test "P185: classifier resolves pure-user-business description (persona + journey)" {
431
- result=$(classify_description "Adopters cannot complete the onboarding workflow without UX friction")
432
- [ "$result" = "user-business" ]
433
- }
434
-
435
- @test "P185: classifier resolves pure-user-business description (JTBD-shaped need)" {
436
- result=$(classify_description "JTBD-101 names a desired outcome the plugin-user cannot achieve")
437
- [ "$result" = "user-business" ]
438
- }
439
-
440
- @test "P185: classifier resolves pure-user-business description (unmet-need variant)" {
441
- result=$(classify_description "Solo-developers want to discover scaffold templates but cannot find them")
442
- [ "$result" = "user-business" ]
443
- }
444
-
445
- @test "P185: classifier resolves mixed-signal description as ambiguous" {
446
- # Mentions both technical mechanism (hook drift) and user-business (adopter friction)
447
- # — exactly the case the AskUserQuestion fallback is designed for.
448
- result=$(classify_description "The hook drift affects adopters in the onboarding workflow")
449
- [ "$result" = "ambiguous" ]
450
- }
451
-
452
- @test "P185: classifier resolves no-signal description as ambiguous" {
453
- # Plain prose with no technical or user-business signals — fallback to ask.
454
- result=$(classify_description "Something is off but I cannot describe it well")
455
- [ "$result" = "ambiguous" ]
456
- }
457
-
458
- # ---------------------------------------------------------------------------
459
- # Stderr-advisory shape — I2 isomorphism guard.
460
- # Per architect-review rider: the advisory text MUST be identical in
461
- # sentence structure across `technical` vs `user-business` classifications
462
- # beyond the substituted type-value tokens. Otherwise the advisory itself
463
- # becomes a control-flow asymmetry keyed on `type` and re-introduces an
464
- # I2 leak through the back door.
465
- # ---------------------------------------------------------------------------
466
-
467
- # Mirror of the SKILL.md advisory template. P132 Phase 2a-iii-A renamed
468
- # the verb from `classified` to `derived` to align with the shared helper
469
- # `packages/itil/lib/derive-first-dispatch.sh`'s emit_stderr_advisory
470
- # function — I2-isomorphic format `<skill>: derived <field>=<value> from
471
- # <source>; <reversibility>` across all three derive-first declaration-skill
472
- # surfaces.
473
- format_stderr_advisory() {
474
- local resolved_type="$1"
475
- local other_type="$2"
476
- local signals="$3"
477
- printf 'capture-problem: derived type=%s from description signals: %s; re-invoke with --type=%s to override\n' \
478
- "$resolved_type" "$signals" "$other_type"
479
- }
480
-
481
- # Strip the type-value tokens + signal-name list so we can compare the
482
- # sentence skeleton in isolation.
483
- strip_substituted_tokens() {
484
- sed -E 's/type=[a-z-]+/type=<X>/g; s/signals: [^;]+;/signals: <S>;/g'
485
- }
486
-
487
- @test "P185: stderr advisory shape is isomorphic across technical vs user-business classifications" {
488
- tech_msg=$(format_stderr_advisory technical user-business "camelCase-id, packages/path")
489
- ub_msg=$(format_stderr_advisory user-business technical "adopter, onboarding")
490
- tech_shape=$(echo "$tech_msg" | strip_substituted_tokens)
491
- ub_shape=$(echo "$ub_msg" | strip_substituted_tokens)
492
- [ "$tech_shape" = "$ub_shape" ]
493
- }
494
-
495
- @test "P185: stderr advisory names the override flag with the OTHER type value" {
496
- tech_msg=$(format_stderr_advisory technical user-business "sig")
497
- ub_msg=$(format_stderr_advisory user-business technical "sig")
498
- # The technical-classified advisory must offer the user-business override.
499
- echo "$tech_msg" | grep -q -- '--type=user-business'
500
- # The user-business-classified advisory must offer the technical override.
501
- echo "$ub_msg" | grep -q -- '--type=technical'
502
- }
503
-
504
- @test "P185: stderr advisory does NOT prefix with type-value when describing the contract" {
505
- # The shape `derived type=<value> from description signals: <list>;
506
- # re-invoke with --type=<other> to override` — the leading prose
507
- # "capture-problem: derived type=" must be identical regardless of
508
- # type value (substitution happens AFTER the equals sign). P132 Phase
509
- # 2a-iii-A renamed `classified` -> `derived` to align with the shared
510
- # helper `packages/itil/lib/derive-first-dispatch.sh`'s I2-isomorphic
511
- # format across all three declaration-skill surfaces.
512
- tech_msg=$(format_stderr_advisory technical user-business "sig")
513
- ub_msg=$(format_stderr_advisory user-business technical "sig")
514
- echo "$tech_msg" | grep -q '^capture-problem: derived type='
515
- echo "$ub_msg" | grep -q '^capture-problem: derived type='
516
- }
517
-
518
- # ---------------------------------------------------------------------------
519
- # Pre-resolution flag precedence — caller-side flags MUST short-circuit
520
- # before the classifier runs. Order: --type=<value> > --no-prompt >
521
- # classifier > AskUserQuestion fallback.
522
- # ---------------------------------------------------------------------------
523
-
524
- # Mirror of SKILL.md Step 1.5 dispatch order.
525
- resolve_type_dispatch() {
526
- local type_flag="$1" # value from --type=<X>, empty if not passed
527
- local no_prompt="$2" # "1" if --no-prompt passed, empty otherwise
528
- local desc="$3"
529
- if [ -n "$type_flag" ]; then
530
- echo "$type_flag:pre-resolved-flag"
531
- return 0
532
- fi
533
- if [ "$no_prompt" = "1" ]; then
534
- echo "technical:no-prompt-default"
535
- return 0
536
- fi
537
- local classified
538
- classified=$(classify_description "$desc")
539
- if [ "$classified" = "ambiguous" ]; then
540
- echo "ambiguous:fallback-to-ask"
541
- else
542
- echo "$classified:derived-from-signals"
543
- fi
544
- return 0
545
- }
546
-
547
- @test "P185: --type=user-business pre-resolves even on pure-technical description" {
548
- # Caller-side flag MUST win over the classifier — explicit override.
549
- result=$(resolve_type_dispatch user-business "" "The hook in packages/itil/hooks/lib drifts")
550
- [ "$result" = "user-business:pre-resolved-flag" ]
551
- }
552
-
553
- @test "P185: --no-prompt pre-resolves to technical even on pure-user-business description" {
554
- # AFK contract: --no-prompt always lands `technical`, regardless of description signals.
555
- result=$(resolve_type_dispatch "" "1" "Adopters cannot complete the onboarding workflow")
556
- [ "$result" = "technical:no-prompt-default" ]
557
- }
558
-
559
- @test "P185: no flags + pure-technical description → derived-from-signals technical" {
560
- result=$(resolve_type_dispatch "" "" "The captureProblem.bats test fails with exit 1")
561
- [ "$result" = "technical:derived-from-signals" ]
562
- }
563
-
564
- @test "P185: no flags + pure-user-business description → derived-from-signals user-business" {
565
- result=$(resolve_type_dispatch "" "" "JTBD-301 plugin-user persona constraint")
566
- [ "$result" = "user-business:derived-from-signals" ]
567
- }
568
-
569
- @test "P185: no flags + ambiguous description → fallback to AskUserQuestion" {
570
- result=$(resolve_type_dispatch "" "" "Something feels wrong but I cannot say what")
571
- [ "$result" = "ambiguous:fallback-to-ask" ]
572
- }
573
-
574
- # ---------------------------------------------------------------------------
575
- # Meta-recursive corpus validation — exercise the classifier against
576
- # real problem-ticket descriptions and assert the classifier's verdict
577
- # matches the existing `**Type**:` field on each ticket. Limited to the
578
- # tickets whose descriptions are short enough to be deterministic; the
579
- # point is to catch obvious classifier regressions on the canonical
580
- # corpus, not to validate every edge case.
335
+ # The P287 regression guards (capture-problem skeleton template has no
336
+ # **Type**: line; SKILL.md has no Step 1.5 Type classification header;
337
+ # no committed ticket carries a **Type**: body field; the helper has
338
+ # no lexical_classify_two_sided function) live in the sibling fixture
339
+ # packages/itil/scripts/test/no-type-regression-guard.bats.
581
340
  # ---------------------------------------------------------------------------
582
341
 
583
- @test "P185: classifier matches the P185 ticket's own self-classification (meta-recursive)" {
584
- # P185's body opens with: "/wr-itil:capture-problem Step 1.5 currently
585
- # fires an AskUserQuestion for type ..." — command-name pattern +
586
- # camelCase + mechanism words ("dispatch", "regex"). The ticket's
587
- # **Type**: field is `technical`. Classifier must agree.
588
- desc="/wr-itil:capture-problem Step 1.5 fires an AskUserQuestion for type via a regex dispatch the SKILL.md frontmatter resolves"
589
- result=$(classify_description "$desc")
590
- [ "$result" = "technical" ]
591
- }
342
+ # (P287: classifier + advisory + flag-precedence + meta-recursive tests
343
+ # retired with the type axis. Regression guards now live in
344
+ # packages/itil/scripts/test/no-type-regression-guard.bats.)
592
345
 
593
346
  # ---------------------------------------------------------------------------
594
347
  # P281 — ADR-031 per-state-subdir layout conformance on the SKILL.md
@@ -1,320 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # @problem P170 — Slice 4 B7.T4 (item 8d): I2 (uniform problem
4
- # ontology) load-bearing behavioural enforcement. ADR-060 architect
5
- # finding 2: "I2 needs load-bearing behavioural test, not prose
6
- # prohibition" — without this test ADR-060 ships I2 in name only.
7
- #
8
- # Contract: the type-tag is a CLASSIFICATION facet, never a workflow
9
- # split. No skill or supporting script branches on the `type` field.
10
- # This test asserts the property behaviourally for the pure-bash
11
- # supporting-script surface — for each script that reads problem-
12
- # ticket frontmatter, observable outputs (stdout / exit code / file
13
- # mutations) are isomorphic across two synthetic ticket-set variants
14
- # (one `type: technical`, one `type: user-business`).
15
- #
16
- # Coverage SCOPE — pure-bash supporting scripts only:
17
- # - `reconcile-readme.sh`
18
- # - `update-problem-rfcs-section.sh`
19
- # - `classify-readme-drift.sh`
20
- # - `reconcile-rfcs.sh`
21
- # - `migrate-problems-add-type.sh` (idempotency on already-migrated
22
- # tickets, regardless of type value)
23
- #
24
- # Coverage GAP — agent-driven SKILL.md surface:
25
- # The SKILL.md files (`/wr-itil:capture-problem`,
26
- # `/wr-itil:manage-problem`, `/wr-itil:work-problems`,
27
- # `/wr-itil:review-problems`, `/wr-itil:transition-problem(s)`) are
28
- # agent-driven instructions, not scripts. Behaviourally testing them
29
- # requires invoking each skill against two type-variant ticket sets
30
- # and asserting observable agent action is isomorphic — that
31
- # primitive does not yet exist. **Tracked as P176** (agent-side I2
32
- # coverage gap; descendant of P012 skill testing harness scope).
33
- # Per ADR-052 § Surface 2 (in-file justification with cited harness-
34
- # gap ticket): the deferral is named, ticketed, audit-trailed — NOT
35
- # silent.
36
- #
37
- # P081 protection: this test is behavioural per ADR-052 (observable
38
- # input → output assertions on script invocations). It does NOT grep
39
- # SKILL.md / agent.md content for `type` token references — that
40
- # would be the structural-test-disguised-as-behavioural anti-pattern
41
- # P081 forbids.
42
- #
43
- # @adr ADR-060 (Phase 1 invariant I2; architect finding 2 + finding 10
44
- # item 8d; Confirmation criterion 8)
45
- # @adr ADR-051 (load-bearing-from-the-start — I2 ships at the same
46
- # time as the type-tag, NOT later by graceful drift)
47
- # @adr ADR-052 (behavioural-bats default; § Surface 2 escape-hatch
48
- # for the agent-side coverage gap)
49
- # @adr ADR-014 (single-purpose; one mechanical invariant guard)
50
- # @problem P081 (no structural grep on SKILL.md content — protects
51
- # against quick-fix workarounds that would re-introduce drift)
52
- # @problem P176 (agent-side I2 coverage gap follow-up — covers what
53
- # this bats explicitly does not)
54
- # @jtbd JTBD-001 (extended scope: multi-commit coordinated-change
55
- # governance at the change-set level — this test gates a CLASS of
56
- # future changes, not just a single edit)
57
- # @jtbd JTBD-008 (decompose-fix-into-coordinated-changes — preserves
58
- # uniform mechanism across the type facet)
59
-
60
- setup() {
61
- SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
62
- TECH_DIR="$(mktemp -d)"
63
- USER_DIR="$(mktemp -d)"
64
- RFCS_TECH="$(mktemp -d)"
65
- RFCS_USER="$(mktemp -d)"
66
- }
67
-
68
- teardown() {
69
- rm -rf "$TECH_DIR" "$USER_DIR" "$RFCS_TECH" "$RFCS_USER"
70
- }
71
-
72
- # Build twin ticket-set fixtures: identical content except for the
73
- # `**Type**:` field value. Three tickets per fixture (Open / Verifying
74
- # / Closed) so each script's branching surface gets exercised.
75
- build_twin_fixtures() {
76
- for type_pair in "technical:$TECH_DIR" "user-business:$USER_DIR"; do
77
- typev="${type_pair%%:*}"
78
- dir="${type_pair##*:}"
79
- cat > "$dir/100-foo.open.md" <<EOF
80
- # Problem 100: Foo
81
-
82
- **Status**: Open
83
- **WSJF**: 5.0
84
- **Type**: ${typev}
85
-
86
- ## Description
87
-
88
- stub
89
-
90
- ## Related
91
-
92
- stub
93
- EOF
94
- cat > "$dir/101-bar.verifying.md" <<EOF
95
- # Problem 101: Bar
96
-
97
- **Status**: Verification Pending
98
- **WSJF**: 0
99
- **Type**: ${typev}
100
-
101
- ## Description
102
-
103
- stub
104
- EOF
105
- cat > "$dir/102-baz.closed.md" <<EOF
106
- # Problem 102: Baz
107
-
108
- **Status**: Closed
109
- **Type**: ${typev}
110
-
111
- ## Description
112
-
113
- stub
114
- EOF
115
- done
116
-
117
- for type_pair in "technical:$TECH_DIR" "user-business:$USER_DIR"; do
118
- dir="${type_pair##*:}"
119
- cat > "$dir/README.md" <<'EOF'
120
- # Problem Backlog
121
-
122
- > Last reviewed: 2026-01-01.
123
-
124
- ## WSJF Rankings
125
-
126
- | WSJF | ID | Title | Severity | Status | Effort |
127
- |------|-----|-------|----------|--------|--------|
128
- | 5.0 | P100 | Foo | 12 High | Open | M |
129
-
130
- ## Verification Queue
131
-
132
- | ID | Title | Released | Likely verified? |
133
- |----|-------|----------|------------------|
134
- | P101 | Bar | 2026-01-01 | no (0 days) |
135
-
136
- ## Closed
137
-
138
- | ID | Title | Closed |
139
- |----|-------|--------|
140
- | P102 | Baz | 2026-01-01 |
141
- EOF
142
- done
143
- }
144
-
145
- # Build twin RFC-set fixtures alongside the problem fixtures, so
146
- # `update-problem-rfcs-section.sh` and `reconcile-rfcs.sh` get exercised
147
- # on traced-RFCs across both type variants.
148
- build_twin_rfc_fixtures() {
149
- for type_pair in "technical:$RFCS_TECH" "user-business:$RFCS_USER"; do
150
- dir="${type_pair##*:}"
151
- cat > "$dir/RFC-001-foo.accepted.md" <<EOF
152
- ---
153
- status: accepted
154
- rfc-id: foo
155
- reported: 2026-01-01
156
- decision-makers: [test]
157
- problems: [P100]
158
- ---
159
-
160
- # RFC-001: foo
161
-
162
- stub
163
- EOF
164
- cat > "$dir/README.md" <<'EOF'
165
- # RFC Backlog
166
-
167
- ## WSJF Rankings
168
-
169
- | WSJF | ID | Title | Status |
170
- |------|-----|-------|--------|
171
- | 5.0 | RFC-001 | foo | accepted |
172
-
173
- ## Verification Queue
174
-
175
- | ID | Title | Released | Likely verified? |
176
- |----|-------|----------|------------------|
177
-
178
- ## Closed
179
-
180
- | ID | Title | Closed |
181
- |----|-------|--------|
182
- EOF
183
- done
184
- }
185
-
186
- # Strip `**Type**:` lines and `type:` YAML lines from a stream so
187
- # isomorphism comparison ignores the SOLE legitimate point of
188
- # differentiation. Anything else differing between the two variants
189
- # is an I2 violation.
190
- strip_type_lines() {
191
- grep -v -E '^(\*\*Type\*\*:|type: )' || true
192
- }
193
-
194
- # Diff two files modulo type lines; expect empty diff (= isomorphic).
195
- assert_isomorphic_files() {
196
- local a="$1" b="$2"
197
- diff <(strip_type_lines < "$a") <(strip_type_lines < "$b")
198
- }
199
-
200
- # ── reconcile-readme.sh: exit code + stdout invariant across types ───────────
201
-
202
- @test "I2: reconcile-readme.sh exits identically across type variants" {
203
- build_twin_fixtures
204
- set +e
205
- bash "$SCRIPTS_DIR/reconcile-readme.sh" "$TECH_DIR" > /tmp/i2-rrt-tech.out 2>&1
206
- ec_tech=$?
207
- bash "$SCRIPTS_DIR/reconcile-readme.sh" "$USER_DIR" > /tmp/i2-rrt-user.out 2>&1
208
- ec_user=$?
209
- set -e
210
- [ "$ec_tech" = "$ec_user" ]
211
- }
212
-
213
- @test "I2: reconcile-readme.sh stdout isomorphic modulo type field" {
214
- build_twin_fixtures
215
- set +e
216
- bash "$SCRIPTS_DIR/reconcile-readme.sh" "$TECH_DIR" > /tmp/i2-rrs-tech.out 2>&1 || true
217
- bash "$SCRIPTS_DIR/reconcile-readme.sh" "$USER_DIR" > /tmp/i2-rrs-user.out 2>&1 || true
218
- set -e
219
- assert_isomorphic_files /tmp/i2-rrs-tech.out /tmp/i2-rrs-user.out
220
- }
221
-
222
- # ── update-problem-rfcs-section.sh: file mutation invariant across types ─────
223
-
224
- @test "I2: update-problem-rfcs-section.sh mutates ticket files identically across type variants" {
225
- build_twin_fixtures
226
- build_twin_rfc_fixtures
227
- bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
228
- "$TECH_DIR/100-foo.open.md" "$RFCS_TECH"
229
- bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
230
- "$USER_DIR/100-foo.open.md" "$RFCS_USER"
231
- assert_isomorphic_files \
232
- "$TECH_DIR/100-foo.open.md" \
233
- "$USER_DIR/100-foo.open.md"
234
- }
235
-
236
- @test "I2: update-problem-rfcs-section.sh produces same ## RFCs section regardless of type" {
237
- build_twin_fixtures
238
- build_twin_rfc_fixtures
239
- bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
240
- "$TECH_DIR/100-foo.open.md" "$RFCS_TECH"
241
- bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
242
- "$USER_DIR/100-foo.open.md" "$RFCS_USER"
243
- # Both files now carry the SAME ## RFCs table row.
244
- grep -q '| RFC-001 | accepted | foo |' "$TECH_DIR/100-foo.open.md"
245
- grep -q '| RFC-001 | accepted | foo |' "$USER_DIR/100-foo.open.md"
246
- }
247
-
248
- # ── classify-readme-drift.sh: exit code + stdout invariant across types ──────
249
-
250
- @test "I2: classify-readme-drift.sh exits identically across type variants" {
251
- build_twin_fixtures
252
- # Manufacture a drift output file (the script reads stdout from
253
- # reconcile-readme; here we build a minimal canned drift line).
254
- drift_in="$(mktemp)"
255
- echo "DRIFT P100 wsjf-rankings: claims=open actual=open" > "$drift_in"
256
- set +e
257
- bash "$SCRIPTS_DIR/classify-readme-drift.sh" "$drift_in" "$TECH_DIR" > /tmp/i2-cdt-tech.out 2>&1
258
- ec_tech=$?
259
- bash "$SCRIPTS_DIR/classify-readme-drift.sh" "$drift_in" "$USER_DIR" > /tmp/i2-cdt-user.out 2>&1
260
- ec_user=$?
261
- set -e
262
- rm -f "$drift_in"
263
- [ "$ec_tech" = "$ec_user" ]
264
- assert_isomorphic_files /tmp/i2-cdt-tech.out /tmp/i2-cdt-user.out
265
- }
266
-
267
- # ── reconcile-rfcs.sh: exit code + stdout invariant across types ─────────────
268
-
269
- @test "I2: reconcile-rfcs.sh exits identically across problem-type variants" {
270
- build_twin_fixtures
271
- build_twin_rfc_fixtures
272
- set +e
273
- bash "$SCRIPTS_DIR/reconcile-rfcs.sh" "$RFCS_TECH" "$TECH_DIR" > /tmp/i2-rrf-tech.out 2>&1
274
- ec_tech=$?
275
- bash "$SCRIPTS_DIR/reconcile-rfcs.sh" "$RFCS_USER" "$USER_DIR" > /tmp/i2-rrf-user.out 2>&1
276
- ec_user=$?
277
- set -e
278
- [ "$ec_tech" = "$ec_user" ]
279
- assert_isomorphic_files /tmp/i2-rrf-tech.out /tmp/i2-rrf-user.out
280
- }
281
-
282
- # ── migrate-problems-add-type.sh: idempotency invariant across both type values ──
283
-
284
- @test "I2: migrate-problems-add-type.sh is no-op on already-typed tickets, both type values" {
285
- build_twin_fixtures
286
- hash_tech_before=$(shasum "$TECH_DIR"/*.md | shasum | cut -d' ' -f1)
287
- hash_user_before=$(shasum "$USER_DIR"/*.md | shasum | cut -d' ' -f1)
288
- bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" --apply "$TECH_DIR"
289
- bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" --apply "$USER_DIR"
290
- hash_tech_after=$(shasum "$TECH_DIR"/*.md | shasum | cut -d' ' -f1)
291
- hash_user_after=$(shasum "$USER_DIR"/*.md | shasum | cut -d' ' -f1)
292
- [ "$hash_tech_before" = "$hash_tech_after" ]
293
- [ "$hash_user_before" = "$hash_user_after" ]
294
- }
295
-
296
- @test "I2: migrate-problems-add-type.sh diagnose-mode exit code identical across type variants" {
297
- build_twin_fixtures
298
- set +e
299
- bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$TECH_DIR" > /tmp/i2-mig-tech.out 2>&1
300
- ec_tech=$?
301
- bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$USER_DIR" > /tmp/i2-mig-user.out 2>&1
302
- ec_user=$?
303
- set -e
304
- [ "$ec_tech" = "$ec_user" ]
305
- assert_isomorphic_files /tmp/i2-mig-tech.out /tmp/i2-mig-user.out
306
- }
307
-
308
- # ── Cross-script invariant: full-pipeline behaviour identical across types ───
309
-
310
- @test "I2: full diagnose pipeline (migrate diagnose + reconcile-readme) invariant across types" {
311
- build_twin_fixtures
312
- set +e
313
- bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$TECH_DIR" > /tmp/i2-pipe-tech-mig.out 2>&1
314
- bash "$SCRIPTS_DIR/reconcile-readme.sh" "$TECH_DIR" > /tmp/i2-pipe-tech-rec.out 2>&1
315
- bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$USER_DIR" > /tmp/i2-pipe-user-mig.out 2>&1
316
- bash "$SCRIPTS_DIR/reconcile-readme.sh" "$USER_DIR" > /tmp/i2-pipe-user-rec.out 2>&1
317
- set -e
318
- assert_isomorphic_files /tmp/i2-pipe-tech-mig.out /tmp/i2-pipe-user-mig.out
319
- assert_isomorphic_files /tmp/i2-pipe-tech-rec.out /tmp/i2-pipe-user-rec.out
320
- }