Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"name": "sdlc-wizard",
"source": ".",
"description": "SDLC enforcement for AI agents — TDD, planning, self-review, CI shepherd",
"version": "1.69.0",
"version": "1.70.0",
"author": {
"name": "Stefan Ayala"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sdlc-wizard",
"version": "1.69.0",
"version": "1.70.0",
"description": "SDLC enforcement for AI agents — TDD, planning, self-review, CI shepherd",
"author": {
"name": "Stefan Ayala",
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ jobs:
- name: Run baseline-fires-once-per-session tests (token bloat audit)
run: ./tests/test-baseline-fires-once.sh

- name: Run tdd-pretool-fires-once tests (token bloat audit)
run: ./tests/test-tdd-pretool-fires-once.sh

- name: Run community scanner tests (#207)
run: ./tests/test-community-scanner.sh

Expand Down
47 changes: 47 additions & 0 deletions .reviews/preflight-tdd-pretool-fires-once-001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Preflight Self-Review: tdd-pretool-check.sh fires once per CC session

## What changed

`hooks/tdd-pretool-check.sh`:
- Extract `session_id` from stdin JSON via `grep -o | head -1 | sed` (jq-independent, same pattern as v1.69.0 sdlc-prompt-check.sh).
- Gate the TDD CHECK JSON output on a per-session sentinel `$SDLC_WIZARD_CACHE_DIR/tdd-shown-<safe_sid>`.
- Atomic claim via subshell `set -C` (noclobber) `: > sentinel` — same proven pattern from v1.69.0.
- Fallback when claim fails AND file missing (cache unwritable) → emit (best-effort, never lose nudge).
- Prune sentinels older than 7d on emit.
- Sanitize session_id with `tr -cd 'A-Za-z0-9._-'` before filename use.

## What did NOT change

- File-path matching (`*"/src/"*`) — non-src files still produce zero output.
- The TDD CHECK message text (still emits same JSON when fired).
- jq dependency for file_path extraction (file paths can have escapes; UUIDs cannot).

## Self-review checklist

- [x] `tests/test-tdd-pretool-fires-once.sh` — 9/9 PASS (first-fire, suppression, different-session, no-session_id back-compat, non-src/ no-emit, non-src/-doesn't-consume-sentinel, cache-isolation, 50-parallel concurrency, suppressed-fire-empty)
- [x] `tests/test-hooks.sh` — 154/154 PASS (no regression in existing hook tests; old tests don't pass session_id, so back-compat path keeps them green)
- [x] `tests/test-baseline-fires-once.sh` — 10/10 PASS (v1.69.0 sibling)
- [x] `tests/test-audit-session-load.sh` — 9/9 PASS (SKILL.md still under 5K threshold)
- [x] `tests/test-cli.sh`, `test-plugin.sh`, `test-doc-consistency.sh` — all green
- [x] `tests/test-workflow-triggers.sh` — green after wiring new test into ci.yml + CONTRIBUTING.md
- [x] Version bumped 1.69.0 → 1.70.0 across 7 metadata sites
- [x] CHANGELOG.md v1.70.0 entry written
- [x] CI wiring: `.github/workflows/ci.yml` runs new test
- [x] CONTRIBUTING.md lists new test in dev-loop checklist

## Specific things to verify in review

1. **Non-src/ edit doesn't consume sentinel** — Test 6 covers this. The sentinel write only happens INSIDE the `*"/src/"*` branch. Confirm no path where non-src/ edit could pre-claim the sentinel.

2. **Concurrency same as v1.69.0** — atomic noclobber claim at the top of the src/ branch. Verify the conditional tree (claim succeeds → emit / claim fails AND file exists → suppress / claim fails AND file missing → emit fallback) is identical in semantics to the v1.69.0 BASELINE gate.

3. **session_id grep extraction** — same regex as v1.69.0. Verify it doesn't false-match an escaped `"session_id"` inside `tool_input.content` (which CC sends as part of Write tool calls).

4. **Suppressed fire is empty stdout** — Test 9 asserts. CC's PreToolUse contract treats empty stdout as "allow tool to proceed unmodified". Suppression must NOT emit `{}` or empty JSON wrapper.

5. **Hook still requires jq for file_path** — non-jq users would see no output for any edit, which is the existing behavior. Acceptable.

## Known limitations

- TDD nudge fires once per session, even if the user does 30 unrelated src/ edits over 2 hours. Acceptable trade-off — once Claude has the SDLC skill loaded, the nudge is duplicate.
- Sentinel survives CC restarts (7d prune). User who somehow reuses a session_id post-restart sees no nudge first prompt. CC session_ids are UUIDs — collision is implausible.
14 changes: 4 additions & 10 deletions .reviews/response.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
{
"review_id": "baseline-fires-once-001",
"review_id": "tdd-pretool-fires-once-001",
"round": 2,
"responding_to": ".reviews/baseline-fires-once-001/round-1-review.md",
"responding_to": ".reviews/tdd-pretool-fires-once-001/round-1-review.md",
"responses": [
{
"finding": "1",
"action": "FIXED",
"summary": "Atomic claim via subshell `set -C` (noclobber) + `: > sentinel` redirect. Across N parallel fires with the same session_id, exactly one wins the claim and emits; the rest see file-exists and suppress. New regression test (test_concurrency_same_session_emits_once) fires 50 parallel invocations and asserts exactly 1 BASELINE in aggregated stdout.",
"evidence": "hooks/sdlc-prompt-check.sh:131 (claim block), tests/test-baseline-fires-once.sh:166-189 (50-parallel test)"
},
{
"finding": "2",
"action": "FIXED",
"summary": "Decoupled session_id extraction from jq. Stdin is now read whenever stdin is non-tty (regardless of jq); session_id extracted via grep/sed. Prompt extraction still requires jq (multi-line + arbitrary escape content). New regression test (test_session_id_works_without_jq) builds a restricted PATH excluding jq, fires hook twice with valid session_id JSON stdin, asserts first emits / second suppresses.",
"evidence": "hooks/sdlc-prompt-check.sh:46-66 (decoupled extraction), tests/test-baseline-fires-once.sh:191-228 (no-jq regression test using type -P for binary-only resolution)"
"summary": "Staged the new test file. `git status --short tests/test-tdd-pretool-fires-once.sh` now shows `A tests/test-tdd-pretool-fires-once.sh`. The file will land on origin in the same commit as the rest of the change.",
"evidence": "tests/test-tdd-pretool-fires-once.sh (now staged in git index)"
}
]
}
18 changes: 18 additions & 0 deletions .reviews/tdd-pretool-fires-once-001/round-1-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
**Findings**
1. **P1 - New CI-referenced test is untracked**
Evidence: `git status --short tests/test-tdd-pretool-fires-once.sh .github/workflows/ci.yml CONTRIBUTING.md` shows `?? tests/test-tdd-pretool-fires-once.sh`; `git ls-files --error-unmatch tests/test-tdd-pretool-fires-once.sh` returns `pathspec ... did not match any file(s) known to git`. CI references it at [.github/workflows/ci.yml](/Users/stefanayala/sdlc-wizard/.github/workflows/ci.yml:220).
Certify condition: add `tests/test-tdd-pretool-fires-once.sh` to git before certification/commit.

**Checklist Evidence**
- (a) Sentinel logic is inside the `src/` branch: [hooks/tdd-pretool-check.sh](/Users/stefanayala/sdlc-wizard/hooks/tdd-pretool-check.sh:30), sentinel starts at line 40, emit block at line 63. Manual probe: `non_src_stdout_bytes=0`, `after_non_src_sentinels=0`, then first src edit `src_tdd_count=1`.
- (b) `session_id` extraction uses `grep | head | sed`, no jq: [hooks/tdd-pretool-check.sh](/Users/stefanayala/sdlc-wizard/hooks/tdd-pretool-check.sh:23). Matches prior pattern in [hooks/sdlc-prompt-check.sh](/Users/stefanayala/sdlc-wizard/hooks/sdlc-prompt-check.sh:58).
- (c) Atomic noclobber claim present: [hooks/tdd-pretool-check.sh](/Users/stefanayala/sdlc-wizard/hooks/tdd-pretool-check.sh:53). Fallback tree lines 53-59. Manual unwritable-cache probe: `rc=0`, `stdout_tdd_count=1`, `stderr_bytes=0`, `sentinel_exists=no`.
- (d) Non-src does not consume sentinel: test at [tests/test-tdd-pretool-fires-once.sh](/Users/stefanayala/sdlc-wizard/tests/test-tdd-pretool-fires-once.sh:124); focused test output: `PASS: non-src/ edit doesn't consume sentinel`.
- (e) 50-parallel test exists at [tests/test-tdd-pretool-fires-once.sh](/Users/stefanayala/sdlc-wizard/tests/test-tdd-pretool-fires-once.sh:153). Manual run: `parallel_tdd_count=1`, `parallel_failures=0`, `parallel_sentinels=1`.
- (f) CI and contributor docs are wired: [.github/workflows/ci.yml](/Users/stefanayala/sdlc-wizard/.github/workflows/ci.yml:220), [CONTRIBUTING.md](/Users/stefanayala/sdlc-wizard/CONTRIBUTING.md:44). `./tests/test-workflow-triggers.sh`: `Passed: 169`, `Failed: 0`.
- (g) Version bump complete at 7 required sites: `rg` found `1.70.0` in `package.json`, `plugin.json`, `marketplace.json`, `SDLC.md` x2, `CLAUDE_CODE_SDLC_WIZARD.md` x2; `rg` found no remaining `1.69.0` in those version sites.

Focused validation passed: `./tests/test-tdd-pretool-fires-once.sh` passed 9/9, `./tests/test-hooks.sh` passed 154/154, workflow YAML parsed cleanly.

Score: **8/10**
**NOT CERTIFIED**
26 changes: 26 additions & 0 deletions .reviews/tdd-pretool-fires-once-001/round-2-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
**Findings**
None.

Finding 1 is **FIXED**: `tests/test-tdd-pretool-fires-once.sh` is now in the git index (`100755`) and `git ls-files --error-unmatch` succeeds, satisfying the original certify condition.

Prior pass criteria still hold:
- TDD nudge emits once per `session_id`.
- Non-`src/` edits are silent and do not pre-consume the sentinel.
- No `session_id` preserves emit-every-fire behavior.
- Cache writes are best-effort.
- 50 parallel same-session `src/` edits emit exactly once.
- Suppressed fires produce empty stdout.
- CI/docs wiring and all 7 version bumps are present.

Validation run:
- `./tests/test-tdd-pretool-fires-once.sh` passed 9/9
- `./tests/test-hooks.sh` passed 154/154
- `./tests/test-workflow-triggers.sh` passed 169/169
- `.github/workflows/ci.yml` parsed as YAML
- `git diff --check` and `git diff --cached --check` clean

**Notes for next review**
Most implementation files remain unstaged while the new test is staged. Non-blocking for this targeted recheck, but commit packaging should include all intended files.

Score: **9/10**
**CERTIFIED**
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ All notable changes to the SDLC Wizard.

> **Note:** This changelog is for humans to read. Don't manually apply these changes - just run the wizard ("Check for SDLC wizard updates") and it handles everything automatically.

## [1.70.0] - 2026-05-05

### Token-bloat fix: TDD CHECK nudge fires once per CC session

`hooks/tdd-pretool-check.sh` was emitting a ~50-token JSON nudge ("TDD CHECK: Are you writing IMPLEMENTATION before a FAILING TEST?") on every Write/Edit/MultiEdit touching `src/**`. After the SDLC skill auto-invokes (which already covers TDD RED/GREEN), the per-Edit nudge is duplicate context — typical SDLC session has 10-30 src Edits = ~0.5-1.5K wasted tokens.

Now gated on per-`session_id` sentinel under `$SDLC_WIZARD_CACHE_DIR/tdd-shown-<id>`, atomic-claimed via subshell `set -C` (noclobber). Same pattern as v1.69.0 BASELINE gate.

### Behavior

- **First src/ edit of a CC session** → TDD CHECK emits as before.
- **Subsequent src/ edits (same session_id)** → TDD CHECK suppressed.
- **New CC session (different session_id)** → TDD CHECK re-emits.
- **Non-src/ files** → no output (existing behavior, regardless of sentinel). Editing README first does NOT consume the sentinel slot — TDD CHECK still fires on first src/ edit afterward.
- **No session_id in stdin** (legacy CC, direct shell tests) → emits every src/ edit (back-compat preserved).
- **N parallel src/ edits with same session_id** → exactly 1 TDD CHECK emit (atomic claim).

### Files

- `hooks/tdd-pretool-check.sh` — atomic-claim sentinel + jq-decoupled session_id extraction.
- `tests/test-tdd-pretool-fires-once.sh` (new — 9 cases including 50-parallel concurrency, non-src/ doesn't consume sentinel, suppressed-fire-empty assertion).
- `.github/workflows/ci.yml`, `CONTRIBUTING.md` — wire new test into validate job + contributor checklist.
- `CHANGELOG.md`, `SDLC.md`, `skills/update/SKILL.md`, `package.json`, `.claude-plugin/plugin.json` + `marketplace.json`, `CLAUDE_CODE_SDLC_WIZARD.md` (1.69.0 → 1.70.0).

### Notes

ROADMAP #236 functional-bloat audit, phase 2. Phase 1 (v1.69.0) trimmed the BASELINE block (~12K tokens/session). Phase 2 trims the per-Edit nudge. Combined savings on a 50-prompt + 20-Edit session: ~13.5K tokens. Audit method continues — measure cost × frequency, judge value, don't blind-delete. Other always-on hooks (`model-effort-check`, `precompact-seam-check`, `token-spike-check`) remain silent at healthy state and are not bloat.

## [1.69.0] - 2026-05-04

### Token-bloat fix: BASELINE block fires once per CC session
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE_CODE_SDLC_WIZARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -2976,7 +2976,7 @@ If deployment fails or post-deploy verification catches issues:

**SDLC.md:**
```markdown
<!-- SDLC Wizard Version: 1.69.0 -->
<!-- SDLC Wizard Version: 1.70.0 -->
<!-- Setup Date: [DATE] -->
<!-- Completed Steps: step-0.1, step-0.2, step-0.4, step-1, step-2, step-3, step-4, step-5, step-6, step-7, step-8, step-9 -->
<!-- Git Workflow: [PRs or Solo] -->
Expand Down Expand Up @@ -4055,7 +4055,7 @@ Walk through updates? (y/n)
Store wizard state in `SDLC.md` as metadata comments (invisible to readers, parseable by Claude):

```markdown
<!-- SDLC Wizard Version: 1.69.0 -->
<!-- SDLC Wizard Version: 1.70.0 -->
<!-- Setup Date: 2026-01-24 -->
<!-- Completed Steps: step-0.1, step-0.2, step-1, step-2, step-3, step-4, step-5, step-6, step-7, step-8, step-9 -->
<!-- Git Workflow: PRs -->
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Thank you for your interest in improving the SDLC Wizard!
./tests/test-repo-complexity.sh && \
./tests/test-prompt-hook-fires-once.sh && \
./tests/test-baseline-fires-once.sh && \
./tests/test-tdd-pretool-fires-once.sh && \
./tests/test-community-scanner.sh && \
./tests/test-community-fetch.sh && \
./tests/test-ground-truth.sh && \
Expand Down
4 changes: 2 additions & 2 deletions SDLC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- SDLC Wizard Version: 1.69.0 -->
<!-- SDLC Wizard Version: 1.70.0 -->
<!-- Setup Date: 2026-01-24 -->
<!-- Completed Steps: step-0.1, step-0.2, step-1, step-2, step-3, step-4, step-5, step-6, step-7, step-8, step-9 -->
# SDLC Configuration
Expand All @@ -7,7 +7,7 @@

| Property | Value |
|----------|-------|
| Wizard Version | 1.69.0 |
| Wizard Version | 1.70.0 |
| Last Updated | 2026-05-04 |
| Claude Code Baseline | v2.1.111+ (required for Opus 4.7 / `opus[1m]`) |
| Recommended Model | `opus[1m]` (Opus 4.7, 1M context) — run `/model opus[1m]` |
Expand Down
50 changes: 48 additions & 2 deletions hooks/tdd-pretool-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,59 @@ TOOL_INPUT=$(cat)
# Extract the file path being edited (requires jq)
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.tool_input.file_path // empty')

# session_id extraction is jq-independent (same pattern as sdlc-prompt-check.sh
# v1.69.0 — Codex round 1 P1 from that PR proved jq-coupling silently disabled
# the gate when jq was missing/broken). UUIDs are simple strings, no escapes.
SESSION_ID=$(printf '%s' "$TOOL_INPUT" \
| grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' \
| head -1 \
| sed 's/.*"\([^"]*\)"$/\1/')

# CUSTOMIZE: Change this pattern to match YOUR source directory
# Examples: "/src/", "/app/", "/lib/", "/packages/", "/server/"
if [[ "$FILE_PATH" == *"/src/"* ]]; then
# Output additionalContext that Claude will read
cat << 'EOF'
# Token-bloat fix (v1.70.0): nudge fires once per CC session. Once Claude
# has the SDLC skill auto-invoked (covers TDD RED/GREEN), the per-Edit
# nudge becomes duplicate context — typical session has 10-30 src Edits
# = ~0.5-1.5K wasted tokens. Same atomic-noclobber claim pattern as
# sdlc-prompt-check.sh BASELINE gate.
#
# No-session_id stdin (legacy CC, direct shell tests) → emit every fire,
# preserving back-compat with existing tests in test-hooks.sh that don't
# pass session_id.
SHOULD_EMIT=1
if [ -n "$SESSION_ID" ]; then
CACHE_DIR="${SDLC_WIZARD_CACHE_DIR:-$HOME/.cache/sdlc-wizard}"
SAFE_SID=$(printf '%s' "$SESSION_ID" | tr -cd 'A-Za-z0-9._-')
if [ -n "$SAFE_SID" ]; then
SENTINEL="$CACHE_DIR/tdd-shown-${SAFE_SID}"
mkdir -p "$CACHE_DIR" 2>/dev/null || true
# Atomic claim: subshell `set -C` makes `: > path` create-or-fail.
# Conditional tree:
# - claim succeeds → emit (we won the race)
# - claim fails AND file exists → suppress (someone else won)
# - claim fails AND file missing → cache unwritable; fall back
# to emit so user never loses cold-start nudge.
if (set -C; : > "$SENTINEL") 2>/dev/null; then
SHOULD_EMIT=1
elif [ -f "$SENTINEL" ]; then
SHOULD_EMIT=0
else
SHOULD_EMIT=1
fi
fi
fi

if [ "$SHOULD_EMIT" -eq 1 ]; then
# Output additionalContext that Claude will read
cat << 'EOF'
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": "TDD CHECK: Are you writing IMPLEMENTATION before a FAILING TEST? If yes, STOP. Write the test first (TDD RED), then implement (TDD GREEN)."}}
EOF
# Prune sentinels older than 7d so cache doesn't grow forever.
if [ -n "$SESSION_ID" ] && [ -n "$SAFE_SID" ]; then
find "$CACHE_DIR" -name 'tdd-shown-*' -type f -mtime +7 -delete 2>/dev/null || true
fi
fi
fi

# No output = allow the tool to proceed
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentic-sdlc-wizard",
"version": "1.69.0",
"version": "1.70.0",
"description": "SDLC enforcement for Claude Code — hooks, skills, and wizard setup in one command",
"bin": {
"sdlc-wizard": "cli/bin/sdlc-wizard.js"
Expand Down
5 changes: 3 additions & 2 deletions skills/update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,11 @@ Parse CHANGELOG entries between the user's installed version and latest. Present

```
Installed: 1.42.0
Latest: 1.69.0
Latest: 1.70.0

What changed:
- [1.69.0] token-bloat fix — `hooks/sdlc-prompt-check.sh` BASELINE block (the ~250-token "TodoWrite FIRST / STATE CONFIDENCE / AUTO-INVOKE" reminder) now fires once per CC `session_id` instead of every prompt. Saves ~12K tokens/session for any user with >3 prompts. SETUP-not-complete + EFFORT-bump warnings still fire every prompt (dynamic state). Sentinel pruned at 7d. No-session-id stdin keeps current behavior (legacy CC + tests).
- [1.70.0] token-bloat fix phase 2 — `hooks/tdd-pretool-check.sh` TDD CHECK JSON nudge (the per-`Write/Edit` "Are you writing IMPLEMENTATION before a FAILING TEST?" reminder) now fires once per CC `session_id` instead of every src/ edit. Saves ~0.5-1.5K tokens/session (10-30 src Edits × ~50 tok). Same atomic-noclobber claim pattern as v1.69.0 BASELINE gate. Non-src/ edits don't consume the sentinel slot.
- [1.69.0] token-bloat fix phase 1 — `hooks/sdlc-prompt-check.sh` BASELINE block (the ~250-token "TodoWrite FIRST / STATE CONFIDENCE / AUTO-INVOKE" reminder) now fires once per CC `session_id`. Saves ~12K tokens/session. SETUP-not-complete + EFFORT-bump warnings still fire every prompt (dynamic state).
- [1.68.0–1.65.0] roadmap hygiene — five paperwork closes: #97 Anthropic Policy NO-GO + AAR-paper validating parallel; #99 AutoGPT NO-GO; #95 Nous NO-GO; #243 token-history liveness verified; #210 Node-24 false-green; #235 Thoughtworks AI Evals NO-GO. **6/6 external-product audits NO-GO** (continues #76, #77). Research write-ups in `.reviews/research-*.md`.
- [1.64.0] XDLC ecosystem cross-references — README, wizard doc, and ROADMAP now cross-reference all three sibling packages (`agentic-sdlc-wizard`, `codex-sdlc-wizard`, `claude-gdlc-wizard`). New "Ecosystem (Sibling Projects)" section in README. 3 new doc-consistency tests prevent drift.
- [1.63.0] cache-cost observability closeout (#204 absorbed by #220) — `tests/test-token-spike.sh` gains explicit cache-miss regression test + negative-control test. SDLC skill + wizard doc gain "Cache-Cost Surprises" sections covering 10-20× silent cost blowups (mid-session CLAUDE.md edits, idle pruning, upstream cache bugs) and detection via `hooks/token-spike-check.sh`'s `costly_tokens` metric.
Expand Down
Loading