@windyroad/jtbd 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env bats
2
2
 
3
- # Tests for jtbd-enforce-edit.sh — verifies broadened scope with exclusions
4
- # Mix of grep-based pattern tests and functional execution tests.
3
+ # Tests for jtbd-enforce-edit.sh — verifies broadened scope with exclusions.
4
+ # All tests are functional: they execute the hook with mock JSON input
5
+ # and assert on exit status and BLOCKED output. Source-grep assertions
6
+ # were removed (P011) — they over-specified the implementation and
7
+ # false-positived on legitimate refactors.
5
8
 
6
9
  setup() {
7
10
  SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
@@ -16,12 +19,6 @@ teardown() {
16
19
  rm -rf "$TEST_DIR"
17
20
  }
18
21
 
19
- # Helper: check if a pattern is in the exclusion list by grepping the hook
20
- file_is_excluded() {
21
- local pattern="$1"
22
- grep -q "$pattern" "$HOOK"
23
- }
24
-
25
22
  # Helper: run the hook with a mock JSON input for a given file path
26
23
  run_hook_with_file() {
27
24
  local file_path="$1"
@@ -29,46 +26,75 @@ run_hook_with_file() {
29
26
  echo "$json" | bash "$HOOK"
30
27
  }
31
28
 
32
- # --- Pattern-based exclusion tests (grep) ---
29
+ # Helper: assert the hook exits 0 and does NOT emit BLOCKED for the given path
30
+ assert_path_allowed() {
31
+ local file_path="$1"
32
+ run run_hook_with_file "$file_path"
33
+ [ "$status" -eq 0 ]
34
+ [[ "$output" != *"BLOCKED"* ]]
35
+ }
36
+
37
+ # Helper: assert the hook BLOCKS the given path
38
+ assert_path_blocked() {
39
+ local file_path="$1"
40
+ run run_hook_with_file "$file_path"
41
+ [ "$status" -eq 0 ]
42
+ [[ "$output" == *"BLOCKED"* ]]
43
+ }
44
+
45
+ # --- Exclusion tests (functional) ---
46
+
47
+ # Claude Code passes absolute file paths in tool_input.file_path, so tests
48
+ # use $PWD-prefixed paths to match the real shape (after the P004 root check).
33
49
 
34
50
  @test "enforce: excludes CSS files" {
35
- file_is_excluded '\.css'
51
+ assert_path_allowed "$PWD/src/styles.css"
36
52
  }
37
53
 
38
54
  @test "enforce: excludes image files" {
39
- file_is_excluded '\.png'
55
+ assert_path_allowed "$PWD/public/logo.png"
40
56
  }
41
57
 
42
58
  @test "enforce: excludes font files" {
43
- file_is_excluded '\.woff'
59
+ assert_path_allowed "$PWD/public/fonts/regular.woff"
44
60
  }
45
61
 
46
62
  @test "enforce: excludes lockfiles" {
47
- file_is_excluded 'package-lock.json'
63
+ assert_path_allowed "$PWD/package-lock.json"
48
64
  }
49
65
 
50
66
  @test "enforce: excludes changeset files" {
51
- file_is_excluded '\.changeset'
67
+ assert_path_allowed "$PWD/.changeset/some-change.md"
52
68
  }
53
69
 
54
70
  @test "enforce: excludes memory files" {
55
- file_is_excluded 'MEMORY.md'
71
+ assert_path_allowed "$PWD/MEMORY.md"
56
72
  }
57
73
 
58
74
  @test "enforce: excludes plan files" {
59
- file_is_excluded '\.claude/plans'
75
+ assert_path_allowed "$PWD/.claude/plans/2026-01-01-plan.md"
60
76
  }
61
77
 
62
78
  @test "enforce: excludes risk reports" {
63
- file_is_excluded '\.risk-reports'
79
+ assert_path_allowed "$PWD/.risk-reports/2026-01-01.md"
64
80
  }
65
81
 
66
82
  @test "enforce: excludes RISK-POLICY.md" {
67
- file_is_excluded 'RISK-POLICY.md'
83
+ assert_path_allowed "$PWD/RISK-POLICY.md"
84
+ }
85
+
86
+ @test "enforce: does NOT exempt by UI-only extension (ADR-007/008)" {
87
+ # ADR-007/008 broadened scope: the hook must gate UI files like any
88
+ # other source file, not silently allow them.
89
+ assert_path_blocked "$PWD/src/Component.tsx"
90
+ }
91
+
92
+ @test "enforce: does NOT exempt .html files (ADR-007/008)" {
93
+ assert_path_blocked "$PWD/public/index.html"
68
94
  }
69
95
 
70
- @test "enforce: does NOT have UI-only case guard" {
71
- ! grep -q '\*) exit 0 ;;' "$HOOK"
96
+ @test "enforce: does NOT exempt .vue files (ADR-007/008)" {
97
+ assert_path_blocked "$PWD/src/App.vue"
72
98
  }
73
99
 
74
100
  # --- Functional tests (execute hook with mock JSON) ---
@@ -1,19 +1,163 @@
1
1
  #!/usr/bin/env bats
2
2
 
3
- # Tests for JTBD mark-reviewed hook — verifies hash path supports both formats
3
+ # Tests for jtbd-mark-reviewed.sh — verifies the PostToolUse:Agent hook
4
+ # creates session markers and stores the right policy-path hash when
5
+ # wr-jtbd:agent (or legacy jtbd-lead) returns a PASS verdict.
6
+ #
7
+ # Per ADR-005 (P011): assertions are functional — execute the hook with
8
+ # mock JSON, assert on side-effects (marker files, hash file contents).
9
+ # Source-grep assertions for "the script mentions docs/jtbd" were
10
+ # removed because they passed even when the surrounding code path was
11
+ # unreachable.
4
12
 
5
- @test "mark-reviewed supports docs/jtbd directory path" {
13
+ setup() {
6
14
  SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
7
- grep -q '"docs/jtbd"' "$SCRIPT_DIR/jtbd-mark-reviewed.sh"
15
+ HOOK="$SCRIPT_DIR/jtbd-mark-reviewed.sh"
16
+ ORIG_DIR="$PWD"
17
+ TEST_DIR=$(mktemp -d)
18
+ cd "$TEST_DIR"
19
+ SESSION_ID="test-session-$$"
20
+ MARKER="/tmp/jtbd-reviewed-${SESSION_ID}"
21
+ PLAN_MARKER="/tmp/jtbd-plan-reviewed-${SESSION_ID}"
22
+ HASH_FILE="/tmp/jtbd-reviewed-${SESSION_ID}.hash"
23
+ VERDICT_FILE="/tmp/jtbd-verdict"
24
+ rm -f "$MARKER" "$PLAN_MARKER" "$HASH_FILE" "$VERDICT_FILE"
8
25
  }
9
26
 
10
- @test "mark-reviewed supports docs/JOBS_TO_BE_DONE.md fallback" {
11
- SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
12
- grep -q 'docs/JOBS_TO_BE_DONE.md' "$SCRIPT_DIR/jtbd-mark-reviewed.sh"
27
+ teardown() {
28
+ cd "$ORIG_DIR"
29
+ rm -rf "$TEST_DIR"
30
+ rm -f "$MARKER" "$PLAN_MARKER" "$HASH_FILE" "$VERDICT_FILE"
13
31
  }
14
32
 
15
- @test "enforce-edit and mark-reviewed both support docs/jtbd" {
16
- SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
17
- grep -q '"docs/jtbd"' "$SCRIPT_DIR/jtbd-enforce-edit.sh"
18
- grep -q '"docs/jtbd"' "$SCRIPT_DIR/jtbd-mark-reviewed.sh"
33
+ # Helper: pipe a PostToolUse:Agent JSON to the hook for the given subagent.
34
+ run_hook() {
35
+ local subagent="$1"
36
+ local json="{\"tool_input\":{\"subagent_type\":\"${subagent}\"},\"session_id\":\"${SESSION_ID}\"}"
37
+ echo "$json" | bash "$HOOK"
38
+ }
39
+
40
+ # --- Path support: docs/jtbd directory (preferred) ---
41
+
42
+ @test "uses docs/jtbd directory when present (creates marker + hash)" {
43
+ mkdir -p docs/jtbd/solo-developer
44
+ echo "# Persona" > docs/jtbd/solo-developer/persona.md
45
+ echo "# Index" > docs/jtbd/README.md
46
+ echo "PASS" > "$VERDICT_FILE"
47
+
48
+ run_hook "wr-jtbd:agent"
49
+
50
+ [ -f "$MARKER" ]
51
+ [ -f "$HASH_FILE" ]
52
+ [ "$(cat "$HASH_FILE")" != "missing" ]
53
+ [ -n "$(cat "$HASH_FILE")" ]
54
+ }
55
+
56
+ @test "directory hash excludes README.md (only persona/job files contribute)" {
57
+ mkdir -p docs/jtbd
58
+ echo "# Index" > docs/jtbd/README.md
59
+ echo "PASS" > "$VERDICT_FILE"
60
+ run_hook "wr-jtbd:agent"
61
+ HASH_README_ONLY="$(cat "$HASH_FILE")"
62
+
63
+ rm -f "$HASH_FILE" "$MARKER"
64
+ echo "different content" >> docs/jtbd/README.md
65
+ echo "PASS" > "$VERDICT_FILE"
66
+ run_hook "wr-jtbd:agent"
67
+ HASH_README_CHANGED="$(cat "$HASH_FILE")"
68
+
69
+ # Changing README.md alone must not change the hash — README is excluded.
70
+ [ "$HASH_README_ONLY" = "$HASH_README_CHANGED" ]
71
+ }
72
+
73
+ # --- Path support: docs/JOBS_TO_BE_DONE.md fallback (legacy) ---
74
+
75
+ @test "uses docs/JOBS_TO_BE_DONE.md when docs/jtbd does not exist" {
76
+ mkdir -p docs
77
+ echo "# Jobs" > docs/JOBS_TO_BE_DONE.md
78
+ echo "PASS" > "$VERDICT_FILE"
79
+
80
+ run_hook "wr-jtbd:agent"
81
+
82
+ [ -f "$MARKER" ]
83
+ [ -f "$HASH_FILE" ]
84
+ [ "$(cat "$HASH_FILE")" != "missing" ]
85
+
86
+ # Hash should match the file's content hash. _hashcmd in
87
+ # gate-helpers.sh prefers md5sum, falls back to md5 -r, then shasum.
88
+ EXPECTED=$(cat docs/JOBS_TO_BE_DONE.md \
89
+ | (md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null) \
90
+ | cut -d' ' -f1)
91
+ [ "$(cat "$HASH_FILE")" = "$EXPECTED" ]
92
+ }
93
+
94
+ @test "prefers docs/jtbd over docs/JOBS_TO_BE_DONE.md when both exist" {
95
+ mkdir -p docs/jtbd
96
+ echo "# job" > docs/jtbd/job.md
97
+ echo "# legacy jobs" > docs/JOBS_TO_BE_DONE.md
98
+ echo "PASS" > "$VERDICT_FILE"
99
+
100
+ run_hook "wr-jtbd:agent"
101
+
102
+ # The directory-derived hash must NOT equal the standalone-file hash.
103
+ DIR_HASH="$(cat "$HASH_FILE")"
104
+ FILE_HASH=$(cat docs/JOBS_TO_BE_DONE.md \
105
+ | (md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null) \
106
+ | cut -d' ' -f1)
107
+ [ "$DIR_HASH" != "$FILE_HASH" ]
108
+ }
109
+
110
+ # --- Verdict handling ---
111
+
112
+ @test "FAIL verdict does NOT create review marker (but plan marker still set)" {
113
+ mkdir -p docs/jtbd
114
+ echo "# job" > docs/jtbd/job.md
115
+ echo "FAIL" > "$VERDICT_FILE"
116
+
117
+ run_hook "wr-jtbd:agent"
118
+
119
+ [ ! -f "$MARKER" ]
120
+ [ -f "$PLAN_MARKER" ]
121
+ }
122
+
123
+ @test "missing verdict file allows marker (backward compat)" {
124
+ mkdir -p docs/jtbd
125
+ echo "# job" > docs/jtbd/job.md
126
+
127
+ run_hook "wr-jtbd:agent"
128
+
129
+ [ -f "$MARKER" ]
130
+ }
131
+
132
+ @test "verdict file is consumed (removed) after hook runs" {
133
+ mkdir -p docs/jtbd
134
+ echo "# job" > docs/jtbd/job.md
135
+ echo "PASS" > "$VERDICT_FILE"
136
+
137
+ run_hook "wr-jtbd:agent"
138
+
139
+ [ ! -f "$VERDICT_FILE" ]
140
+ }
141
+
142
+ # --- Subagent routing ---
143
+
144
+ @test "ignores unrelated subagent (no marker created)" {
145
+ mkdir -p docs/jtbd
146
+ echo "# job" > docs/jtbd/job.md
147
+ echo "PASS" > "$VERDICT_FILE"
148
+
149
+ run_hook "wr-architect:agent"
150
+
151
+ [ ! -f "$MARKER" ]
152
+ [ ! -f "$PLAN_MARKER" ]
153
+ }
154
+
155
+ @test "matches legacy jtbd-lead subagent name" {
156
+ mkdir -p docs/jtbd
157
+ echo "# job" > docs/jtbd/job.md
158
+ echo "PASS" > "$VERDICT_FILE"
159
+
160
+ run_hook "jtbd-lead"
161
+
162
+ [ -f "$MARKER" ]
19
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/jtbd",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Jobs-to-be-done enforcement for UI changes",
5
5
  "bin": {
6
6
  "windyroad-jtbd": "./bin/install.mjs"
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: wr-jtbd:review-jobs
3
+ description: On-demand JTBD alignment review. Checks staged changes and recent commits against documented persona jobs in docs/jtbd/. Use before a release or when adding new features.
4
+ allowed-tools: Read, Glob, Grep, Bash, AskUserQuestion, Skill
5
+ ---
6
+
7
+ # JTBD Alignment Review Skill
8
+
9
+ Run a Jobs To Be Done alignment review on demand — outside the pre-tool-use hook gate. Reviews staged changes and recent commits against the documented persona jobs in `docs/jtbd/`.
10
+
11
+ This skill is **read-only**. It does not commit, push, or modify files.
12
+
13
+ ## When to use
14
+
15
+ - Pre-flight before a release or client handover: confirm delivered features trace to documented persona needs
16
+ - When adding new features: verify the feature serves a documented JTBD before building
17
+ - After a significant capability change: check whether existing jobs are still served
18
+ - Any time the hook gate is not convenient: planning mode, spike work, design review
19
+
20
+ ## Steps
21
+
22
+ ### 1. Parse arguments
23
+
24
+ Read `$ARGUMENTS` for an explicit review scope (e.g., "review the new skill I just wrote", "check persona alignment for the release", "does this feature serve a documented job?"). If a scope is provided, use it. If empty, proceed to auto-detection.
25
+
26
+ ### 2. Auto-detect context
27
+
28
+ Run the following to establish what needs reviewing:
29
+
30
+ ```bash
31
+ # Staged changes
32
+ git diff --cached --stat
33
+ git diff --cached --name-only
34
+
35
+ # Recent commits not yet pushed
36
+ git log origin/$(git rev-parse --abbrev-ref HEAD)..HEAD --oneline 2>/dev/null || git log HEAD -5 --oneline
37
+
38
+ # All unstaged changes
39
+ git diff --name-only HEAD
40
+ ```
41
+
42
+ Summarise:
43
+ - Files staged or recently committed
44
+ - Whether the changes are feature additions, behavioural changes, or purely documentary
45
+
46
+ ### 3. Resolve ambiguity
47
+
48
+ If there are no staged changes and no recent unpushed commits, use `AskUserQuestion` to ask:
49
+
50
+ > "I don't see any staged or unpushed changes. What would you like me to review?
51
+ > (a) A specific set of files or a planned feature — please describe it
52
+ > (b) All changes since the last tag
53
+ > (c) Cancel"
54
+
55
+ Do not ask if there is an obvious set of changed files.
56
+
57
+ ### 4. Construct the assessment prompt
58
+
59
+ Build a self-contained prompt for the JTBD subagent that includes:
60
+ - The list of changed/staged files
61
+ - The git diff summary (stat output)
62
+ - Any explicit scope from the user
63
+ - The request: "Review these changes against the project's documented JTBD personas and jobs. Identify which jobs are served, whether any gaps exist (changes that don't trace to a documented job), and whether any jobs are unintentionally broken."
64
+
65
+ ### 5. Delegate to wr-jtbd:agent
66
+
67
+ Invoke the JTBD subagent via the `Skill` tool:
68
+
69
+ ```
70
+ subagent_type: wr-jtbd:agent
71
+ prompt: <constructed review prompt from step 4>
72
+ ```
73
+
74
+ Wait for the subagent to complete.
75
+
76
+ ### 6. Present results
77
+
78
+ Present the full alignment report to the user. The JTBD subagent will report:
79
+ - PASS: changes trace to documented jobs, no gaps
80
+ - GAPS: changes that don't trace to any documented job — new JTBD entry may be needed
81
+ - BREAKS: changes that appear to remove or degrade a documented job outcome
82
+ - NEW JOB NEEDED: capabilities being added that serve an undocumented need
83
+
84
+ If gaps or breaks are identified, use `AskUserQuestion` to ask how the user wants to proceed:
85
+ - (a) Document the new job before continuing (recommended)
86
+ - (b) Proceed with a documented exception
87
+ - (c) Revise the approach to serve an existing job
88
+
89
+ Do not make the decision unilaterally — per ADR-013 Rule 1, JTBD alignment decisions are the user's.
90
+
91
+ $ARGUMENTS