@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.
|
@@ -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
|
+
}
|
package/hooks/tdd-inject.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|