diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3f42ae..1ed5ab8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,14 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Checkout uses: actions/checkout@v4 # --- Run the full invariant validator (platform-native) --------------- - name: Validate (bash) - if: runner.os == 'Linux' + if: runner.os == 'Linux' || runner.os == 'macOS' shell: bash run: bash scripts/validate.sh @@ -28,9 +28,21 @@ jobs: shell: pwsh run: ./scripts/validate.ps1 + # --- Hook smoke tests: pipe fixture JSON into every hook, assert exit + # codes and key output substrings (not just "did not crash") ----------- + - name: Hook smoke tests (bash) + if: runner.os == 'Linux' || runner.os == 'macOS' + shell: bash + run: bash scripts/smoke-hooks.sh + + - name: Hook smoke tests (PowerShell) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/smoke-hooks.ps1 + # --- Install -> uninstall round-trip (CLAUDE.md sandbox recipe) -------- - name: Install -> uninstall round-trip (bash) - if: runner.os == 'Linux' + if: runner.os == 'Linux' || runner.os == 'macOS' shell: bash run: | set -euo pipefail diff --git a/.gitignore b/.gitignore index ee1218c..82ef160 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ desktop.ini .claude/settings.local.json .claude/.cache/ +# ---- Local AI work docs (not shipped) ---- +.ai-docs/ + # ---- Logs ---- *.log logs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index adcc97a..64d806d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,118 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 hard approval gate. The agent (model `sonnet`, tools Read/Write/Glob/Grep, skill `sd-evidence-citation`) writes only the ADR file and never invents decisions; the command owns numbering and supersession links. Bumps command count 10 -> 11 and agent count 5 -> 6 across docs and the validators. +- `scripts/smoke-hooks.sh` + `scripts/smoke-hooks.ps1` - pipe fixture Claude Code hook JSON into + `prompt-router`, `spec-gate`, and `subagent-retro` against a temp `.specs/` tree and assert exit + codes AND key output substrings, not just "did not crash": keyword-match routing (bash and + PowerShell must agree), spec-gate allow/warn/block across in-progress / header-only-marker / + docs-edit / malformed-stdin cases, and subagent-retro naming the real spec ID then debouncing a + second run. `.github/workflows/ci.yml` adds `macos-latest` to the OS matrix (exercising the + BSD-specific `stat -f %m` / `date -j -f` fallback branches that only run there) and a smoke-test + step on every OS. + +### Changed +- Removed hardcoded MSSQL/C#/TS references from `agents/debugger.md`, `commands/perf.md`, + `commands/rca.md`, and `commands/bug.md`, per CLAUDE.md's stack-agnostic rule. `sd-debugger`'s + tool allowlist no longer bakes in `mcp__mssql__execute_sql`; its "Database discipline" section + (renamed from "MSSQL discipline") now describes the same read-only SELECT/EXPLAIN discipline + generically, deferring to whatever database MCP tool or CLI client the project provides. + `templates/project-config.template.json`'s `mcp.mssql` entry is renamed to `mcp.database`. + `perf.md`/`rca.md` generalize "MSSQL access (via MCP)" to "database access (via the project's + MCP tool or CLI)"; `perf.md`'s final-review check drops the C#/TS-specific `dynamic`/`any` + example in favor of "type-safety escapes for the project's language (as defined in + `constitution.md`)"; `bug.md`'s failing-test step now references `paths.tests` from + project-config instead of a hardcoded `tests//` with a C#-style example name. +- De-duplicated rules that were copy-pasted from skills into agent bodies and commands (CLAUDE.md: + "a rule used by multiple agents lives in one `SKILL.md`, never copy-pasted"), replacing each + copy with a reference to the owning skill: `agents/debugger.md`'s and `agents/reviewer.md`'s + Anti-patterns sections no longer restate `sd-hypothesis-tree`/`sd-severity-taxonomy`/ + `sd-evidence-citation` (role-specific bullets are kept); `agents/code-explorer.md`'s + Anti-patterns section no longer restates `sd-evidence-citation`. `commands/feature.md` and + `commands/refactor.md` no longer inline the atomic task-block format - both now point at + `sd-atomic-task-format`, which gains a documented "Refactor mode" `Parallel batch` field (the + field `refactor.md`'s inline copy had already drifted to include while `feature.md`'s copy + lacked it). `commands/bug.md` and `commands/rca.md` no longer restate the 5-mental-models / + `(Likelihood x Impact) / Cost-to-verify` method inline - both now point at `sd-hypothesis-tree`. +- `scripts/validate.sh` and `scripts/validate.ps1` now derive their expected install-target + counts (commands / agents / skills / hooks / templates) from the source tree instead of + hardcoding them as literals in both files - a new command/agent/skill/template only needs to + land in its source dir, never a constant bumped in two scripts (this already bit PR #12, which + had to bump both). Each derived count is asserted `> 0` so an empty or misnamed source dir + fails loudly instead of vacuously passing Check 5. ### Fixed +- `hooks/bash/prompt-router.sh` emitted `- /sd:0` instead of `- /sd:` under bash 3.2 + (macOS system bash): `declare -A` is a bash-4 feature, so the associative arrays silently + degraded to indexed arrays with all string subscripts arithmetic-evaluating to `0`. Caught by + the macOS CI smoke test (`validate (macos-latest)` was red since the matrix gained macOS). + Rewrote keyword matching with parallel indexed arrays; the hook is now bash-3.2 compatible. +- `hooks/powershell/subagent-retro.ps1`'s debounce silently stopped persisting/reading state on + PowerShell 7+, found by writing `scripts/smoke-hooks.ps1`: (1) `Save-State`'s + `Split-Path -LiteralPath $StatePath -Parent` throws "Parameter set cannot be resolved" on some + PS7 builds (`-LiteralPath` there has no `-Parent` parameter set) - the surrounding `try/catch` + swallowed it, so the state directory/file were never written; switched to `Split-Path -Path` + (safe here - `-Parent` does no filesystem globbing, only `-Resolve` would). (2) Even once the + state file wrote, `Test-DebounceElapsed` re-broke: PS7's `ConvertFrom-Json` auto-converts an + ISO-8601 `...Z` string to a `[datetime]` (PS 5.1 leaves it as a string), and re-`Parse`-ing an + already-converted `[datetime]` stringifies it with the local culture - dropping the UTC marker - + so `[datetimeoffset]::Parse` silently re-interpreted it as local time, skewing `$age` by the + machine's UTC offset exactly like the bug fixed earlier in this file, just triggered a different + way. Both are PowerShell-only; `hooks/bash/subagent-retro.sh` was unaffected (no bash twin + change needed). +- `install/install.sh` hardening: aligned to `set -euo pipefail` (was `set -e` only, so unset- + variable typos and mid-pipeline failures - e.g. a `sha256sum`/`shasum` error - passed silently; + those two pipelines now end `|| true` since a hash-tool failure is expected-recoverable, not a + reason to abort); added the same `--prefix` safety guard `uninstall.sh` already had (empty, + `/`, `\`, or `..` components rejected) to `install/install.ps1` too, so install and uninstall + accept the same set of prefixes on both platforms - previously only uninstall validated it, so + `--prefix ../evil` would have written outside the intended tree; quoted the unquoted + `rel="${f#$src_root/}"` strip pattern (glob-interpreted `$src_root` broke on a repo path + containing `[`, `*`, or `?`); and added an `ERR` trap that reports how many files already + landed and the exact `uninstall.sh` command to run if a copy fails mid-install (no full + transactional rollback - per-file `.bak.*` backups already protect overwritten files). +- Post-1.3.0 docs drift: `README.md`'s tagline said "Ten slash commands, five specialized + subagents" (now eleven / six); the Commands table was missing `/sd:adr` and listed + `/sd:feature` at 4 hard gates (the merged review+integration gate makes it 3); the Agents table + was missing `sd-docs-writer` and listed a hardcoded `MSSQL` tool for `sd-debugger`; the `.specs/` + tree diagram omitted `_explorations/`, `_reviews/`, `_adr/`; the Roadmap highlights repeated two + items that already shipped. `ROADMAP.md`'s `## Planned` section still listed the `/sd:setup` + codebase scan and `sd-docs-writer` agent, both shipped in 1.3.0+ (CHANGELOG is the source of + truth for shipped work). `docs/usage.md`'s Utility commands section had no `/sd:adr` entry. + `templates/project-config.template.json`'s `workflow.gates.feature` still listed the pre-merge + 4-gate sequence; collapsed to 3 and marked `_comment`-descriptive since no hook or command reads + the block. `CONTRIBUTING.md`'s agent frontmatter example omitted the mandated `color:` and + `skills:` fields. `examples/README.md` gated a promised-features list on "not in v1.0.0", three + minor versions after v1.0.0; reworded to point at `ROADMAP.md`. +- Phase 0 of `/sd:feature`, `/sd:bug`, `/sd:refactor`, `/sd:perf`, and `/sd:rca` now guards + against missing or malformed Layer-2 context instead of silently reading `CLAUDE.md`, + `.specs/constitution.md`, `.claude/project-config.json`, and `.specs/index.md` and letting + later phases fail on undefined config values. Missing `.specs/`, `.specs/constitution.md`, or + `.specs/index.md` now STOPs with "No `.specs/` found - run `/sd:setup` first." (matching + `spec.md`/`release.md`/`adr.md`); malformed `.claude/project-config.json` STOPs naming the file; + a missing `CLAUDE.md` only WARNs and continues, since the constitution (not `CLAUDE.md`) is the + binding Layer-2 contract - matching the stance the four utility commands already took. +- `sd-code-explorer`'s `impact-map` task no longer instructs the agent to APPEND to + `OUTPUT_APPEND_TO` - its tool allowlist has no `Write`/`Edit`, so it physically could not + perform that write, silently starving `03-decisions.md` (and everything downstream that reads + it as `IMPACT`). The task now returns the structured analysis as final output; the informational + `OUTPUT_TARGET` input names the file, and the calling command appends it. `commands/feature.md` + and `commands/refactor.md` each gained an explicit main-thread append step after the impact-map + invocation. `commands/perf.md`, `commands/bug.md`, and `commands/rca.md`'s equivalent + "Append ... to `03-decisions.md`" steps after `sd-debugger` invocations (also write-tool-less) + are now explicitly labeled as main-thread steps for the same reason. +- `/sd:bug`, `/sd:rca`, and `/sd:perf` now walk every state in `/sd:spec`'s + `draft -> approved -> in-progress -> done -> archived` machine instead of jumping straight from + `draft`/`approved` to `done` - a history `/sd:spec status` itself would have refused as an + illegal transition. `bug.md` sets `approved` at Gate 2 (reproduction confirmed) and + `in-progress` at the start of Phase 5 (fix implementation); its Gate 3a "abort" (hypothesis tree + exhausted) now passes through `in-progress` on its way to `done` instead of jumping directly + from `approved`. `rca.md` sets `approved` at Gate 3 (root cause confirmed) and `in-progress` at + the start of Phase 4 (isolate + document) - RCAs produce no code, so "in-progress" now means + report-writing is underway. `perf.md` sets `draft` at spec creation (previously jumped straight + to `approved` at Gate 1) and `in-progress` at the start of Phase 4 (the per-hotspot loop), + including the Gate 2 Case A shortcut (baseline already meets SLA) which now passes through + `in-progress` before `done`. `docs/troubleshooting.md`'s "Illegal status transition" entry no + longer tells users that `/sd:rca` intentionally skips straight to `done`. - `/sd:setup` now migrates `.claude/*` drift instead of exiting blind on a `complete` project. A new Phase 1.5 (drift check & migrate) runs whenever `.claude/project-config.json` or `.claude/settings.json` exists (states `complete` and `partial`) and rule-based-compares them @@ -38,6 +148,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 non-existent `~/.claude/hooks/ck/` directory after the `ck` -> `specwright` rename. - `/sd:setup` Phase 7 (and Phase 1.5) now verify every hook `command` path in `.claude/settings.json` resolves to a file on disk, warning loudly when a hook is not firing. +- `hooks/powershell/subagent-retro.ps1`, `prompt-router.ps1`, and `spec-gate.ps1` no longer assign + parsed hook JSON to `$input` - PowerShell's reserved automatic pipeline variable. Assigning to it + threw a non-terminating `ParameterBindingException` on every real (piped/redirected) stdin + invocation, leaving it unbound and causing every PowerShell hook to exit silently before reading + any input. Renamed to `$hookInput` in all three files. +- `hooks/bash/spec-gate.sh` in-progress detection now requires `in-progress` and a spec ID on the + SAME line, matching `spec-gate.ps1` and `prompt-router.sh`'s existing same-line semantics. The + previous two independent file-wide `grep`s let an `in-progress` legend/header line combine with a + spec ID on an unrelated `done` row, so bash allowed a code edit that PowerShell would warn/block + on the identical `.specs/index.md`. +- `commands/feature.md` subagent invocations now use the field names `sd-spec-architect` and + `sd-code-explorer` actually read: `TICKET_CONTEXT` (was `TICKET_DATA`), `TASK`/`SPEC`/`IMPACT` + (was `TASK_TYPE`/`SPEC_REF`/`IMPACT_REF`), a full `feature.template.md` filename (was the bare + `feature`), and both `refine` invocations now carry the required `SPEC` path. `sd-reviewer` + invocations, which legitimately use `TASK_TYPE`/`SPEC_REF` as their own contract, are unchanged. + `commands/refactor.md`'s characterization-test loop now invokes `sd-implementer` with + `TASK_DETAILS`/`SPEC_REF`/`WORKFLOW_TYPE` instead of the unrecognized `TASK_TYPE`, matching every + other implementer invocation in the repo. +- `/sd:spec validate` no longer requires `01-plan.md`/`02-tasks.md` for in-progress bug and perf + specs. Only `/sd:feature` and `/sd:refactor` produce those artifacts; `/sd:bug` and `/sd:perf` + go straight from spec to investigation/baseline artifacts, so the old "except RCA" exemption + reported FAIL on every correctly executed bug/perf spec. Also corrected the same overgeneralized + claim in `docs/usage.md`'s resume heuristic. +- `hooks/powershell/subagent-retro.ps1`'s debounce check now parses `lastReminderUtc` as UTC via + `[datetimeoffset]::Parse(...).UtcDateTime` instead of `[datetime]::Parse(...)`, which returned a + local-`Kind` value silently converted from the UTC string, skewing `$age` by the machine's UTC + offset (negative for ~UTC offset hours on UTC+N machines, wrongly suppressing reminders; always + past-debounce on UTC-N machines, never suppressing). The bash twin was already correct + (epoch seconds throughout). +- `hooks/powershell/prompt-router.ps1` now applies the built-in default keyword list PER WORKFLOW + when the loaded `.claude/project-config.json` has no list (or an empty list) for that workflow, + matching `prompt-router.sh`'s per-workflow fallback. Previously PS only fell back to defaults + when the config file itself was absent/unparseable, then silently skipped any workflow whose + list was `$null` once a config file existed - so a valid config that simply omitted + `workflow.keywords` (or one workflow's entry) lost keyword routing hints on Windows while bash + kept emitting them from defaults on Linux/macOS. The five built-in keyword lists are unchanged, + just reused instead of duplicated. +- Agent frontmatter `tools:` allowlists and body instructions in `agents/code-explorer.md`, + `agents/debugger.md`, `agents/reviewer.md`, `agents/implementer.md`, `agents/spec-architect.md`, + `commands/explore.md`, and `skills/sd-evidence-citation/SKILL.md` referenced MCP tool names that + no longer exist on the live servers (`mcp__gitnexus__search`/`get_file`/`find_references`/ + `get_call_graph`/`list_symbols`, `mcp__context7__get-library-docs`, `mcp__tavily__search`), + so every "verify via MCP" instruction pointed at a dead tool. Remapped to the current GitNexus + surface (`query`, `context`, `impact`, `list_repos`) and renamed `context7`/`tavily` tools to + their current names (`query-docs`, `tavily_search`), keeping each agent's frontmatter allowlist + and body usage in parity. +- `hooks/bash/prompt-router.sh`'s per-workflow keyword lookup silently dropped every keyword but + the last in a workflow's list when `.claude/project-config.json` defined `workflow.keywords`: + some `jq` builds (observed with a Windows `jq.exe`) emit CRLF line endings for `join("\n")` + output even from an LF-only input, so `while IFS= read -r kw` left a trailing `\r` on every + keyword but the final one, and `[[ "$prompt_lower" == *"$kw_lower"* ]]` never matched a + CR-suffixed keyword. Found by piping real prompts through the hook against a live project's + config (not the smoke-test fixture, which omitted `workflow.keywords` and only ever exercised + the hardcoded default-list fallback). Fixed by stripping a trailing `\r` off each line read from + the list; also added a `workflow.keywords` block to both `scripts/smoke-hooks.sh` and + `scripts/smoke-hooks.ps1` fixtures so the `jq`/config-driven path is exercised going forward. + `hooks/powershell/prompt-router.ps1` was unaffected (native `ConvertFrom-Json`, no `jq`). --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ad1363..7111a69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,15 +126,19 @@ A subagent is a markdown file with YAML frontmatter consumed by the `Task` tool. ```yaml --- name: sd- +color: # e.g. cyan, orange, purple, green, blue - used for display only description: One-line summary used by routing. model: sonnet # MUST be an alias: sonnet | haiku | opus | inherit tools: Read, Grep, Glob, ... # MINIMAL allowlist +skills: + - sd- # any skill this agent's body references; see Skills below --- ``` **Critical:** - `model:` MUST be an alias. Full IDs like `claude-sonnet-4-7` are not portable and may not even exist. The alias `sonnet` auto-resolves to the latest Sonnet. - `tools:` should be the minimum set the agent needs. Read-only agents do not get `Write`. Implementer does not get `WebSearch`. +- `skills:` must list every skill the agent body references (`**skill-name**` in prose). A rule used by multiple agents lives in one `SKILL.md`, never copy-pasted into agent bodies. - Agent must read `CLAUDE.md` and `constitution.md` at runtime. No hardcoded stack assumptions (no `cs`, `csproj`, `dotnet`, etc. literal references unless they come from project config). - Every finding cites `file:line`. No prose without citations. diff --git a/README.md b/README.md index 65225af..305785b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # specwright > **Spec-driven development workflows for Claude Code.** -> Ten slash commands, five specialized subagents, three guard-rail hooks, nine templates, six reusable skills - all under the `sd:` namespace, stack-agnostic, cross-platform, and ready to drop into any project. +> Eleven slash commands, six specialized subagents, three guard-rail hooks, nine templates, six reusable skills - all under the `sd:` namespace, stack-agnostic, cross-platform, and ready to drop into any project. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Claude Code](https://img.shields.io/badge/Claude%20Code-compatible-blue)](https://docs.claude.com/en/docs/claude-code) @@ -88,7 +88,7 @@ Per-project artifacts (`.specs/`, `.claude/`, project `CLAUDE.md`) remain untouc | Command | Type | Hard gates | Purpose | |---|---|---|---| -| `/sd:feature ` | Workflow | 4 | Spec-driven feature: spec -> impact -> plan -> execute -> review -> close | +| `/sd:feature ` | Workflow | 3 | Spec-driven feature: spec -> impact -> plan -> execute -> batch review -> close | | `/sd:bug ` | Workflow | 5 | Root-cause-first fix: capture -> reproduce -> investigate -> failing test -> minimal fix -> regression | | `/sd:rca ` | Workflow | 3 | Incident analysis. **Output is the spec - no code change.** | | `/sd:refactor ` | Workflow | 6 | Coverage-gated restructure: requires >=80% coverage before touching code | @@ -98,6 +98,7 @@ Per-project artifacts (`.specs/`, `.claude/`, project `CLAUDE.md`) remain untouc | `/sd:review [path / "recent" / "spec ID"]` | Utility | - | Standalone constitution-compliance review with severity tags | | `/sd:setup` | Utility | - | Idempotent project scaffold (interactive) | | `/sd:release [version]` | Utility | 1 | Release notes from `done` specs -> Keep-a-Changelog sections, then archive them | +| `/sd:adr ` | Utility | 1 | Author an ADR from a spec's decisions under `.specs/_adr/` | --- @@ -107,9 +108,10 @@ Per-project artifacts (`.specs/`, `.claude/`, project `CLAUDE.md`) remain untouc |---|---|---|---| | `sd-spec-architect` | sonnet | Read, Write, Edit, Grep, Glob, Atlassian MCP, Context7 MCP | Create / refine specs, plans, and tasks. Constitution-aware. | | `sd-code-explorer` | haiku | Read, Grep, Glob, GitNexus MCP | Read-only navigation. Every finding cites `file:line`. | -| `sd-debugger` | sonnet | Read, Grep, Glob, Bash, sequential-thinking, GitNexus, MSSQL (SELECT only), Tavily, Context7 | Hypothesis-tree investigation. Distinguishes proximate vs root cause. | +| `sd-debugger` | sonnet | Read, Grep, Glob, Bash, sequential-thinking, GitNexus, Tavily, Context7 | Hypothesis-tree investigation. Distinguishes proximate vs root cause. | | `sd-implementer` | haiku | Read, Write, Edit, MultiEdit, Grep, Glob, Bash, Context7 | Executes ONE atomic task. Scope-disciplined, no opportunism. | | `sd-reviewer` | sonnet | Read, Grep, Glob, sequential-thinking, GitNexus | Severity-tagged review: BLOCK / WARN / SUGGEST / PASS. | +| `sd-docs-writer` | sonnet | Read, Write, Glob, Grep | Authors one MADR-style ADR from a spec's decisions. Writes only the ADR file. | All models use **portable aliases** (`sonnet`, `haiku`) so they auto-update. @@ -154,6 +156,9 @@ Every project that adopts `specwright` ends up with: .specs/ constitution.md # Architectural rules + conventions + quality bars index.md # Registry of all specs with lifecycle states + _explorations/ # Scratchpad for /sd:explore saves + _reviews/ # Scratchpad for /sd:review saves + _adr/ # Architecture decision records from /sd:adr FEAT-INV-2501/ # One folder per spec 00-spec.md # Why / What / Success criteria / Constitution check 01-plan.md # Implementation plan @@ -257,8 +262,7 @@ Configure per project in `.claude/project-config.json` under the `mcp` section. Forward-looking work lives in [`ROADMAP.md`](ROADMAP.md). Highlights: - **Near-term** - GitHub Issue auto-fetch (`gh issue view`) to match the existing JIRA snapshot path. -- **Planned** - `/sd:setup` codebase scan (detected defaults instead of `<>`s) and an - optional `sd-docs-writer` agent for ADRs. +- **Planned** - nothing queued right now. - **Exploratory** - local-only, opt-in usage analytics. Shipped work is in [`CHANGELOG.md`](CHANGELOG.md). diff --git a/REVIEW-TODO.md b/REVIEW-TODO.md new file mode 100644 index 0000000..d7c9298 --- /dev/null +++ b/REVIEW-TODO.md @@ -0,0 +1,45 @@ +# Review findings to fix (from 2026-07-03 deep review) - delete this file when done + +Branch: hotfix/ps-hooks-audit. CI is green; these are defects found in the branch's own new code. + +## Must fix before PR + +1. `install/install.sh:93` - guard validates PREFIX but not BASE_PATH. + - `--base-path ""` accepted -> install plan targets filesystem root (`/commands/sd` ...). + - Also `install.sh:74`: `--base-path` with NO value -> `shift 2` fails under `set -e`, dies exit 1 with zero output. + - Fix: reject empty/whitespace BASE_PATH; print usage on missing flag value. Mirror in install.ps1. + +2. `install/install.sh:204` - partial-install ERR trap never fires. + - `trap on_error ERR` without `set -E` does not fire inside functions; all copy work is in copy_one(). + - Fix: change line 16 to `set -Eeuo pipefail` (or trap EXIT + exit-code check). + - Also: install.ps1 has NO equivalent partial-install guard at all - add one (pairs rule). + +3. `hooks/bash/subagent-retro.sh:~166` - UTC debounce fix landed only in the PS twin. + - macOS fallback `date -j -f '%Y-%m-%dT%H:%M:%S'` parses saved UTC timestamp as LOCAL time. + - On UTC+7 Mac: debounce always elapsed -> retro-reminder spam; smoke test fails locally (CI green only because runners are UTC). + - Fix: parse as UTC (e.g. append `TZ=UTC0` / use `-u`), mirror the ps1 fix (hooks ship in pairs). + +4. `commands/rca.md:78` - Phase 2 step 3 still passive: "Hypothesis tree written to 00-spec.md". + - Debugger has no Write tool -> tree never persisted, Gate 2 empty. + - Fix: reword to "Main thread appends the returned hypothesis tree ..." (match bug.md:111 / perf.md / rca.md Phase 3). + +## Judgment calls (fix or file follow-up issues) + +5. `agents/debugger.md:6` + body line ~46 - body prescribes "project-provided database MCP tool" + but frontmatter allowlist has no DB tool -> path unreachable. Also docs/architecture.md:92,309 + still documents removed mcp__mssql__execute_sql. + +6. `scripts/validate.sh:26` + `scripts/validate.ps1:39` - counts derived from source tree are + self-referential: a deleted/renamed asset moves expected+actual in lockstep, CI stays green. + Consider minimum-count floor or manifest. + +## Cleanups (fast-follow OK) + +7. `scripts/smoke-hooks.sh:1` - add `set -euo pipefail` (repo bash rule); run_hook line 96 needs + `CODE=0; ... || CODE=$?` to stay set-e-safe. +8. `scripts/smoke-hooks.sh:106` - add jq preflight: `command -v jq || { echo 'jq required'; exit 1; }` + (hooks exit 0 silently without jq -> assertions blame the hooks). +9. `commands/bug.md:42` (+ feature.md:32, perf.md:41, rca.md:30, refactor.md:44) - Phase 0 bootstrap + guard copy-pasted 5x, already drifted in feature.md -> dedupe into a shared skill/rule pack. +10. `install/install.sh:94` - prefix emptiness check `${PREFIX// /}` strips spaces only; use + `${PREFIX//[[:space:]]/}` to match install.ps1's IsNullOrWhiteSpace. diff --git a/ROADMAP.md b/ROADMAP.md index c458e0f..bcf6eab 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,13 +19,8 @@ Small, well-scoped items targeted at the next minor release. Larger items that each warrant a full `/sd:feature` spec before building. -- **`/sd:setup` codebase scan** - pre-fill `constitution.md` (architectural rules, conventions) and - `CLAUDE.md` from sampled source files instead of leaving `<>`s, with an optional - `paths.layers` map in `project-config.json`. Turns first-run setup from blank-template into - detected-defaults. -- **`sd-docs-writer` agent** - an optional subagent that authors Architecture Decision Records (ADRs) - and architecture docs from spec artifacts, promoting durable `.specs/` decisions into human-facing - documentation. +_Nothing queued right now - the last two Planned items (`/sd:setup` codebase scan, `sd-docs-writer` +agent) shipped; see [`CHANGELOG.md`](CHANGELOG.md)._ ## Exploratory diff --git a/agents/code-explorer.md b/agents/code-explorer.md index 3894f6d..ad2d7da 100644 --- a/agents/code-explorer.md +++ b/agents/code-explorer.md @@ -3,7 +3,7 @@ name: sd-code-explorer color: cyan description: Read-only code navigation. Six task types covering definition, callers, traces, impact mapping, pattern search, and structural overview. Every finding cites file:line. Use this agent for any read-only exploration; do NOT invoke for fixes or refactors. model: haiku -tools: Read, Grep, Glob, mcp__gitnexus__search, mcp__gitnexus__get_file, mcp__gitnexus__find_references, mcp__gitnexus__get_call_graph, mcp__gitnexus__list_symbols +tools: Read, Grep, Glob, mcp__gitnexus__query, mcp__gitnexus__context, mcp__gitnexus__impact, mcp__gitnexus__list_repos skills: - sd-evidence-citation --- @@ -17,7 +17,7 @@ You are the code explorer for specwright. You navigate codebases and report find 1. **Read `CLAUDE.md`** for stack hints (file extensions, layer names, conventions). 2. Read the `TASK` field. It selects which workflow you run. 3. Check `GITNEXUS_AVAILABLE` (passed by the caller from `project-config.mcp.gitnexus.enabled`): - - `true` -> GitNexus-first. Test with a cheap call (e.g. `mcp__gitnexus__list_symbols` on a known small file). If it fails, fall back to grep with a noted caveat. + - `true` -> GitNexus-first. Test with a cheap call (e.g. `mcp__gitnexus__list_repos`). If it fails, fall back to grep with a noted caveat. - `false` -> grep / Glob only. Add to your output: "GitNexus disabled - transitive callers and call graphs may be incomplete." --- @@ -32,14 +32,17 @@ Route internally based on `DETECTED_INTENT`. Use the matching sub-routine below. ### `TASK = impact-map` -Inputs: `SPEC` (path to `00-spec.md`), `OUTPUT_APPEND_TO` (typically `03-decisions.md`). +Inputs: `SPEC` (path to `00-spec.md`), `OUTPUT_TARGET` (informational - typically `03-decisions.md`; +identifies which file the caller will append your output to). Behavior: 1. Read the spec. Identify the target: feature scope, bug-affected components, refactor primary file(s), or perf hotspot endpoint. -2. Produce structured analysis (sections below). APPEND to `OUTPUT_APPEND_TO`. Do not overwrite. -3. For "Precedents & conventions": derive conventions by sampling, never by stack assumption - `Glob` the target directory, then `Read` the top ~30 lines (or `mcp__gitnexus__list_symbols`) of at most 3 sibling files, and state the observed pattern with evidence. +2. Produce the structured analysis (sections below) as your final output. Do not attempt to write + files - your tool allowlist has no `Write`/`Edit` by design. The calling command appends your + returned analysis to `OUTPUT_TARGET`. +3. For "Precedents & conventions": derive conventions by sampling, never by stack assumption - `Glob` the target directory, then `Read` the top ~30 lines (or `mcp__gitnexus__query` with a goal naming the directory) of at most 3 sibling files, and state the observed pattern with evidence. -Structure of appended content: +Structure of the returned analysis (the caller appends this verbatim): ```markdown ## Impact analysis (sd-code-explorer) @@ -91,7 +94,7 @@ Structure of appended content: Inputs: `SYMBOL` or `QUERY`. -GitNexus-first: `mcp__gitnexus__find_references` with the symbol. Fall back: `Grep` for invocation patterns (`SymbolName(`, `\.SymbolName\(`). +GitNexus-first: `mcp__gitnexus__impact` with `target: SYMBOL`, `direction: upstream`. Fall back: `Grep` for invocation patterns (`SymbolName(`, `\.SymbolName\(`). Output: list of `file:line` with the calling context (one line of code). @@ -99,7 +102,7 @@ Output: list of `file:line` with the calling context (one line of code). Inputs: `SYMBOL` or `QUERY`. -GitNexus-first: `mcp__gitnexus__list_symbols` filtered by name, then `mcp__gitnexus__get_file` for context. Fall back: `Grep` for definition markers (e.g. `class SymbolName`, `def SymbolName`, `function SymbolName`, `interface SymbolName`). +GitNexus-first: `mcp__gitnexus__context` with the symbol name (pass `file_path` to disambiguate if multiple candidates are returned). Fall back: `Grep` for definition markers (e.g. `class SymbolName`, `def SymbolName`, `function SymbolName`, `interface SymbolName`). Output: `file:line` + 5-line snippet showing the definition. @@ -107,7 +110,7 @@ Output: `file:line` + 5-line snippet showing the definition. Inputs: `ENTRY_POINT` (symbol or `file:line`), optional `DEPTH` (default 2). -GitNexus-first: `mcp__gitnexus__get_call_graph` with the entry point and depth. Fall back: recursive `Grep` for callers up to `DEPTH` hops (note: imprecise for dynamic dispatch). +GitNexus-first: `mcp__gitnexus__impact` with `target: ENTRY_POINT`, `direction: downstream`, `maxDepth: DEPTH`. Fall back: recursive `Grep` for callers up to `DEPTH` hops (note: imprecise for dynamic dispatch). Output: indented tree with `file:line` at each node. @@ -123,7 +126,7 @@ Output: grouped by file when >5 hits in one file. Limit total to 50 results; tel Inputs: `PATH` (directory) or none (project root). -Use `Glob` to list files, `mcp__gitnexus__list_symbols` per file (or top-of-file `Read` for the first 30 lines). +Use `Glob` to list files, `mcp__gitnexus__query` (goal naming the directory) for a symbol overview (or top-of-file `Read` for the first 30 lines). Output: tree of directories + files + top-level symbols per file. @@ -141,10 +144,11 @@ Additional rules: ## Anti-patterns (do NOT do these) +Apply the **sd-evidence-citation** skill's Anti-patterns section in full (no citation = invalid, +trusting memory over a live grep, vendored/generated directories without explicit request). + +Explorer-specific, not covered by the skill: - **Suggesting fixes.** You report. The reviewer or implementer decides what to do. - **Opining on code quality.** "This is poorly structured" is not a finding. "Class `Foo` has 12 callers across 3 layers" is a finding. - **Modifying files.** Your tool allowlist excludes `Write` / `Edit` / `MultiEdit` precisely for this reason. -- **Producing prose without citations.** "I noticed that..." with no `file:line` is invalid output. Re-prompt yourself. -- **Trusting your memory over the grep.** If you "recall" that a class lives in `src/Foo.cs`, that recall is stale by default - verify with `Glob` or `Read`. - **Burning tool calls when a single grep suffices.** Haiku model = cost-aware. Plan the cheapest sequence that answers the question. 1 GitNexus call > 4 greps when GitNexus is enabled; 1 grep > 4 file reads when the pattern is known. -- **Following call graphs into vendored / generated code** (e.g. `node_modules/`, `bin/`, `obj/`) unless the caller explicitly asks. Filter those out. diff --git a/agents/debugger.md b/agents/debugger.md index 18d22c1..c77ee78 100644 --- a/agents/debugger.md +++ b/agents/debugger.md @@ -3,7 +3,7 @@ name: sd-debugger color: orange description: Hypothesis-tree investigation. Enumerates ranked hypotheses, verifies them with evidence, and identifies performance hotspots. Distinguishes proximate cause from root cause. Use this agent for bug investigation, RCA hypothesis work, and perf hotspot analysis. model: sonnet -tools: Read, Grep, Glob, Bash, mcp__sequential-thinking__sequentialthinking, mcp__gitnexus__search, mcp__gitnexus__get_file, mcp__gitnexus__find_references, mcp__gitnexus__get_call_graph, mcp__gitnexus__list_symbols, mcp__mssql__execute_sql, mcp__tavily__search, mcp__context7__get-library-docs +tools: Read, Grep, Glob, Bash, mcp__sequential-thinking__sequentialthinking, mcp__gitnexus__context, mcp__gitnexus__impact, mcp__tavily__tavily_search, mcp__context7__query-docs skills: - sd-hypothesis-tree - sd-evidence-citation @@ -39,10 +39,13 @@ Inputs: `HYPOTHESIS` (one entry from the tree), `EVIDENCE_DIR` (for saving artif Goal: produce a CONFIRMED / REJECTED / INCONCLUSIVE verdict for ONE hypothesis, following the **sd-hypothesis-tree** skill's verdict format and proximate-vs-root ladder. Evidence discipline follows the **sd-evidence-citation** skill: -- Source code: `file:line` + ≤5-line snippet. +- Source code: `file:line` + ≤5-line snippet. For symbol relations (callers, blast radius), use + `mcp__gitnexus__context` (single symbol) or `mcp__gitnexus__impact` (upstream/downstream) if + GitNexus is available; otherwise `Grep`/`Read`. - Logs: save to `EVIDENCE_DIR/-logs.txt`. -- DB: `mcp__mssql__execute_sql` (SELECT / EXPLAIN only) → `EVIDENCE_DIR/-db.txt`. -- Library: `mcp__context7__get-library-docs`. Web: `mcp__tavily__search`. +- DB: a project-provided database MCP tool, or a read-only CLI client via `Bash` (SELECT / EXPLAIN + equivalents only) → `EVIDENCE_DIR/-db.txt`. See "Database discipline" below. +- Library: `mcp__context7__query-docs`. Web: `mcp__tavily__tavily_search`. --- @@ -54,9 +57,10 @@ Inputs: `SPEC_REF`, `SUB_MODE` (`A` or `B`), `BASELINE_ARTIFACT` (mode A) or `HO Goal: rank hotspots from profile data, query plans, or code reads (80/20). -1. Read the baseline artifact. If it has a profile (e.g. trace, flame graph, BenchmarkDotNet output), parse it. +1. Read the baseline artifact. If it has a profile (e.g. trace, flame graph, benchmark harness output), parse it. 2. If no profile - infer from code: hot loops, N+1 queries (grep ORM patterns), unbatched I/O, sync-over-async, missing indexes (read schema if available). -3. For database hotspots: `mcp__mssql__execute_sql` with `EXPLAIN`/`SET STATISTICS` style read-only queries. Save plans to artifacts. +3. For database hotspots: use a project-provided database MCP tool if one exists, or a read-only + CLI client via `Bash`, with `EXPLAIN`/query-plan style read-only queries. Save plans to artifacts. Output: ranked list with file:line, contribution percentage estimate, evidence. @@ -87,11 +91,17 @@ For each candidate: --- -## MSSQL discipline +## Database discipline -You have `mcp__mssql__execute_sql`. **READ-ONLY ONLY**: -- Allowed: `SELECT`, `EXPLAIN`, `SHOWPLAN`, `SET STATISTICS`, `sp_helptext`, `sp_help`, system catalog reads (`sys.*`, `INFORMATION_SCHEMA.*`). -- **Forbidden**: `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `TRUNCATE`, `DROP`, `ALTER`, `CREATE`, `EXEC` of unknown procedures, anything mutating. +Do not assume any specific database MCP tool exists - this agent ships to arbitrary stacks. If +the project provides one (see project-config / the project's `CLAUDE.md`), or you reach the +database via a read-only CLI client through `Bash`, queries are **READ-ONLY ONLY**: +- Allowed: `SELECT` / read-only equivalents, query-plan commands (e.g. `EXPLAIN`, `SHOWPLAN`, + `SET STATISTICS` or the project database's equivalent), read-only schema/catalog introspection. +- **Forbidden**: `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `TRUNCATE`, `DROP`, `ALTER`, `CREATE`, `EXEC` + of unknown procedures, anything mutating. +- If no database access mechanism is available, say so and mark DB-dependent evidence + unavailable rather than inventing a tool. If your verification requires mutation (e.g. "I need to add an index to test the perf hypothesis") -> STOP and surface to the main thread: "Mutation required - cannot proceed under debugger constraints." @@ -101,10 +111,12 @@ A violation here is a constitution violation, not a slip-up. ## Anti-patterns (do NOT do these) -- **Stopping at proximate cause.** "NRE on line 142" is a symptom of a state assumption. Keep asking why. -- **One hypothesis only.** Enumerate at least 4. Single-hypothesis tunnel vision is how bugs ship deeper. -- **Skipping REJECTED reasoning.** Rejected hypotheses are KNOWLEDGE. Future-you (or future-other-engineer) needs to see why H2 was rejected so they don't re-investigate it. -- **Inventing evidence.** Every claim cites a `file:line`, log line, query result, or doc URL. If you "remember" that a library does X, look it up via `mcp__context7__get-library-docs`. +Apply the **sd-hypothesis-tree** skill's Anti-patterns section in full: stopping at proximate +cause, single-hypothesis tunnel vision, skipping REJECTED reasoning, inventing evidence, and +acting on a CONFIRMED hypothesis instead of reporting it. Apply the **sd-evidence-citation** +skill's Anti-patterns for citation discipline. If you "remember" that a library does X, that is +inventing evidence - look it up via `mcp__context7__query-docs` instead. + +Debugger-specific, not covered by either skill: - **Mutating database state** to test a hypothesis. Read-only is hard rule. - **Confusing perf hypothesis with bug hypothesis.** Perf mode B asks for 2-4 OPTIONS; bug mode produces a tree to verify until one is CONFIRMED. Different shape. -- **Acting on a CONFIRMED hypothesis.** You report; the workflow's `/sd:bug` Phase 5 calls the implementer for the fix. You do not write fixes. diff --git a/agents/implementer.md b/agents/implementer.md index e2f7eb1..f7fccfd 100644 --- a/agents/implementer.md +++ b/agents/implementer.md @@ -3,7 +3,7 @@ name: sd-implementer color: green description: Executes ONE atomic task per invocation. Scope-disciplined - edits only files declared in TASK_DETAILS.Files. Workflow-specific constraints for feature/bug/refactor/perf. Main thread can override to sonnet model for complex tasks. model: haiku -tools: Read, Write, Edit, MultiEdit, Grep, Glob, Bash, mcp__context7__resolve-library-id, mcp__context7__get-library-docs +tools: Read, Write, Edit, MultiEdit, Grep, Glob, Bash, mcp__context7__resolve-library-id, mcp__context7__query-docs skills: - sd-atomic-task-format - sd-pattern-discipline @@ -122,7 +122,7 @@ Do this in your head (or via sequential-thinking if complex). Do not produce a p - Use `Edit` for surgical changes (preferred for single-region edits). - Use `MultiEdit` for multiple non-overlapping changes in the same file. - Use `Write` only for new files declared in `Files`. -- For each library you import or use, if unfamiliar with current API: `mcp__context7__get-library-docs` to verify. Stale training data on library APIs is a real failure mode. +- For each library you import or use, if unfamiliar with current API: `mcp__context7__resolve-library-id` then `mcp__context7__query-docs` to verify. Stale training data on library APIs is a real failure mode. ### 5. Verify - After every `Edit` / `Write`: `Read` the file again to confirm the change took. @@ -153,7 +153,7 @@ Return to main thread with: file list edited, test results, one-line summary. - **Skipping the post-edit `Read`.** Edit tools can fail silently in rare cases (whitespace mismatch, etc.). Verify by reading. - **Writing tests that pass by being lenient.** Tests must FAIL FIRST (for bugs), or assert ACCEPTANCE concretely (for features). `Assert.True(true)` is malpractice. - **Inventing layer names** that aren't in the constitution. -- **Using `dynamic` (C#) / `any` (TS)** to satisfy a type mismatch instead of solving it correctly. -- **Importing from libraries based on training-data memory.** APIs change. If the import / call is non-trivial, verify with `mcp__context7__get-library-docs`. +- **Using a type-safety escape** (the project language's equivalent of `dynamic`/`any`) to satisfy a type mismatch instead of solving it correctly. +- **Importing from libraries based on training-data memory.** APIs change. If the import / call is non-trivial, verify with `mcp__context7__query-docs`. - **Catching `Exception` to swallow errors.** Constitution §2.3 forbids this in most projects; verify and respect. - **Producing the diff in your response.** Edits happen via tools. Your response is a summary. diff --git a/agents/reviewer.md b/agents/reviewer.md index 75e73d1..15d917a 100644 --- a/agents/reviewer.md +++ b/agents/reviewer.md @@ -3,7 +3,7 @@ name: sd-reviewer color: purple description: Severity-tagged compliance review. Five task types covering per-task, holistic, standalone, bug-fix-final, and perf-final review. Every finding cites file:line and a constitution §section. Never auto-fixes, never prescribes exact code. model: sonnet -tools: Read, Grep, Glob, mcp__sequential-thinking__sequentialthinking, mcp__gitnexus__search, mcp__gitnexus__find_references +tools: Read, Grep, Glob, mcp__sequential-thinking__sequentialthinking, mcp__gitnexus__impact skills: - sd-severity-taxonomy - sd-evidence-citation @@ -108,7 +108,7 @@ Checklist: ## How to find things - Use `Grep` and `Glob` to enumerate code-smell patterns (e.g. forbidden `dynamic` keyword, `catch (Exception)`, `// TODO`). -- Use `mcp__gitnexus__find_references` to check public API impact for refactor reviews. +- Use `mcp__gitnexus__impact` (`direction: upstream`) to check public API impact for refactor reviews. - Use `mcp__sequential-thinking__sequentialthinking` for complex holistic reviews where you need to trace invariants across many files. If GitNexus is unavailable, do API-impact analysis via `Grep` with caveat: "GitNexus unavailable - public API impact verified via grep; dynamic dispatch may be undercounted." @@ -117,12 +117,13 @@ If GitNexus is unavailable, do API-impact analysis via `Grep` with caveat: "GitN ## Anti-patterns (do NOT do these) +Apply the **sd-severity-taxonomy** skill's Anti-patterns section in full (conflating SUGGEST with +WARN, flagging style preferences as BLOCK, issuing BLOCK/WARN without a `§N.M` or spec-acceptance +anchor). Apply the **sd-evidence-citation** skill's Anti-patterns for citation discipline. + +Reviewer-specific, not covered by either skill: - **Auto-fixing.** You have no write tools. If you find yourself wanting to "just patch it" - your tool allowlist correctly prevents that. Surface as a finding instead. - **Prescribing exact fix code.** "Change line 84 to `return result.Where(x => x.Id != null)`" is too prescriptive. "Filter out null IDs at the boundary" is the right shape - leaves the implementer to choose how. -- **Conflating SUGGEST with WARN.** They serve different functions in the workflow: SUGGEST gets logged; WARN gets a user decision; BLOCK gets fixed. -- **Flagging style preferences as BLOCK.** Unless the constitution explicitly mandates the style, it's WARN at most. BLOCK is reserved for constitution violations, broken behavior, security issues. -- **Missing the constitution citation.** Every BLOCK / WARN should reference `§N.M` (or a spec acceptance criterion) to anchor the severity. Findings without anchors are unreliable. - **Reviewing the diff in isolation.** Read the surrounding context. A line that looks fine may violate a layer rule that's only visible from imports / project boundaries. -- **Producing findings without `file:line`.** No citation = no finding. Re-prompt yourself. - **Re-reviewing the spec itself.** The spec architect handled that. Your job is code vs spec. - **Being verbose.** Each finding is one paragraph. Reviewer reports are scanned, not read. diff --git a/agents/spec-architect.md b/agents/spec-architect.md index f1eb813..3a5323e 100644 --- a/agents/spec-architect.md +++ b/agents/spec-architect.md @@ -3,7 +3,7 @@ name: sd-spec-architect color: blue description: Creates, refines, and plans specs across all 5 workflow types (feature, bug, refactor, perf, rca). Reads CLAUDE.md and constitution.md at runtime. Use this agent for any spec authoring or atomic-task planning. model: sonnet -tools: Read, Write, Edit, Grep, Glob, mcp__atlassian__getJiraIssue, mcp__atlassian__searchJiraIssuesUsingJql, mcp__atlassian__getJiraIssueRemoteIssueLinks, mcp__atlassian__getConfluencePage, mcp__context7__resolve-library-id, mcp__context7__get-library-docs +tools: Read, Write, Edit, Grep, Glob, mcp__atlassian__getJiraIssue, mcp__atlassian__searchJiraIssuesUsingJql, mcp__atlassian__getJiraIssueRemoteIssueLinks, mcp__atlassian__getConfluencePage, mcp__context7__resolve-library-id, mcp__context7__query-docs skills: - sd-atomic-task-format - sd-spec-templates @@ -72,6 +72,9 @@ For every task that creates a new file or introduces a new public symbol: 2. If `IMPACT` lacks a suitable precedent, run your own discovery (max 2 Glob/Grep rounds) per the **sd-pattern-discipline** skill. 3. Fill the task's `Pattern refs` field: 1-3 `file:line` refs, each with a one-line instruction of what to mirror. 4. `none` only for tasks that exclusively edit existing files. +5. If a task's `Acceptance` depends on an unfamiliar library API, verify current syntax via + `mcp__context7__resolve-library-id` + `mcp__context7__query-docs` before writing the criterion - + stale training data on library APIs is a real failure mode (same rule the implementer follows). --- @@ -141,7 +144,7 @@ For every spec you produce, in the "Constitution check" section: ## Anti-patterns (do NOT do these) -- **Hardcoding stack assumptions**. If you write `dotnet test` when the project is Node, you have failed. Read CLAUDE.md every invocation - your prior knowledge of the project is stale by default. +- **Hardcoding stack assumptions**. If you write a build/test command from a prior invocation instead of reading this project's `commands.test` (via `CLAUDE.md`), you have failed. Read CLAUDE.md every invocation - your prior knowledge of the project is stale by default. - **Skipping the template structure**. The template is the contract. If you "improve" it by reordering sections, downstream agents that key off section headers break. - **Filling cross-phase fields prematurely**. Bug's Root cause is empty for a reason. Perf's Results log is empty for a reason. - **Inventing task structure**. The 9 required fields in the task format are required, not suggested. diff --git a/commands/bug.md b/commands/bug.md index 953d1c3..005e55b 100644 --- a/commands/bug.md +++ b/commands/bug.md @@ -39,9 +39,15 @@ These rules override any user pressure to "just patch it". ## Phase 0 - Bootstrap -1. Read `CLAUDE.md`, `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. -2. If `ticket.system == "jira"` and `` matches `ticket.pattern`, fetch ticket via Atlassian MCP. -3. Detect state. Print one-line resume plan. +1. Read `CLAUDE.md`. If missing, WARN and continue - print "No `CLAUDE.md` found; stack + conventions may be incomplete." (the constitution is the binding Layer-2 contract, not + `CLAUDE.md`). +2. Read `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. If `.specs/` + or any of these is missing, STOP: "No `.specs/` found - run `/sd:setup` first." If + `.claude/project-config.json` is present but fails to parse as JSON, STOP: + "`.claude/project-config.json` failed to parse - fix it or re-run `/sd:setup`." +3. If `ticket.system == "jira"` and `` matches `ticket.pattern`, fetch ticket via Atlassian MCP. +4. Detect state. Print one-line resume plan. --- @@ -86,7 +92,7 @@ STOP. This gate is HARD - no overrides. Ask: > Is reproduction confirmed? (yes - I can trigger it / partial - intermittent / no - cannot reproduce) -- `yes` -> proceed. +- `yes` -> status=`approved`, proceed. - `partial` -> ask user if they accept investigating with partial repro (logs / traces only). Log the decision and risks to retro. - `no` -> **REFUSE to proceed**. Tell the user: investigation without reproduction risks fixing the wrong thing. Options: gather more telemetry, add observability, or close as "cannot reproduce". @@ -101,12 +107,12 @@ If the user insists on proceeding without repro, log a constitution exception to - `SPEC_REF = .specs/BUG-/00-spec.md` - `REPRODUCTION = ` - `EVIDENCE_DIR = .specs/BUG-/04-artifacts/` -2. Debugger uses sequential-thinking + 5 mental models (boundary / state / concurrency / recent-changes / environment) to enumerate 4-8 hypotheses, ranked by `(Likelihood x Impact) / Cost-to-verify`. -3. Append hypothesis tree to `.specs/BUG-/03-decisions.md`. +2. Debugger enumerates hypotheses per the **sd-hypothesis-tree** skill (5 mental models, `(Likelihood x Impact) / Cost-to-verify` ranking). +3. Main thread appends the returned hypothesis tree to `.specs/BUG-/03-decisions.md` (debugger has no write tool). 4. Loop: - Invoke `sd-debugger` with `TASK = verify`, `HYPOTHESIS = `. - Result: CONFIRMED / REJECTED / INCONCLUSIVE. - - Append result with evidence pointers (file:line, log lines, query results) to `03-decisions.md`. + - Main thread appends the result with evidence pointers (file:line, log lines, query results) to `03-decisions.md`. - Document REJECTED hypotheses with FULL reasoning - this is knowledge preservation for future similar bugs. - Terminate the loop when EITHER one hypothesis is CONFIRMED (proceed to Gate 3) OR every enumerated hypothesis is exhausted - all REJECTED, or only INCONCLUSIVE ones remain with no new evidence to act @@ -117,8 +123,9 @@ If the user insists on proceeding without repro, log a constitution exception to Reached ONLY when the loop ends with no CONFIRMED hypothesis. STOP. Do NOT proceed to a fix - a fix on an unconfirmed root cause risks treating a symptom. -Append the exhausted tree (every hypothesis with its REJECTED / INCONCLUSIVE verdict and reasoning) to -`.specs/BUG-/03-decisions.md` - this is the knowledge record for the next investigation. +Main thread appends the exhausted tree (every hypothesis with its REJECTED / INCONCLUSIVE verdict +and reasoning) to `.specs/BUG-/03-decisions.md` - this is the knowledge record for the next +investigation. Ask: @@ -129,7 +136,8 @@ Ask: hypotheses. Do not re-run identical hypotheses. - `observe` -> add observability (logging, tracing, metrics), reproduce again to gather evidence, then re-enumerate. Log the gap to `05-retro.md`. -- `abort` -> set status and close the spec as "root cause not found"; record the exhausted tree as the +- `abort` -> set status=`in-progress` then `done` (the state machine has no approved -> done + shortcut) and close the spec as "root cause not found"; record the exhausted tree as the outcome in `05-retro.md`. ### ⛔ Gate 3 - Root cause confirmed @@ -154,9 +162,11 @@ Ask: Main thread does this, NOT a subagent. The test must: -1. Live under `tests//` per project conventions. +1. Live under `paths.tests` (from `.claude/project-config.json`), mirroring the source path per + project convention. 2. Reproduce the symptom (must FAIL when run). -3. Be named for the bug, not the fix (e.g. `Should_NotDoubleDecrement_When_RetryAfterTransientFailure`). +3. Be named for the bug, not the fix (example, adapt to project/language naming convention: + "does not double-decrement when a transient failure is retried"). 4. Be added to `00-spec.md` Regression test checklist. Run the test once. Confirm it fails for the documented reason. @@ -174,17 +184,18 @@ STOP. Display the test name and the failure output. Ask: ## Phase 5 - Implement minimal fix -1. Fill `00-spec.md` Fix approach (MINIMAL). Confirm scope-discipline checklist: +1. Set status=`in-progress`, update index. +2. Fill `00-spec.md` Fix approach (MINIMAL). Confirm scope-discipline checklist: - [ ] Touches only files implicated by root cause. - [ ] No "while I'm here" cleanups. - [ ] No reformatting unrelated code. -2. Invoke `sd-implementer` with: +3. Invoke `sd-implementer` with: - `TASK_DETAILS = ` - `SPEC_REF = .specs/BUG-/00-spec.md` - `IMPACT_REF = .specs/BUG-/03-decisions.md` (investigation evidence) - `WORKFLOW_TYPE = bug` - `ROOT_CAUSE = ` -3. Implementer applies fix. Re-runs the failing test (now passing). +4. Implementer applies fix. Re-runs the failing test (now passing). --- diff --git a/commands/explore.md b/commands/explore.md index 0be49d4..8b58ec5 100644 --- a/commands/explore.md +++ b/commands/explore.md @@ -46,9 +46,9 @@ Invoke with: - `GITNEXUS_AVAILABLE = ` Explorer routes internally based on `DETECTED_INTENT`: -- `definition` -> GitNexus `list_symbols` + `get_file`, fallback to `Grep` for definition markers. -- `callers` -> GitNexus `find_references`, fallback to `Grep` for invocation patterns. -- `trace` -> GitNexus `get_call_graph` (1-2 hops). +- `definition` -> GitNexus `context`, fallback to `Grep` for definition markers. +- `callers` -> GitNexus `impact` (`direction: upstream`), fallback to `Grep` for invocation patterns. +- `trace` -> GitNexus `impact` (`direction: downstream`, 1-2 hops). - `impact` -> direct callers (1-hop) + transitive (2-3 hop) + tests touching the target + DI / config refs. - `pattern` -> `Grep` with refined query, return file:line snippets. - `structure` -> directory listing + top-level symbols per file. diff --git a/commands/feature.md b/commands/feature.md index f52ddee..a99cb07 100644 --- a/commands/feature.md +++ b/commands/feature.md @@ -29,10 +29,16 @@ On re-invocation with the same ``, detect the current state of `.specs/FEAT ## Phase 0 - Bootstrap (always runs) -1. Read `CLAUDE.md` at project root. -2. Read `.specs/constitution.md`. -3. Read `.claude/project-config.json` (for `commands.*`, `spec.*`, `ticket.*`, `workflow.*`). -4. Read `.specs/index.md` for existing spec states. +1. Read `CLAUDE.md` at project root. If missing, WARN and continue - print "No `CLAUDE.md` found; + stack conventions may be incomplete." (the constitution is the binding Layer-2 contract, not + `CLAUDE.md`). +2. Read `.specs/constitution.md`. If `.specs/` or this file is missing, STOP: "No `.specs/` found - + run `/sd:setup` first." +3. Read `.claude/project-config.json` (for `commands.*`, `spec.*`, `ticket.*`, `workflow.*`). If + missing, STOP with the same message. If present but fails to parse as JSON, STOP: + "`.claude/project-config.json` failed to parse - fix it or re-run `/sd:setup`." +4. Read `.specs/index.md` for existing spec states. If missing, STOP with the same + "run `/sd:setup` first" message. 5. Determine state from table above. --- @@ -42,8 +48,8 @@ On re-invocation with the same ``, detect the current state of `.specs/FEAT 1. If `ticket.system == "jira"` and `` matches `ticket.pattern`, fetch ticket via `mcp__atlassian__getJiraIssue`. If MCP unavailable, ask user for a paste or proceed with slug. 2. Invoke `sd-spec-architect` with: - `TASK = create` - - `TEMPLATE = feature` - - `TICKET_DATA = ` + - `TEMPLATE = feature.template.md` + - `TICKET_CONTEXT = ` - `SPEC_ID = FEAT-` 3. Spec-architect produces `.specs/FEAT-/00-spec.md` with: Why (business value), What (Given/When/Then), Success criteria, Out of scope, Open questions, Constitution check, Linked specs. 4. If a ticket was fetched, spec-architect also snapshots it (ticket content + related tickets + linked Confluence pages, per its Ticket snapshot protocol) to `.specs/FEAT-/04-artifacts/ticket/`. @@ -56,7 +62,7 @@ STOP. Present the spec to the user. Ask: > Approve spec FEAT-? (yes / refine / abort) - `yes` -> set status=`approved`, proceed. -- `refine` -> invoke `sd-spec-architect` with `TASK = refine`, `FEEDBACK = `. Loop. +- `refine` -> invoke `sd-spec-architect` with `TASK = refine`, `SPEC = .specs/FEAT-/00-spec.md`, `FEEDBACK = `. Loop. - `abort` -> set status=`archived`, exit. --- @@ -64,11 +70,13 @@ STOP. Present the spec to the user. Ask: ## Phase 2 - Impact analysis 1. Invoke `sd-code-explorer` with: - - `TASK_TYPE = impact-map` - - `SPEC_REF = .specs/FEAT-/00-spec.md` - - `OUTPUT_APPEND_TO = .specs/FEAT-/03-decisions.md` + - `TASK = impact-map` + - `SPEC = .specs/FEAT-/00-spec.md` + - `OUTPUT_TARGET = .specs/FEAT-/03-decisions.md` 2. Explorer produces: direct callers (1-hop), transitive (2-3 hop), test coverage scan, DI/config grep, public API surface, risk assessment. 3. All findings cite `file:line`. +4. Main thread appends the explorer's returned analysis to `.specs/FEAT-/03-decisions.md` + (create the file if missing; never overwrite existing content). No gate here - impact analysis is informational. User reviews it in Phase 3. @@ -78,26 +86,13 @@ No gate here - impact analysis is informational. User reviews it in Phase 3. 1. Invoke `sd-spec-architect` with: - `TASK = plan` - - `SPEC_REF = .specs/FEAT-/00-spec.md` - - `IMPACT_REF = .specs/FEAT-/03-decisions.md` + - `SPEC = .specs/FEAT-/00-spec.md` + - `IMPACT = .specs/FEAT-/03-decisions.md` 2. Spec-architect produces: - `.specs/FEAT-/01-plan.md` (approach, alternatives considered, rationale). - - `.specs/FEAT-/02-tasks.md` with atomic tasks, each having: - -``` -### T - -- Files: <list of files to touch> -- Layer: <Domain | Application | Infrastructure | Presentation> -- Step type: <foundation | behavior | wiring | polish | test> -- Test: <test file/method to create or update> -- Acceptance: <one-line criterion> -- Depends on: <T## or "none"> -- Conflicts with: <T## or "none"> -- Complexity: <S | M | L> -- Reversibility: <trivial | moderate | hard> -- Pattern refs: <1-3 file:line precedent citations + what to mirror, or "none"> -``` - + - `.specs/FEAT-<arg>/02-tasks.md` with atomic tasks, each formatted per the + **sd-atomic-task-format** skill (9 required fields + `Pattern refs`; the architect applies + this format, do not re-specify it here). 3. Set status=`in-progress` in `00-spec.md` and `index.md`. ### ⛔ Gate 2 - Plan approval @@ -107,7 +102,7 @@ STOP. Present the plan and task list. Ask: > Approve plan for FEAT-<arg>? (<N> tasks, estimated <complexity>) (yes / refine <feedback> / abort) - `yes` -> proceed. -- `refine` -> invoke `sd-spec-architect` with `TASK = refine`. Loop. +- `refine` -> invoke `sd-spec-architect` with `TASK = refine`, `SPEC = .specs/FEAT-<arg>/00-spec.md`, `FEEDBACK = <user feedback>`. Loop. - `abort` -> set status=`archived`, exit. --- diff --git a/commands/perf.md b/commands/perf.md index 347f98c..9a56a77 100644 --- a/commands/perf.md +++ b/commands/perf.md @@ -38,9 +38,15 @@ Drives an optimization from a measured baseline to a measured improvement, with ## Phase 0 - Bootstrap -1. Read `CLAUDE.md`, `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. -2. Compute UTC date for spec ID. -3. Detect state. Print resume plan. +1. Read `CLAUDE.md`. If missing, WARN and continue - print "No `CLAUDE.md` found; stack + conventions may be incomplete." (the constitution is the binding Layer-2 contract, not + `CLAUDE.md`). +2. Read `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. If `.specs/` + or any of these is missing, STOP: "No `.specs/` found - run `/sd:setup` first." If + `.claude/project-config.json` is present but fails to parse as JSON, STOP: + "`.claude/project-config.json` failed to parse - fix it or re-run `/sd:setup`." +3. Compute UTC date for spec ID. +4. Detect state. Print resume plan. --- @@ -52,6 +58,7 @@ Drives an optimization from a measured baseline to a measured improvement, with - `SPEC_ID = PERF-<slug>-<YYYYMMDD>` 2. Architect fills Target (metric, goal SLA, environment, load profile, workload type), Measurement methodology, Constraints, Out of scope. 3. Architect leaves **Current observed EMPTY** and **Results log EMPTY**. These are filled by measurement, not by assumption. +4. Register in `.specs/index.md` with status=`draft`. ### ⛔ Gate 1 - Target defined @@ -80,7 +87,8 @@ STOP. Two cases: **Case A: baseline already meets SLA goal.** > Baseline p95=<X> already meets SLA goal p95<<goal>. No optimization needed. Close PERF-<slug> as 'done' with no changes? (yes / proceed anyway / abort) -- `yes` -> jump to Phase 6 close-out with summary "no work needed". +- `yes` -> set status=`in-progress` (no hotspot work occurs, but the state machine has no + approved -> done shortcut), then jump to Phase 6 close-out with summary "no work needed". - `proceed anyway` -> requires explicit constitution exception ("optimizing past SLA"). Log to retro. **Case B: baseline below SLA goal.** @@ -98,7 +106,7 @@ This gate is HARD - the workflow CANNOT enter Phase 3 without a checked-in basel - `SPEC_REF = .specs/PERF-<slug>-<YYYYMMDD>/00-spec.md` - `BASELINE_ARTIFACT = .specs/PERF-<slug>-<YYYYMMDD>/04-artifacts/baseline-<...>.json` 2. Debugger's job: identify the 80/20 hotspots from profile data, query plans, or code reads. Output: ranked list of hotspots with file:line citations and contribution percentage to total latency / CPU / memory. -3. Append hotspot ranking to `03-decisions.md`. +3. Main thread appends the returned hotspot ranking to `03-decisions.md` (debugger has no write tool). ### ⛔ Gate 3 - Hotspot identified @@ -113,6 +121,8 @@ STOP. Display hotspot ranking. Ask: ## Phase 4 - Per-hotspot loop +Set status=`in-progress`, update index (once, on first entry to this phase). + For each selected hotspot, repeat this entire loop. Multiple hotspots = multiple loop iterations. ### 4a. Deep dive @@ -125,7 +135,7 @@ For each selected hotspot, repeat this entire loop. Multiple hotspots = multiple - Expected impact (e.g. "p95 -200ms based on current 350ms in this function"). - Implementation cost (S / M / L). - Risk profile (correctness risk, scope of change, reversibility). -3. Append hypotheses to `03-decisions.md`. +3. Main thread appends the returned hypotheses to `03-decisions.md` (debugger has no write tool). ### ⛔ Gate 4 - Select hypothesis @@ -227,7 +237,7 @@ STOP. Display final test results + final measurement. Ask: - `SPEC_REF = .specs/PERF-<slug>-<YYYYMMDD>/00-spec.md` - `CHANGED_FILES = <all files touched across kept attempts>` - `RESULTS_LOG = <Results log>` -2. Reviewer checks: correctness preserved (no test edits to make them pass), no behavior change beyond what spec accepted under "Trade-offs", no new constitution exceptions, no static state introduced, no `dynamic` (C#) / `any` (TS) sneaked in. +2. Reviewer checks: correctness preserved (no test edits to make them pass), no behavior change beyond what spec accepted under "Trade-offs", no new constitution exceptions, no static state introduced, no type-safety escapes for the project's language (as defined in `constitution.md`) sneaked in. ### ⛔ Gate 8 - Final review pass @@ -259,5 +269,5 @@ STOP. Display reviewer verdict. Ask: - Revert on no measurable improvement. The Results log is the source of truth. - Reverted attempts are LOGGED, not deleted. They are knowledge. - Correctness tests must remain unchanged. If the optimization requires changing a test, it changes behavior - that needs a FEAT-* or BUG-* spec, not PERF-*. -- MSSQL access (via MCP) for hotspot analysis is SELECT / EXPLAIN only. +- Database access (via the project's MCP tool or CLI) for hotspot analysis is read-only: SELECT / EXPLAIN only. - If SLA cannot be met after exhausting hypotheses, close the PERF spec with the documented gap and lessons. Do not "ship anyway". diff --git a/commands/rca.md b/commands/rca.md index c80df47..da0fd58 100644 --- a/commands/rca.md +++ b/commands/rca.md @@ -27,9 +27,15 @@ Drives an incident analysis from raw signals to a documented root cause, recorde ## Phase 0 - Bootstrap -1. Read `CLAUDE.md`, `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. -2. Compute current UTC date for the spec ID stamp. -3. Detect state. Print resume plan. +1. Read `CLAUDE.md`. If missing, WARN and continue - print "No `CLAUDE.md` found; stack + conventions may be incomplete." (the constitution is the binding Layer-2 contract, not + `CLAUDE.md`). +2. Read `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. If `.specs/` + or any of these is missing, STOP: "No `.specs/` found - run `/sd:setup` first." If + `.claude/project-config.json` is present but fails to parse as JSON, STOP: + "`.claude/project-config.json` failed to parse - fix it or re-run `/sd:setup`." +3. Compute current UTC date for the spec ID stamp. +4. Detect state. Print resume plan. --- @@ -67,9 +73,9 @@ STOP. Display the populated Timeline, Symptoms, Affected scope, Recent changes. - `SPEC_REF = .specs/RCA-<slug>-<YYYYMMDD>/00-spec.md` - `EVIDENCE_DIR = .specs/RCA-<slug>-<YYYYMMDD>/04-artifacts/` - `MODE = incident` -2. Debugger uses sequential-thinking + 5 mental models (boundary / state / concurrency / recent-changes / environment) to enumerate **4 to 8** hypotheses. -3. Each hypothesis ranked by `(Likelihood x Impact) / Cost-to-verify`. -4. Hypothesis tree written to `00-spec.md` "Hypothesis tree" section. +2. Debugger enumerates hypotheses per the **sd-hypothesis-tree** skill (5 mental models, + `(Likelihood x Impact) / Cost-to-verify` ranking). +3. Hypothesis tree written to `00-spec.md` "Hypothesis tree" section. ### ⛔ Gate 2 - Hypotheses enumerated @@ -91,8 +97,9 @@ For each hypothesis in rank order: - `TASK = verify` - `HYPOTHESIS = <H#>` - `EVIDENCE_DIR = .specs/RCA-<slug>-<YYYYMMDD>/04-artifacts/` -2. Debugger gathers evidence (logs, queries, code reads). For MSSQL, **SELECT / EXPLAIN only** - never UPDATE / DELETE / INSERT. -3. Result: `CONFIRMED` / `REJECTED` / `INCONCLUSIVE`. Append result with evidence pointers to "Verification results (Phase 3)". +2. Debugger gathers evidence (logs, queries, code reads). Database access (via the project's MCP + tool or CLI) is **SELECT / EXPLAIN only** - never UPDATE / DELETE / INSERT. +3. Result: `CONFIRMED` / `REJECTED` / `INCONCLUSIVE`. Main thread appends the result with evidence pointers to "Verification results (Phase 3)" (debugger has no write tool). 4. Document REJECTED with FULL reasoning. This is knowledge preservation. 5. Continue until one hypothesis is `CONFIRMED`. @@ -107,18 +114,19 @@ Ask: > Confirm root cause: <one-line>. Proceed to mitigation documentation? (yes / dig deeper / abort) -- `yes` -> proceed. +- `yes` -> status=`approved`, proceed. - `dig deeper` -> back to Phase 3, additional hypotheses if needed. --- ## Phase 4 - Isolate + document -1. Fill in `00-spec.md`: +1. Set status=`in-progress`, update index. +2. Fill in `00-spec.md`: - **Affected components** - file:line, service names, config keys. - **Why this is root cause** - the "why" chain (use 5-whys discipline; stop when answer is fixable). -2. Fill **Mitigation applied** - what stopped the bleeding. Reference timestamps from Timeline. -3. Note: mitigation IS NOT a fix. It is the immediate action that contained the impact. The actual fix lands in a spawned BUG-* spec. +3. Fill **Mitigation applied** - what stopped the bleeding. Reference timestamps from Timeline. +4. Note: mitigation IS NOT a fix. It is the immediate action that contained the impact. The actual fix lands in a spawned BUG-* spec. No gate here - documentation-only phase. @@ -152,6 +160,6 @@ No gate here - documentation-only phase. - Reproduction is rarely possible for incidents (the incident is over). Verification relies on logs, traces, queries, and code reads from the relevant time window. - All evidence lives under `04-artifacts/` with descriptive filenames. Never reference "the dashboard" - save a screenshot or query. - Rejected hypotheses are documented in full. They are as valuable as the confirmed one for future incidents. -- MSSQL access (via MCP) is SELECT / EXPLAIN only. Any UPDATE attempt is a constitution violation. +- Database access (via the project's MCP tool or CLI) is SELECT / EXPLAIN only. Any UPDATE attempt is a constitution violation. - The Spawned specs section is a CONTRACT. Each reserved ID should be created within the agreed timeline; if not, log to retro. - RCAs do not get a `revive` action. New incident -> new RCA. diff --git a/commands/refactor.md b/commands/refactor.md index 997000c..0735c9f 100644 --- a/commands/refactor.md +++ b/commands/refactor.md @@ -41,10 +41,16 @@ Spec ID = `REF-<slug>-<YYYYMMDD>`. ## Phase 0 - Bootstrap -1. Read `CLAUDE.md`, `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. -2. Compute UTC date for spec ID. -3. Read coverage threshold from project-config or default to 80%. -4. Detect state. Print resume plan. +1. Read `CLAUDE.md`. If missing, WARN and continue - print "No `CLAUDE.md` found; stack + conventions may be incomplete." (the constitution is the binding Layer-2 contract, not + `CLAUDE.md`). +2. Read `.specs/constitution.md`, `.claude/project-config.json`, `.specs/index.md`. If `.specs/` + or any of these is missing, STOP: "No `.specs/` found - run `/sd:setup` first." If + `.claude/project-config.json` is present but fails to parse as JSON, STOP: + "`.claude/project-config.json` failed to parse - fix it or re-run `/sd:setup`." +3. Compute UTC date for spec ID. +4. Read coverage threshold from project-config or default to 80%. +5. Detect state. Print resume plan. --- @@ -74,9 +80,12 @@ STOP. Display spec summary, especially Invariants and Out-of-scope. Ask: 1. Invoke `sd-code-explorer` with: - `TASK = impact-map` - `SPEC = .specs/REF-<slug>-<YYYYMMDD>/00-spec.md` - - `OUTPUT_APPEND_TO = .specs/REF-<slug>-<YYYYMMDD>/03-decisions.md` + - `OUTPUT_TARGET = .specs/REF-<slug>-<YYYYMMDD>/03-decisions.md` 2. Explorer enumerates: direct callers, transitive callers (2-3 hop), test coverage scan of affected paths, DI / config grep, public API surface, risk assessment. 3. Every finding cites file:line. +4. Main thread appends the explorer's returned analysis to + `.specs/REF-<slug>-<YYYYMMDD>/03-decisions.md` (create the file if missing; never overwrite + existing content). No gate here - read-only. @@ -100,7 +109,7 @@ STOP. Compare measured vs threshold. If user picks (1), enter the characterization sub-loop: 1. Identify uncovered branches via coverage report. -2. Invoke `sd-implementer` with `TASK_TYPE = characterization-test` per uncovered area. +2. Invoke `sd-implementer` with `TASK_DETAILS = <characterization test task for the uncovered area>`, `SPEC_REF = .specs/REF-<slug>-<YYYYMMDD>/00-spec.md`, `WORKFLOW_TYPE = refactor` per uncovered area. 3. Each new test must FAIL FAST if current behavior changes - characterization tests pin the CURRENT behavior, correct or not. 4. Re-measure coverage. @@ -122,22 +131,10 @@ If user picks (2) explicit exception, document the threshold reduction in `05-re - `SPEC = .specs/REF-<slug>-<YYYYMMDD>/00-spec.md` - `IMPACT = .specs/REF-<slug>-<YYYYMMDD>/03-decisions.md` - `MODE = refactor` -2. Architect writes `01-plan.md` (sequencing) and `02-tasks.md`. Each task uses the canonical format PLUS refactor-specific fields: - -``` -### T<NN> - <title> -- Files: <list of files to touch> -- Layer: <Domain | Application | Infrastructure | Presentation> -- Step type: <foundation | behavior | wiring | polish | test> -- Test: <test file/method to create or update> -- Acceptance: <one-line criterion> -- Depends on: <T## or "none"> -- Conflicts with: <T## or "none"> -- Complexity: <S | M | L> -- Reversibility: <trivial | moderate | hard> -- Pattern refs: <1-3 file:line precedent citations + what to mirror, or "none"> -- Parallel batch: <batch number or "solo"> -``` +2. Architect writes `01-plan.md` (sequencing) and `02-tasks.md`. Each task follows the + **sd-atomic-task-format** skill (9 required fields + `Pattern refs`, plus the skill's + "Refactor mode" `Parallel batch` field - do not re-specify the format here). Tasks sharing a + batch number must have disjoint file sets and no `Depends on` / `Conflicts with` relationship. ### ⛔ Gate 4 - Plan approval diff --git a/commands/spec.md b/commands/spec.md index 26e0163..60e3409 100644 --- a/commands/spec.md +++ b/commands/spec.md @@ -202,7 +202,8 @@ Behavior: - `status` is in `spec.lifecycle` from project-config. - Expected files present per status: - status >= `approved` -> `00-spec.md` must NOT have "<<placeholder>>" tokens remaining. - - status >= `in-progress` -> `01-plan.md` and `02-tasks.md` exist (except RCA). + - status >= `in-progress` -> `01-plan.md` and `02-tasks.md` exist (feature and refactor + only; bug, perf, and rca do not produce plan/tasks artifacts). - status == `done` -> `05-retro.md` exists with at least one entry. - Index row matches frontmatter status. 2. Output: one line per spec with PASS / FAIL and the first failure reason. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6cec1ad..fd6d2b3 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -206,7 +206,7 @@ This lists every inconsistency. Then fix manually (recommended) or remove the mi **Cause**: skipping intermediate states. -**Fix**: lifecycle is `draft -> approved -> in-progress -> done`. Use `/sd:spec status <ID> approved` then `in-progress` then `done`. If you're closing a spec that genuinely had no execution (e.g. an RCA that documents itself), `/sd:rca` flows directly to `done` at workflow end - don't manually transition. +**Fix**: lifecycle is `draft -> approved -> in-progress -> done`. Use `/sd:spec status <ID> approved` then `in-progress` then `done`. Every workflow (`/sd:feature`, `/sd:bug`, `/sd:refactor`, `/sd:perf`, `/sd:rca`) walks all four states itself, including specs with no code execution (e.g. an RCA or a PERF spec whose baseline already meets SLA) - don't manually transition mid-workflow. ### A spec is stuck in `in-progress` for weeks diff --git a/docs/usage.md b/docs/usage.md index 2876769..ca9f126 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -262,6 +262,33 @@ Examples: --- +### `/sd:adr <spec-ID | "decision title">` + +Promotes a durable decision into a numbered, MADR-style ADR under `.specs/_adr/`. Drives the `sd-docs-writer` agent. + +Argument: a spec ID (e.g. `FEAT-012`) whose `03-decisions.md` holds the decision, or a free-text decision title for an ad-hoc ADR with no spec. + +| Phase | Actor | Gate | +|---|---|---| +| 0 - Bootstrap | main thread | - | +| 1 - Resolve decision source | main thread | - | +| 2 - Assign number and slug | main thread | - | +| 3 - Draft ADR | `sd-docs-writer` | - | +| Gate - approve before keeping | main thread | ⛔ Gate 1 (HARD - nothing is kept without approval) | +| 4 - Report | main thread | - | + +Numbers are 4-digit, sequential (`0001-`, `0002-`, ...), derived by scanning `.specs/_adr/`. The +agent never invents decisions - an empty or absent `03-decisions.md` aborts the command. ADRs are +not specs: they have no `.specs/index.md` lifecycle entry. + +Examples: +``` +/sd:adr FEAT-012 # ADR from an existing spec's decisions +/sd:adr "Adopt CQRS for the order service" # ad-hoc ADR, no spec +``` + +--- + ## Common patterns ### Picking the right workflow @@ -282,7 +309,7 @@ Examples: Workflow commands are **resumable**. Re-running `/sd:feature INV-2501` after closing your terminal mid-execution detects the current state of `.specs/FEAT-INV-2501/` and jumps to the next phase. The state machine is documented at the top of each command file. -The main heuristic: workflow checks for the presence and contents of `00-spec.md`, `01-plan.md`, `02-tasks.md` (with task completion ratio), and `05-retro.md` to determine where you are. +The main heuristic: workflow checks for the presence and contents of `00-spec.md`, `05-retro.md`, and (feature/refactor only) `01-plan.md` and `02-tasks.md` (with task completion ratio) to determine where you are. ### Linking specs diff --git a/examples/README.md b/examples/README.md index a0cf3f7..72938c9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,7 +17,8 @@ It covers: ## What this folder will hold over time -Future additions (not in v1.0.0): +Ideas under consideration, no committed timeline (see [`../ROADMAP.md`](../ROADMAP.md) for what +is actually scheduled): - `examples/fixture-projects/` - tiny example repos (Node, .NET, Python) with pre-populated `.specs/` for demoing. - `examples/transcripts/` - anonymized real-run transcripts showing prompt-router and spec-gate behavior. diff --git a/hooks/bash/prompt-router.sh b/hooks/bash/prompt-router.sh index 780305f..6f1f3bf 100644 --- a/hooks/bash/prompt-router.sh +++ b/hooks/bash/prompt-router.sh @@ -6,8 +6,9 @@ # Exits 0 silently if jq is missing, if stdin is empty/invalid, or if no hints # apply. Never writes to disk. # -# Note: we deliberately do NOT use `set -u` because bash's empty-associative-array +# Note: we deliberately do NOT use `set -u` because bash 3.2's empty-array # expansion is brittle under it; the hook must never fail noisily. +# Must stay bash-3.2 compatible (macOS system bash): no `declare -A`. # --- graceful exits ----------------------------------------------------------- @@ -58,8 +59,10 @@ index_path="${cwd}/${index_rel}" # --- keyword match ------------------------------------------------------------ prompt_lower="$(printf '%s' "${prompt}" | tr '[:upper:]' '[:lower:]')" -declare -A matched -declare -A matched_terms +# Parallel indexed arrays (bash 3.2 has no associative arrays): index i holds +# the i-th matched workflow and its comma-joined matched terms. +matched_workflows=() +matched_terms_list=() match_keywords() { local workflow="$1" @@ -69,20 +72,26 @@ match_keywords() { if [[ -z "${list}" ]]; then list="${default_list}" fi - local kw + local kw terms="" while IFS= read -r kw; do + # Some jq builds (e.g. Windows jq.exe) emit CRLF for join("\n") output; + # strip a trailing CR so the comparison below isn't corrupted. + kw="${kw%$'\r'}" [[ -z "${kw}" ]] && continue local kw_lower kw_lower="$(printf '%s' "${kw}" | tr '[:upper:]' '[:lower:]')" if [[ "${prompt_lower}" == *"${kw_lower}"* ]]; then - matched["${workflow}"]=1 - if [[ -z "${matched_terms[${workflow}]:-}" ]]; then - matched_terms["${workflow}"]="${kw}" + if [[ -z "${terms}" ]]; then + terms="${kw}" else - matched_terms["${workflow}"]="${matched_terms[${workflow}]}, ${kw}" + terms="${terms}, ${kw}" fi fi done <<< "${list}" + if [[ -n "${terms}" ]]; then + matched_workflows+=("${workflow}") + matched_terms_list+=("${terms}") + fi } match_keywords bug $'bug\nfix\nbroken\nerror\ncrash\nregression\ndefect' @@ -142,7 +151,7 @@ fi # --- nothing to say? ---------------------------------------------------------- -if [[ ${#matched[@]} -eq 0 && ${#ticket_ids[@]} -eq 0 && ${#in_progress[@]} -eq 0 ]]; then +if [[ ${#matched_workflows[@]} -eq 0 && ${#ticket_ids[@]} -eq 0 && ${#in_progress[@]} -eq 0 ]]; then exit 0 fi @@ -152,11 +161,11 @@ fi echo '<context-router>' echo 'Routing hints from specwright (UserPromptSubmit hook):' - if [[ ${#matched[@]} -gt 0 ]]; then + if [[ ${#matched_workflows[@]} -gt 0 ]]; then echo '' echo 'Workflow keyword matches:' - for k in "${!matched[@]}"; do - echo " - /sd:${k} (matched: ${matched_terms[${k}]})" + for ((i=0; i<${#matched_workflows[@]}; i++)); do + echo " - /sd:${matched_workflows[i]} (matched: ${matched_terms_list[i]})" done fi diff --git a/hooks/bash/spec-gate.sh b/hooks/bash/spec-gate.sh index 5c5a649..f822242 100644 --- a/hooks/bash/spec-gate.sh +++ b/hooks/bash/spec-gate.sh @@ -166,11 +166,12 @@ if [[ ${is_code} -eq 0 ]]; then exit 0 fi -# Check for in-progress spec. +# Check for in-progress spec. Both markers must appear on the SAME line +# (mirrors prompt-router.sh and spec-gate.ps1's same-line semantics). has_in_progress=0 if [[ -f "${index_path}" ]]; then - if grep -q 'in-progress' "${index_path}" 2>/dev/null && \ - grep -q -E '(FEAT|BUG|REF|PERF|RCA)-[A-Za-z0-9_-]+' "${index_path}" 2>/dev/null; then + if grep -E 'in-progress' "${index_path}" 2>/dev/null \ + | grep -q -E '(FEAT|BUG|REF|PERF|RCA)-[A-Za-z0-9_-]+'; then has_in_progress=1 fi fi diff --git a/hooks/powershell/prompt-router.ps1 b/hooks/powershell/prompt-router.ps1 index 8103e80..3d742cd 100644 --- a/hooks/powershell/prompt-router.ps1 +++ b/hooks/powershell/prompt-router.ps1 @@ -39,6 +39,14 @@ function Read-StdinJson { } } +$script:DefaultKeywords = [pscustomobject]@{ + bug = @('bug','fix','broken','error','crash','regression','defect') + feature = @('feature','add','implement','new','support') + refactor = @('refactor','restructure','clean up','extract','rename') + perf = @('perf','performance','slow','optimize','latency','throughput') + rca = @('incident','outage','rca','root cause','post-mortem','postmortem') +} + function Get-ProjectConfig { param([string]$Cwd) @@ -52,13 +60,7 @@ function Get-ProjectConfig { baseUrl = '' } workflow = [pscustomobject]@{ - keywords = [pscustomobject]@{ - bug = @('bug','fix','broken','error','crash','regression','defect') - feature = @('feature','add','implement','new','support') - refactor = @('refactor','restructure','clean up','extract','rename') - perf = @('perf','performance','slow','optimize','latency','throughput') - rca = @('incident','outage','rca','root cause','post-mortem','postmortem') - } + keywords = $script:DefaultKeywords } hooks = [pscustomobject]@{ userPromptRouter = [pscustomobject]@{ enabled = $true } @@ -90,13 +92,17 @@ function Test-HookEnabled { function Get-KeywordMatches { param( [string]$Prompt, - $KeywordMap + $KeywordMap, + $DefaultKeywordMap ) $matches = @{} - if ($null -eq $KeywordMap) { return $matches } $lower = $Prompt.ToLowerInvariant() foreach ($workflow in @('bug','feature','refactor','perf','rca')) { - $list = $KeywordMap.$workflow + $list = $null + if ($null -ne $KeywordMap) { $list = $KeywordMap.$workflow } + if ($null -eq $list -or @($list).Count -eq 0) { + $list = $DefaultKeywordMap.$workflow + } if ($null -eq $list) { continue } foreach ($kw in $list) { $kwLower = $kw.ToLowerInvariant() @@ -173,11 +179,11 @@ function Get-InProgressSpecs { # ---- main ---- -$input = Read-StdinJson -if ($null -eq $input) { exit 0 } +$hookInput = Read-StdinJson +if ($null -eq $hookInput) { exit 0 } -$prompt = $input.prompt -$cwd = $input.cwd +$prompt = $hookInput.prompt +$cwd = $hookInput.cwd if ([string]::IsNullOrWhiteSpace($prompt) -or [string]::IsNullOrWhiteSpace($cwd)) { exit 0 } if (-not (Test-Path -LiteralPath $cwd)) { exit 0 } @@ -189,7 +195,7 @@ $indexFile = if ($config.spec.indexFile) { Join-Path $cwd $config.spec.indexFile $pattern = if ($config.ticket.pattern) { $config.ticket.pattern } else { '^[A-Z]+-[0-9]+$' } $kwMap = $config.workflow.keywords -$workflowMatches = Get-KeywordMatches -Prompt $prompt -KeywordMap $kwMap +$workflowMatches = Get-KeywordMatches -Prompt $prompt -KeywordMap $kwMap -DefaultKeywordMap $script:DefaultKeywords $ticketIds = Get-TicketIds -Prompt $prompt -Pattern $pattern $ticketSpecs = Find-SpecsByTicket -SpecDir $specDir -TicketIds $ticketIds $inProgress = Get-InProgressSpecs -IndexPath $indexFile diff --git a/hooks/powershell/spec-gate.ps1 b/hooks/powershell/spec-gate.ps1 index 3ba4e72..97fcd41 100644 --- a/hooks/powershell/spec-gate.ps1 +++ b/hooks/powershell/spec-gate.ps1 @@ -166,13 +166,13 @@ function Write-BlockDecision { # ---- main ---- -$input = Read-StdinJson -if ($null -eq $input) { exit 0 } +$hookInput = Read-StdinJson +if ($null -eq $hookInput) { exit 0 } -$toolName = $input.tool_name +$toolName = $hookInput.tool_name if ($toolName -ne 'Edit' -and $toolName -ne 'Write' -and $toolName -ne 'MultiEdit') { exit 0 } -$cwd = $input.cwd +$cwd = $hookInput.cwd if ([string]::IsNullOrWhiteSpace($cwd)) { $cwd = (Get-Location).Path } $config = Get-ProjectConfig -Cwd $cwd @@ -188,7 +188,7 @@ $mode = 'warn' try { if ($config.hooks.specGate.mode) { $mode = [string]$config.hooks.specGate.mode } } catch { } if ($mode -eq 'off') { exit 0 } -$filePath = $input.tool_input.file_path +$filePath = $hookInput.tool_input.file_path if ([string]::IsNullOrWhiteSpace($filePath)) { exit 0 } $rel = ConvertTo-RelativePath -Cwd $cwd -FilePath $filePath diff --git a/hooks/powershell/subagent-retro.ps1 b/hooks/powershell/subagent-retro.ps1 index 229d0ae..86e6125 100644 --- a/hooks/powershell/subagent-retro.ps1 +++ b/hooks/powershell/subagent-retro.ps1 @@ -72,7 +72,7 @@ function Get-IndexSpecs { return $result } foreach ($line in $lines) { - if ($line -match '(FEAT|BUG|REF|PERF|RCA)-[A-Za-z0-9_\-]+' -and $line -match 'in-progress') { + if ($line -match 'in-progress' -and $line -match '(FEAT|BUG|REF|PERF|RCA)-[A-Za-z0-9_\-]+') { $id = $Matches[0] $type = ($id -split '-')[0] $obj = [pscustomobject]@{ @@ -120,7 +120,16 @@ function Test-DebounceElapsed { if (-not (Test-Path -LiteralPath $StatePath)) { return $true } try { $st = Get-Content -LiteralPath $StatePath -Raw -Encoding UTF8 | ConvertFrom-Json - $last = [datetime]::Parse($st.lastReminderUtc) + # PowerShell 7's ConvertFrom-Json auto-converts an ISO-8601 "...Z" string to a + # [datetime] with Kind=Utc; PowerShell 5.1 leaves it as a plain string. Re-Parse-ing + # an already-converted [datetime] stringifies it with the local culture (dropping the + # UTC marker), so [datetimeoffset]::Parse silently re-interprets it as local time - + # skewing $age by the machine's UTC offset. Only Parse when it is still a string. + if ($st.lastReminderUtc -is [datetime]) { + $last = $st.lastReminderUtc.ToUniversalTime() + } else { + $last = [datetimeoffset]::Parse([string]$st.lastReminderUtc).UtcDateTime + } $age = (Get-Date).ToUniversalTime() - $last return ($age.TotalMinutes -ge $DebounceMinutes) } catch { @@ -131,7 +140,10 @@ function Test-DebounceElapsed { function Save-State { param([string]$StatePath) try { - $dir = Split-Path -LiteralPath $StatePath -Parent + # -Path, not -LiteralPath: some PowerShell builds reject -LiteralPath combined + # with -Parent as an unresolvable parameter set. -Parent does no filesystem + # globbing (only -Resolve would), so -Path is exactly as safe here. + $dir = Split-Path -Path $StatePath -Parent if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } @@ -156,14 +168,14 @@ function Remove-StaleStateFiles { # ---- main ---- -$input = Read-StdinJson -if ($null -eq $input) { exit 0 } +$hookInput = Read-StdinJson +if ($null -eq $hookInput) { exit 0 } -$cwd = $input.cwd +$cwd = $hookInput.cwd if ([string]::IsNullOrWhiteSpace($cwd)) { $cwd = (Get-Location).Path } if (-not (Test-Path -LiteralPath $cwd)) { exit 0 } -$sessionId = $input.session_id +$sessionId = $hookInput.session_id if ([string]::IsNullOrWhiteSpace($sessionId)) { $sessionId = 'no-session' } $config = Get-ProjectConfig -Cwd $cwd diff --git a/install/install.ps1 b/install/install.ps1 index 154c38d..45eeceb 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -75,6 +75,17 @@ function Get-FileHashSafe { } } +# ---- prefix safety guard --------------------------------------------------- +# Mirrors uninstall.ps1's guard exactly - install and uninstall must accept the +# same set of prefixes, or a prefix legal for one and rejected by the other +# leaves orphaned or unreachable files. + +if ([string]::IsNullOrWhiteSpace($Prefix) -or + $Prefix.Contains('/') -or $Prefix.Contains('\') -or $Prefix.Contains('..')) { + Write-Fail "Invalid prefix '$Prefix'. Must be a plain folder name (no separators, no '..')." + exit 1 +} + # ---- repo root detection --------------------------------------------------- $scriptDir = $PSScriptRoot diff --git a/install/install.sh b/install/install.sh index 9c62bac..9e1a558 100755 --- a/install/install.sh +++ b/install/install.sh @@ -13,7 +13,7 @@ # - Interactive prompt on differing files (y / N / a=all). Suppressed by --force. # - chmod +x for bash hooks after install. -set -e +set -euo pipefail # ---- defaults -------------------------------------------------------------- @@ -71,9 +71,9 @@ EOF while [[ $# -gt 0 ]]; do case "$1" in --base-path) - BASE_PATH="$2"; shift 2 ;; + BASE_PATH="${2:-}"; shift 2 ;; --prefix) - PREFIX="$2"; shift 2 ;; + PREFIX="${2:-}"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; --force) @@ -86,14 +86,24 @@ while [[ $# -gt 0 ]]; do esac done +# ---- prefix safety guard --------------------------------------------------- +# Mirrors uninstall.sh's guard exactly - install and uninstall must accept the +# same set of prefixes, or a prefix legal for one and rejected by the other +# leaves orphaned or unreachable files. + +if [[ -z "${PREFIX// /}" || "$PREFIX" == */* || "$PREFIX" == *\\* || "$PREFIX" == *..* ]]; then + fail "Invalid prefix '$PREFIX'. Must be a plain folder name (no separators, no '..')." + exit 1 +fi + # ---- portable helpers ------------------------------------------------------ sha256_of() { local f="$1" if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$f" 2>/dev/null | awk '{print $1}' + sha256sum "$f" 2>/dev/null | awk '{print $1}' || true elif command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$f" 2>/dev/null | awk '{print $1}' + shasum -a 256 "$f" 2>/dev/null | awk '{print $1}' || true else echo "" fi @@ -176,6 +186,23 @@ SKIPPED_SAME=0 SKIPPED_DECLINE=0 BACKED_UP=0 +# ---- partial-install guard -------------------------------------------------- +# No transactional rollback (per-file backups already protect overwritten +# files) - on an unexpected failure mid-copy, tell the user what already +# landed and how to clean up rather than leaving a silent partial install. + +on_error() { + local code=$? + if [[ $DRY_RUN -ne 1 && $INSTALLED -gt 0 ]]; then + echo + fail "Install aborted (exit $code) after installing $INSTALLED file(s)." + warn "Partial install at '$BASE_PATH' (prefix '$PREFIX'). Run:" + warn " ./install/uninstall.sh --base-path \"$BASE_PATH\" --prefix \"$PREFIX\" --force" + warn "then retry the install." + fi +} +trap on_error ERR + # ---- copy one file --------------------------------------------------------- copy_one() { @@ -250,7 +277,7 @@ for entry in "${PLAN[@]}"; do tgt_root="$BASE_PATH/$tgt" while IFS= read -r -d '' f; do - rel="${f#$src_root/}" + rel="${f#"$src_root"/}" copy_one "$f" "$tgt_root/$rel" "$exec_flag" done < <(find "$src_root" -type f -print0) done diff --git a/scripts/smoke-hooks.ps1 b/scripts/smoke-hooks.ps1 new file mode 100644 index 0000000..8f87259 --- /dev/null +++ b/scripts/smoke-hooks.ps1 @@ -0,0 +1,203 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + specwright: hook smoke test (Windows / PowerShell). + +.DESCRIPTION + Pipes sample Claude Code hook JSON into hooks/powershell/*.ps1 against a + fixture .specs/ tree and asserts exit codes + key output substrings - not + just "did not crash". Mirror of scripts/smoke-hooks.sh (runs the bash hook + twins). Both must agree on the routed workflow for prompt-router. + + Exit code 0 = all cases passed; 1 = at least one failed. + +.NOTES + PURE ASCII ONLY. See hooks/powershell/prompt-router.ps1 for the rationale. + +.EXAMPLE + .\scripts\smoke-hooks.ps1 +#> + +param() + +$ErrorActionPreference = 'Stop' + +$scriptDir = $PSScriptRoot +$repoRoot = Split-Path -Parent $scriptDir + +$script:Pass = 0 +$script:Fail = 0 + +function Write-Section { param([string]$Title) Write-Host ''; Write-Host "=== $Title ===" -ForegroundColor Cyan } +function Add-Ok { param([string]$m) Write-Host " [OK] $m" -ForegroundColor Green; $script:Pass++ } +function Add-Bad { param([string]$m) Write-Host " [FAIL] $m" -ForegroundColor Red; $script:Fail++ } + +function Assert-Exit0 { + param([string]$Desc, [int]$Code) + if ($Code -eq 0) { Add-Ok "$Desc : exit 0" } else { Add-Bad "$Desc : exit $Code (expected 0)" } +} + +function Assert-Contains { + param([string]$Desc, [string]$Haystack, [string]$Needle) + if ($Haystack -and $Haystack.Contains($Needle)) { + Add-Ok "$Desc : contains `"$Needle`"" + } else { + $preview = if ($Haystack) { $Haystack.Substring(0, [Math]::Min(200, $Haystack.Length)) } else { '' } + Add-Bad "$Desc : missing `"$Needle`" -- got: $preview" + } +} + +function Assert-Empty { + param([string]$Desc, [string]$Haystack) + if ([string]::IsNullOrEmpty($Haystack)) { + Add-Ok "$Desc : empty output" + } else { + Add-Bad "$Desc : expected empty, got: $($Haystack.Substring(0, [Math]::Min(200, $Haystack.Length)))" + } +} + +# ---- fixture repo ------------------------------------------------------------ + +$fixture = Join-Path $env:TEMP "sd-smoke-hooks-$PID" +if (Test-Path -LiteralPath $fixture) { Remove-Item -Recurse -Force $fixture } +New-Item -ItemType Directory -Force -Path (Join-Path $fixture '.claude') | Out-Null +New-Item -ItemType Directory -Force -Path (Join-Path $fixture '.specs\FEAT-TEST-001') | Out-Null + +$configPath = Join-Path $fixture '.claude\project-config.json' +$configBlockJson = @' +{ + "spec": {"dir": ".specs", "indexFile": ".specs/index.md"}, + "ticket": {"pattern": "^[A-Z]+-[0-9]+$"}, + "workflow": { + "keywords": { + "bug": ["bug", "fix", "broken", "error", "crash", "regression", "defect"], + "feature": ["feature", "add", "implement", "new", "support"], + "refactor": ["refactor", "restructure", "clean up", "extract", "rename"], + "perf": ["perf", "performance", "slow", "optimize", "latency", "throughput"], + "rca": ["incident", "outage", "rca", "root cause", "post-mortem", "postmortem"] + } + }, + "hooks": { + "userPromptRouter": {"enabled": true}, + "specGate": {"enabled": true, "mode": "block"}, + "subagentRetro": {"enabled": true, "retroStaleMinutes": 0, "debounceMinutes": 10} + } +} +'@ +Set-Content -LiteralPath $configPath -Value $configBlockJson -Encoding UTF8 -NoNewline + +$indexRealPath = Join-Path $fixture '.specs\index.md' +$indexRealContent = @' +| ID | Type | Status | Title | +|---|---|---|---| +| FEAT-TEST-001 | feature | in-progress | Test feature | +'@ +Set-Content -LiteralPath $indexRealPath -Value $indexRealContent -Encoding UTF8 -NoNewline + +# "in-progress" appears only in a header/legend line - no row has it on the +# same line as a spec ID (mirrors the doc-02 same-line-detection fix). +$indexHeaderOnlyContent = @' +| ID | Type | Status (in-progress = active work) | Title | +|---|---|---|---| +| FEAT-DONE-002 | feature | done | Finished feature | +'@ + +function Invoke-Hook { + # Runs a hook .ps1 with $Payload on stdin. Sets script-scope Stdout/Stderr/Code. + param([string]$HookPath, [string]$Payload) + $outFile = [System.IO.Path]::GetTempFileName() + $errFile = [System.IO.Path]::GetTempFileName() + try { + $psExe = (Get-Process -Id $PID).Path + $Payload | & $psExe -NoProfile -File $HookPath 1>$outFile 2>$errFile + $script:Code = $LASTEXITCODE + $script:Stdout = (Get-Content -LiteralPath $outFile -Raw -ErrorAction SilentlyContinue) + $script:Stderr = (Get-Content -LiteralPath $errFile -Raw -ErrorAction SilentlyContinue) + if ($null -eq $script:Stdout) { $script:Stdout = '' } + if ($null -eq $script:Stderr) { $script:Stderr = '' } + } finally { + Remove-Item -LiteralPath $outFile, $errFile -ErrorAction SilentlyContinue + } +} + +# ---- prompt-router: keyword match -------------------------------------------- + +Write-Section 'prompt-router (PowerShell): keyword match routes to /sd:bug' +$payload = "{`"prompt`":`"please fix this bug`",`"cwd`":`"$($fixture -replace '\\','\\\\')`"}" +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\prompt-router.ps1') $payload +Assert-Exit0 'prompt-router keyword match' $script:Code +Assert-Contains 'prompt-router keyword match' $script:Stdout '<context-router>' +Assert-Contains 'prompt-router keyword match' $script:Stdout '/sd:bug' + +# ---- spec-gate: (a) code edit with in-progress spec -> allow ---------------- + +Write-Section 'spec-gate (PowerShell): (a) code edit with in-progress spec -> allow' +$fixtureEsc = $fixture -replace '\\', '\\\\' +$payload = "{`"tool_name`":`"Edit`",`"cwd`":`"$fixtureEsc`",`"tool_input`":{`"file_path`":`"$fixtureEsc\\\\src\\\\Foo.py`"}}" +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\spec-gate.ps1') $payload +Assert-Exit0 'spec-gate (a) in-progress -> allow' $script:Code +Assert-Empty 'spec-gate (a) in-progress -> allow' $script:Stdout + +# ---- spec-gate: (b) header-only "in-progress" -> block/warn, not allow ------ + +Write-Section 'spec-gate (PowerShell): (b) header-only in-progress text -> block (mode=block)' +Set-Content -LiteralPath $indexRealPath -Value $indexHeaderOnlyContent -Encoding UTF8 -NoNewline +$payload = "{`"tool_name`":`"Edit`",`"cwd`":`"$fixtureEsc`",`"tool_input`":{`"file_path`":`"$fixtureEsc\\\\src\\\\Bar.py`"}}" +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\spec-gate.ps1') $payload +Assert-Exit0 'spec-gate (b) header-only, mode=block' $script:Code +Assert-Contains 'spec-gate (b) header-only, mode=block' $script:Stdout '"decision":"block"' +Assert-Contains 'spec-gate (b) header-only, mode=block' $script:Stdout '"permissionDecision":"deny"' + +Write-Section 'spec-gate (PowerShell): (b) header-only in-progress text -> warn (mode=warn)' +(Get-Content -LiteralPath $configPath -Raw) -replace '"mode": "block"', '"mode": "warn"' | + Set-Content -LiteralPath $configPath -Encoding UTF8 -NoNewline +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\spec-gate.ps1') $payload +Assert-Exit0 'spec-gate (b) header-only, mode=warn' $script:Code +Assert-Empty 'spec-gate (b) header-only, mode=warn stdout' $script:Stdout +Assert-Contains 'spec-gate (b) header-only, mode=warn stderr' $script:Stderr '[WARN]' +(Get-Content -LiteralPath $configPath -Raw) -replace '"mode": "warn"', '"mode": "block"' | + Set-Content -LiteralPath $configPath -Encoding UTF8 -NoNewline +Set-Content -LiteralPath $indexRealPath -Value $indexRealContent -Encoding UTF8 -NoNewline + +# ---- spec-gate: (c) docs edit -> always allow -------------------------------- + +Write-Section 'spec-gate (PowerShell): (c) docs edit -> allow regardless of spec state' +$payload = "{`"tool_name`":`"Edit`",`"cwd`":`"$fixtureEsc`",`"tool_input`":{`"file_path`":`"$fixtureEsc\\\\docs\\\\guide.md`"}}" +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\spec-gate.ps1') $payload +Assert-Exit0 'spec-gate (c) docs edit -> allow' $script:Code +Assert-Empty 'spec-gate (c) docs edit -> allow' $script:Stdout + +# ---- spec-gate: (d) malformed JSON on stdin -> exit 0 silently -------------- + +Write-Section 'spec-gate (PowerShell): (d) malformed JSON on stdin -> exit 0 silently' +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\spec-gate.ps1') '{not valid json' +Assert-Exit0 'spec-gate (d) malformed JSON' $script:Code +Assert-Empty 'spec-gate (d) malformed JSON' $script:Stdout + +# ---- subagent-retro: missing retro names the spec, then debounces ---------- + +Write-Section 'subagent-retro (PowerShell): missing 05-retro.md names the real spec ID' +$payload = "{`"cwd`":`"$fixtureEsc`",`"session_id`":`"smoke-test-session`"}" +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\subagent-retro.ps1') $payload +Assert-Exit0 'subagent-retro first run' $script:Code +Assert-Contains 'subagent-retro first run' $script:Stdout '<retro-reminder>' +Assert-Contains 'subagent-retro first run' $script:Stdout 'FEAT-TEST-001' + +Write-Section 'subagent-retro (PowerShell): second run within debounce window is silent' +Invoke-Hook (Join-Path $repoRoot 'hooks\powershell\subagent-retro.ps1') $payload +Assert-Exit0 'subagent-retro second run (debounced)' $script:Code +Assert-Empty 'subagent-retro second run (debounced)' $script:Stdout + +# ---- cleanup + summary -------------------------------------------------------- + +Remove-Item -Recurse -Force $fixture -ErrorAction SilentlyContinue + +Write-Section 'Summary' +Write-Host " $($script:Pass) passed, $($script:Fail) failed" +if ($script:Fail -eq 0) { + Write-Host ' [OK] All hook smoke tests passed.' -ForegroundColor Green + exit 0 +} else { + Write-Host " [FAIL] $($script:Fail) smoke test(s) failed." -ForegroundColor Red + exit 1 +} diff --git a/scripts/smoke-hooks.sh b/scripts/smoke-hooks.sh new file mode 100644 index 0000000..f1054a0 --- /dev/null +++ b/scripts/smoke-hooks.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# specwright: hook smoke test (Unix / bash). +# +# Pipes sample Claude Code hook JSON into hooks/bash/*.sh against a fixture +# .specs/ tree and asserts exit codes + key output substrings - not just +# "did not crash". Mirror of scripts/smoke-hooks.ps1 (runs the PowerShell +# hook twins). Both must agree on the routed workflow for prompt-router. +# +# Exit 0 = all cases passed; 1 = at least one failed. + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" + +pass=0 +fail=0 + +if [[ -t 1 ]]; then + c_reset=$'\033[0m'; c_green=$'\033[32m'; c_red=$'\033[31m'; c_cyan=$'\033[36m' +else + c_reset=''; c_green=''; c_red=''; c_cyan='' +fi + +section() { echo; echo "${c_cyan}=== $* ===${c_reset}"; } +ok() { echo " ${c_green}[OK]${c_reset} $*"; pass=$((pass + 1)); } +bad() { echo " ${c_red}[FAIL]${c_reset} $*"; fail=$((fail + 1)); } + +assert_exit0() { + local desc="$1" code="$2" + if [[ "$code" -eq 0 ]]; then ok "$desc : exit 0"; else bad "$desc : exit $code (expected 0)"; fi +} + +assert_contains() { + local desc="$1" haystack="$2" needle="$3" + if [[ "$haystack" == *"$needle"* ]]; then + ok "$desc : contains \"$needle\"" + else + bad "$desc : missing \"$needle\" -- got: ${haystack:0:200}" + fi +} + +assert_empty() { + local desc="$1" haystack="$2" + if [[ -z "$haystack" ]]; then ok "$desc : empty output"; else bad "$desc : expected empty, got: ${haystack:0:200}"; fi +} + +# ---- fixture repo ----------------------------------------------------------- + +fixture="$(mktemp -d)" +cleanup() { rm -rf "$fixture"; } +trap cleanup EXIT + +mkdir -p "$fixture/.claude" "$fixture/.specs/FEAT-TEST-001" + +cat > "$fixture/.claude/project-config.json" <<JSON +{ + "spec": {"dir": ".specs", "indexFile": ".specs/index.md"}, + "ticket": {"pattern": "^[A-Z]+-[0-9]+\$"}, + "workflow": { + "keywords": { + "bug": ["bug", "fix", "broken", "error", "crash", "regression", "defect"], + "feature": ["feature", "add", "implement", "new", "support"], + "refactor": ["refactor", "restructure", "clean up", "extract", "rename"], + "perf": ["perf", "performance", "slow", "optimize", "latency", "throughput"], + "rca": ["incident", "outage", "rca", "root cause", "post-mortem", "postmortem"] + } + }, + "hooks": { + "userPromptRouter": {"enabled": true}, + "specGate": {"enabled": true, "mode": "block"}, + "subagentRetro": {"enabled": true, "retroStaleMinutes": 0, "debounceMinutes": 10} + } +} +JSON + +# in-progress spec with the marker and ID on the same line +cat > "$fixture/.specs/index.md" <<'MD' +| ID | Type | Status | Title | +|---|---|---|---| +| FEAT-TEST-001 | feature | in-progress | Test feature | +MD + +# "in-progress" appears only in a header/legend line - no row has it on the +# same line as a spec ID (mirrors the doc-02 same-line-detection fix). +cat > "$fixture/.specs/index-header-only.md" <<'MD' +| ID | Type | Status (in-progress = active work) | Title | +|---|---|---|---| +| FEAT-DONE-002 | feature | done | Finished feature | +MD + +run_hook() { + # run_hook <hook-script> <stdin-payload> -> sets STDOUT, STDERR, CODE + local hook="$1" payload="$2" + local out_file err_file + out_file="$(mktemp)"; err_file="$(mktemp)" + printf '%s' "$payload" | bash "$hook" >"$out_file" 2>"$err_file" + CODE=$? + STDOUT="$(cat "$out_file")" + STDERR="$(cat "$err_file")" + rm -f "$out_file" "$err_file" +} + +# ---- prompt-router: keyword match -------------------------------------------- + +section "prompt-router (bash): keyword match routes to /sd:bug" +payload="$(printf '{"prompt":"please fix this bug","cwd":"%s"}' "$fixture")" +run_hook "$repo_root/hooks/bash/prompt-router.sh" "$payload" +assert_exit0 "prompt-router keyword match" "$CODE" +assert_contains "prompt-router keyword match" "$STDOUT" "<context-router>" +assert_contains "prompt-router keyword match" "$STDOUT" "/sd:bug" +BASH_ROUTER_OUT="$STDOUT" + +# ---- spec-gate: (a) code edit with in-progress spec -> allow ---------------- + +section "spec-gate (bash): (a) code edit with in-progress spec -> allow" +payload="$(printf '{"tool_name":"Edit","cwd":"%s","tool_input":{"file_path":"%s/src/Foo.py"}}' "$fixture" "$fixture")" +run_hook "$repo_root/hooks/bash/spec-gate.sh" "$payload" +assert_exit0 "spec-gate (a) in-progress -> allow" "$CODE" +assert_empty "spec-gate (a) in-progress -> allow" "$STDOUT" + +# ---- spec-gate: (b) header-only "in-progress" -> block/warn, not allow ------ + +section "spec-gate (bash): (b) header-only in-progress text -> block (mode=block)" +config_block="$fixture/.claude/project-config.json" +cp "$fixture/.specs/index-header-only.md" "$fixture/.specs/index.md.bak-swap" +mv "$fixture/.specs/index.md" "$fixture/.specs/index.md.real" +mv "$fixture/.specs/index-header-only.md" "$fixture/.specs/index.md" +payload="$(printf '{"tool_name":"Edit","cwd":"%s","tool_input":{"file_path":"%s/src/Bar.py"}}' "$fixture" "$fixture")" +run_hook "$repo_root/hooks/bash/spec-gate.sh" "$payload" +assert_exit0 "spec-gate (b) header-only, mode=block" "$CODE" +assert_contains "spec-gate (b) header-only, mode=block" "$STDOUT" '"decision":"block"' +assert_contains "spec-gate (b) header-only, mode=block" "$STDOUT" '"permissionDecision":"deny"' + +section "spec-gate (bash): (b) header-only in-progress text -> warn (mode=warn)" +python_free_sed() { sed -i.bak 's/"mode": "block"/"mode": "warn"/' "$config_block" && rm -f "$config_block.bak"; } +python_free_sed +run_hook "$repo_root/hooks/bash/spec-gate.sh" "$payload" +assert_exit0 "spec-gate (b) header-only, mode=warn" "$CODE" +assert_empty "spec-gate (b) header-only, mode=warn stdout" "$STDOUT" +assert_contains "spec-gate (b) header-only, mode=warn stderr" "$STDERR" "[WARN]" +sed -i.bak 's/"mode": "warn"/"mode": "block"/' "$config_block" && rm -f "$config_block.bak" +mv "$fixture/.specs/index.md" "$fixture/.specs/index-header-only.md.used" +mv "$fixture/.specs/index.md.real" "$fixture/.specs/index.md" +rm -f "$fixture/.specs/index.md.bak-swap" "$fixture/.specs/index-header-only.md.used" + +# ---- spec-gate: (c) docs edit -> always allow -------------------------------- + +section "spec-gate (bash): (c) docs edit -> allow regardless of spec state" +payload="$(printf '{"tool_name":"Edit","cwd":"%s","tool_input":{"file_path":"%s/docs/guide.md"}}' "$fixture" "$fixture")" +run_hook "$repo_root/hooks/bash/spec-gate.sh" "$payload" +assert_exit0 "spec-gate (c) docs edit -> allow" "$CODE" +assert_empty "spec-gate (c) docs edit -> allow" "$STDOUT" + +# ---- spec-gate: (d) malformed JSON on stdin -> exit 0 silently -------------- + +section "spec-gate (bash): (d) malformed JSON on stdin -> exit 0 silently" +run_hook "$repo_root/hooks/bash/spec-gate.sh" '{not valid json' +assert_exit0 "spec-gate (d) malformed JSON" "$CODE" +assert_empty "spec-gate (d) malformed JSON" "$STDOUT" + +# ---- subagent-retro: missing retro names the spec, then debounces ---------- + +section "subagent-retro (bash): missing 05-retro.md names the real spec ID" +payload="$(printf '{"cwd":"%s","session_id":"smoke-test-session"}' "$fixture")" +run_hook "$repo_root/hooks/bash/subagent-retro.sh" "$payload" +assert_exit0 "subagent-retro first run" "$CODE" +assert_contains "subagent-retro first run" "$STDOUT" "<retro-reminder>" +assert_contains "subagent-retro first run" "$STDOUT" "FEAT-TEST-001" + +section "subagent-retro (bash): second run within debounce window is silent" +run_hook "$repo_root/hooks/bash/subagent-retro.sh" "$payload" +assert_exit0 "subagent-retro second run (debounced)" "$CODE" +assert_empty "subagent-retro second run (debounced)" "$STDOUT" + +# ---- summary ----------------------------------------------------------------- + +section "Summary" +echo " $pass passed, $fail failed" +if [[ $fail -eq 0 ]]; then + echo " ${c_green}[OK]${c_reset} All hook smoke tests passed." + exit 0 +else + echo " ${c_red}[FAIL]${c_reset} $fail smoke test(s) failed." + exit 1 +fi diff --git a/scripts/validate.ps1 b/scripts/validate.ps1 index fbf166c..64b2a81 100644 --- a/scripts/validate.ps1 +++ b/scripts/validate.ps1 @@ -31,12 +31,29 @@ $scriptDir = $PSScriptRoot $repoRoot = Split-Path -Parent $scriptDir # ---- expected install-target counts ---------------------------------------- -# One platform's hooks land per install (PowerShell hooks here), so 3 not 6. -$ExpectedCommands = 11 -$ExpectedAgents = 6 -$ExpectedSkills = 6 -$ExpectedHooks = 3 -$ExpectedTemplates = 9 +# Counts are derived from the source tree, not hardcoded - a new command/agent/skill/template +# only needs to land in its source dir, never a constant bumped in two scripts. Each count is +# asserted > 0 below so an empty/misnamed source dir fails loudly instead of vacuously passing. +# One platform's hooks land per install (PowerShell hooks here), so this counts +# hooks/powershell/*.ps1 only; Check 3 (hook-pair parity) already asserts the bash count matches. +$ExpectedCommands = (Get-ChildItem (Join-Path $repoRoot 'commands') -Filter *.md -File).Count +$ExpectedAgents = (Get-ChildItem (Join-Path $repoRoot 'agents') -Filter *.md -File).Count +$ExpectedSkills = (Get-ChildItem (Join-Path $repoRoot 'skills') -Filter 'SKILL.md' -File -Recurse).Count +$ExpectedHooks = (Get-ChildItem (Join-Path $repoRoot 'hooks\powershell') -Filter *.ps1 -File).Count +$ExpectedTemplates = (Get-ChildItem (Join-Path $repoRoot 'templates') -File -Recurse).Count + +foreach ($pair in @( + @{ Name = 'commands'; Count = $ExpectedCommands }, + @{ Name = 'agents'; Count = $ExpectedAgents }, + @{ Name = 'skills'; Count = $ExpectedSkills }, + @{ Name = 'hooks'; Count = $ExpectedHooks }, + @{ Name = 'templates'; Count = $ExpectedTemplates } +)) { + if ($pair.Count -eq 0) { + Write-Host "FATAL: derived expected count for $($pair.Name) is 0 - source dir empty or missing?" -ForegroundColor Red + exit 1 + } +} $ModelAliases = @('sonnet', 'haiku', 'opus', 'inherit') diff --git a/scripts/validate.sh b/scripts/validate.sh index 9f7d5e3..2f6a84f 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -18,12 +18,24 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "$script_dir/.." && pwd)" -# One platform's hooks land per install (bash hooks here), so 3 not 6. -EXPECTED_COMMANDS=11 -EXPECTED_AGENTS=6 -EXPECTED_SKILLS=6 -EXPECTED_HOOKS=3 -EXPECTED_TEMPLATES=9 +# Counts are derived from the source tree, not hardcoded - a new command/agent/skill/template +# only needs to land in its source dir, never a constant bumped in two scripts. Each count is +# asserted > 0 below so an empty/misnamed source dir fails loudly instead of vacuously passing. +# One platform's hooks land per install (bash hooks here), so this counts hooks/bash/*.sh only; +# Check 3 (hook-pair parity) already asserts the powershell count matches. +EXPECTED_COMMANDS="$(find "$repo_root/commands" -maxdepth 1 -type f -name '*.md' | wc -l | tr -d ' ')" +EXPECTED_AGENTS="$(find "$repo_root/agents" -maxdepth 1 -type f -name '*.md' | wc -l | tr -d ' ')" +EXPECTED_SKILLS="$(find "$repo_root/skills" -mindepth 2 -maxdepth 2 -type f -name 'SKILL.md' | wc -l | tr -d ' ')" +EXPECTED_HOOKS="$(find "$repo_root/hooks/bash" -maxdepth 1 -type f -name '*.sh' | wc -l | tr -d ' ')" +EXPECTED_TEMPLATES="$(find "$repo_root/templates" -type f | wc -l | tr -d ' ')" + +for pair in "commands:$EXPECTED_COMMANDS" "agents:$EXPECTED_AGENTS" "skills:$EXPECTED_SKILLS" \ + "hooks:$EXPECTED_HOOKS" "templates:$EXPECTED_TEMPLATES"; do + if [[ "${pair#*:}" -eq 0 ]]; then + echo "FATAL: derived expected count for ${pair%%:*} is 0 - source dir empty or missing?" >&2 + exit 1 + fi +done MODEL_ALIASES="sonnet haiku opus inherit" diff --git a/skills/sd-atomic-task-format/SKILL.md b/skills/sd-atomic-task-format/SKILL.md index 7512667..954b7ab 100644 --- a/skills/sd-atomic-task-format/SKILL.md +++ b/skills/sd-atomic-task-format/SKILL.md @@ -27,6 +27,18 @@ The first 9 fields are **required**, not optional. A task block missing any of t recommended otherwise. A block without the field is treated as `Pattern refs: none` (backward compatible with existing `.specs/` folders). +### Refactor mode adds one field + +`/sd:refactor` tasks append a 10th field after `Pattern refs`: + +```markdown +- **Parallel batch**: <batch number | "solo"> +``` + +Tasks sharing a batch number have disjoint file sets and no `Depends on` / `Conflicts with` +relationship between them - they are safe to execute in parallel. `solo` means the task cannot +be batched with any other. Other workflow types (feature, bug, perf) do not use this field. + --- ## Field rules diff --git a/skills/sd-evidence-citation/SKILL.md b/skills/sd-evidence-citation/SKILL.md index 3b6b6fd..25ad987 100644 --- a/skills/sd-evidence-citation/SKILL.md +++ b/skills/sd-evidence-citation/SKILL.md @@ -53,8 +53,8 @@ Every BLOCK or WARN must pair a `file:line` with a `§N.M` anchor. | Code | `file:line` + snippet | "I recall the code does X" | | Logs | Artifact path + quoted line | "Logs probably show..." | | DB | Query output (read-only) | "The table is probably slow" | -| Library | `mcp__context7__get-library-docs` result | Training-data memory | -| Web | `mcp__tavily__search` result + URL | General knowledge | +| Library | `mcp__context7__query-docs` result | Training-data memory | +| Web | `mcp__tavily__tavily_search` result + URL | General knowledge | If evidence is unavailable (e.g. no prod log access), state it explicitly: "Evidence unavailable — log access required. Marking INCONCLUSIVE." diff --git a/templates/constitution.template.md b/templates/constitution.template.md index ec9e0c5..7222a67 100644 --- a/templates/constitution.template.md +++ b/templates/constitution.template.md @@ -132,7 +132,7 @@ All transitions logged to the spec's `05-retro.md` with timestamp + reason. - **Static singletons holding state** - configuration is fine; mutable state is not. - **`// TODO` or `// HACK` in committed code** - either fix, ticket, or spec it. - **Hardcoded secrets** - use the configured secret store. -- **`dynamic` (C#) / `any` (TS)** - outside justified boundaries; document the exception inline. +- **Type-safety escapes** (e.g. `dynamic` in C#, `any` in TS) - outside justified boundaries; document the exception inline. - **Catch-and-swallow** - `catch { }` or `catch (Exception) { _logger.Log... }` without re-throw is forbidden. - **Direct DB calls from controllers** - must go through application layer. - **Opportunistic refactor inside a feature/bug spec** - separate spec; one concern per workflow. diff --git a/templates/project-config.template.json b/templates/project-config.template.json index 70a4c07..d4c5b2b 100644 --- a/templates/project-config.template.json +++ b/templates/project-config.template.json @@ -66,7 +66,8 @@ "rca": ["incident", "outage", "rca", "root cause", "post-mortem", "postmortem"] }, "gates": { - "feature": ["spec-approved", "plan-approved", "review-pass", "integration-pass"], + "_comment": "Descriptive only - no hook or command reads this block today; it documents each workflow's hard-gate sequence for humans/tooling that may consume it later.", + "feature": ["spec-approved", "plan-approved", "integration-review-pass"], "bug": ["symptom-captured", "reproduction-confirmed", "root-cause-confirmed", "failing-test-written", "regression-pass"], "rca": ["evidence-gathered", "hypotheses-enumerated", "root-cause-confirmed"], "refactor": ["spec-approved", "coverage-threshold-met", "post-test-added", "plan-approved", "batch-tests-green", "holistic-review-pass"], @@ -99,9 +100,9 @@ "enabled": false, "_use": "Fast symbol search, callers, call graph for sd-code-explorer" }, - "mssql": { + "database": { "enabled": false, - "_use": "Read-only schema and query-plan inspection in sd-debugger (SELECT/EXPLAIN only)" + "_use": "Read-only schema and query-plan inspection in sd-debugger (SELECT/EXPLAIN only) - set to whichever database MCP tool this project provides (e.g. mssql, postgres)" }, "playwright": { "enabled": false,