@windyroad/tdd 0.2.3 → 0.3.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,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-tdd",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "TDD state machine enforcement (IDLE/RED/GREEN/BLOCKED) for Claude Code"
5
- }
5
+ }
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Shared session-announcement marker helpers (P095 / ADR-038).
3
+ #
4
+ # Used by UserPromptSubmit hooks to gate verbose MANDATORY instruction
5
+ # prose behind a once-per-session check. First prompt of a session emits
6
+ # the full block AND calls mark_announced; subsequent prompts see the
7
+ # marker via has_announced and emit only a terse reminder.
8
+ #
9
+ # Why no TTL or drift check (unlike review-gate.sh): announcement is
10
+ # bookkeeping for prose verbosity, not enforcement. PreToolUse gates
11
+ # still block unauthorised edits regardless of announcement state; the
12
+ # delegated agent re-reads policy when it runs. Extending the marker's
13
+ # lifetime across policy changes mid-session is safe — the gate, not
14
+ # the announcement, is load-bearing.
15
+ #
16
+ # Marker path convention: /tmp/${SYSTEM}-announced-${SESSION_ID}
17
+ # (mirrors the /tmp/${SYSTEM}-reviewed-${SESSION_ID} convention from
18
+ # style-guide/voice-tone/risk-scorer review-gate.sh; the -announced-
19
+ # suffix distinguishes announcement markers from clearance markers).
20
+ #
21
+ # Empty SESSION_ID fallback: has_announced returns 1 (not announced,
22
+ # full block emits) and mark_announced is a no-op (no file written).
23
+ # This covers manual hook invocation, test harnesses, and any rare
24
+ # case where Claude Code does not pass a session_id on stdin.
25
+
26
+ # Returns 0 if the hook for SYSTEM has already announced in SESSION_ID,
27
+ # 1 otherwise. Empty SESSION_ID => returns 1 (never announced).
28
+ #
29
+ # Usage: has_announced "architect" "$SESSION_ID"
30
+ has_announced() {
31
+ local SYSTEM="$1"
32
+ local SESSION_ID="$2"
33
+ [ -n "$SESSION_ID" ] || return 1
34
+ [ -f "/tmp/${SYSTEM}-announced-${SESSION_ID}" ]
35
+ }
36
+
37
+ # Writes the announcement marker for SYSTEM in SESSION_ID. Empty
38
+ # SESSION_ID => no-op. Safe to call more than once per session.
39
+ #
40
+ # Usage: mark_announced "architect" "$SESSION_ID"
41
+ mark_announced() {
42
+ local SYSTEM="$1"
43
+ local SESSION_ID="$2"
44
+ [ -n "$SESSION_ID" ] || return 0
45
+ : > "/tmp/${SYSTEM}-announced-${SESSION_ID}"
46
+ }
@@ -1,15 +1,22 @@
1
1
  #!/bin/bash
2
- # TDD - UserPromptSubmit hook
2
+ # TDD - UserPromptSubmit hook (P095 / ADR-038)
3
3
  # Injects TDD instructions and per-file state into every prompt.
4
4
  # Only active when a test script is configured in package.json.
5
+ #
6
+ # Progressive disclosure (ADR-038) with tdd-inject carve-out: static
7
+ # prose (STATE RULES table, WORKFLOW, IMPORTANT blocks) is gated
8
+ # behind the once-per-session announcement marker; dynamic content
9
+ # (current TDD state + tracked test files) emits on every prompt.
5
10
 
6
11
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
12
  source "$SCRIPT_DIR/lib/tdd-gate.sh"
13
+ # shellcheck source=lib/session-marker.sh
14
+ source "$SCRIPT_DIR/lib/session-marker.sh"
8
15
 
9
16
  INPUT=$(cat)
10
17
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
11
18
 
12
- # If no test script configured, inject setup instructions
19
+ # If no test script configured, inject setup instructions (unchanged by ADR-038)
13
20
  if ! tdd_has_test_script; then
14
21
  cat <<'HOOK_OUTPUT'
