@windyroad/itil 0.45.0 → 0.46.0-preview.514
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.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/derive-first-dispatch.sh +8 -55
- package/package.json +1 -1
- package/scripts/test/derive-first-dispatch.bats +23 -53
- package/scripts/test/no-type-regression-guard.bats +73 -0
- package/skills/capture-problem/SKILL.md +32 -76
- package/skills/capture-problem/test/capture-problem-step-1-5b-jtbd-trace.bats +23 -26
- package/skills/capture-problem/test/capture-problem.bats +14 -261
- package/scripts/test/i2-no-type-branching.bats +0 -320
|
@@ -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
|
@@ -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 (
|
|
10
|
-
# Step 4,
|
|
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
|
-
#
|
|
122
|
-
#
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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 "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 (
|
|
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
|
-
|
|
|
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
|
-
|
|
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.
|
|
66
|
+
| Flag | Effect on Step 1.5b |
|
|
64
67
|
|------|-------------------|
|
|
65
|
-
| `--
|
|
66
|
-
| `--
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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 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).
|
|
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
|
|
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
|
|
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
|
|
370
|
-
| Type-tag prompt |
|
|
371
|
-
|
|
|
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
|
|
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
|
|
395
|
-
- **P185** — `/wr-itil:capture-problem`
|
|
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`) —
|
|
399
|
-
- **JTBD-301** (`docs/jtbd/plugin-user/JTBD-301-...md`) — plugin-user no-pre-classification persona constraint;
|
|
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/
|
|
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
|
|
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
|
-
# (
|
|
15
|
-
#
|
|
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
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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
|
-
|
|
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 "
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 "--
|
|
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
|
-
#
|
|
327
|
+
# P287 (2026-06-02) — retirement of P185 Step 1.5 derive-first classifier.
|
|
328
328
|
#
|
|
329
|
-
# The
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
# that
|
|
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
|
-
#
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
# the
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
}
|