@windyroad/itil 0.47.0 → 0.47.1-preview.521

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.47.0"
500
+ "version": "0.47.1"
501
501
  }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/check-locale-discipline.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/check-locale-discipline.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.47.0",
3
+ "version": "0.47.1-preview.521",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bash
2
+ # check-locale-discipline.sh — advisory lint for P328 (BSD locale UTF-8
3
+ # friction on macOS).
4
+ #
5
+ # BSD `grep` / `sed` / `awk` on macOS silently fail (or emit
6
+ # `multibyte conversion failure` / `illegal byte sequence`) when processing
7
+ # UTF-8 multi-byte characters (em-dash `—`, smart quotes, en-dash) without
8
+ # `LC_ALL=en_US.UTF-8` set. Our prose surfaces (ADRs, problem-ticket bodies,
9
+ # briefing entries, SKILL.md files) are pervasively em-dash / smart-quote
10
+ # rich, so any script that grep / sed / awks those surfaces is exposed.
11
+ # P328 captures three distinct incident classes from the 2026-05-30
12
+ # ADR-077 compendium session.
13
+ #
14
+ # This lint walks `packages/*/scripts/*.sh`, `packages/*/hooks/*.sh`, and
15
+ # `packages/*/lib/*.sh` (including `packages/*/hooks/lib/*.sh`) and reports
16
+ # any line invoking `grep` / `sed` / `awk` that is NOT preceded — in the
17
+ # same script — by an `export LC_ALL=` statement OR an inline `LC_ALL=`
18
+ # prefix on the same line. `git grep` is skipped (different binary).
19
+ #
20
+ # Usage:
21
+ # check-locale-discipline.sh [<repo-root>]
22
+ # <repo-root> defaults to the current working directory.
23
+ #
24
+ # Environment:
25
+ # WR_LOCALE_DISCIPLINE_WARN_ONLY=1 Phase 1 advisory (default) — exit 0
26
+ # even when violations exist.
27
+ # WR_LOCALE_DISCIPLINE_WARN_ONLY=0 Phase 2 load-bearing — exit 1 when
28
+ # violations exist.
29
+ #
30
+ # Exit codes:
31
+ # 0 = clean OR Phase 1 advisory with violations
32
+ # 1 = Phase 2 load-bearing with violations
33
+ # 2 = usage / path error
34
+ #
35
+ # Output format (one line per violation, to stderr):
36
+ # WARN <relpath>:<line> <tool> without preceding LC_ALL=en_US.UTF-8
37
+ #
38
+ # Promotion criteria (Phase 1 → Phase 2):
39
+ # Promote `WR_LOCALE_DISCIPLINE_WARN_ONLY=0` once existing scripts have
40
+ # been migrated. Until then, the warnings are signal-only — they identify
41
+ # scripts that may silently mis-process UTF-8 input on macOS.
42
+ #
43
+ # @adr ADR-040 (advisory-then-load-bearing reusable pattern)
44
+ # @adr ADR-049 (plugin-bundled scripts; PATH shim)
45
+ # @adr ADR-052 (behavioural-tests-default)
46
+ # @adr ADR-080 (highest-version-wins shim wrapper)
47
+ # @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory exit 0)
48
+ # @adr ADR-005 (Plugin testing strategy)
49
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down)
50
+ # @jtbd JTBD-101 (Extend the Suite with New Plugins)
51
+ # @problem P328 (BSD grep/sed/awk on macOS friction with UTF-8)
52
+
53
+ set -uo pipefail
54
+
55
+ # Self-application: this lint itself grep / sed / awks script content.
56
+ export LC_ALL=en_US.UTF-8
57
+
58
+ REPO_ROOT="${1:-$(pwd)}"
59
+ WARN_ONLY="${WR_LOCALE_DISCIPLINE_WARN_ONLY:-1}"
60
+
61
+ if [ ! -d "$REPO_ROOT" ]; then
62
+ echo "check-locale-discipline: not a directory: $REPO_ROOT" >&2
63
+ exit 2
64
+ fi
65
+
66
+ if [ ! -d "$REPO_ROOT/packages" ]; then
67
+ echo "check-locale-discipline: no packages/ subdir under $REPO_ROOT" >&2
68
+ exit 2
69
+ fi
70
+
71
+ # Enumerate target scripts: any *.sh under packages/<pkg>/scripts/,
72
+ # packages/<pkg>/hooks/ (incl. nested lib/), packages/<pkg>/lib/.
73
+ # Depth 3-5 covers packages/<pkg>/scripts/foo.sh and
74
+ # packages/<pkg>/hooks/lib/foo.sh.
75
+ mapfile -t TARGETS < <(
76
+ find "$REPO_ROOT/packages" \
77
+ -mindepth 3 -maxdepth 5 \
78
+ -type f -name '*.sh' \
79
+ \( -path '*/scripts/*' -o -path '*/hooks/*' -o -path '*/lib/*' \) \
80
+ 2>/dev/null | sort
81
+ )
82
+
83
+ violations=0
84
+ scanned=0
85
+
86
+ scan_one() {
87
+ local file="$1"
88
+ local rel="${file#"$REPO_ROOT"/}"
89
+ local line_no=0
90
+ local lc_all_set=0
91
+ local in_heredoc=0
92
+ local heredoc_token=''
93
+ local line
94
+
95
+ while IFS= read -r line || [ -n "$line" ]; do
96
+ line_no=$((line_no + 1))
97
+
98
+ # Heredoc body — skip until the closing token. The closing token
99
+ # appears on a line of its own (optionally leading whitespace when
100
+ # opened with `<<-`).
101
+ if [ "$in_heredoc" -eq 1 ]; then
102
+ local trimmed="${line#"${line%%[![:space:]]*}"}"
103
+ if [ "$trimmed" = "$heredoc_token" ]; then
104
+ in_heredoc=0
105
+ heredoc_token=''
106
+ fi
107
+ continue
108
+ fi
109
+
110
+ # Comment-only line — skip.
111
+ if [[ "$line" =~ ^[[:space:]]*\# ]]; then
112
+ continue
113
+ fi
114
+
115
+ # LC_ALL set state. An `export LC_ALL=...` line flips the file-wide
116
+ # protection on. An inline `LC_ALL=...` prefix on the same line as a
117
+ # grep / sed / awk invocation protects only that line.
118
+ if [[ "$line" =~ ^[[:space:]]*export[[:space:]]+LC_ALL= ]]; then
119
+ lc_all_set=1
120
+ continue
121
+ fi
122
+ if [[ "$line" =~ ^[[:space:]]*LC_ALL= ]]; then
123
+ continue
124
+ fi
125
+
126
+ # Detect a heredoc-open ON THIS LINE (before deciding it's a violation
127
+ # — heredoc-open lines may carry a grep/sed/awk invocation that runs).
128
+ # Grammar: `<<` or `<<-` followed by optional single/double quote then
129
+ # an identifier-shaped token. Set state for subsequent lines.
130
+ local opened_heredoc=0
131
+ if [[ "$line" =~ \<\<-?[\'\"]?([A-Za-z_][A-Za-z0-9_]*)[\'\"]? ]]; then
132
+ heredoc_token="${BASH_REMATCH[1]}"
133
+ opened_heredoc=1
134
+ fi
135
+
136
+ # If file-wide LC_ALL is set above this point, no violation possible.
137
+ if [ "$lc_all_set" -eq 1 ]; then
138
+ [ "$opened_heredoc" -eq 1 ] && in_heredoc=1
139
+ continue
140
+ fi
141
+
142
+ # Tool-invocation detector. Word-boundary match on grep / sed / awk
143
+ # at a command position: start of line, after a pipe, semicolon,
144
+ # ampersand, open-paren, backtick, or whitespace. Followed by
145
+ # whitespace (so we skip `grep_helper`, `$grep`, etc.). We scan
146
+ # for ALL three tools by checking each in turn so multiple
147
+ # invocations on one line still flag the line once per distinct
148
+ # tool — keeps output deterministic and bounded.
149
+ local matched_tool=''
150
+ if [[ "$line" =~ (^|[[:space:]\|\;\&\(\`\$\{])(grep|sed|awk)([[:space:]]|$) ]]; then
151
+ matched_tool="${BASH_REMATCH[2]}"
152
+ fi
153
+
154
+ if [ -n "$matched_tool" ]; then
155
+ # Skip `git grep` (different binary; uses git's own pattern engine).
156
+ if [ "$matched_tool" = "grep" ] && [[ "$line" =~ git[[:space:]]+grep ]]; then
157
+ [ "$opened_heredoc" -eq 1 ] && in_heredoc=1
158
+ continue
159
+ fi
160
+ printf 'WARN %s:%d %s without preceding LC_ALL=en_US.UTF-8\n' \
161
+ "$rel" "$line_no" "$matched_tool" >&2
162
+ violations=$((violations + 1))
163
+ fi
164
+
165
+ [ "$opened_heredoc" -eq 1 ] && in_heredoc=1
166
+ done < "$file"
167
+ }
168
+
169
+ for f in "${TARGETS[@]}"; do
170
+ scanned=$((scanned + 1))
171
+ scan_one "$f"
172
+ done
173
+
174
+ if [ "$violations" -gt 0 ]; then
175
+ printf 'check-locale-discipline: %d violation(s) across %d script(s) — add `export LC_ALL=en_US.UTF-8` at the top of each script that calls grep/sed/awk on UTF-8 content, or use an inline `LC_ALL=...` prefix per invocation (P328).\n' \
176
+ "$violations" "$scanned" >&2
177
+ if [ "$WARN_ONLY" = "1" ]; then
178
+ exit 0
179
+ else
180
+ exit 1
181
+ fi
182
+ fi
183
+
184
+ printf 'check-locale-discipline: clean (%d script(s) scanned; no unprotected grep/sed/awk invocations).\n' "$scanned"
185
+ exit 0
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # @problem P328 — BSD `grep` / `sed` / `awk` on macOS silently mis-process
4
+ # UTF-8 multi-byte characters (em-dash, smart quotes) without
5
+ # `LC_ALL=en_US.UTF-8` set. Lint detects unprotected invocations so the
6
+ # class of silent-wrong-result bugs can't reach the codebase undetected.
7
+ #
8
+ # Contract: `check-locale-discipline.sh [<repo-root>]` walks scripts under
9
+ # `packages/*/scripts/`, `packages/*/hooks/` (incl. nested `lib/`), and
10
+ # `packages/*/lib/`, then emits one WARN line per unprotected grep/sed/awk
11
+ # invocation. Default Phase 1 advisory exits 0; `WR_LOCALE_DISCIPLINE_WARN_ONLY=0`
12
+ # promotes to Phase 2 load-bearing (exit 1 on any violation).
13
+ #
14
+ # Behavioural fixture (ADR-052 default): every test exercises the script
15
+ # against a synthesised packages/ fixture tree containing known-state
16
+ # scripts. No grep of SKILL.md / agent prose / source content.
17
+ #
18
+ # @adr ADR-040 (advisory-then-load-bearing reusable pattern)
19
+ # @adr ADR-049 (plugin-bundled scripts; PATH shim)
20
+ # @adr ADR-052 (behavioural-tests-default — temp script with known violations)
21
+ # @adr ADR-080 (highest-version-wins shim wrapper)
22
+ # @adr ADR-005 (Plugin testing strategy — bats coverage)
23
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down)
24
+ # @jtbd JTBD-101 (Extend the Suite with New Plugins)
25
+
26
+ setup() {
27
+ SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
28
+ SCRIPT="$SCRIPTS_DIR/check-locale-discipline.sh"
29
+ FIXTURE_ROOT="$(mktemp -d)"
30
+ mkdir -p "$FIXTURE_ROOT/packages/demo/scripts"
31
+ mkdir -p "$FIXTURE_ROOT/packages/demo/hooks"
32
+ mkdir -p "$FIXTURE_ROOT/packages/demo/hooks/lib"
33
+ mkdir -p "$FIXTURE_ROOT/packages/demo/lib"
34
+ }
35
+
36
+ teardown() {
37
+ rm -rf "$FIXTURE_ROOT"
38
+ }
39
+
40
+ # ── Existence + executable ──────────────────────────────────────────────────
41
+
42
+ @test "check-locale-discipline: script exists" {
43
+ [ -f "$SCRIPT" ]
44
+ }
45
+
46
+ @test "check-locale-discipline: script is executable" {
47
+ [ -x "$SCRIPT" ]
48
+ }
49
+
50
+ # ── Negative cases (violations expected) ────────────────────────────────────
51
+
52
+ @test "check-locale-discipline: grep without LC_ALL emits WARN and counts violation" {
53
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/raw-grep.sh" <<'EOF'
54
+ #!/usr/bin/env bash
55
+ grep -c '^### ' README.md
56
+ EOF
57
+ run "$SCRIPT" "$FIXTURE_ROOT"
58
+ [ "$status" -eq 0 ] # Phase 1 advisory
59
+ echo "$output" | grep -E "WARN.*raw-grep.sh:2.*grep without preceding LC_ALL"
60
+ echo "$output" | grep -E "1 violation"
61
+ }
62
+
63
+ @test "check-locale-discipline: sed without LC_ALL emits WARN" {
64
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/raw-sed.sh" <<'EOF'
65
+ #!/usr/bin/env bash
66
+ sed -n '/^### /p' README.md
67
+ EOF
68
+ run "$SCRIPT" "$FIXTURE_ROOT"
69
+ [ "$status" -eq 0 ]
70
+ echo "$output" | grep -E "WARN.*raw-sed.sh:2.*sed without preceding LC_ALL"
71
+ }
72
+
73
+ @test "check-locale-discipline: awk without LC_ALL emits WARN" {
74
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/raw-awk.sh" <<'EOF'
75
+ #!/usr/bin/env bash
76
+ awk '/^### /' README.md
77
+ EOF
78
+ run "$SCRIPT" "$FIXTURE_ROOT"
79
+ [ "$status" -eq 0 ]
80
+ echo "$output" | grep -E "WARN.*raw-awk.sh:2.*awk without preceding LC_ALL"
81
+ }
82
+
83
+ # ── Positive cases (file-wide protection — silent pass) ─────────────────────
84
+
85
+ @test "check-locale-discipline: file-wide export LC_ALL above grep — silent pass" {
86
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/protected.sh" <<'EOF'
87
+ #!/usr/bin/env bash
88
+ export LC_ALL=en_US.UTF-8
89
+ grep -c '^### ' README.md
90
+ sed -n '/^### /p' README.md
91
+ awk '/^### /' README.md
92
+ EOF
93
+ run "$SCRIPT" "$FIXTURE_ROOT"
94
+ [ "$status" -eq 0 ]
95
+ ! echo "$output" | grep -qE "protected.sh.*WARN"
96
+ echo "$output" | grep -E "clean"
97
+ }
98
+
99
+ @test "check-locale-discipline: inline LC_ALL= prefix on same line — silent pass" {
100
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/inline.sh" <<'EOF'
101
+ #!/usr/bin/env bash
102
+ LC_ALL=en_US.UTF-8 grep -c '^### ' README.md
103
+ LC_ALL=en_US.UTF-8 sed -n '/^### /p' README.md
104
+ LC_ALL=en_US.UTF-8 awk '/^### /' README.md
105
+ EOF
106
+ run "$SCRIPT" "$FIXTURE_ROOT"
107
+ [ "$status" -eq 0 ]
108
+ ! echo "$output" | grep -qE "inline.sh.*WARN"
109
+ }
110
+
111
+ # ── Edge cases ──────────────────────────────────────────────────────────────
112
+
113
+ @test "check-locale-discipline: multiple grep/sed/awk on one line — line is flagged" {
114
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/multi.sh" <<'EOF'
115
+ #!/usr/bin/env bash
116
+ grep -c '^### ' README.md | sed 's/ //g' | awk '{print $1}'
117
+ EOF
118
+ run "$SCRIPT" "$FIXTURE_ROOT"
119
+ [ "$status" -eq 0 ]
120
+ # At least one tool is reported; we don't over-specify the count.
121
+ echo "$output" | grep -E "WARN.*multi.sh:2.*(grep|sed|awk) without preceding LC_ALL"
122
+ }
123
+
124
+ @test "check-locale-discipline: git grep is skipped" {
125
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/git-grep.sh" <<'EOF'
126
+ #!/usr/bin/env bash
127
+ git grep -nE 'pattern' -- '*.md'
128
+ EOF
129
+ run "$SCRIPT" "$FIXTURE_ROOT"
130
+ [ "$status" -eq 0 ]
131
+ ! echo "$output" | grep -qE "git-grep.sh.*WARN"
132
+ }
133
+
134
+ @test "check-locale-discipline: heredoc body containing grep is not flagged" {
135
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/heredoc.sh" <<'EOF'
136
+ #!/usr/bin/env bash
137
+ cat <<'INNER'
138
+ The way to do this is to run: grep '^### ' README.md
139
+ And then: sed -n '/^### /p'
140
+ INNER
141
+ echo "done"
142
+ EOF
143
+ run "$SCRIPT" "$FIXTURE_ROOT"
144
+ [ "$status" -eq 0 ]
145
+ # Heredoc body lines must NOT produce WARNs.
146
+ ! echo "$output" | grep -qE "heredoc.sh:[34].*WARN"
147
+ }
148
+
149
+ @test "check-locale-discipline: comment-only line is not flagged" {
150
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/commented.sh" <<'EOF'
151
+ #!/usr/bin/env bash
152
+ # Future work: replace this awk with a sed | grep pipeline.
153
+ echo "no grep here"
154
+ EOF
155
+ run "$SCRIPT" "$FIXTURE_ROOT"
156
+ [ "$status" -eq 0 ]
157
+ ! echo "$output" | grep -qE "commented.sh.*WARN"
158
+ }
159
+
160
+ @test "check-locale-discipline: identifiers containing grep/sed/awk substrings are not flagged" {
161
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/identifiers.sh" <<'EOF'
162
+ #!/usr/bin/env bash
163
+ grep_helper="something"
164
+ result_from_sed=42
165
+ my_awkward_var="x"
166
+ echo "$grep_helper $result_from_sed $my_awkward_var"
167
+ EOF
168
+ run "$SCRIPT" "$FIXTURE_ROOT"
169
+ [ "$status" -eq 0 ]
170
+ ! echo "$output" | grep -qE "identifiers.sh.*WARN"
171
+ }
172
+
173
+ # ── Scope: hooks/ + lib/ + nested hooks/lib/ ────────────────────────────────
174
+
175
+ @test "check-locale-discipline: scans packages/*/hooks/ scripts" {
176
+ cat > "$FIXTURE_ROOT/packages/demo/hooks/raw-grep-hook.sh" <<'EOF'
177
+ #!/usr/bin/env bash
178
+ grep -c '^### ' README.md
179
+ EOF
180
+ run "$SCRIPT" "$FIXTURE_ROOT"
181
+ [ "$status" -eq 0 ]
182
+ echo "$output" | grep -E "WARN.*hooks/raw-grep-hook.sh:2"
183
+ }
184
+
185
+ @test "check-locale-discipline: scans packages/*/hooks/lib/ scripts" {
186
+ cat > "$FIXTURE_ROOT/packages/demo/hooks/lib/nested.sh" <<'EOF'
187
+ #!/usr/bin/env bash
188
+ awk '/^### /' README.md
189
+ EOF
190
+ run "$SCRIPT" "$FIXTURE_ROOT"
191
+ [ "$status" -eq 0 ]
192
+ echo "$output" | grep -E "WARN.*hooks/lib/nested.sh:2"
193
+ }
194
+
195
+ @test "check-locale-discipline: scans packages/*/lib/ scripts" {
196
+ cat > "$FIXTURE_ROOT/packages/demo/lib/raw-sed-lib.sh" <<'EOF'
197
+ #!/usr/bin/env bash
198
+ sed -n '/^### /p' README.md
199
+ EOF
200
+ run "$SCRIPT" "$FIXTURE_ROOT"
201
+ [ "$status" -eq 0 ]
202
+ echo "$output" | grep -E "WARN.*lib/raw-sed-lib.sh:2"
203
+ }
204
+
205
+ # ── Phase 1 advisory vs Phase 2 load-bearing ────────────────────────────────
206
+
207
+ @test "check-locale-discipline: Phase 1 default exits 0 even with violations" {
208
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
209
+ #!/usr/bin/env bash
210
+ grep -c '^### ' README.md
211
+ EOF
212
+ run "$SCRIPT" "$FIXTURE_ROOT"
213
+ [ "$status" -eq 0 ]
214
+ }
215
+
216
+ @test "check-locale-discipline: Phase 2 load-bearing exits 1 on violations" {
217
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
218
+ #!/usr/bin/env bash
219
+ grep -c '^### ' README.md
220
+ EOF
221
+ WR_LOCALE_DISCIPLINE_WARN_ONLY=0 run "$SCRIPT" "$FIXTURE_ROOT"
222
+ [ "$status" -eq 1 ]
223
+ }
224
+
225
+ @test "check-locale-discipline: Phase 2 load-bearing exits 0 on clean tree" {
226
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/clean.sh" <<'EOF'
227
+ #!/usr/bin/env bash
228
+ export LC_ALL=en_US.UTF-8
229
+ grep -c '^### ' README.md
230
+ EOF
231
+ WR_LOCALE_DISCIPLINE_WARN_ONLY=0 run "$SCRIPT" "$FIXTURE_ROOT"
232
+ [ "$status" -eq 0 ]
233
+ }
234
+
235
+ # ── Argument and error handling ─────────────────────────────────────────────
236
+
237
+ @test "check-locale-discipline: missing repo-root directory exits 2" {
238
+ run "$SCRIPT" "$FIXTURE_ROOT/does-not-exist"
239
+ [ "$status" -eq 2 ]
240
+ echo "$output" | grep -iE "not a directory"
241
+ }
242
+
243
+ @test "check-locale-discipline: repo-root without packages/ subdir exits 2" {
244
+ mkdir -p "$FIXTURE_ROOT/empty"
245
+ run "$SCRIPT" "$FIXTURE_ROOT/empty"
246
+ [ "$status" -eq 2 ]
247
+ echo "$output" | grep -iE "no packages/"
248
+ }
249
+
250
+ @test "check-locale-discipline: defaults to current working directory when no arg" {
251
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
252
+ #!/usr/bin/env bash
253
+ grep -c '^### ' README.md
254
+ EOF
255
+ cd "$FIXTURE_ROOT"
256
+ run "$SCRIPT"
257
+ [ "$status" -eq 0 ]
258
+ echo "$output" | grep -E "WARN.*scripts/violation.sh:2"
259
+ }
260
+
261
+ # ── Output shape ────────────────────────────────────────────────────────────
262
+
263
+ @test "check-locale-discipline: clean tree emits summary on stdout" {
264
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/clean.sh" <<'EOF'
265
+ #!/usr/bin/env bash
266
+ export LC_ALL=en_US.UTF-8
267
+ grep -c '^### ' README.md
268
+ EOF
269
+ run "$SCRIPT" "$FIXTURE_ROOT"
270
+ [ "$status" -eq 0 ]
271
+ echo "$output" | grep -E "^check-locale-discipline: clean"
272
+ }
273
+
274
+ @test "check-locale-discipline: violation summary cites P328" {
275
+ cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
276
+ #!/usr/bin/env bash
277
+ grep -c '^### ' README.md
278
+ EOF
279
+ run "$SCRIPT" "$FIXTURE_ROOT"
280
+ [ "$status" -eq 0 ]
281
+ echo "$output" | grep -E "P328"
282
+ }
283
+
284
+ # ── Self-application: lint script itself is locale-discipline-compliant ─────
285
+
286
+ @test "check-locale-discipline: own script declares export LC_ALL at top" {
287
+ # The lint walks UTF-8 prose; it must lead by example. This guards
288
+ # against a future edit that strips the export and reintroduces P328
289
+ # in the lint itself.
290
+ head -80 "$SCRIPT" | grep -qE "^export LC_ALL=en_US.UTF-8$"
291
+ }
@@ -65,9 +65,10 @@ setup() {
65
65
  diff -q "$HELPER" "$REPO_ROOT/packages/architect/lib/derive-first-dispatch.sh"
66
66
  }
67
67
 
68
- @test "P287: capture-problem SKILL.md does not carry --type= or --no-prompt flag rows" {
68
+ @test "P287: capture-problem SKILL.md does not carry --type= flag rows (--no-prompt REVIVED per ADR-060 Amendment 2026-06-02 as AFK marker)" {
69
69
  # The retired Step 1.5 dispatch flags must not appear in the flag table.
70
+ # NB: --no-prompt was REVIVED per ADR-060 Amendment 2026-06-02 — see
71
+ # capture-problem-step-1-5b-jtbd-trace.bats line 327 (positive assertion).
70
72
  ! grep -E '^\| `--type=technical`' "$SKILL_FILE"
71
73
  ! grep -E '^\| `--type=user-business`' "$SKILL_FILE"
72
- ! grep -E '^\| `--no-prompt`' "$SKILL_FILE"
73
74
  }
@@ -317,7 +317,7 @@ parse_no_prompt_flag() {
317
317
  }
318
318
 
319
319
  @test "SKILL.md: Step 1.5b preserves JTBD-301 firewall on plugin-user-side intake" {
320
- grep -qE 'plugin-user-side .* MUST NOT (prompt|carry)' "$SKILL_FILE"
320
+ grep -qE '[Pp]lugin-user-side.*MUST NOT (prompt|carry)' "$SKILL_FILE"
321
321
  }
322
322
 
323
323
  @test "SKILL.md: Step 1.5b names derive-then-ratify contract" {