15
22
  INSTRUCTION: MANDATORY TDD ENFORCEMENT. YOU MUST FOLLOW THIS.
@@ -25,13 +32,13 @@ HOOK_OUTPUT
25
32
  exit 0
26
33
  fi
27
34
 
28
- # Collect per-file states
35
+ # Collect per-file states (always per-prompt)
29
36
  ALL_STATES=""
30
37
  if [ -n "$SESSION_ID" ]; then
31
38
  ALL_STATES=$(tdd_get_all_states "$SESSION_ID")
32
39
  fi
33
40
 
34
- # Determine overall status for the header
41
+ # Determine overall status for the header (always per-prompt)
35
42
  OVERALL="IDLE"
36
43
  if [ -n "$ALL_STATES" ]; then
37
44
  if echo "$ALL_STATES" | grep -q ":BLOCKED$"; then
@@ -43,7 +50,14 @@ if [ -n "$ALL_STATES" ]; then
43
50
  fi
44
51
  fi
45
52
 
46
- cat <<HOOK_OUTPUT
53
+ if has_announced "tdd" "$SESSION_ID"; then
54
+ # Subsequent prompt: terse reminder + dynamic state only.
55
+ cat <<HOOK_OUTPUT
56
+ MANDATORY TDD gate active (test script present). Current TDD state: **${OVERALL}**. Write failing test before implementation; .ts/.tsx/.js/.jsx edits gated per test state. See turn-1 instructions for full rules and workflow.
57
+ HOOK_OUTPUT
58
+ else
59
+ # First prompt of session: full MANDATORY block + mark announced.
60
+ cat <<HOOK_OUTPUT
47
61
  INSTRUCTION: MANDATORY TDD ENFORCEMENT. YOU MUST FOLLOW THIS.
48
62
 
49
63
  This project enforces Red-Green-Refactor via hooks. Your current TDD state is: **${OVERALL}**
@@ -72,7 +86,10 @@ IMPORTANT:
72
86
  - The hook runs only the relevant test after each file write (not the full suite)
73
87
  - To refactor existing code, touch the relevant test file first to enter the cycle
74
88
  HOOK_OUTPUT
89
+ mark_announced "tdd" "$SESSION_ID"
90
+ fi
75
91
 
92
+ # Dynamic tracked test files (always per-prompt, both branches)
76
93
  if [ -n "$ALL_STATES" ]; then
77
94
  echo ""
