diff --git a/.specsmith/requirements.json b/.specsmith/requirements.json index 225edac..fe07bb5 100644 --- a/.specsmith/requirements.json +++ b/.specsmith/requirements.json @@ -3142,5 +3142,55 @@ "test_ids": [ "TEST-356" ] + }, + { + "id": "REQ-357", + "title": "Audit accepted_warnings Suppression in scaffold.yml", + "description": "scaffold.yml MUST support an `accepted_warnings` list field. When a check's name (or a canonical alias) appears in `accepted_warnings`, `specsmith audit` MUST render that check as `~ (accepted)` instead of `✗`, exclude it from the failure count and the non-zero exit code, and prevent `specsmith audit --fix` from auto-correcting that field. Supported canonical aliases MUST include at minimum: `scaffold_type_mismatch` (for the `type-mismatch` check), `ledger_line_threshold` (for `ledger-size`), and `open_todo_count` (for `ledger-open-todos`).", + "source": "GitHub issue", + "status": "implemented", + "test_ids": [ + "TEST-358" + ] + }, + { + "id": "REQ-358", + "title": "Sync Markdown Fallback When YAML Mode Has No YAML Files", + "description": "When `specsmith sync` is invoked in YAML-first mode (`governance-mode == yaml`) but `load_yaml_requirements` returns zero entries AND `docs/REQUIREMENTS.md` exists with non-trivial content (≥ 5 REQ- patterns), `sync` MUST fall back to Markdown parsing for the current sync run rather than treating the empty YAML result as authoritative. This prevents a fresh YAML-mode project from silently losing its Markdown-authored requirements in the JSON machine-state cache.", + "source": "GitHub issue", + "status": "implemented", + "test_ids": [ + "TEST-359" + ] + }, + { + "id": "REQ-359", + "title": "Phase Check _req_count Detects H2 REQ Headings", + "description": "The `_req_count` readiness check in `phase.py` MUST count requirement headings at both H2 (`##`) and H3 (`###`) depth. The current implementation only detects `^###\\s+REQ-` patterns, causing false `At least N requirements defined` failures for projects whose `REQUIREMENTS.md` uses `## REQ-DOMAIN-NNN` H2-style headings. The fix MUST also count `## REQ-` (H2) headings so phase-readiness percentages reflect the actual requirement count visible to `specsmith validate` and `specsmith audit`.", + "source": "GitHub issue", + "status": "implemented", + "test_ids": [ + "TEST-360" + ] + }, + { + "id": "REQ-360", + "title": "Skills Catalog Self-Referential Entries and Subdirectory Install Format", + "description": "specsmith.skills MUST include three self-referential SkillEntry entries in the GOVERNANCE domain: `specsmith` (master CLI reference), `specsmith-save` (save workflow), and `specsmith-audit` (audit workflow). These MUST be installable via `specsmith skill install `. The `install()` function MUST write skills to `.agents/skills//SKILL.md` (subdirectory format) rather than `.agents/skills/.md` (flat format) so Warp, Claude Code, and Codex discover them automatically. The `installed_skills()` function MUST detect both legacy flat files and subdirectory format.", + "source": "GitHub issue", + "status": "implemented", + "test_ids": [ + "TEST-361" + ] + }, + { + "id": "REQ-361", + "title": "Skills System Documented in RTD, README, AGENTS.md, and CHANGELOG", + "description": "The specsmith skills system MUST be documented in four locations: (1) `README.md` MUST have a `## Skills` section showing `specsmith skill list`, `specsmith skill install `, the `.agents/skills/` directory format, Warp/Claude Code/Codex compatibility, and the remote reference format `--skill \"layer1labs/specsmith:\"`. (2) `docs/site/skills-index.md` MUST include the three new `specsmith-*` skills in the Governance table. (3) `AGENTS.md` MUST mention `.agents/skills/` and list the three self-referential skills. (4) `CHANGELOG.md` MUST have an entry for the skills feature addition.", + "source": "GitHub issue", + "status": "implemented", + "test_ids": [ + "TEST-362" + ] } ] \ No newline at end of file diff --git a/.specsmith/testcases.json b/.specsmith/testcases.json index e22c9b3..097b905 100644 --- a/.specsmith/testcases.json +++ b/.specsmith/testcases.json @@ -3496,5 +3496,60 @@ "input": "Read src/specsmith/templates/agents.md.j2 directly; render via Jinja2 with minimal context", "expected_behavior": "Template contains Codity section with review --staged, HIGH severity, MEDIUM, integrate codity", "confidence": 0.95 + }, + { + "id": "TEST-358", + "title": "accepted_warnings Suppresses Matching Audit Check", + "description": "When scaffold.yml contains `accepted_warnings: [scaffold_type_mismatch]` and the type-mismatch check fires, `run_audit` MUST mark that result as suppressed=True, AuditReport.failed MUST NOT count it, AuditReport.healthy MUST be True if no other failures exist, and the CLI MUST render it as '~ type-mismatch (accepted)' rather than '✗ type-mismatch'. ledger_line_threshold suppresses ledger-size similarly.", + "requirement_id": "REQ-357", + "type": "unit", + "verification_method": "pytest", + "input": "run_audit(tmp_path) with scaffold.yml containing type!=detected AND accepted_warnings: [scaffold_type_mismatch]; repeat for ledger_line_threshold", + "expected_behavior": "suppressed=True on matched result; failed count excludes suppressed; healthy=True; ledger-size suppressed by ledger_line_threshold alias", + "confidence": 0.95 + }, + { + "id": "TEST-359", + "title": "Sync Falls Back to Markdown When YAML Mode Has No YAML Files", + "description": "`run_sync(root)` on a project where governance-mode=yaml but docs/requirements/ has no .yml files AND docs/REQUIREMENTS.md has >= 5 REQ- patterns MUST parse the Markdown and populate .specsmith/requirements.json with those requirements rather than writing an empty list. The sync result MUST show reqs_after >= 5.", + "requirement_id": "REQ-358", + "type": "unit", + "verification_method": "pytest", + "input": "tmp_path with .specsmith/governance-mode=yaml; no docs/requirements/*.yml; docs/REQUIREMENTS.md with 6 ## REQ-BE-NNN headings; run_sync(root)", + "expected_behavior": "requirements.json contains 6 entries; reqs_after=6", + "confidence": 0.95 + }, + { + "id": "TEST-360", + "title": "_req_count Returns True for H2 REQ Headings", + "description": "`_req_count(5)(root)` MUST return True when docs/REQUIREMENTS.md uses `## REQ-BE-001` through `## REQ-BE-005` H2 headings (not H3 `###`). Currently it returns False for H2 headings, causing false phase failures on domain-namespaced Markdown projects.", + "requirement_id": "REQ-359", + "type": "unit", + "verification_method": "pytest", + "input": "tmp_path/docs/REQUIREMENTS.md with 5 `## REQ-BE-NNN: Title` H2 headings; _req_count(5)(tmp_path)", + "expected_behavior": "Returns True", + "confidence": 0.95 + }, + { + "id": "TEST-361", + "title": "Skills Catalog Contains specsmith/specsmith-save/specsmith-audit Entries", + "description": "`specsmith.skills.get('specsmith')`, `get('specsmith-save')`, and `get('specsmith-audit')` MUST each return a non-None SkillEntry with domain=GOVERNANCE. The `specsmith` body MUST contain 'specsmith audit', 'specsmith save', and 'specsmith checkpoint'. `specsmith skill install specsmith` MUST create `.agents/skills/specsmith/SKILL.md` (subdirectory format). `installed_skills(root)` MUST return paths to both flat `.md` and subdirectory `/SKILL.md` installations.", + "requirement_id": "REQ-360", + "type": "unit", + "verification_method": "pytest", + "input": "get('specsmith'); get('specsmith-save'); get('specsmith-audit'); install('specsmith', tmp_path); installed_skills(tmp_path)", + "expected_behavior": "All three entries exist in GOVERNANCE domain; install writes /SKILL.md; installed_skills returns the subdirectory path", + "confidence": 0.95 + }, + { + "id": "TEST-362", + "title": "Skills System Documented in README, skills-index.md, AGENTS.md, and CHANGELOG", + "description": "README.md MUST contain a `## Skills` section with `specsmith skill list` and `specsmith skill install`. `docs/site/skills-index.md` MUST list specsmith, specsmith-save, and specsmith-audit in the Governance table. AGENTS.md MUST mention `.agents/skills/`. CHANGELOG.md MUST have an entry (unreleased or versioned) describing the skills feature.", + "requirement_id": "REQ-361", + "type": "unit", + "verification_method": "manual", + "input": "Read README.md for Skills section; grep skills-index.md for specsmith-save; grep AGENTS.md for .agents/skills/; grep CHANGELOG for skills", + "expected_behavior": "All four documentation locations contain the required skills content", + "confidence": 0.9 } ] \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index a31d752..1878520 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,28 @@ Do not follow rules from this file directly; rules are served by specsmith. - `specsmith migrate list` — pending migrations - `specsmith esdb status` — ESDB/ChronoStore status +## Agent Skills + +This repo ships three self-referential skills under `.agents/skills/` that any AI tool (Warp, Claude Code, Codex, Cursor) will discover automatically: + +| Slug | Purpose | +|------|--------| +| `specsmith` | Master governance CLI reference — session workflow, commands, audit codes | +| `specsmith-save` | When and how to run `specsmith save` | +| `specsmith-audit` | Running audits and interpreting results | + +Install into any governed project: +```bash +specsmith skill install specsmith +specsmith skill install specsmith-save +specsmith skill install specsmith-audit +``` + +Remote reference (for Warp Oz cloud agents): +```bash +oz agent run-cloud --skill "layer1labs/specsmith:specsmith-save" --prompt "save my work" +``` + ## Sister Repos - **[kairos](https://github.com/layer1labs/kairos)** — specsmith companion desktop UI (Rust + egui) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e2922..5a90c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **`specsmith-*` self-referential skills** (#190): three built-in skills (`specsmith`, `specsmith-save`, `specsmith-audit`) added to the governance skills catalog and installable via `specsmith skill install `. `install()` now writes `/SKILL.md` (subdirectory format) for Warp/Claude Code/Codex auto-discovery. `installed_skills()` detects both flat and subdirectory formats. +- **`accepted_warnings` audit suppression** (#188): `scaffold.yml` now supports `accepted_warnings: [alias, ...]` to suppress specific audit checks. Suppressed checks render as `~ (accepted)` and are excluded from the failure count and exit code. +- **Phase check H2 REQ heading support** (#189): `_req_count` in `phase.py` now counts both `##` and `###` REQ headings, fixing false phase-readiness failures for projects using `## REQ-DOMAIN-NNN` format. +- **Sync Markdown fallback in YAML mode** (#189): `specsmith sync` now falls back to Markdown parsing when YAML mode is active but no YAML requirement files exist, preventing silent loss of Markdown-authored requirements. + ## [0.11.7] - 2026-05-24 ### Added diff --git a/README.md b/README.md index 1461018..39f4f6d 100644 --- a/README.md +++ b/README.md @@ -885,6 +885,43 @@ See the `codity-ai-review` governance skill (`specsmith skill install codity-ai- --- +## Skills + +specsmith ships **70+ built-in skills** across 11 domains that AI agents (Warp, Claude Code, Codex, Cursor) can install and use. + +```bash +# List all available skills +specsmith skill list + +# Search by keyword +specsmith skill search zephyr + +# Install a skill into .agents/skills/ +specsmith skill install specsmith +specsmith skill install specsmith-save +specsmith skill install specsmith-audit +``` + +Skills are installed as `.agents/skills//SKILL.md` and are auto-discovered by any AI tool that scans `.agents/skills/`. + +### Self-referential governance skills + +Three skills document specsmith itself: + +| Slug | Purpose | +|------|--------| +| `specsmith` | Master CLI reference — session workflow, commands, audit codes | +| `specsmith-save` | When and how to run `specsmith save` | +| `specsmith-audit` | Running audits and interpreting results | + +### Remote reference (Warp Oz cloud agents) + +```bash +oz agent run-cloud --skill "layer1labs/specsmith:specsmith-save" --prompt "save my work" +``` + +--- + ## The specsmith Bootstrap specsmith governs itself — the specsmith repo is a specsmith-managed project. Run `specsmith audit` diff --git a/docs/LEDGER.md b/docs/LEDGER.md index 8ae3296..f8474ea 100644 --- a/docs/LEDGER.md +++ b/docs/LEDGER.md @@ -308,3 +308,9 @@ - **Status**: complete - **Epistemic status**: high - **Chain hash**: `13907d72d47f3708...` + +## 2026-05-29T22:14 — specsmith migration: 0.11.3.dev420 → 0.11.7 +- **Author**: specsmith +- **Type**: migration +- **Status**: complete +- **Chain hash**: `cf168f65d973f62b...` diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index f9a0bf4..a3e323f 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -2514,3 +2514,43 @@ - **Source:** ARCHITECTURE.md §39 - **Test_Ids:** ['TEST-356'] +## REQ-357. Audit accepted_warnings Suppression in scaffold.yml +- **ID:** REQ-357 +- **Title:** Audit accepted_warnings Suppression in scaffold.yml +- **Description:** scaffold.yml MUST support an `accepted_warnings` list field. When a check's name (or a canonical alias) appears in `accepted_warnings`, `specsmith audit` MUST render that check as `~ (accepted)` instead of `✗`, exclude it from the failure count and the non-zero exit code, and prevent `specsmith audit --fix` from auto-correcting that field. Supported canonical aliases MUST include at minimum: `scaffold_type_mismatch` (for the `type-mismatch` check), `ledger_line_threshold` (for `ledger-size`), and `open_todo_count` (for `ledger-open-todos`). +- **Status:** implemented +- **Source:** GitHub issue +- **Test_Ids:** ['TEST-358'] + +## REQ-358. Sync Markdown Fallback When YAML Mode Has No YAML Files +- **ID:** REQ-358 +- **Title:** Sync Markdown Fallback When YAML Mode Has No YAML Files +- **Description:** When `specsmith sync` is invoked in YAML-first mode (`governance-mode == yaml`) but `load_yaml_requirements` returns zero entries AND `docs/REQUIREMENTS.md` exists with non-trivial content (≥ 5 REQ- patterns), `sync` MUST fall back to Markdown parsing for the current sync run rather than treating the empty YAML result as authoritative. This prevents a fresh YAML-mode project from silently losing its Markdown-authored requirements in the JSON machine-state cache. +- **Status:** implemented +- **Source:** GitHub issue +- **Test_Ids:** ['TEST-359'] + +## REQ-359. Phase Check _req_count Detects H2 REQ Headings +- **ID:** REQ-359 +- **Title:** Phase Check _req_count Detects H2 REQ Headings +- **Description:** The `_req_count` readiness check in `phase.py` MUST count requirement headings at both H2 (`##`) and H3 (`###`) depth. The current implementation only detects `^###\s+REQ-` patterns, causing false `At least N requirements defined` failures for projects whose `REQUIREMENTS.md` uses `## REQ-DOMAIN-NNN` H2-style headings. The fix MUST also count `## REQ-` (H2) headings so phase-readiness percentages reflect the actual requirement count visible to `specsmith validate` and `specsmith audit`. +- **Status:** implemented +- **Source:** GitHub issue +- **Test_Ids:** ['TEST-360'] + +## REQ-360. Skills Catalog Self-Referential Entries and Subdirectory Install Format +- **ID:** REQ-360 +- **Title:** Skills Catalog Self-Referential Entries and Subdirectory Install Format +- **Description:** specsmith.skills MUST include three self-referential SkillEntry entries in the GOVERNANCE domain: `specsmith` (master CLI reference), `specsmith-save` (save workflow), and `specsmith-audit` (audit workflow). These MUST be installable via `specsmith skill install `. The `install()` function MUST write skills to `.agents/skills//SKILL.md` (subdirectory format) rather than `.agents/skills/.md` (flat format) so Warp, Claude Code, and Codex discover them automatically. The `installed_skills()` function MUST detect both legacy flat files and subdirectory format. +- **Status:** implemented +- **Source:** GitHub issue +- **Test_Ids:** ['TEST-361'] + +## REQ-361. Skills System Documented in RTD, README, AGENTS.md, and CHANGELOG +- **ID:** REQ-361 +- **Title:** Skills System Documented in RTD, README, AGENTS.md, and CHANGELOG +- **Description:** The specsmith skills system MUST be documented in four locations: (1) `README.md` MUST have a `## Skills` section showing `specsmith skill list`, `specsmith skill install `, the `.agents/skills/` directory format, Warp/Claude Code/Codex compatibility, and the remote reference format `--skill "layer1labs/specsmith:"`. (2) `docs/site/skills-index.md` MUST include the three new `specsmith-*` skills in the Governance table. (3) `AGENTS.md` MUST mention `.agents/skills/` and list the three self-referential skills. (4) `CHANGELOG.md` MUST have an entry for the skills feature addition. +- **Status:** implemented +- **Source:** GitHub issue +- **Test_Ids:** ['TEST-362'] + diff --git a/docs/TESTS.md b/docs/TESTS.md index bd2749c..f6ece95 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -2979,3 +2979,58 @@ - **Expected Behavior:** Template contains Codity section with review --staged, HIGH severity, MEDIUM, integrate codity - **Confidence:** 0.95 +## TEST-358. accepted_warnings Suppresses Matching Audit Check +- **ID:** TEST-358 +- **Title:** accepted_warnings Suppresses Matching Audit Check +- **Description:** When scaffold.yml contains `accepted_warnings: [scaffold_type_mismatch]` and the type-mismatch check fires, `run_audit` MUST mark that result as suppressed=True, AuditReport.failed MUST NOT count it, AuditReport.healthy MUST be True if no other failures exist, and the CLI MUST render it as '~ type-mismatch (accepted)' rather than '✗ type-mismatch'. ledger_line_threshold suppresses ledger-size similarly. +- **Requirement ID:** REQ-357 +- **Type:** unit +- **Verification Method:** pytest +- **Input:** run_audit(tmp_path) with scaffold.yml containing type!=detected AND accepted_warnings: [scaffold_type_mismatch]; repeat for ledger_line_threshold +- **Expected Behavior:** suppressed=True on matched result; failed count excludes suppressed; healthy=True; ledger-size suppressed by ledger_line_threshold alias +- **Confidence:** 0.95 + +## TEST-359. Sync Falls Back to Markdown When YAML Mode Has No YAML Files +- **ID:** TEST-359 +- **Title:** Sync Falls Back to Markdown When YAML Mode Has No YAML Files +- **Description:** `run_sync(root)` on a project where governance-mode=yaml but docs/requirements/ has no .yml files AND docs/REQUIREMENTS.md has >= 5 REQ- patterns MUST parse the Markdown and populate .specsmith/requirements.json with those requirements rather than writing an empty list. The sync result MUST show reqs_after >= 5. +- **Requirement ID:** REQ-358 +- **Type:** unit +- **Verification Method:** pytest +- **Input:** tmp_path with .specsmith/governance-mode=yaml; no docs/requirements/*.yml; docs/REQUIREMENTS.md with 6 ## REQ-BE-NNN headings; run_sync(root) +- **Expected Behavior:** requirements.json contains 6 entries; reqs_after=6 +- **Confidence:** 0.95 + +## TEST-360. _req_count Returns True for H2 REQ Headings +- **ID:** TEST-360 +- **Title:** _req_count Returns True for H2 REQ Headings +- **Description:** `_req_count(5)(root)` MUST return True when docs/REQUIREMENTS.md uses `## REQ-BE-001` through `## REQ-BE-005` H2 headings (not H3 `###`). Currently it returns False for H2 headings, causing false phase failures on domain-namespaced Markdown projects. +- **Requirement ID:** REQ-359 +- **Type:** unit +- **Verification Method:** pytest +- **Input:** tmp_path/docs/REQUIREMENTS.md with 5 `## REQ-BE-NNN: Title` H2 headings; _req_count(5)(tmp_path) +- **Expected Behavior:** Returns True +- **Confidence:** 0.95 + +## TEST-361. Skills Catalog Contains specsmith/specsmith-save/specsmith-audit Entries +- **ID:** TEST-361 +- **Title:** Skills Catalog Contains specsmith/specsmith-save/specsmith-audit Entries +- **Description:** `specsmith.skills.get('specsmith')`, `get('specsmith-save')`, and `get('specsmith-audit')` MUST each return a non-None SkillEntry with domain=GOVERNANCE. The `specsmith` body MUST contain 'specsmith audit', 'specsmith save', and 'specsmith checkpoint'. `specsmith skill install specsmith` MUST create `.agents/skills/specsmith/SKILL.md` (subdirectory format). `installed_skills(root)` MUST return paths to both flat `.md` and subdirectory `/SKILL.md` installations. +- **Requirement ID:** REQ-360 +- **Type:** unit +- **Verification Method:** pytest +- **Input:** get('specsmith'); get('specsmith-save'); get('specsmith-audit'); install('specsmith', tmp_path); installed_skills(tmp_path) +- **Expected Behavior:** All three entries exist in GOVERNANCE domain; install writes /SKILL.md; installed_skills returns the subdirectory path +- **Confidence:** 0.95 + +## TEST-362. Skills System Documented in README, skills-index.md, AGENTS.md, and CHANGELOG +- **ID:** TEST-362 +- **Title:** Skills System Documented in README, skills-index.md, AGENTS.md, and CHANGELOG +- **Description:** README.md MUST contain a `## Skills` section with `specsmith skill list` and `specsmith skill install`. `docs/site/skills-index.md` MUST list specsmith, specsmith-save, and specsmith-audit in the Governance table. AGENTS.md MUST mention `.agents/skills/`. CHANGELOG.md MUST have an entry (unreleased or versioned) describing the skills feature. +- **Requirement ID:** REQ-361 +- **Type:** unit +- **Verification Method:** manual +- **Input:** Read README.md for Skills section; grep skills-index.md for specsmith-save; grep AGENTS.md for .agents/skills/; grep CHANGELOG for skills +- **Expected Behavior:** All four documentation locations contain the required skills content +- **Confidence:** 0.9 + diff --git a/docs/requirements/overflow.yml b/docs/requirements/overflow.yml index ab1b0c3..065eba9 100644 --- a/docs/requirements/overflow.yml +++ b/docs/requirements/overflow.yml @@ -377,3 +377,64 @@ azure, staged, pre-commit and discoverable via specsmith skill list. source: ARCHITECTURE.md §39 status: implemented +- id: REQ-357 + title: Audit accepted_warnings Suppression in scaffold.yml + description: >- + scaffold.yml MUST support an `accepted_warnings` list field. When a check's name + (or a canonical alias) appears in `accepted_warnings`, `specsmith audit` MUST render + that check as `~ (accepted)` instead of `✗`, exclude it from the failure + count and the non-zero exit code, and prevent `specsmith audit --fix` from + auto-correcting that field. Supported canonical aliases MUST include at minimum: + `scaffold_type_mismatch` (for the `type-mismatch` check), `ledger_line_threshold` + (for `ledger-size`), and `open_todo_count` (for `ledger-open-todos`). + source: GitHub issue #188 + status: implemented +- id: REQ-358 + title: Sync Markdown Fallback When YAML Mode Has No YAML Files + description: >- + When `specsmith sync` is invoked in YAML-first mode (`governance-mode == yaml`) + but `load_yaml_requirements` returns zero entries AND `docs/REQUIREMENTS.md` exists + with non-trivial content (≥ 5 REQ- patterns), `sync` MUST fall back to Markdown + parsing for the current sync run rather than treating the empty YAML result as + authoritative. This prevents a fresh YAML-mode project from silently losing its + Markdown-authored requirements in the JSON machine-state cache. + source: GitHub issue #189 + status: implemented +- id: REQ-359 + title: Phase Check _req_count Detects H2 REQ Headings + description: >- + The `_req_count` readiness check in `phase.py` MUST count requirement headings at + both H2 (`##`) and H3 (`###`) depth. The current implementation only detects + `^###\s+REQ-` patterns, causing false `At least N requirements defined` failures for + projects whose `REQUIREMENTS.md` uses `## REQ-DOMAIN-NNN` H2-style headings. + The fix MUST also count `## REQ-` (H2) headings so phase-readiness percentages + reflect the actual requirement count visible to `specsmith validate` and `specsmith audit`. + source: GitHub issue #189 + status: implemented +- id: REQ-360 + title: Skills Catalog Self-Referential Entries and Subdirectory Install Format + description: >- + specsmith.skills MUST include three self-referential SkillEntry entries in the + GOVERNANCE domain: `specsmith` (master CLI reference), `specsmith-save` (save + workflow), and `specsmith-audit` (audit workflow). These MUST be installable via + `specsmith skill install `. The `install()` function MUST write skills to + `.agents/skills//SKILL.md` (subdirectory format) rather than `.agents/skills/.md` + (flat format) so Warp, Claude Code, and Codex discover them automatically. The + `installed_skills()` function MUST detect both legacy flat files and subdirectory + format. + source: GitHub issue #190 + status: implemented +- id: REQ-361 + title: Skills System Documented in RTD, README, AGENTS.md, and CHANGELOG + description: >- + The specsmith skills system MUST be documented in four locations: (1) `README.md` + MUST have a `## Skills` section showing `specsmith skill list`, `specsmith skill install + `, the `.agents/skills/` directory format, Warp/Claude Code/Codex compatibility, + and the remote reference format `--skill "layer1labs/specsmith:"`. (2) + `docs/site/skills-index.md` MUST include the three new `specsmith-*` skills in the + Governance table. (3) `AGENTS.md` MUST mention `.agents/skills/` and list the three + self-referential skills. (4) `CHANGELOG.md` MUST have an entry for the skills + feature addition. + source: GitHub issue #190 + status: implemented + diff --git a/docs/site/skills-index.md b/docs/site/skills-index.md index 1747619..a0e73d6 100644 --- a/docs/site/skills-index.md +++ b/docs/site/skills-index.md @@ -16,7 +16,7 @@ Each skill is a curated `SKILL.md` injected into the agent context with --- -## Governance (11) +## Governance (14) Skills for project governance workflows, verification, release management, ESDB, CI polling, IP prosecution, and AI code review. @@ -31,6 +31,9 @@ Skills for project governance workflows, verification, release management, ESDB, | `patent-prosecution-workflow` | Patent Prosecution Workflow — prior-art, USPTO MCP, PAR | patent, uspto, ppubs, claim-themes, ip | | `planner` | Planner — propose-then-execute | planning, aee, governance | | `release-pilot` | Release Pilot — gitflow release cut | git, semver, release, gitflow | +| `specsmith` | Specsmith — master governance CLI reference | specsmith, aee, session, audit, phase | +| `specsmith-audit` | Specsmith Audit — drift detection and governance health | specsmith, audit, drift, health, aee | +| `specsmith-save` | Specsmith Save — governance-aware save workflow | specsmith, save, commit, esdb, backup | | `specsmith-session-governance` | Specsmith Session Governance — drift prevention, heartbeat, preflight gate | governance, session, drift, checkpoint, anchor | | `verifier` | Verifier — five-gate verification | audit, tests, verification | diff --git a/docs/specsmith.yml b/docs/specsmith.yml index fedf386..6d7b7ab 100644 --- a/docs/specsmith.yml +++ b/docs/specsmith.yml @@ -12,9 +12,10 @@ platforms: - linux - windows - macos -spec_version: 0.11.3.dev420 +spec_version: 0.11.7 aee_phase: release -description: Applied Epistemic Engineering toolkit for AI-assisted development. Governance backend for the Kairos terminal (BitConcepts/kairos). +description: Applied Epistemic Engineering toolkit for AI-assisted development. Governance + backend for the Kairos terminal (BitConcepts/kairos). vcs_platform: github branching_strategy: gitflow default_branch: main diff --git a/docs/tests/overflow.yml b/docs/tests/overflow.yml index 64f9780..9c2d521 100644 --- a/docs/tests/overflow.yml +++ b/docs/tests/overflow.yml @@ -488,3 +488,85 @@ input: Read src/specsmith/templates/agents.md.j2 directly; render via Jinja2 with minimal context expected_behavior: Template contains Codity section with review --staged, HIGH severity, MEDIUM, integrate codity confidence: 0.95 +- id: TEST-358 + title: accepted_warnings Suppresses Matching Audit Check + description: >- + When scaffold.yml contains `accepted_warnings: [scaffold_type_mismatch]` and the + type-mismatch check fires, `run_audit` MUST mark that result as suppressed=True, + AuditReport.failed MUST NOT count it, AuditReport.healthy MUST be True if no other + failures exist, and the CLI MUST render it as '~ type-mismatch (accepted)' rather + than '✗ type-mismatch'. ledger_line_threshold suppresses ledger-size similarly. + requirement_id: REQ-357 + type: unit + verification_method: pytest + input: >- + run_audit(tmp_path) with scaffold.yml containing type!=detected AND + accepted_warnings: [scaffold_type_mismatch]; repeat for ledger_line_threshold + expected_behavior: >- + suppressed=True on matched result; failed count excludes suppressed; healthy=True; + ledger-size suppressed by ledger_line_threshold alias + confidence: 0.95 +- id: TEST-359 + title: Sync Falls Back to Markdown When YAML Mode Has No YAML Files + description: >- + `run_sync(root)` on a project where governance-mode=yaml but docs/requirements/ has + no .yml files AND docs/REQUIREMENTS.md has >= 5 REQ- patterns MUST parse the Markdown + and populate .specsmith/requirements.json with those requirements rather than writing + an empty list. The sync result MUST show reqs_after >= 5. + requirement_id: REQ-358 + type: unit + verification_method: pytest + input: >- + tmp_path with .specsmith/governance-mode=yaml; no docs/requirements/*.yml; + docs/REQUIREMENTS.md with 6 ## REQ-BE-NNN headings; run_sync(root) + expected_behavior: requirements.json contains 6 entries; reqs_after=6 + confidence: 0.95 +- id: TEST-360 + title: _req_count Returns True for H2 REQ Headings + description: >- + `_req_count(5)(root)` MUST return True when docs/REQUIREMENTS.md uses `## REQ-BE-001` + through `## REQ-BE-005` H2 headings (not H3 `###`). Currently it returns False for H2 + headings, causing false phase failures on domain-namespaced Markdown projects. + requirement_id: REQ-359 + type: unit + verification_method: pytest + input: >- + tmp_path/docs/REQUIREMENTS.md with 5 `## REQ-BE-NNN: Title` H2 headings; + _req_count(5)(tmp_path) + expected_behavior: Returns True + confidence: 0.95 +- id: TEST-361 + title: Skills Catalog Contains specsmith/specsmith-save/specsmith-audit Entries + description: >- + `specsmith.skills.get('specsmith')`, `get('specsmith-save')`, and + `get('specsmith-audit')` MUST each return a non-None SkillEntry with + domain=GOVERNANCE. The `specsmith` body MUST contain 'specsmith audit', 'specsmith save', + and 'specsmith checkpoint'. `specsmith skill install specsmith` MUST create + `.agents/skills/specsmith/SKILL.md` (subdirectory format). `installed_skills(root)` MUST + return paths to both flat `.md` and subdirectory `/SKILL.md` installations. + requirement_id: REQ-360 + type: unit + verification_method: pytest + input: >- + get('specsmith'); get('specsmith-save'); get('specsmith-audit'); + install('specsmith', tmp_path); installed_skills(tmp_path) + expected_behavior: >- + All three entries exist in GOVERNANCE domain; install writes /SKILL.md; + installed_skills returns the subdirectory path + confidence: 0.95 +- id: TEST-362 + title: Skills System Documented in README, skills-index.md, AGENTS.md, and CHANGELOG + description: >- + README.md MUST contain a `## Skills` section with `specsmith skill list` and + `specsmith skill install`. `docs/site/skills-index.md` MUST list specsmith, specsmith-save, + and specsmith-audit in the Governance table. AGENTS.md MUST mention `.agents/skills/`. + CHANGELOG.md MUST have an entry (unreleased or versioned) describing the skills feature. + requirement_id: REQ-361 + type: unit + verification_method: manual + input: >- + Read README.md for Skills section; grep skills-index.md for specsmith-save; + grep AGENTS.md for .agents/skills/; grep CHANGELOG for skills + expected_behavior: >- + All four documentation locations contain the required skills content + confidence: 0.9 diff --git a/src/specsmith/auditor.py b/src/specsmith/auditor.py index 1c6bbbc..ab15d06 100644 --- a/src/specsmith/auditor.py +++ b/src/specsmith/auditor.py @@ -23,6 +23,7 @@ class AuditResult: passed: bool message: str fixable: bool = False + suppressed: bool = False @dataclass @@ -37,7 +38,7 @@ def passed(self) -> int: @property def failed(self) -> int: - return sum(1 for r in self.results if not r.passed) + return sum(1 for r in self.results if not r.passed and not r.suppressed) @property def fixable(self) -> int: @@ -47,6 +48,39 @@ def fixable(self) -> int: def healthy(self) -> bool: return self.failed == 0 + @property + def suppressed_count(self) -> int: + return sum(1 for r in self.results if r.suppressed) + + +# --------------------------------------------------------------------------- +# Suppression aliases (scaffold.yml accepted_warnings → AuditResult.name) +# --------------------------------------------------------------------------- + +_SUPPRESSION_ALIASES: dict[str, str] = { + "scaffold_type_mismatch": "type-mismatch", + "ledger_line_threshold": "ledger-size", + "open_todo_count": "ledger-open-todos", + "ledger_size": "ledger-size", +} + + +def _apply_accepted_warnings(report: AuditReport, accepted: list[str]) -> None: + """Mark audit results matching *accepted* warning names as suppressed. + + Each entry in *accepted* is either a key in ``_SUPPRESSION_ALIASES`` or a + direct ``AuditResult.name`` (exact match or prefix match up to ``:``). + Matched results are set to ``suppressed=True`` and ``passed=True`` so they + no longer count as failures. + """ + resolved: list[str] = [_SUPPRESSION_ALIASES.get(a, a) for a in accepted] + for result in report.results: + for name in resolved: + if result.name == name or result.name.startswith(name + ":"): + result.suppressed = True + result.passed = True + break + # --------------------------------------------------------------------------- # Governance file existence checks @@ -386,10 +420,7 @@ def check_ledger_health(root: Path) -> list[AuditResult]: # Size check — uses configurable threshold (#145) threshold = _get_ledger_threshold(root) - # Check audit_suppressions for opt-out - raw = _read_scaffold_raw(root) - suppressed = "ledger_size" in (raw.get("audit_suppressions") or []) - if not suppressed and line_count > threshold: + if line_count > threshold: results.append( AuditResult( name="ledger-size", @@ -1171,6 +1202,15 @@ def run_audit(root: Path) -> AuditReport: report.results.extend(check_industrial_artifacts(root)) report.results.extend(check_derived_artifacts(root)) report.results.extend(check_cross_repo_dependencies(root)) + + # Apply accepted_warnings / audit_suppressions from scaffold.yml (REQ-357) + raw = _read_scaffold_raw(root) + _accepted: list[str] = list(raw.get("accepted_warnings") or []) + # backward-compat: audit_suppressions list (pre-#188 ledger_size suppression) + _old_suppressed: list[str] = list(raw.get("audit_suppressions") or []) + _accepted = _accepted + _old_suppressed + if _accepted: + _apply_accepted_warnings(report, _accepted) return report @@ -1182,7 +1222,7 @@ def run_auto_fix(root: Path, report: AuditReport) -> list[str]: fixed: list[str] = [] for result in report.results: - if result.passed: + if result.passed or result.suppressed: continue # Fix missing required files with minimal stubs diff --git a/src/specsmith/cli.py b/src/specsmith/cli.py index bd60f55..b37a381 100644 --- a/src/specsmith/cli.py +++ b/src/specsmith/cli.py @@ -441,8 +441,14 @@ def audit(fix: bool, project_dir: str) -> None: report = run_audit(root) for r in report.results: - icon = "[green]✓[/green]" if r.passed else "[red]✗[/red]" - console.print(f" {icon} {r.message}") + if r.suppressed: + icon = "[dim]~[/dim]" + elif r.passed: + icon = "[green]✓[/green]" + else: + icon = "[red]✗[/red]" + msg = r.message + " [dim](accepted)[/dim]" if r.suppressed else r.message + console.print(f" {icon} {msg}") console.print() if report.healthy: @@ -8524,9 +8530,19 @@ def skill_list(project_dir: str, as_json: bool) -> None: from specsmith import skills as _skills root = Path(project_dir).resolve() - installed = [p.name for p in _skills.installed_skills(root)] + installed_paths = _skills.installed_skills(root) + # Build identifier list: slug (subdir) or filename (flat legacy) + installed: list[str] = [] + installed_slugs: set[str] = set() + for p in installed_paths: + if p.name == "SKILL.md": # subdir format: /SKILL.md + installed.append(p.parent.name) + installed_slugs.add(p.parent.name) + else: # legacy flat format: .md + installed.append(p.name) + installed_slugs.add(p.stem) catalog = [ - {"slug": entry.slug, "name": entry.name, "installed": f"{entry.slug}.md" in installed} + {"slug": entry.slug, "name": entry.name, "installed": entry.slug in installed_slugs} for entry in _skills.CATALOG ] if as_json: diff --git a/src/specsmith/phase.py b/src/specsmith/phase.py index aa536b5..cd05843 100644 --- a/src/specsmith/phase.py +++ b/src/specsmith/phase.py @@ -77,8 +77,8 @@ def _check(root: Path) -> bool: if p.exists(): try: text = p.read_text(encoding="utf-8", errors="ignore") - # Support both ### REQ-NNN (old) and ## N. Title / - **ID:** REQ-NNN (new) - count = len(re.findall(r"^###\s+REQ-", text, re.MULTILINE)) + # Support H2/H3 headings (## REQ-NNN, ### REQ-NNN, ## REQ-BE-001) (REQ-359) + count = len(re.findall(r"^#{2,3}\s+REQ-", text, re.MULTILINE)) if count == 0: count = len(re.findall(r"- \*\*ID:\*\* REQ-", text)) if count == 0: diff --git a/src/specsmith/skills/__init__.py b/src/specsmith/skills/__init__.py index 2f1f09f..83a1bd9 100644 --- a/src/specsmith/skills/__init__.py +++ b/src/specsmith/skills/__init__.py @@ -207,15 +207,21 @@ def by_project_type(project_type: str) -> list[SkillEntry]: def installed_skills(project_dir: Path) -> list[Path]: - """Return SKILL.md files installed under ``.agents/skills/``.""" + """Return installed skill files under ``.agents/skills/`` (both flat and subdir formats).""" base = project_dir / ".agents" / "skills" if not base.is_dir(): return [] - return sorted(p for p in base.iterdir() if p.is_file() and p.suffix == ".md") + paths: list[Path] = [] + for item in sorted(base.iterdir()): + if item.is_file() and item.suffix == ".md": # legacy flat format + paths.append(item) + elif item.is_dir() and (item / "SKILL.md").exists(): # subdir format + paths.append(item / "SKILL.md") + return sorted(paths) def install(slug: str, project_dir: Path, *, force: bool = False) -> Path: - """Copy skill *slug* into ``/.agents/skills/.md``. + """Copy skill *slug* into ``/.agents/skills//SKILL.md``. Raises ------ @@ -229,7 +235,10 @@ def install(slug: str, project_dir: Path, *, force: bool = False) -> Path: raise KeyError(f"Unknown skill: {slug!r}") base = project_dir / ".agents" / "skills" base.mkdir(parents=True, exist_ok=True) - target = base / f"{slug}.md" + # Subdirectory format: /SKILL.md (REQ-360) + skill_dir = base / slug + skill_dir.mkdir(exist_ok=True) + target = skill_dir / "SKILL.md" if target.exists() and not force: raise FileExistsError(f"Already installed: {target}. Pass force=True to overwrite.") target.write_text(entry.body, encoding="utf-8") diff --git a/src/specsmith/skills/governance.py b/src/specsmith/skills/governance.py index f2cd80b..b5dc887 100644 --- a/src/specsmith/skills/governance.py +++ b/src/specsmith/skills/governance.py @@ -293,6 +293,172 @@ """ ), ), + SkillEntry( + slug="specsmith", + name="Specsmith — master governance CLI reference", + description=( + "Master reference for the specsmith AEE governance tool — key concepts, " + "common commands, session workflow, phase advancement, and audit codes. " + "Use whenever working in a specsmith-governed project." + ), + domain=SkillDomain.GOVERNANCE, + tags=["specsmith", "governance", "aee", "cli", "session", "audit", "phase", "ledger"], + prerequisites=["specsmith"], + body="""\ +# Specsmith — Master Governance CLI Reference + +specsmith is the Applied Epistemic Engineering (AEE) toolkit for AI-assisted +development. It governs projects through a 7-phase lifecycle and enforces +22 hard rules (H1–H22) via preflight gates, audits, and cryptographic trace +vaults. + +## Key Concepts +- **AEE Phase** — inception → architecture → requirements → test_spec → + implementation → verification → release. `specsmith phase` shows the + current phase and readiness percentage. +- **Preflight Gate** — every proposed change must pass + `specsmith preflight "" --json` before execution. +- **Work Item** — a scoped unit of work (`WI-`) assigned by preflight. +- **Governance Anchor** — `specsmith checkpoint` emits a timestamped snapshot + of phase, health, and active work items. +- **ESDB** — EpiStemic State Database (ChronoMemory WAL) stores beliefs, + decisions, and trace seals. + +## Common Commands +```bash +specsmith audit # governance health (28 checks) +specsmith sync # YAML → JSON → Markdown sync +specsmith validate --strict # schema validation +specsmith preflight "..." # preflight gate +specsmith checkpoint # emit GOVERNANCE ANCHOR +specsmith save # ESDB backup + commit + push +specsmith phase # show current phase + readiness +specsmith phase next # advance phase (checks prerequisites) +specsmith kill-session # terminate all agent processes +specsmith ledger add "msg" # append to LEDGER.md +``` + +## Session Workflow +1. `specsmith audit && specsmith sync && specsmith checkpoint` +2. `specsmith preflight "" --json` +3. Execute work (only if decision == accepted) +4. Every 8-10 turns: `specsmith checkpoint` (output verbatim) +5. `specsmith save && specsmith kill-session` + +## Audit Codes +- 28 checks across structure, REQ↔TEST coverage, governance size, + tool config, phase readiness, and supplementary rules. +- `specsmith audit --fix` auto-repairs missing files and CI configs. + +## Phase Advancement +- `specsmith phase next` checks prerequisites before advancing. +- `specsmith phase set ` jumps (with `--force` to skip checks). +- Phase stored as `aee_phase` in `scaffold.yml`. +""", + ), + SkillEntry( + slug="specsmith-audit", + name="Specsmith Audit — drift detection and governance health", + description=( + "Run specsmith audit to check for governance drift between requirements, " + "tests, and architecture. Required before advancing an AEE phase." + ), + domain=SkillDomain.GOVERNANCE, + tags=["specsmith", "audit", "drift", "health", "governance", "aee", "requirements"], + prerequisites=["specsmith"], + body="""\ +# Specsmith Audit — Drift Detection and Governance Health + +## When to Run +- At the start of every session (part of bootstrap) +- Before advancing an AEE phase (`specsmith phase next`) +- After significant code or requirements changes +- When drift is suspected (stale coverage, missing tests) + +## Command +```bash +specsmith audit # full 28-check governance audit +specsmith audit --fix # auto-repair missing files +specsmith audit --project-dir . # explicit project root +``` + +## What It Checks (28 checks) +1. Governance file structure (.specsmith/, docs/, AGENTS.md) +2. REQ↔TEST bidirectional coverage (no orphan tests, no untested REQs) +3. Governance file size thresholds +4. CI tool configuration matches project type +5. Phase readiness prerequisites +6. Supplementary rules referenced in AGENTS.md +7. Duplicate governance files (root vs docs/ canonical) +8. YAML↔JSON↔Markdown sync state + +## Interpreting Results +- **28/28 checks passed** — governance is healthy +- **Failures** — surface to user before starting work +- **Warnings** — non-blocking but should be addressed + +## Suppressing Warnings +In `scaffold.yml`: +```yaml +accepted_warnings: + - # renders as ~ (accepted), excluded from failure count +``` + +## Integration with Phase Advancement +`specsmith phase next` runs audit internally. All checks must pass +(or be accepted) before the phase advances. +""", + ), + SkillEntry( + slug="specsmith-save", + name="Specsmith Save — governance-aware save workflow", + description=( + "Run specsmith save to commit and push all current changes with governance " + "state backup. Use at the end of any work session or after completing a feature/fix." + ), + domain=SkillDomain.GOVERNANCE, + tags=["specsmith", "save", "commit", "push", "esdb", "backup", "governance"], + prerequisites=["specsmith"], + body="""\ +# Specsmith Save — Governance-Aware Save Workflow + +## When to Use +- At the end of every work session +- After completing a feature or fix +- Before switching branches or contexts +- Never end a session with uncommitted governance changes + +## Command +```bash +specsmith save # ESDB backup + commit + push +specsmith save --project-dir . # explicit project root +``` + +## What It Does (in order) +1. Creates a timestamped ESDB backup (.specsmith/backups/) +2. Runs `git add -A` to stage all changes +3. Commits with a governance-stamped message +4. Pushes to the current remote branch + +## Prerequisites +- All governance changes must be consistent (run `specsmith audit` first + if unsure) +- A git remote must be configured +- Working tree should have changes to commit + +## Session End Protocol +```bash +specsmith save --project-dir . # ESDB backup + commit + push +specsmith kill-session # stop governance-serve and tracked processes +``` + +## ESDB Backup Details +- Backups are written to `.specsmith/backups/` +- Filename format: `backup-YYYY-MM-DDTHH-MM-SS.json` +- Contains requirements.json and testcases.json snapshots +- Use `specsmith esdb rollback --steps N` to restore from backup +""", + ), SkillEntry( slug="specsmith-session-governance", name="Specsmith Session Governance — drift prevention, heartbeat, preflight gate", diff --git a/src/specsmith/sync.py b/src/specsmith/sync.py index 7223036..bf564f0 100644 --- a/src/specsmith/sync.py +++ b/src/specsmith/sync.py @@ -240,6 +240,18 @@ def run_sync(root: Path, *, dry_run: bool = False) -> SyncResult: new_reqs = load_yaml_requirements(root) new_tests = load_yaml_tests(root) + # REQ-358: If YAML mode has no YAML files but REQUIREMENTS.md has content, + # fall back to Markdown parsing rather than silently producing an empty state. + if not new_reqs and reqs_md_path.exists(): + _md_text = reqs_md_path.read_text(encoding="utf-8") + import re as _re + + if len(_re.findall(r"REQ-[A-Z0-9-]+", _md_text)) >= 5: + new_reqs = parse_requirements_md(_md_text) + + if not new_tests and tests_md_path.exists(): + new_tests = parse_tests_md(tests_md_path.read_text(encoding="utf-8")) + # Normalise to the same schema that the Markdown path produces # Build req_id → [test_ids] map from the loaded tests so requirements.json # includes test_ids for audit coverage checks (#147). diff --git a/tests/conftest.py b/tests/conftest.py index cc171c0..fc839c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,6 +58,11 @@ def _disable_specsmith_auto_update(monkeypatch: pytest.MonkeyPatch) -> None: ``--interactive`` protocol. The second hits the network. Pinning the suppression env vars keeps tests hermetic, deterministic, and free of network access. + + ``SPECSMITH_ALLOW_NON_PIPX=1`` bypasses the pipx-only enforcement gate + (introduced in 0.11.7) so tests running under a dev editable install + (``pip install -e``) are not rejected at startup. """ monkeypatch.setenv("SPECSMITH_NO_AUTO_UPDATE", "1") monkeypatch.setenv("SPECSMITH_PYPI_CHECKED", "1") + monkeypatch.setenv("SPECSMITH_ALLOW_NON_PIPX", "1") diff --git a/tests/test_auditor.py b/tests/test_auditor.py index d6ea30a..4f76f39 100644 --- a/tests/test_auditor.py +++ b/tests/test_auditor.py @@ -199,3 +199,108 @@ def test_sync_parse_tests_md_preserves_letter_suffix(self, tmp_path: Path) -> No assert "TEST-NN-002a" in ids assert "TEST-NN-002b" in ids assert "TEST-NN-002" not in ids # must not be truncated + + +# --------------------------------------------------------------------------- +# REQ-357: accepted_warnings suppression +# --------------------------------------------------------------------------- + + +class TestAcceptedWarningsSuppression: + """Tests for REQ-357 — accepted_warnings suppression in auditor.""" + + def test_accepted_warnings_suppresses_type_mismatch(self, tmp_path: Path) -> None: + """scaffold_type_mismatch alias should suppress the type-mismatch check.""" + # Set up a minimal project with a type that will mismatch detection + (tmp_path / "AGENTS.md").write_text("# AGENTS\nShort.\n", encoding="utf-8") + (tmp_path / "LEDGER.md").write_text("# Ledger\nDone.\n", encoding="utf-8") + docs = tmp_path / "docs" + docs.mkdir() + (docs / "TESTS.md").write_text("# Tests\n", encoding="utf-8") + # Use a type that differs from what detect_project will infer. + # "backend-frontend" is unlikely to match a near-empty tmp project. + (tmp_path / "scaffold.yml").write_text( + "name: test\n" + "type: backend-frontend\n" + "spec_version: 0.10.1\n" + "vcs_platform: github\n" + "accepted_warnings:\n" + " - scaffold_type_mismatch\n", + encoding="utf-8", + ) + + report = run_audit(tmp_path) + + # Find the type-mismatch result + tm_results = [r for r in report.results if r.name == "type-mismatch"] + if tm_results: + assert tm_results[0].suppressed is True + assert tm_results[0].passed is True + # The suppressed result must not count as a failure + type_mismatch_failures = [ + r for r in report.results if r.name == "type-mismatch" and not r.passed + ] + assert len(type_mismatch_failures) == 0 + + def test_ledger_line_threshold_suppresses_ledger_size(self, tmp_path: Path) -> None: + """ledger_line_threshold alias should suppress ledger-size check.""" + (tmp_path / "AGENTS.md").write_text("# AGENTS\nShort.\n", encoding="utf-8") + big_ledger = "# Ledger\n" + "\n".join(f"Line {i}" for i in range(600)) + (tmp_path / "LEDGER.md").write_text(big_ledger, encoding="utf-8") + (tmp_path / "scaffold.yml").write_text( + "name: test\n" + "type: cli-python\n" + "spec_version: 0.10.1\n" + "vcs_platform: github\n" + "accepted_warnings:\n" + " - ledger_line_threshold\n", + encoding="utf-8", + ) + + report = run_audit(tmp_path) + + size_results = [r for r in report.results if r.name == "ledger-size"] + assert len(size_results) == 1 + assert size_results[0].suppressed is True + assert size_results[0].passed is True + # Should not count toward failures + assert all(r.passed or r.name != "ledger-size" for r in report.results) + + def test_audit_suppressions_backward_compat(self, tmp_path: Path) -> None: + """Old audit_suppressions: [ledger_size] field should still suppress ledger-size.""" + (tmp_path / "AGENTS.md").write_text("# AGENTS\nShort.\n", encoding="utf-8") + big_ledger = "# Ledger\n" + "\n".join(f"Line {i}" for i in range(600)) + (tmp_path / "LEDGER.md").write_text(big_ledger, encoding="utf-8") + (tmp_path / "scaffold.yml").write_text( + "name: test\n" + "type: cli-python\n" + "spec_version: 0.10.1\n" + "vcs_platform: github\n" + "audit_suppressions:\n" + " - ledger_size\n", + encoding="utf-8", + ) + + report = run_audit(tmp_path) + + size_results = [r for r in report.results if r.name == "ledger-size"] + assert len(size_results) == 1 + assert size_results[0].suppressed is True + assert size_results[0].passed is True + + def test_suppressed_count_property(self, tmp_path: Path) -> None: + """AuditReport.suppressed_count should reflect the number of suppressed results.""" + from specsmith.auditor import AuditReport, AuditResult, _apply_accepted_warnings + + report = AuditReport( + results=[ + AuditResult(name="ledger-size", passed=False, message="too big", fixable=True), + AuditResult(name="type-mismatch", passed=False, message="mismatch"), + AuditResult(name="other-check", passed=True, message="ok"), + ] + ) + _apply_accepted_warnings(report, ["ledger_line_threshold", "scaffold_type_mismatch"]) + + assert report.suppressed_count == 2 + assert report.failed == 0 + assert report.healthy is True diff --git a/tests/test_req_358_359.py b/tests/test_req_358_359.py new file mode 100644 index 0000000..f83053c --- /dev/null +++ b/tests/test_req_358_359.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 BitConcepts, LLC. All rights reserved. +"""Tests for REQ-358 (_req_count H2 support) and REQ-359 (sync YAML-mode MD fallback).""" + +from __future__ import annotations + +import json +from pathlib import Path + +# ── REQ-359 / TEST-359: _req_count H2 heading support ───────────────────────── + + +class TestReqCountH2: + """_req_count should match ## REQ-XX-NNN (H2) headings, not just ### (H3).""" + + def test_req_count_h2_headings(self, tmp_path: Path) -> None: + from specsmith.phase import _req_count + + docs = tmp_path / "docs" + docs.mkdir() + lines = [f"## REQ-BE-{i:03d}: Requirement {i}" for i in range(1, 6)] + (docs / "REQUIREMENTS.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + + assert _req_count(5)(tmp_path) is True + assert _req_count(6)(tmp_path) is False # strict boundary + + def test_req_count_h3_still_works(self, tmp_path: Path) -> None: + from specsmith.phase import _req_count + + docs = tmp_path / "docs" + docs.mkdir() + lines = [f"### REQ-{i:03d}: Requirement {i}" for i in range(1, 4)] + (docs / "REQUIREMENTS.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + + assert _req_count(3)(tmp_path) is True + assert _req_count(4)(tmp_path) is False + + def test_req_count_mixed_h2_h3(self, tmp_path: Path) -> None: + from specsmith.phase import _req_count + + docs = tmp_path / "docs" + docs.mkdir() + content = "## REQ-BE-001: First\n### REQ-BE-002: Second\n## REQ-BE-003: Third\n" + (docs / "REQUIREMENTS.md").write_text(content, encoding="utf-8") + + assert _req_count(3)(tmp_path) is True + assert _req_count(4)(tmp_path) is False + + +# ── REQ-358 / TEST-358: sync YAML-mode Markdown fallback ────────────────────── + + +class TestSyncYamlModeMarkdownFallback: + """run_sync in YAML mode should fall back to REQUIREMENTS.md parsing + when no YAML requirement files exist but REQUIREMENTS.md has content.""" + + def test_sync_yaml_mode_markdown_fallback(self, tmp_path: Path) -> None: + from specsmith.sync import run_sync + + # Set up YAML mode + state_dir = tmp_path / ".specsmith" + state_dir.mkdir() + (state_dir / "governance-mode").write_text("yaml", encoding="utf-8") + + # Create REQUIREMENTS.md with H2 headings (no YAML files) + docs = tmp_path / "docs" + docs.mkdir() + lines = [f"## REQ-BE-{i:03d}: Backend requirement {i}" for i in range(1, 7)] + (docs / "REQUIREMENTS.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + + # No docs/requirements/ directory — forces fallback + + result = run_sync(tmp_path) + + assert result.reqs_after >= 6 + reqs_json = state_dir / "requirements.json" + assert reqs_json.exists() + data = json.loads(reqs_json.read_text(encoding="utf-8")) + assert len(data) == 6 + + def test_sync_yaml_mode_no_fallback_when_yaml_exists(self, tmp_path: Path) -> None: + """When YAML files exist, no fallback should occur.""" + from specsmith.sync import run_sync + + state_dir = tmp_path / ".specsmith" + state_dir.mkdir() + (state_dir / "governance-mode").write_text("yaml", encoding="utf-8") + + docs = tmp_path / "docs" + docs.mkdir() + req_dir = docs / "requirements" + req_dir.mkdir() + (req_dir / "core.yml").write_text( + "- id: REQ-001\n title: First\n status: defined\n", + encoding="utf-8", + ) + + result = run_sync(tmp_path) + + # Should use YAML source, not fallback + assert result.reqs_after == 1 diff --git a/tests/test_skill_marketplace.py b/tests/test_skill_marketplace.py index 16497d8..5d6edff 100644 --- a/tests/test_skill_marketplace.py +++ b/tests/test_skill_marketplace.py @@ -55,7 +55,8 @@ def test_get_returns_none_for_unknown_slug() -> None: def test_install_writes_skill_md(tmp_path: Path) -> None: target = skills.install("verifier", tmp_path) assert target.is_file() - assert target.parent == tmp_path / ".agents" / "skills" + assert target.name == "SKILL.md" + assert target.parent == tmp_path / ".agents" / "skills" / "verifier" assert target.read_text(encoding="utf-8").startswith("# Verifier Skill") @@ -89,8 +90,8 @@ def test_installed_skills_lists_md_files(tmp_path: Path) -> None: skills.install("verifier", tmp_path) skills.install("planner", tmp_path) listed = skills.installed_skills(tmp_path) - names = sorted(p.name for p in listed) - assert names == ["planner.md", "verifier.md"] + names = sorted(p.parent.name for p in listed if p.name == "SKILL.md") + assert names == ["planner", "verifier"] # --------------------------------------------------------------------------- @@ -133,14 +134,14 @@ def test_cli_skill_install_then_list(tmp_path: Path) -> None: ["skill", "install", "verifier", "--project-dir", str(tmp_path)], ) assert res.exit_code == 0, res.output - assert (tmp_path / ".agents" / "skills" / "verifier.md").is_file() + assert (tmp_path / ".agents" / "skills" / "verifier" / "SKILL.md").is_file() res2 = runner.invoke( main, ["skill", "list", "--json", "--project-dir", str(tmp_path)], ) payload = json.loads(res2.output) - assert "verifier.md" in payload["installed"] + assert "verifier" in payload["installed"] verifier_entry = next(e for e in payload["catalog"] if e["slug"] == "verifier") assert verifier_entry["installed"] is True @@ -175,7 +176,7 @@ def test_cli_skill_install_force_overwrites(tmp_path: Path) -> None: main, ["skill", "install", "verifier", "--project-dir", str(tmp_path)], ) - target = tmp_path / ".agents" / "skills" / "verifier.md" + target = tmp_path / ".agents" / "skills" / "verifier" / "SKILL.md" target.write_text("STALE", encoding="utf-8") res = runner.invoke( main, @@ -183,3 +184,45 @@ def test_cli_skill_install_force_overwrites(tmp_path: Path) -> None: ) assert res.exit_code == 0, res.output assert target.read_text(encoding="utf-8").startswith("# Verifier Skill") + + +# --------------------------------------------------------------------------- +# New tests for REQ-360/REQ-361 +# --------------------------------------------------------------------------- + + +def test_catalog_has_specsmith_skills() -> None: + assert skills.get("specsmith") is not None + assert skills.get("specsmith-save") is not None + assert skills.get("specsmith-audit") is not None + + +def test_specsmith_skill_domain_is_governance() -> None: + from specsmith.skills import SkillDomain + + entry = skills.get("specsmith") + assert entry is not None + assert entry.domain == SkillDomain.GOVERNANCE + + +def test_install_writes_subdirectory_format(tmp_path: Path) -> None: + target = skills.install("verifier", tmp_path) + assert target.name == "SKILL.md" + assert target.parent.name == "verifier" # /SKILL.md + assert target.parent.parent == tmp_path / ".agents" / "skills" + assert target.is_file() + + +def test_installed_skills_detects_subdir_format(tmp_path: Path) -> None: + skills.install("verifier", tmp_path) + listed = skills.installed_skills(tmp_path) + assert any(p.name == "SKILL.md" and p.parent.name == "verifier" for p in listed) + + +def test_installed_skills_detects_flat_format(tmp_path: Path) -> None: + # Simulate legacy flat install + base = tmp_path / ".agents" / "skills" + base.mkdir(parents=True) + (base / "legacy.md").write_text("# Legacy", encoding="utf-8") + listed = skills.installed_skills(tmp_path) + assert any(p.name == "legacy.md" for p in listed)