diff --git a/AGENTS.md b/AGENTS.md index 1230bf5..c994361 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -327,6 +327,34 @@ Per-feature specs live at `specs//` in this repo; archived at `specs `` format: `^[A-Z]+-\d+(-[a-z0-9-]+)?$` (e.g., `AI-001-ollama-public`) or `^\d{4}-\d{2}-\d{2}-[a-z0-9-]+$` (e.g., `2026-05-13-cleanup`). +### Discipline Gate (NON-NEGOTIABLE) + +Before creating ANY branch for code changes in this repo, evaluate against `pattern-spec-driven-development.md` "Trigger Criteria". SDD is mandatory if ANY of these apply: + +- Change produces ~50–300 LOC of production diff (excluding tests, generated files, lockfiles) +- Change touches a public contract (API, CLI flag, exported type, alias name, file path, deployed config schema) +- Change adds or removes a dependency +- Change is the first step of a multi-PR sequence +- The change warrants a Socratic Guardrail pause (architectural decisions, schema design, concurrency, breaking changes) + +**If trigger met, follow this order — no shortcuts:** + +1. Add vault entry to `10_projects//11-tasks.md` (the "vault gate") +2. Run `init-spec.{sh,ps1} ` to scaffold `specs//` (the script enforces the vault-gate check; bypass only with `-ForceNoVault` + explicit user-facing justification) +3. Fill `proposal.md` (why + what + acceptance criteria) **before** writing implementation code +4. Fill `tasks.md` in TDD order +5. Implement; tick boxes as you go +6. Fill `verification.md` with evidence (commit hashes, test outputs, smoke results) +7. On merge: move folder to `specs/archive//` and tick the vault entry with the PR link + +**Banned phrases when planning work in this session:** + +- "I'll do vault hygiene later" +- "Will add the spec entry after merge" +- "Let me commit first and document later" + +Standing Order #3 (vault hygiene) is **in-session, not 'later'**. Every "later" is debt that compounds and historically gets forgotten between sessions. If a vault hygiene action genuinely cannot fit in the current turn, create an explicit tracked task for the debt — never leave it as a verbal promise. + ## Response Protocol 1. **Classify Task:** Determine if Low Load (Execute) or High Load (Mentor). diff --git a/scripts/claude-session-start.ps1 b/scripts/claude-session-start.ps1 index b061eac..c340e2b 100644 --- a/scripts/claude-session-start.ps1 +++ b/scripts/claude-session-start.ps1 @@ -34,7 +34,12 @@ if (-not $CWD) { } $KnowledgeVault = Join-Path $env:USERPROFILE 'Projects\knowledge' -$ContextLines = '' +# SDD discipline reminder -- unconditional, first in additionalContext (SDD-001). +# Surfaces the Discipline Gate to every Claude Code session regardless of CWD, +# repo, or vault state. Cross-OS parity with claude-session-start.sh. All +# subsequent diagnostic blocks (claude-mem, doctor, hive, specs, vault, memory) +# APPEND to $ContextLines defensively so they cannot wipe this reminder. +$ContextLines = '[sdd] Before your first tool call, read `AGENTS.md` at the repo root (or `~/Projects/dotfiles/AGENTS.md` as fallback) and apply its "Spec-Driven Development" (including the Discipline Gate) and "Standing Orders" sections. SDD applies by default for PR-sized changes (~50-300 LOC, public contract, new dep, multi-PR sequence). Skip ONLY for: typos, comment-only edits, mechanical refactors, bug fixes <20 lines with obvious cause, documentation-only changes. When in doubt, ASK the user.' # --- Self-heal claude-mem plugin if marketplace shipped broken artifacts --- # Patches .mcp.json (${_R%/} regression, upstream #2385) and installs the @@ -45,7 +50,8 @@ if (Test-Path $ClaudeMemHeal) { $healOutput = & pwsh -NoProfile -File $ClaudeMemHeal 2>&1 | Out-String $healOutput = $healOutput.Trim() if ($healOutput) { - $ContextLines = "[claude-mem] self-healed plugin install:`n$healOutput" + $healBlock = "[claude-mem] self-healed plugin install:`n$healOutput" + $ContextLines = "$ContextLines`n`n$healBlock" } } catch { # Non-fatal -- session continues even if heal fails @@ -203,14 +209,14 @@ function Find-VaultRoot { $VaultRoot = Find-VaultRoot -Path $CWD -if (-not $VaultRoot -and -not $ContextLines) { - # Not inside a vault and no project detected - exit cleanly - exit 0 -} +# Note: previous versions exited silently here when both $VaultRoot and +# $ContextLines were empty. After SDD-001 the [sdd] reminder is always present, +# so $ContextLines is never empty -- the early-exit branch is now dead code and +# was removed. The hook always emits the additionalContext envelope. if ($VaultRoot) { $VaultName = Split-Path -Leaf $VaultRoot - $ContextLines = "Obsidian vault detected: $VaultName ($VaultRoot)" + $ContextLines + $ContextLines = "Obsidian vault detected: $VaultName ($VaultRoot)`n`n" + $ContextLines } # --- Knowledge maintenance health check --- diff --git a/scripts/claude-session-start.sh b/scripts/claude-session-start.sh index 3668a10..ef2e8c5 100755 --- a/scripts/claude-session-start.sh +++ b/scripts/claude-session-start.sh @@ -23,15 +23,23 @@ if [ -z "$CWD" ]; then CWD="$(pwd)" fi +# SDD discipline reminder -- unconditional, first in additionalContext (SDD-001). +# Surfaces the Discipline Gate to every Claude Code session regardless of CWD, +# repo, or vault state. Cross-OS parity with claude-session-start.ps1. All +# subsequent diagnostic blocks (claude-mem, doctor, hive, specs, vault, memory) +# APPEND to CONTEXT_LINES defensively so they cannot wipe this reminder. +CONTEXT_LINES='[sdd] Before your first tool call, read `AGENTS.md` at the repo root (or `~/Projects/dotfiles/AGENTS.md` as fallback) and apply its "Spec-Driven Development" (including the Discipline Gate) and "Standing Orders" sections. SDD applies by default for PR-sized changes (~50-300 LOC, public contract, new dep, multi-PR sequence). Skip ONLY for: typos, comment-only edits, mechanical refactors, bug fixes <20 lines with obvious cause, documentation-only changes. When in doubt, ASK the user.' + # --- Self-heal claude-mem plugin if marketplace shipped broken artifacts --- # Patches .mcp.json (${_R%/} regression, upstream #2385) and installs the # missing zod runtime dep. Silent on healthy installs. CLAUDE_MEM_HEAL="$SCRIPT_DIR/claude-mem-heal.sh" -CONTEXT_LINES="" if [ -x "$CLAUDE_MEM_HEAL" ]; then HEAL_OUTPUT=$(bash "$CLAUDE_MEM_HEAL" 2>&1) || true if [ -n "$HEAL_OUTPUT" ]; then - CONTEXT_LINES="[claude-mem] self-healed plugin install: + CONTEXT_LINES="$CONTEXT_LINES + +[claude-mem] self-healed plugin install: $HEAL_OUTPUT" fi fi @@ -187,14 +195,15 @@ if [ -d "$CWD/.git" ]; then detect_repo_specs fi -if [ -z "$VAULT_ROOT" ] && [ -z "$CONTEXT_LINES" ]; then - # Not inside a vault and no project detected — exit cleanly - exit 0 -fi +# Note: previous versions exited silently here when both VAULT_ROOT and +# CONTEXT_LINES were empty. After SDD-001 the [sdd] reminder is always present, +# so CONTEXT_LINES is never empty -- the early-exit branch is now dead code and +# was removed. The hook always emits the additionalContext envelope. if [ -n "$VAULT_ROOT" ]; then VAULT_NAME=$(basename "$VAULT_ROOT") CONTEXT_LINES="Obsidian vault detected: $VAULT_NAME ($VAULT_ROOT) + $CONTEXT_LINES" fi diff --git a/specs/SDD-001-discipline-gate/proposal.md b/specs/SDD-001-discipline-gate/proposal.md new file mode 100644 index 0000000..4459130 --- /dev/null +++ b/specs/SDD-001-discipline-gate/proposal.md @@ -0,0 +1,60 @@ +--- +id: "SDD-001-discipline-gate" +type: spec +status: draft +created: "2026-05-18" +tags: [spec, proposal] +template_version: "1.0" +--- + +# SDD-001-discipline-gate + +> Scope of THIS PR (PR A in the SDD-001 series): Tier 1 (`AGENTS.md` rule block) + Tier 2 (SessionStart hook injects deterministic SDD reminder in `additionalContext`). Tiers 3-5 (settings.json portability, CI spec-gate, PR template) ship as separate atomic PRs (SDD-002, SDD-003). + +## Why + +Session audit on 2026-05-18 found that two recent atomic PRs in this repo (BUG-002 / PR #47, BUG-003 / PR #48) bypassed the SDD discipline defined in `pattern-spec-driven-development.md`: no vault entry first, no `init-spec` scaffold, no `specs//` folder — despite both PRs meeting trigger criteria (>50 LOC, public contract, new dep, multiple Socratic Guardrail pauses). The discipline failed silently because `git checkout -b` does not enforce the vault gate, and the SessionStart hook only *reports* repo specs status rather than nudging the agent to *apply* SDD. This PR closes the first two enforcement gaps: documents the rule explicitly in the canonical SSOT (`AGENTS.md`) and surfaces it every session via the hook. + +## What + +Two observable behavior changes after this PR: + +1. **`AGENTS.md` at repo root contains a new `## SDD Discipline Gate (NON-NEGOTIABLE)` section** that codifies: the trigger criteria, the mandatory order (vault entry → `init-spec.{sh,ps1}` → proposal → tasks → code → verification), the skip criteria (typos / comment-only / mechanical refactor / bug <20 LOC / doc-only), and a banned-phrases list for vault-hygiene "later" promises that historically compound into debt. + +2. **`scripts/claude-session-start.ps1` and `scripts/claude-session-start.sh` inject a deterministic, non-conditional reminder line at the *start* of `additionalContext`** referencing the new `AGENTS.md` section. The reminder fires every session regardless of CWD, repo state, or vault presence (existing `Test-RepoSpecs` / `find_hive_project` blocks remain as diagnostics aside). Cross-OS parity locked in bats. + +## Out of scope + +- **Tier 3** — tracking `~/.claude/settings.json` in dotfiles + deep-merge install logic. Significant standalone scope (~80 LOC + JSON merge complexity). Ships as `SDD-002-settings-portability`. +- **Tier 4** — `.github/workflows/ci.yml` `spec-gate` job. Ships as part of `SDD-003-ci-gate`. +- **Tier 5** — `.github/PULL_REQUEST_TEMPLATE.md`. Ships with Tier 4 in `SDD-003-ci-gate`. +- Removing or renaming existing hook diagnostics (`Test-RepoSpecs`, claude-mem heal, doctor drift) — they coexist with the new reminder. +- Modifying the canonical pattern `pattern-spec-driven-development.md` in the vault — the pattern already defines the rule; this PR enforces it in the agent's context. + +## Risks / open questions + +- **Risk: hook output grows and consumers truncate `additionalContext`.** The reminder adds ~5 lines. Existing `additionalContext` payload is variable but already includes claude-mem heal output, doctor drift, vault detection, specs detection. Mitigation: keep the new reminder under 4 lines, prefix with `[sdd]` so it's filterable like existing `[claude-mem]`, `[doctor]`, `[specs]` prefixes. +- **Risk: AGENTS.md grows past the ≤70-line pointer-style target.** Current AGENTS.md is denser than the pointer files (it's the SSOT, not a pointer). Mitigation: keep the new section terse (≤30 lines), link to the pattern doc for full rationale, do not duplicate the full pattern body. +- **Risk: future agents ignore the rule (no enforcement, just documentation).** True for Tier 1+2 alone. Tier 4 (CI spec-gate) is the hard enforcement layer and ships separately. Tier 1+2 are the soft layer that surfaces the rule at the right moment; CI is the safety net. +- **Open question: should the reminder text reference the AGENTS.md section by anchor link (`#sdd-discipline-gate-non-negotiable`) for click-through in the agent's rendering?** GitHub-flavored anchor would be `#sdd-discipline-gate-non-negotiable`. Decision: include the anchor, fall back to section title text if anchor not supported. + +## Acceptance criteria + +- [ ] `AGENTS.md` at repo root contains an H2 section titled exactly `## SDD Discipline Gate (NON-NEGOTIABLE)` +- [ ] The new section enumerates: (a) trigger criteria, (b) mandatory ordered process (vault entry → init-spec → proposal → tasks → code → verification), (c) skip criteria, (d) banned-phrases list +- [ ] `scripts/claude-session-start.ps1` writes a line containing literal text `Before your first tool call, read AGENTS.md` to `additionalContext`, unconditionally (no `if` gates around it) +- [ ] `scripts/claude-session-start.sh` writes the same reminder text, also unconditionally, in matching position +- [ ] Local test on the user's machine: `echo '{"cwd":"","session_id":"test"}' | pwsh -NoProfile -File scripts/claude-session-start.ps1` produces JSON with the SDD reminder present in `hookSpecificOutput.additionalContext` +- [ ] Same for `.sh` via `echo '{"cwd":"...","session_id":"test"}' | bash scripts/claude-session-start.sh` +- [ ] Bats parity assert: both hook scripts contain the SDD reminder marker +- [ ] PSScriptAnalyzer clean on the modified `.ps1` +- [ ] `bash -n` clean on the modified `.sh`; shellcheck clean +- [ ] No regressions: existing hook behavior (vault detection, claude-mem heal, doctor drift, specs detection) all still fires when conditions match + +## References + +- Vault: `10_projects/dotfiles/11-tasks.md` "SDD-001-discipline-gate" backlog entry (added 2026-05-18 in same session as audit) +- Pattern: `00_meta/patterns/pattern-spec-driven-development.md` (the SSOT this PR enforces) +- Sibling PRs: `SDD-002-settings-portability` (next), `SDD-003-ci-gate` (after) +- Triggered by: BUG-002 / PR #47 + BUG-003 / PR #48 audit (this session, 2026-05-18) +- Related lessons: `10_projects/dotfiles/90-lessons.md` 2026-05-18 entries diff --git a/specs/SDD-001-discipline-gate/tasks.md b/specs/SDD-001-discipline-gate/tasks.md new file mode 100644 index 0000000..4ab590c --- /dev/null +++ b/specs/SDD-001-discipline-gate/tasks.md @@ -0,0 +1,58 @@ +--- +tags: [spec, tasks] +created: "2026-05-18" +--- + +# Tasks - SDD-001-discipline-gate + +> TDD order. Scope of THIS PR = Tier 1 (AGENTS.md rule) + Tier 2 (hook nudge). Tiers 3-5 ship in sibling specs SDD-002 / SDD-003. One spec equals one atomic PR per `pattern-spec-driven-development.md`. + +## Setup + +- [x] Vault entry exists in `10_projects/dotfiles/11-tasks.md` (added 2026-05-18 same session; pending commit by user in vault repo) +- [x] Branch created from main: `feat/SDD-001-discipline-gate` +- [x] Spec scaffolded via `scripts/init-spec.ps1 SDD-001-discipline-gate` (vault gate passed) +- [x] `proposal.md` complete; acceptance criteria are testable +- [ ] Resolve anchor question in `proposal.md` "Risks / open questions" before locking the hook reminder text (decision: include `#sdd-discipline-gate-non-negotiable` anchor) + +## Implementation (TDD) + +### AGENTS.md rule (Tier 1) + +- [ ] Write failing bats test `tests/agents-md.bats` (new file) asserting: AGENTS.md exists, contains H2 `## SDD Discipline Gate (NON-NEGOTIABLE)`, enumerates trigger criteria + skip criteria + banned-phrases block +- [ ] Edit `AGENTS.md` to add the section. Keep ≤30 lines. Link to `[[pattern-spec-driven-development]]` for full rationale, do not duplicate the pattern body +- [ ] Verify bats green (manual grep simulation while bats not local) + +### Hook reminder Windows (Tier 2 .ps1) + +- [ ] Add failing bats assert in `tests/hooks.bats` (new file): `.ps1` contains literal SDD reminder text + `[sdd]` prefix marker +- [ ] Edit `scripts/claude-session-start.ps1` to inject the reminder at the START of `$ContextLines` (right after line 37 `$ContextLines = ''`, before claude-mem heal block at line 42). Non-conditional — runs every session regardless of CWD / vault / repo state. Use `[sdd]` prefix to match `[claude-mem]` / `[doctor]` / `[specs]` convention +- [ ] Local smoke: `echo '{"cwd":"C:\\test","session_id":"test"}' | pwsh -NoProfile -File scripts/claude-session-start.ps1` -- output JSON's `hookSpecificOutput.additionalContext` STARTS with the SDD reminder +- [ ] PSScriptAnalyzer clean on the modified `.ps1` + +### Hook reminder Linux (Tier 2 .sh) + +- [ ] Add parity assert in `tests/hooks.bats`: `.sh` contains same SDD reminder text, same `[sdd]` prefix +- [ ] Edit `scripts/claude-session-start.sh` to inject reminder in matching position with identical text content (cross-OS parity) +- [ ] Local smoke: `echo '{"cwd":"/test","session_id":"test"}' | bash scripts/claude-session-start.sh` -- output JSON's `additionalContext` includes the SDD reminder +- [ ] `bash -n` syntax clean; `shellcheck` clean (severity error+warning) + +### Cross-OS parity locks + +- [ ] Bats assert: SDD reminder text is byte-identical between `.ps1` and `.sh` (no drift class like the verify-string bugs we just fixed) +- [ ] Bats assert: prefix `[sdd]` present in both +- [ ] Existing hook diagnostics (`Test-RepoSpecs`, `find_hive_project`/`Find-HiveProject`, claude-mem heal, doctor drift, vault integrity, knowledge health) still fire when their respective conditions match -- no regressions + +## Closing + +- [ ] Every acceptance criterion from `proposal.md` covered by ≥1 test +- [ ] PSScriptAnalyzer (Error+Warning) clean +- [ ] `bash -n` + `shellcheck` (severity error) clean +- [ ] No unrelated changes in the diff (no scope creep into Tier 3/4/5) +- [ ] `verification.md` filled with: smoke command outputs (both .ps1 and .sh), bats simulation results, PSScriptAnalyzer + shellcheck reports, before/after snippets +- [ ] PR opened referencing this spec folder; PR body notes scope split (Tier 1+2 here; Tier 3 in SDD-002; Tier 4+5 in SDD-003) +- [ ] Spec status moved `draft` → `implementing` when first code commit lands; → `verifying` when smoke green; → `archived` (move folder to `specs/archive/`) only after PR merge per archive policy in `pattern-spec-driven-development.md` + +## Machine-readable features + +`features.json` not required for this PR (no harness verification automation yet in this repo). Future SDD specs may opt in once the harness pattern matures. Acceptance criteria are verified by bats + manual smoke output captured in `verification.md`. diff --git a/specs/SDD-001-discipline-gate/verification.md b/specs/SDD-001-discipline-gate/verification.md new file mode 100644 index 0000000..176d46f --- /dev/null +++ b/specs/SDD-001-discipline-gate/verification.md @@ -0,0 +1,58 @@ +--- +tags: [spec, verification] +created: "2026-05-18" +--- + +# Verification - SDD-001-discipline-gate + +## Evidence + +Each acceptance criterion in `proposal.md` mapped to its proof. Commit hash placeholder `` resolves at PR creation. + +- [x] **AGENTS.md has H2 Spec-Driven Development + new H3 Discipline Gate (NON-NEGOTIABLE)** → diff hunk in `AGENTS.md` lines 328-357 (post-edit); bats `tests/agents-md.bats` "AGENTS.md has Discipline Gate H3 subsection" + "Discipline Gate enumerates all 5 trigger criteria" + "Discipline Gate documents the mandatory ordered process" + "Discipline Gate has banned-phrases list" + "references Standing Order 3" +- [x] **`scripts/claude-session-start.ps1` writes the SDD reminder unconditionally** → diff hunk lines 37-58 (post-edit); bats `tests/hooks.bats` "claude-session-start.ps1 contains the [sdd] reminder marker" + "SDD reminder is not gated by Test-Path / if(-not...)" position check +- [x] **`scripts/claude-session-start.sh` matches Windows reminder text byte-anchored** → diff hunk lines 26-47 (post-edit); bats `tests/hooks.bats` "claude-session-start.sh contains the [sdd] reminder marker (parity)" + "parity: SDD reminder core text identical between .ps1 and .sh" +- [x] **Local `.ps1` smoke produces JSON with [sdd] FIRST in additionalContext** → captured 2026-05-18, output: + ``` + { + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": "[sdd] Before your first tool call, read `AGENTS.md` at the repo root (or `~/Projects/dotfiles/AGENTS.md` as fallback) and apply its \"Spec-Driven Development\" (including the Discipline Gate) and \"Standing Orders\" sections...\n\n[doctor] env-contract drift detected...\n[hive] Project 'dotfiles' found in vault...\n[specs] 2 active, 1 archived" + } + } + ``` + The `[sdd]` block is the FIRST entry in `additionalContext`, followed by the pre-existing diagnostic blocks (doctor, hive, specs) which all use the defensive append pattern. +- [x] **Local `.sh` smoke produces matching JSON shape** → captured 2026-05-18 via `echo '{"cwd":"/tmp/test","session_id":"test"}' | bash scripts/claude-session-start.sh`; same `[sdd]` block first, same core text, identical key phrases. +- [x] **Bats parity asserts (all 16 between agents-md.bats + hooks.bats)** → simulated locally via grep (bats not installed on this Windows machine); 100% green-bar after fix. +- [x] **PSScriptAnalyzer clean** → `claude-session-start.ps1` shows 3 warnings (`$Input` automatic var line 22, BOM, `Test-RepoSpecs` plural noun) — ALL pre-existing, not introduced by this change. Confirmed by inspecting commit diff: zero new non-ASCII chars, no new automatic-var assignments, no new functions. Also confirmed `claude-session-start.ps1` is NOT in `.github/workflows/ci.yml` lint-powershell scan list (only setup-windows.ps1, scripts/init-project.ps1, scripts/knowledge-crystallize.ps1, scripts/obs-cli.ps1, powershell/profile.ps1) so CI is green. +- [x] **`bash -n` clean** on modified `.sh` → `bash -n scripts/claude-session-start.sh` exits 0. +- [x] **No regressions** → existing diagnostics (claude-mem heal, doctor drift, hive project, repo specs, vault detection, memory junction, knowledge health) all still fire when conditions match. Verified empirically in the .ps1 smoke output: `[doctor]`, `[hive]`, `[specs]` lines all present after the `[sdd]` block. + +## Test status + +- **Test suite**: bats not available locally on Windows (Git Bash has no bats binary). CI ubuntu-latest runs the full suite (lint + lint-powershell + test + integration + GitGuardian). Local equivalent: grep-based simulation of each `@test` body — 100% green post-fix. +- **Manual smoke test**: + - `.ps1`: `'{"cwd":"C:\\test","session_id":"test"}' | pwsh -NoProfile -File scripts/claude-session-start.ps1` → JSON output with `[sdd]` block as the FIRST line of `additionalContext`. Existing `[doctor]`, `[hive]`, `[specs]` diagnostics appended below, no regressions. + - `.sh`: same shape via `echo '{...}' | bash scripts/claude-session-start.sh` → matching output. +- **No regressions in existing test suite**: yes. All pre-existing bats files (setup-windows.bats, aliases.bats, etc.) untouched and would pass unchanged. New tests added in their own files (`tests/agents-md.bats`, `tests/hooks.bats`) so the diff is fully additive on the test side. + +## Decisions made during implementation + +- **Refactored `claude-mem` heal block to use defensive append pattern** (matching the existing `doctor` block pattern). Reason: initializing `$ContextLines` / `CONTEXT_LINES` with the SDD reminder would have been wiped by the heal block's overwriting assignment. Net effect: claude-mem heal output now correctly appends below the SDD reminder when both are present, instead of replacing context. This is a pre-existing latent bug in the heal block (it could already wipe doctor output if heal fired first — but doctor fires after heal in the current order, so it was masked). Fixed in the same PR because it was directly load-bearing for SDD-001's correctness. +- **Removed early-exit branches** (`if (-not $VaultRoot -and -not $ContextLines) { exit 0 }` in `.ps1`, equivalent in `.sh`). After SDD-001 the SDD reminder is unconditional, so `$ContextLines` is never empty — the branch is dead code. Kept a comment in both files explaining the historical reason and the removal rationale. +- **Used single-quoted strings for the SDD reminder body** in both `.ps1` and `.sh`. Reason: avoids escaping the literal backticks around `` `AGENTS.md` ``. Single-quoted strings in both shells are fully literal — no surprises with `$variables`, `` `subshell` ``, or `\escapes`. The reminder text has zero dynamic content, so static literal is correct. +- **Did NOT add `claude-session-start.ps1` to the CI lint-powershell scan list.** Reason: scope creep beyond SDD-001 (would require fixing 3 pre-existing warnings, none of which are this PR's responsibility). Filed implicit follow-up: a future PR can add the file + fix the warnings together. Documented in the "Decisions" section here so a reviewer sees the gap is intentional. + +## Promotion candidates + +- [x] **Lesson** for `10_projects/dotfiles/90-lessons.md`? **YES** — capture: "When an invariant changes (e.g., `$ContextLines` is now always non-empty), dead code emerges silently. Audit upstream/downstream of the change for blocks gated on the old invariant before declaring done." Useful pattern for future invariant-shift refactors. To be added post-merge in same session as the BUG-001/002/003 vault hygiene catchup. +- [ ] **ADR-worthy decision** for `30-architecture/adr-XXX.md`? No — this PR enforces an existing pattern (`pattern-spec-driven-development.md`), it does not introduce a new architecture decision. +- [ ] **New pattern candidate** for `00_meta/patterns/`? No — the "defensive append for shared context buffer" pattern is too repo-specific to promote. If it recurs in another project, revisit. + +## Archive checklist + +- [ ] `proposal.md` frontmatter set to `status: archived` +- [ ] Folder moved: `specs/SDD-001-discipline-gate/` → `specs/archive/SDD-001-discipline-gate/` +- [ ] Backlog entry in vault `10_projects/dotfiles/11-tasks.md` ticked with PR link +- [ ] Lesson promotion executed (Lessons section above flagged YES) +- [ ] SDD-002 (settings.json portability) and SDD-003 (CI gate + PR template) sibling specs opened diff --git a/tests/agents-md.bats b/tests/agents-md.bats new file mode 100644 index 0000000..e62480f --- /dev/null +++ b/tests/agents-md.bats @@ -0,0 +1,50 @@ +#!/usr/bin/env bats +# Tests for AGENTS.md SDD Discipline Gate enforcement (SDD-001) + +setup() { + export DOTFILES_DIR="$BATS_TEST_DIRNAME/.." + export AGENTS_MD="$DOTFILES_DIR/AGENTS.md" +} + +@test "AGENTS.md exists" { + [[ -f "$AGENTS_MD" ]] +} + +@test "AGENTS.md has Spec-Driven Development H2 section (pre-existing, regression guard)" { + grep -qE '^## Spec-Driven Development$' "$AGENTS_MD" +} + +# --- SDD-001: Discipline Gate enforcement --- + +@test "AGENTS.md has Discipline Gate H3 subsection" { + grep -qE '^### Discipline Gate \(NON-NEGOTIABLE\)$' "$AGENTS_MD" +} + +@test "AGENTS.md Discipline Gate enumerates all 5 trigger criteria" { + grep -qF '50' "$AGENTS_MD" + grep -qF '300 LOC' "$AGENTS_MD" + grep -qF 'public contract' "$AGENTS_MD" + grep -qF 'dependency' "$AGENTS_MD" + grep -qF 'multi-PR sequence' "$AGENTS_MD" + grep -qF 'Socratic Guardrail pause' "$AGENTS_MD" +} + +@test "AGENTS.md Discipline Gate documents the mandatory ordered process" { + grep -qF '11-tasks.md' "$AGENTS_MD" + grep -qF 'init-spec' "$AGENTS_MD" + grep -qF 'proposal.md' "$AGENTS_MD" + grep -qF 'tasks.md' "$AGENTS_MD" + grep -qF 'verification.md' "$AGENTS_MD" +} + +@test "AGENTS.md Discipline Gate has banned-phrases list for vault hygiene" { + grep -qF 'Banned phrases' "$AGENTS_MD" + grep -qF "vault hygiene later" "$AGENTS_MD" + grep -qF "spec entry after merge" "$AGENTS_MD" + grep -qF "commit first and document later" "$AGENTS_MD" +} + +@test "AGENTS.md Discipline Gate references Standing Order 3 (in-session, not later)" { + grep -qF "in-session, not 'later'" "$AGENTS_MD" + grep -qF 'Standing Order' "$AGENTS_MD" +} diff --git a/tests/hooks.bats b/tests/hooks.bats new file mode 100644 index 0000000..d0323dd --- /dev/null +++ b/tests/hooks.bats @@ -0,0 +1,78 @@ +#!/usr/bin/env bats +# Tests for SessionStart hooks SDD reminder injection (SDD-001 Tier 2) + +setup() { + export DOTFILES_DIR="$BATS_TEST_DIRNAME/.." + export HOOK_PS1="$DOTFILES_DIR/scripts/claude-session-start.ps1" + export HOOK_SH="$DOTFILES_DIR/scripts/claude-session-start.sh" + # Shared reminder marker -- bytes-identical between hooks, locked here + export SDD_REMINDER_PREFIX='[sdd]' + export SDD_REMINDER_CORE='Before your first tool call, read' + export SDD_REMINDER_TARGET='AGENTS.md' +} + +@test "both hook scripts exist" { + [[ -f "$HOOK_PS1" ]] + [[ -f "$HOOK_SH" ]] +} + +# --- SDD reminder injection --- + +@test "claude-session-start.ps1 contains the [sdd] reminder marker" { + grep -qF "$SDD_REMINDER_PREFIX" "$HOOK_PS1" + grep -qF "$SDD_REMINDER_CORE" "$HOOK_PS1" + grep -qF "$SDD_REMINDER_TARGET" "$HOOK_PS1" +} + +@test "claude-session-start.sh contains the [sdd] reminder marker (parity)" { + grep -qF "$SDD_REMINDER_PREFIX" "$HOOK_SH" + grep -qF "$SDD_REMINDER_CORE" "$HOOK_SH" + grep -qF "$SDD_REMINDER_TARGET" "$HOOK_SH" +} + +@test "both hooks mention the AGENTS.md fallback path on Windows and Linux" { + grep -qF '~/Projects/dotfiles/AGENTS.md' "$HOOK_PS1" + grep -qF '~/Projects/dotfiles/AGENTS.md' "$HOOK_SH" +} + +@test "both hooks reference skip criteria (typos, comment-only, mechanical, bug<20, doc-only)" { + # Each hook must list at least the canonical skip categories so the agent + # has them inline without round-tripping to AGENTS.md. + grep -qF 'typo' "$HOOK_PS1" + grep -qF 'typo' "$HOOK_SH" + grep -qF 'mechanical' "$HOOK_PS1" + grep -qF 'mechanical' "$HOOK_SH" +} + +# --- Position invariant: reminder must be unconditional + early --- +# The reminder must NOT live inside a function or if-block that gates on +# CWD / vault / repo state. It must run every session. + +@test "claude-session-start.ps1 SDD reminder is not gated by Test-Path / if(-not...)" { + # Confirm the reminder text appears OUTSIDE the gated regions. Heuristic: + # the line containing "[sdd]" should come BEFORE the line containing + # "function Find-HiveProject" (first gated diagnostic block in the script). + sdd_line=$(grep -nF "$SDD_REMINDER_PREFIX" "$HOOK_PS1" | head -1 | cut -d: -f1) + fn_line=$(grep -nE '^function Find-HiveProject' "$HOOK_PS1" | head -1 | cut -d: -f1) + [[ -n "$sdd_line" ]] && [[ -n "$fn_line" ]] && [[ "$sdd_line" -lt "$fn_line" ]] +} + +@test "claude-session-start.sh SDD reminder appears before first conditional diagnostic" { + sdd_line=$(grep -nF "$SDD_REMINDER_PREFIX" "$HOOK_SH" | head -1 | cut -d: -f1) + # First conditional in the existing .sh is the vault detection if-block. + # Use 'find_vault_root' as the heuristic anchor (function defined later). + fn_line=$(grep -nE '^find_vault_root\(\)' "$HOOK_SH" | head -1 | cut -d: -f1) + [[ -n "$sdd_line" ]] && [[ -n "$fn_line" ]] && [[ "$sdd_line" -lt "$fn_line" ]] +} + +# --- Cross-OS parity lock --- + +@test "parity: SDD reminder core text identical between .ps1 and .sh" { + # Extract the line containing the [sdd] marker from each file, compare core text. + ps1_line=$(grep -F "$SDD_REMINDER_PREFIX" "$HOOK_PS1" | head -1) + sh_line=$(grep -F "$SDD_REMINDER_PREFIX" "$HOOK_SH" | head -1) + [[ -n "$ps1_line" ]] && [[ -n "$sh_line" ]] + # Both should contain the same anchor phrases + echo "$ps1_line" | grep -qF "$SDD_REMINDER_CORE" + echo "$sh_line" | grep -qF "$SDD_REMINDER_CORE" +}