78
95
  echo "TRACKED TEST FILES THIS SESSION:"
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P095 / ADR-038: tdd-inject.sh UserPromptSubmit hook is the special
4
+ # case in the cluster — dynamic TDD state (IDLE/RED/GREEN/BLOCKED +
5
+ # tracked test files list) must be emitted on every prompt regardless
6
+ # of announcement state. Only the static prose (STATE RULES, WORKFLOW,
7
+ # IMPORTANT blocks) is gated by the once-per-session marker.
8
+ #
9
+ # Per ADR-038 tdd-inject carve-out.
10
+
11
+ setup() {
12
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
13
+ HOOK="$REPO_ROOT/packages/tdd/hooks/tdd-inject.sh"
14
+
15
+ # Stub project working dir with a test script in package.json so the
16
+ # hook does NOT fall through to the "no test script" branch.
17
+ WORKDIR="$(mktemp -d)"
18
+ cat > "$WORKDIR/package.json" <<'JSON'
19
+ { "name": "test-tdd", "version": "0.0.0", "scripts": { "test": "echo pass" } }
20
+ JSON
21
+
22
+ SID="tdd-inject-test-$$-$RANDOM"
23
+ }
24
+
25
+ teardown() {
26
+ rm -f "/tmp/tdd-announced-${SID}"
27
+ rm -f "/tmp/tdd-announced-${SID}-alt"
28
+ rm -rf "$WORKDIR"
29
+ }
30
+
31
+ run_hook() {
32
+ local sid="$1"
33
+ (cd "$WORKDIR" && echo "{\"session_id\":\"$sid\"}" | bash "$HOOK")
34
+ }
35
+
36
+ @test "tdd-inject: first invocation emits the full MANDATORY block with STATE RULES" {
37
+ run run_hook "$SID"
38
+ [ "$status" -eq 0 ]
39
+ [ "${#output}" -gt 1000 ]
40
+ [[ "$output" == *"MANDATORY TDD ENFORCEMENT"* ]]
41
+ [[ "$output" == *"STATE RULES"* ]]
42
+ [[ "$output" == *"WORKFLOW:"* ]]
43
+ [[ "$output" == *"IMPORTANT:"* ]]
44
+ }
45
+
46
+ @test "tdd-inject: first invocation writes the announcement marker" {
47
+ run run_hook "$SID"
48
+ [ -f "/tmp/tdd-announced-${SID}" ]
49
+ }
50
+
51
+ @test "tdd-inject: subsequent invocations drop the static prose (STATE RULES / WORKFLOW / IMPORTANT)" {
52
+ run_hook "$SID" >/dev/null
53
+ run run_hook "$SID"
54
+ [ "$status" -eq 0 ]
55
+ [ "${#output}" -lt 500 ]
56
+ [[ "$output" != *"STATE RULES"* ]]
57
+ [[ "$output" != *"WORKFLOW:"* ]]
58
+ [[ "$output" != *"IMPORTANT:"* ]]
59
+ }
60
+
61
+ @test "tdd-inject: subsequent invocations PRESERVE dynamic state (current TDD state line)" {
62
+ run_hook "$SID" >/dev/null
63
+ run run_hook "$SID"
64
+ [ "$status" -eq 0 ]
65
+ # Dynamic state line must still appear per-prompt. With no test files
66
+ # tracked yet, the overall state is IDLE.
67
+ [[ "$output" == *"IDLE"* ]]
68
+ # And the terse reminder itself carries the MANDATORY signal + the
69
+ # delegation affordance.
70
+ [[ "$output" == *"MANDATORY"* ]] || [[ "$output" == *"REQUIRED"* ]] || [[ "$output" == *"NON-OPTIONAL"* ]]
71
+ [[ "$output" == *"TDD"* ]]
72
+ }
73
+
74
+ @test "tdd-inject: terse reminder names the trigger artifact" {
75
+ run_hook "$SID" >/dev/null
76
+ run run_hook "$SID"
77
+ # Trigger for this gate is the test script in package.json.
78
+ [[ "$output" == *"test script"* ]] || [[ "$output" == *"package.json"* ]]
79
+ }
80
+
81
+ @test "tdd-inject: different session_id re-emits the full static block" {
82
+ run_hook "$SID" >/dev/null
83
+ local SID2="${SID}-alt"
84
+ run run_hook "$SID2"
85
+ [ "${#output}" -gt 1000 ]
86
+ [[ "$output" == *"STATE RULES"* ]]
87
+ rm -f "/tmp/tdd-announced-${SID2}"
88
+ }
89
+
90
+ @test "tdd-inject: empty session_id emits the full static block and writes no marker" {
91
+ run run_hook ""
92
+ [ "${#output}" -gt 1000 ]
93
+ [[ "$output" == *"STATE RULES"* ]]
94
+ [ ! -f "/tmp/tdd-announced-" ]
95
+ }
96
+
97
+ @test "tdd-inject: no-test-script fallback branch is unchanged by this ADR" {
98
+ local NO_TEST="$(mktemp -d)"
99
+ cat > "$NO_TEST/package.json" <<'JSON'
100
+ { "name": "no-test", "version": "0.0.0" }
101
+ JSON
102
+ run bash -c "cd '$NO_TEST' && echo '{\"session_id\":\"$SID\"}' | bash '$HOOK'"
103
+ [ "$status" -eq 0 ]
104
+ [[ "$output" == *"MANDATORY TDD ENFORCEMENT"* ]]
105
+ [[ "$output" == *"NO test script"* ]]
106
+ rm -rf "$NO_TEST"
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/tdd",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "TDD state machine enforcement (Red-Green-Refactor cycle)",
5
5
  "bin": {
6
6
  "windyroad-tdd": "./bin/install.mjs"