@windyroad/architect 0.7.0 → 0.7.1-preview.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-architect",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Architecture decision enforcement for Claude Code"
5
5
  }
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env bash
2
+ # Shared derive-first dispatch helper — canonical source-of-truth.
3
+ #
4
+ # P132 Phase 2a-iii-A extracted this helper from three declaration-skill
5
+ # surfaces. Phase 2a-iii-B (2026-05-16) added wr-architect:create-adr as
6
+ # the 4th adopter, which required moving the canonical source from
7
+ # packages/itil/lib/ to packages/shared/ per ADR-017 (Shared code
8
+ # duplicated into per-package lib/ kept in sync by script + CI drift
9
+ # check). The per-package lib/ copies are byte-identical to this file:
10
+ #
11
+ # - packages/itil/lib/derive-first-dispatch.sh (sync target)
12
+ # - packages/architect/lib/derive-first-dispatch.sh (sync target)
13
+ #
14
+ # Sync mechanism: scripts/sync-derive-first-dispatch.sh (mirrors the
15
+ # sync-install-utils.sh pattern). CI guard: npm run check:derive-first-dispatch.
16
+ # Drift test: packages/shared/test/sync-derive-first-dispatch.bats.
17
+ #
18
+ # Maintainer-side SKILL.md surfaces that source the helper:
19
+ # - packages/itil/skills/capture-problem/SKILL.md Step 1.5
20
+ # - packages/itil/skills/manage-incident/SKILL.md Step 4
21
+ # - packages/itil/skills/manage-problem/SKILL.md Step 4
22
+ # - packages/architect/skills/create-adr/SKILL.md Step 2 (P132 Phase 2a-iii-B)
23
+ #
24
+ # Each caller passes surface-specific signal definitions; this helper
25
+ # centralises the dispatch mechanism: slug derivation, two-sided lexical
26
+ # classifier, RISK-POLICY matrix lookup, and the I2-isomorphic stderr
27
+ # advisory format.
28
+ #
29
+ # <!-- DERIVE-FIRST-DISPATCH-CONTRACT-SOURCE: P132 Phase 2a-iii-A + Phase 2a-iii-B -->
30
+ # Drift in the stderr advisory format here re-opens P132 — any change MUST
31
+ # update all four caller SKILL.md surfaces in the same commit.
32
+ #
33
+ # Usage (sourced):
34
+ # . packages/<pkg>/lib/derive-first-dispatch.sh # callers source their own package's copy
35
+ #
36
+ # Exported functions:
37
+ # emit_stderr_advisory <skill> <field> <value> <source> [reversibility]
38
+ # derive_kebab_slug <description> [max_tokens=8]
39
+ # lexical_classify_two_sided <text> <side_a_patterns_var> <side_b_patterns_var>
40
+ # risk_policy_matrix_lookup <text> <impact_high> <impact_mod> <impact_low>
41
+ # <likelihood_high> <likelihood_med> <likelihood_low>
42
+ #
43
+ # @adr ADR-002 (Monorepo per-plugin packages — architecture context for ADR-017)
44
+ # @adr ADR-017 (Shared code duplicated into per-package lib/ kept in sync)
45
+ # @adr ADR-044 (Decision-Delegation Contract — derive-first framework boundary)
46
+ # @adr ADR-026 (cost-source grounding — stderr advisory)
47
+ # @adr ADR-013 Rule 5 (policy-authorised silent proceed)
48
+ # @adr ADR-052 (behavioural-by-default — tested via scripts/test/derive-first-dispatch.bats
49
+ # and packages/shared/test/sync-derive-first-dispatch.bats)
50
+ # @problem P132 (agents over-ask in interactive sessions — Phase 2a-iii-A shared helper +
51
+ # Phase 2a-iii-B 4th-adopter migration to packages/shared/)
52
+ # @problem P185 (capture-problem Step 1.5 worked-example precedent)
53
+ # @jtbd JTBD-001 (enforce governance without slowing down — primary)
54
+ # @jtbd JTBD-101 (extend the suite with consistent patterns)
55
+ #
56
+ # NOT exporting `set -e` at file scope — callers source the helper and
57
+ # expect functions that return AMBIGUOUS sentinels rather than errexit.
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # emit_stderr_advisory — canonical I2-isomorphic stderr advisory format.
61
+ #
62
+ # Format: <skill>: derived <field>=<value> from <source>; <reversibility>
63
+ #
64
+ # This is the single source-of-truth for the advisory sentence shape
65
+ # across all derive-first declaration-skill surfaces. The format is
66
+ # load-bearing for cross-skill consistency — drift here re-opens P132.
67
+ # ---------------------------------------------------------------------------
68
+ emit_stderr_advisory() {
69
+ local skill="$1"
70
+ local field="$2"
71
+ local value="$3"
72
+ local source_desc="$4"
73
+ local reversibility="${5:-re-invoke or update if mis-rated}"
74
+ printf '%s: derived %s=%s from %s; %s\n' \
75
+ "$skill" "$field" "$value" "$source_desc" "$reversibility" >&2
76
+ }
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # derive_kebab_slug — kebab-case slug from prose.
80
+ #
81
+ # Lowercases, strips non-alphanumeric (preserves space and hyphen as
82
+ # token separators), drops stopwords, joins surviving tokens with `-`,
83
+ # caps the token count (default 8 per the SKILL.md surface contract).
84
+ #
85
+ # Used at:
86
+ # - capture-problem Step 1.4 Title derivation
87
+ # - manage-incident Step 4 Title derivation
88
+ # - manage-problem Step 4 Title derivation
89
+ # - create-adr Step 2 Title derivation (P132 Phase 2a-iii-B)
90
+ # ---------------------------------------------------------------------------
91
+ derive_kebab_slug() {
92
+ local description="$1"
93
+ local max_tokens="${2:-8}"
94
+ # Stopword list — common English function words plus "I/you/we" pronouns.
95
+ local stopwords='^(the|a|an|and|or|but|if|then|else|when|while|for|to|of|in|on|at|by|from|with|as|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|may|might|must|can|i|you|we|they|it|its|this|that|these|those|so|because|since|just|only|than|like|some|any|all|each|every|no|not)$'
96
+
97
+ printf '%s' "$description" \
98
+ | tr '[:upper:]' '[:lower:]' \
99
+ | tr -c 'a-z0-9 -' ' ' \
100
+ | tr -s ' ' \
101
+ | tr ' ' '\n' \
102
+ | grep -vE "$stopwords" \
103
+ | grep -v '^$' \
104
+ | head -n "$max_tokens" \
105
+ | paste -sd '-' -
106
+ }
107
+
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
+ # ---------------------------------------------------------------------------
163
+ # risk_policy_matrix_lookup — RISK-POLICY.md Impact × Likelihood lookup.
164
+ #
165
+ # Used by:
166
+ # - manage-incident Step 4 Severity derivation
167
+ # - manage-problem Step 4 Priority derivation
168
+ #
169
+ # Caller passes description text plus six regex pattern arrays (by
170
+ # name) keyed by impact band (high/mod/low) and likelihood band
171
+ # (high/med/low). Helper echoes one of:
172
+ #
173
+ # <score>|<label>|impact=<L>+likelihood=<L>
174
+ # Single dominant impact band AND single dominant likelihood band
175
+ # matched. Score = impact_val * likelihood_val; label per
176
+ # RISK-POLICY.md § Label Bands (Very Low / Low / Medium / High /
177
+ # Very High).
178
+ # AMBIGUOUS|<reason>
179
+ # Multi-band hit (signals point to conflicting cells) OR zero hit
180
+ # (no mappable signal). Caller fires AskUserQuestion as the
181
+ # genuine ADR-044 category-5 (taste) fallback surface.
182
+ #
183
+ # Band-to-numeric mapping (preserves RISK-POLICY.md Impact / Likelihood
184
+ # Levels table):
185
+ # impact: high = 5 (Severe), mod = 3 (Moderate), low = 1 (Negligible)
186
+ # likelihood: high = 5 (Almost certain), med = 3 (Possible), low = 1 (Rare)
187
+ #
188
+ # Label bands (RISK-POLICY.md):
189
+ # 1-2 Very Low
190
+ # 3-4 Low
191
+ # 5-9 Medium
192
+ # 10-16 High
193
+ # 17-25 Very High
194
+ #
195
+ # This helper preserves the band-to-score mapping; callers that need a
196
+ # wider granularity (e.g. Significant=4 / Minor=2) must extend the
197
+ # pattern arrays' band-buckets in a follow-on contract change.
198
+ # ---------------------------------------------------------------------------
199
+ risk_policy_matrix_lookup() {
200
+ local description="$1"
201
+ local -n _impact_high_ref="$2"
202
+ local -n _impact_mod_ref="$3"
203
+ local -n _impact_low_ref="$4"
204
+ local -n _likelihood_high_ref="$5"
205
+ local -n _likelihood_med_ref="$6"
206
+ local -n _likelihood_low_ref="$7"
207
+
208
+ local pat
209
+ local impact_high_hits=0
210
+ local impact_mod_hits=0
211
+ local impact_low_hits=0
212
+ local likelihood_high_hits=0
213
+ local likelihood_med_hits=0
214
+ local likelihood_low_hits=0
215
+
216
+ for pat in "${_impact_high_ref[@]}"; do
217
+ if printf '%s' "$description" | grep -qiE "$pat" 2>/dev/null; then
218
+ impact_high_hits=$((impact_high_hits + 1))
219
+ fi
220
+ done
221
+ for pat in "${_impact_mod_ref[@]}"; do
222
+ if printf '%s' "$description" | grep -qiE "$pat" 2>/dev/null; then
223
+ impact_mod_hits=$((impact_mod_hits + 1))
224
+ fi
225
+ done
226
+ for pat in "${_impact_low_ref[@]}"; do
227
+ if printf '%s' "$description" | grep -qiE "$pat" 2>/dev/null; then
228
+ impact_low_hits=$((impact_low_hits + 1))
229
+ fi
230
+ done
231
+ for pat in "${_likelihood_high_ref[@]}"; do
232
+ if printf '%s' "$description" | grep -qiE "$pat" 2>/dev/null; then
233
+ likelihood_high_hits=$((likelihood_high_hits + 1))
234
+ fi
235
+ done
236
+ for pat in "${_likelihood_med_ref[@]}"; do
237
+ if printf '%s' "$description" | grep -qiE "$pat" 2>/dev/null; then
238
+ likelihood_med_hits=$((likelihood_med_hits + 1))
239
+ fi
240
+ done
241
+ for pat in "${_likelihood_low_ref[@]}"; do
242
+ if printf '%s' "$description" | grep -qiE "$pat" 2>/dev/null; then
243
+ likelihood_low_hits=$((likelihood_low_hits + 1))
244
+ fi
245
+ done
246
+
247
+ local nonzero_impact=0
248
+ (( impact_high_hits > 0 )) && nonzero_impact=$((nonzero_impact + 1))
249
+ (( impact_mod_hits > 0 )) && nonzero_impact=$((nonzero_impact + 1))
250
+ (( impact_low_hits > 0 )) && nonzero_impact=$((nonzero_impact + 1))
251
+
252
+ if (( nonzero_impact != 1 )); then
253
+ printf 'AMBIGUOUS|impact-bands-hit=%d\n' "$nonzero_impact"
254
+ return 0
255
+ fi
256
+
257
+ local impact_band=0
258
+ local impact_label=""
259
+ if (( impact_high_hits > 0 )); then
260
+ impact_band=5
261
+ impact_label="Severe"
262
+ elif (( impact_mod_hits > 0 )); then
263
+ impact_band=3
264
+ impact_label="Moderate"
265
+ elif (( impact_low_hits > 0 )); then
266
+ impact_band=1
267
+ impact_label="Negligible"
268
+ fi
269
+
270
+ local nonzero_likelihood=0
271
+ (( likelihood_high_hits > 0 )) && nonzero_likelihood=$((nonzero_likelihood + 1))
272
+ (( likelihood_med_hits > 0 )) && nonzero_likelihood=$((nonzero_likelihood + 1))
273
+ (( likelihood_low_hits > 0 )) && nonzero_likelihood=$((nonzero_likelihood + 1))
274
+
275
+ if (( nonzero_likelihood != 1 )); then
276
+ printf 'AMBIGUOUS|likelihood-bands-hit=%d\n' "$nonzero_likelihood"
277
+ return 0
278
+ fi
279
+
280
+ local likelihood_band=0
281
+ local likelihood_label=""
282
+ if (( likelihood_high_hits > 0 )); then
283
+ likelihood_band=5
284
+ likelihood_label="Almost-certain"
285
+ elif (( likelihood_med_hits > 0 )); then
286
+ likelihood_band=3
287
+ likelihood_label="Possible"
288
+ elif (( likelihood_low_hits > 0 )); then
289
+ likelihood_band=1
290
+ likelihood_label="Rare"
291
+ fi
292
+
293
+ local score=$((impact_band * likelihood_band))
294
+ local label
295
+ if (( score >= 17 )); then
296
+ label="Very High"
297
+ elif (( score >= 10 )); then
298
+ label="High"
299
+ elif (( score >= 5 )); then
300
+ label="Medium"
301
+ elif (( score >= 3 )); then
302
+ label="Low"
303
+ else
304
+ label="Very Low"
305
+ fi
306
+
307
+ printf '%d|%s|impact=%s+likelihood=%s\n' \
308
+ "$score" "$label" "$impact_label" "$likelihood_label"
309
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "0.7.0",
3
+ "version": "0.7.1-preview.317",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"
@@ -18,19 +18,50 @@ Scan for existing ADRs:
18
18
  - Read any decisions related to the topic being discussed (if the user has mentioned a topic)
19
19
  - If `docs/decisions/` does not exist, create it
20
20
 
21
- ### 2. Gather context from the user
21
+ ### 2. Gather context (P132 derive-first; ADR-044 category-4 silent-framework on derivable fields; category-1 direction-setting only on user-judgment fields)
22
22
 
23
- You MUST use the AskUserQuestion tool to collect the decision context. Do not proceed to step 3 until you have answers.
23
+ **Shared dispatch helper**: this surface invokes `packages/architect/lib/derive-first-dispatch.sh` for the canonical slug derivation (Title) and I2-isomorphic stderr advisory format. The canonical source-of-truth lives at `packages/shared/derive-first-dispatch.sh`; the architect package carries a synced per-package copy at `packages/architect/lib/derive-first-dispatch.sh` per ADR-017 (Shared code duplicated into per-package lib/ kept in sync). The same helper is sourced by `/wr-itil:capture-problem` Step 1.5, `/wr-itil:manage-incident` Step 4, and `/wr-itil:manage-problem` Step 4 (each from its own per-package `packages/itil/lib/` copy); drift in the advisory shape across the four surfaces re-opens P132.
24
24
 
25
- Ask the user:
25
+ **Derive-first dispatch.** ADR creation is fundamentally user-judgment-bound — only the user knows the decision space, the alternatives considered, and the chosen-option rationale. But the **declaration-skeleton fields** (Title, status, date, reassessment-date, context-and-problem-statement) carry observable evidence in the user's prose, the working tree, and the wall-clock — the framework can resolve them without firing `AskUserQuestion`. The retained `AskUserQuestion` surfaces (Decision Drivers, Considered Options, Decision Outcome, Consequences, Confirmation, decision-makers) are the genuine **category-1 direction-setting** fields.
26
26
 
27
- 1. **What is the decision about?** A brief title and the problem being solved.
28
- 2. **What options were considered?** At least 2 alternatives (including "do nothing" if applicable). For each option, ask for key pros and cons.
29
- 3. **What was chosen and why?** The selected option and the primary reason.
30
- 4. **Who are the decision-makers?** Who made or is making this decision.
31
- 5. **Any consequences to note?** Known good, neutral, or bad outcomes.
27
+ The P132 inverse-P078 trap (`docs/problems/known-error/132-...md`) is the load-bearing motivation. create-adr Step 2 is the **fourth declaration-skill surface** under Phase 2a to ship the derive-first dispatch (after `/wr-itil:capture-problem` Step 1.5, `/wr-itil:manage-incident` Step 4, and `/wr-itil:manage-problem` Step 4). The pattern is I2-isomorphic across all four — the stderr advisory shape `<skill>: derived <field>=<value> from <source>; <reversibility>` is identical beyond substituted values per the helper's `emit_stderr_advisory` function (architect verdict 2026-05-16 P132 Phase 2a-iii-B: pattern lock-in across the 4-surface set).
32
28
 
33
- If the user has already provided this context in the conversation (e.g., as arguments), use what they've given and only ask about what's missing.
29
+ Resolve each field via the following dispatch. **The order is load-bearing** every derivable field resolves silently with a stderr advisory citing the source; only user-judgment fields fire `AskUserQuestion`.
30
+
31
+ | Field | Dispatch | ADR-044 category |
32
+ |-------|----------|------------------|
33
+ | **Title** | Derive silently. Kebab-case the first 8-10 non-stopword tokens of the user's prose problem-statement (same slug derivation as `/wr-itil:capture-problem` Step 1.4, `/wr-itil:manage-incident` Step 4, and `/wr-itil:manage-problem` Step 4 — uses the shared helper's `derive_kebab_slug` function). Emit stderr advisory: `create-adr: derived title='<slug>' from problem-statement; re-invoke with the desired title or rename the file if the slug is wrong`. Do NOT fire AskUserQuestion. | category-4 silent-framework |
34
+ | **status** (frontmatter) | Always `proposed` for new ADRs per Step 4 template convention. No ask, no advisory needed — SKILL convention is unambiguous. | category-4 silent-framework |
35
+ | **date** (frontmatter) | Today's date (`date +%Y-%m-%d`) per Step 4 template. No ask, no advisory needed — wall-clock derivation is unambiguous. | category-4 silent-framework |
36
+ | **reassessment-date** (frontmatter) | Today + 3 months (`date -v+3m +%Y-%m-%d` on BSD-date / `date -d '+3 months' +%Y-%m-%d` on GNU-date) per Step 4 template. Emit stderr advisory: `create-adr: derived reassessment-date='<YYYY-MM-DD>' from today+3-months default; re-invoke with --reassessment-date= or edit the frontmatter to override`. | category-4 silent-framework |
37
+ | **Context and Problem Statement** | Pull verbatim from `$ARGUMENTS` prose into the Step 4 template's `## Context and Problem Statement` section. **Fallback**: when `$ARGUMENTS` carries NO problem prose (only flags or empty body), fire AskUserQuestion as the genuine category-1 direction-setting surface — *"only the user knows the problem being solved."* Question text: *"What problem does this ADR solve? Why is a decision needed now?"* This is the prose-fallback path; the typical maintainer invocation carries the problem-statement in arguments. | category-1 direction-setting (fallback only; category-4 silent-framework on the typical path where prose is present) |
38
+ | **decision-makers** | Retain AskUserQuestion. Architect verdict 2026-05-16: silent derivation from `git config user.name` would conflate "who committed the ADR" with "who made the decision" — a multi-party decision is one of the canonical mis-attribution risks ADR-013's identity model rejects. Once-per-ADR ask is low-friction in absolute terms. Question text: *"Who are the decision-makers?"* | category-1 direction-setting |
39
+ | **Decision Drivers** | Retain AskUserQuestion. Only the user knows which factors weighted the decision. This is the create-adr-equivalent of manage-problem Step 4's Description (the user-judgment surface). | category-1 direction-setting |
40
+ | **Considered Options** | Retain AskUserQuestion. Only the user knows the alternatives evaluated. ADR-044 cat-5 (taste) would only apply if the framework could offer 2+ valid options — but the alternative space is genuinely user-knowledge (the framework can offer "do nothing" + a status-quo option but the actual alternatives are the user's). Architect verdict 2026-05-16: confirmed cat-1 over cat-5. Per MADR 4.0: ≥2 alternatives including "do nothing" where applicable. | category-1 direction-setting |
41
+ | **Decision Outcome** / **Rationale** | Retain AskUserQuestion. The chosen option + primary reason for the choice. | category-1 direction-setting |
42
+ | **Consequences** (Good / Neutral / Bad) | Retain AskUserQuestion. Only the user knows the expected consequences of the decision. | category-1 direction-setting |
43
+ | **Confirmation** | Retain AskUserQuestion. Testable verification criteria. | category-1 direction-setting |
44
+ | **consulted** / **informed** (frontmatter) | Default to empty list per Step 4 template; fold into the decision-makers AskUserQuestion call if the user surfaces stakeholders. | category-4 silent-framework (default empty); category-1 (when user cites stakeholders) |
45
+
46
+ **Inferred fields (no ask, no advisory needed)**:
47
+
48
+ - **supersedes** (frontmatter): empty list by default; populated only via Step 6 supersession handling when the user explicitly cites a superseded decision.
49
+
50
+ **Stderr advisory contract**: each derived field emits a SINGLE line to stderr (NOT stdout, NOT in the ADR body) via the shared helper's `emit_stderr_advisory` function in `packages/architect/lib/derive-first-dispatch.sh`. The canonical format produced by the helper:
51
+
52
+ ```
53
+ create-adr: derived <field>=<value> from <source>; <reversibility-clause>
54
+ ```
55
+
56
+ The advisory text shape is I2-isomorphic — same sentence structure across all four derive-first declaration-skill surfaces (`capture-problem`, `manage-incident`, `manage-problem`, `create-adr`) beyond substituted values + source names. 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 an ADR-body line; embedding it in the ADR body would violate the MADR 4.0 schema. Stderr is the correct channel — visible to interactive maintainers in the terminal; invisible to ADR consumers; loggable by orchestrators that capture subprocess stderr.
57
+
58
+ **ADR-026 cost-source grounding**: each derived field cites its source in the advisory (problem-statement token sequence for Title; today's date for date / reassessment-date; default convention for status). The `re-invoke or update if mis-rated` clause carries the reversibility marker ADR-026 mandates for ungrounded outputs.
59
+
60
+ **AFK fail-safe (ADR-013 Rule 6)**: under AFK orchestration, derivable fields (Title / status / date / reassessment-date / Context-when-prose-present) resolve without interactive input. The 6 retained cat-1 AskUserQuestion surfaces (decision-makers / Decision Drivers / Considered Options / Decision Outcome / Consequences / Confirmation) WILL halt AFK execution — that is **correct behaviour** because ADR creation is genuinely user-judgment-bound (the user authors the decision; the framework cannot). JTBD-006 protection: AFK orchestrators that need ADR creation should call `/wr-architect:capture-adr` (the lightweight aside surface) for the skeleton + Title derivation, then defer the cat-1 field collection to the user's next interactive session via the capture-adr deferred-flagged-sections mechanism.
61
+
62
+ **Cross-skill consistency note**: this is the **fourth declaration-skill surface** to ship the derive-first dispatch (after `/wr-itil:capture-problem` Step 1.5, `/wr-itil:manage-incident` Step 4, and `/wr-itil:manage-problem` Step 4 in commits b7cc645 / 43255d2 / 30fd22b). Phase 2a-iii-B (2026-05-16) closes Phase 2a's full 4-surface scope — the I2-isomorphic stderr advisory format is now locked-in across `capture-problem`, `manage-incident`, `manage-problem`, AND `create-adr` via the shared helper at `packages/shared/derive-first-dispatch.sh` with synced per-package lib/ copies. Per ADR-017, drift between copies is caught by `npm run check:derive-first-dispatch` in CI.
63
+
64
+ If the user has already provided context in `$ARGUMENTS` or earlier conversation, use what they've given and only fire AskUserQuestion for the cat-1 fields still missing.
34
65
 
35
66
  ### 2b. Decision-boundary analysis (multi-decision check)
36
67
 
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bats
2
+ # ADR-044 alignment contract assertions for create-adr SKILL.md Step 2
3
+ # (P132 Phase 2a-iii-B derive-first refactor, 2026-05-16).
4
+ #
5
+ # tdd-review: structural-permitted (justification: SKILL.md prose contract
6
+ # assertions; behavioural skill-runtime harness pending P012 + P081 Phase 2;
7
+ # expected to migrate to behavioural form once the harness exists. Added
8
+ # during P132 Phase 2a-iii-B per the inline plan's bridge-marker rule —
9
+ # isomorphic precedent at manage-problem-adr-044-step4-derive-first.bats.)
10
+ #
11
+ # This file is the dedicated structural-grep-permitted home for the ADR-044
12
+ # alignment contract during the bridge window. After P081 Phase 2 retrofits
13
+ # the project's structural-grep tests to behavioural form, this file's
14
+ # assertions migrate too.
15
+ #
16
+ # @problem P132 (agents over-ask in interactive sessions — Phase 2a-iii-B
17
+ # create-adr argument-collection derive-first refactor as 4th adopter)
18
+ # @problem P185 (capture-problem Step 1.5 worked-example precedent)
19
+ # @problem P136 (ADR-044 alignment audit master)
20
+ # @adr ADR-002 (Monorepo per-plugin packages)
21
+ # @adr ADR-017 (Shared code duplicated into per-package lib/ kept in sync)
22
+ # @adr ADR-044 (Decision-Delegation Contract)
23
+ # @adr ADR-013 amended Rule 1 (structured user interaction)
24
+ # @adr ADR-026 (cost-source grounding — stderr advisory shape)
25
+ # @adr ADR-052 (behavioural-by-default with structural bridge window)
26
+ # @jtbd JTBD-001 (enforce governance without slowing down — primary)
27
+ # @jtbd JTBD-006 (work backlog AFK — queued for return, not guessed at)
28
+ # @jtbd JTBD-101 (extend the suite with consistent patterns)
29
+
30
+ setup() {
31
+ SKILL_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
32
+ SKILL_FILE="${SKILL_DIR}/SKILL.md"
33
+ [ -f "$SKILL_FILE" ]
34
+ }
35
+
36
+ # ----------------------------------------------------------------------
37
+ # Step 2 derive-first refactor (P132 Phase 2a-iii-B) — cat-4 silent-framework
38
+ # on Title + frontmatter defaults + Context-and-Problem-Statement; cat-1
39
+ # direction-setting on Decision Drivers / Considered Options / Decision
40
+ # Outcome / Consequences / Confirmation / decision-makers.
41
+ # ----------------------------------------------------------------------
42
+
43
+ @test "SKILL.md Step 2 cross-references ADR-044 category-4 (silent-framework) for derivable fields (P132 derive-first)" {
44
+ # P132 Phase 2a-iii-B: Title (kebab from prose), status (always proposed),
45
+ # date (today), reassessment-date (today+3mo), context-and-problem-statement
46
+ # (verbatim from $ARGUMENTS prose) resolve via silent-framework per
47
+ # ADR-044 category 4.
48
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
49
+ [ "$status" -eq 0 ]
50
+ [[ "$output" == *"silent-framework"* ]] || [[ "$output" == *"category 4"* ]] || [[ "$output" == *"category-4"* ]]
51
+ }
52
+
53
+ @test "SKILL.md Step 2 cross-references ADR-044 category-1 (direction-setting) for user-judgment fields" {
54
+ # Decision Drivers / Considered Options / Decision Outcome / Consequences /
55
+ # Confirmation retain AskUserQuestion as the genuine cat-1 surfaces —
56
+ # ADR creation is fundamentally user-judgment-bound; only the user knows
57
+ # the alternative space, the chosen-option rationale, and the testable
58
+ # confirmation criteria.
59
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
60
+ [ "$status" -eq 0 ]
61
+ [[ "$output" == *"direction-setting"* ]] || [[ "$output" == *"category 1"* ]] || [[ "$output" == *"category-1"* ]]
62
+ }
63
+
64
+ @test "SKILL.md Step 2 derives Title from prose silently (P132 inverse-P078)" {
65
+ # The 2026-05-06 I001 declaration regression cited in P132 was the agent
66
+ # asking "Title" with N candidate options when kebab-casing the description
67
+ # would have produced the slug directly. create-adr Step 2 must ship the
68
+ # same derive-first pattern as the 3 itil declaration-skill surfaces.
69
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
70
+ [ "$status" -eq 0 ]
71
+ [[ "$output" == *"Title"* ]] || [[ "$output" == *"title"* ]]
72
+ [[ "$output" == *"derive"* ]] || [[ "$output" == *"derived"* ]]
73
+ [[ "$output" == *"kebab"* ]] || [[ "$output" == *"prose"* ]]
74
+ }
75
+
76
+ @test "SKILL.md Step 2 derives status / date / reassessment-date silently (P132 derive-first)" {
77
+ # Frontmatter defaults (status=proposed, date=today, reassessment-date=
78
+ # today+3mo) are SKILL conventions — no user judgment required at capture.
79
+ # Step 4's existing template already mandates these defaults; Step 2 must
80
+ # name them as silent-framework cat-4 surfaces explicitly so the
81
+ # I2-isomorphic stderr advisory contract covers them.
82
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
83
+ [ "$status" -eq 0 ]
84
+ [[ "$output" == *"status"* ]]
85
+ [[ "$output" == *"date"* ]]
86
+ [[ "$output" == *"reassessment"* ]] || [[ "$output" == *"proposed"* ]]
87
+ }
88
+
89
+ @test "SKILL.md Step 2 retains Considered Options + Decision Outcome as AskUserQuestion (negative-of-negative guard)" {
90
+ # Regression-resistance: the refactor MUST preserve the genuine cat-1
91
+ # direction-setting surfaces on Considered Options + Decision Outcome.
92
+ # The framework cannot generate the alternative space — only the user
93
+ # knows the alternatives evaluated. Architect verdict 2026-05-16:
94
+ # confirmed cat-1 over cat-5.
95
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
96
+ [ "$status" -eq 0 ]
97
+ [[ "$output" == *"AskUserQuestion"* ]]
98
+ [[ "$output" == *"Considered Options"* ]] || [[ "$output" == *"options"* ]] || [[ "$output" == *"alternative"* ]]
99
+ [[ "$output" == *"Decision Outcome"* ]] || [[ "$output" == *"chosen"* ]] || [[ "$output" == *"rationale"* ]]
100
+ }
101
+
102
+ @test "SKILL.md Step 2 retains decision-makers as AskUserQuestion (architect verdict — no silent derive from git user.name)" {
103
+ # Architect verdict 2026-05-16: decision-makers MUST stay cat-1
104
+ # AskUserQuestion. `git config user.name` would conflate "who committed
105
+ # the ADR" with "who made the decision" — a multi-party decision is one
106
+ # of the canonical mis-attribution risks ADR-013's identity model rejects.
107
+ # Once-per-ADR ask is low-friction in absolute terms.
108
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
109
+ [ "$status" -eq 0 ]
110
+ [[ "$output" == *"decision-makers"* ]] || [[ "$output" == *"decision makers"* ]]
111
+ }
112
+
113
+ @test "SKILL.md Step 2 cites P132 (inverse-P078 audit traceability)" {
114
+ # P132 + ADR-044 must appear in Step 2 or Related section so the audit
115
+ # trail for the Phase 2a-iii-B refactor is recoverable from the SKILL.md
116
+ # surface.
117
+ run grep -nE "P132" "$SKILL_FILE"
118
+ [ "$status" -eq 0 ]
119
+ }
120
+
121
+ @test "SKILL.md Step 2 documents stderr advisory shape for derived fields (ADR-026 grounding)" {
122
+ # ADR-026 cost-source grounding: each silent derivation emits a stderr
123
+ # advisory citing the source. Pattern parity with capture-problem Step
124
+ # 1.5 + manage-incident Step 4 + manage-problem Step 4 (I2-isomorphic
125
+ # across the four declaration-skill surfaces per Phase 2a-iii-B).
126
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
127
+ [ "$status" -eq 0 ]
128
+ [[ "$output" == *"stderr"* ]] || [[ "$output" == *"advisory"* ]]
129
+ }
130
+
131
+ @test "SKILL.md Step 2 cross-references the 3 prior derive-first surfaces (cross-skill consistency lock-in)" {
132
+ # Phase 2a-iii-B is the 4th adopter. The Step 2 prose must explicitly
133
+ # cite the 3 prior surfaces (capture-problem + manage-incident +
134
+ # manage-problem) so the I2-isomorphic stderr advisory format is
135
+ # locked-in by reference across the 4-surface set.
136
+ run awk '/^### 2\. /,/^### 2b\. /' "$SKILL_FILE"
137
+ [ "$status" -eq 0 ]
138
+ [[ "$output" == *"capture-problem"* ]]
139
+ [[ "$output" == *"manage-incident"* ]]
140
+ [[ "$output" == *"manage-problem"* ]]
141
+ }
142
+
143
+ @test "SKILL.md Step 2 references derive-first-dispatch.sh helper (sourced from architect package per ADR-017)" {
144
+ # Per ADR-017 self-contained-published-package property: create-adr lives
145
+ # in @windyroad/architect, so it MUST source the helper from its own
146
+ # package lib (packages/architect/lib/derive-first-dispatch.sh), NOT
147
+ # cross-package from packages/itil/lib/. The canonical source lives at
148
+ # packages/shared/derive-first-dispatch.sh; both itil and architect have
149
+ # synced per-package lib/ copies.
150
+ run grep -E "packages/architect/lib/derive-first-dispatch" "$SKILL_FILE"
151
+ [ "$status" -eq 0 ]
152
+ }
153
+
154
+ # ----------------------------------------------------------------------
155
+ # Negative-of-negative guards — Step 2b multi-decision split + Step 5
156
+ # confirm-with-user remain unchanged.
157
+ # ----------------------------------------------------------------------
158
+
159
+ @test "SKILL.md Step 2b multi-decision AskUserQuestion is preserved (cat-1 direction-setting, not touched by Phase 2a-iii-B)" {
160
+ # Step 2b is a separate cat-1 direction-setting surface — only the
161
+ # user knows whether multiple decisions can be independently accepted.
162
+ # The Phase 2a-iii-B refactor MUST NOT touch Step 2b's AskUserQuestion gate.
163
+ run awk '/^### 2b\. /,/^### 3\. /' "$SKILL_FILE"
164
+ [ "$status" -eq 0 ]
165
+ [[ "$output" == *"AskUserQuestion"* ]]
166
+ [[ "$output" == *"decision"* ]] || [[ "$output" == *"split"* ]]
167
+ }
168
+
169
+ @test "SKILL.md Step 5 confirm-with-user AskUserQuestion is preserved (post-write review surface)" {
170
+ # Step 5 is the genuine cat-2 deviation-approval post-write review
171
+ # surface (analogous to manage-incident Step 6 evidence-first gate).
172
+ # The Phase 2a-iii-B refactor MUST NOT touch Step 5's AskUserQuestion gate.
173
+ run awk '/^### 5\. /,/^### 6\. /' "$SKILL_FILE"
174
+ [ "$status" -eq 0 ]
175
+ [[ "$output" == *"AskUserQuestion"* ]]
176
+ }
177
+
178
+ # ----------------------------------------------------------------------
179
+ # P081 + P132 bridge marker
180
+ # ----------------------------------------------------------------------
181
+
182
+ @test "bats file carries the tdd-review: structural-permitted marker" {
183
+ run grep -nE "tdd-review:[[:space:]]+structural-permitted" "${BATS_TEST_FILENAME}"
184
+ [ "$status" -eq 0 ]
185
+ }