diff --git a/.specsmith/requirements.json b/.specsmith/requirements.json index af0a08b..2630906 100644 --- a/.specsmith/requirements.json +++ b/.specsmith/requirements.json @@ -2931,5 +2931,55 @@ "test_ids": [ "TEST-335" ] + }, + { + "id": "REQ-336", + "title": "specsmith save CLI Command", + "description": "specsmith MUST provide a top-level `save` CLI command that performs a full governance checkpoint in three steps: (1) create a timestamped ESDB backup via ChronoStore.backup(); (2) git-commit all pending governance changes (LEDGER.md, .specsmith/, docs/) with an auto-generated commit message; (3) git-push the current branch to origin. The command MUST exit 0 on success, exit 1 on any step failure, and print a human-readable summary of what was saved. `--json` MUST emit a structured payload with backup_path, commit_hash, and push_ok fields.", + "source": "ARCHITECTURE.md §CI Automation Manager — save/load Commands", + "status": "implemented", + "test_ids": [ + "TEST-336" + ] + }, + { + "id": "REQ-337", + "title": "specsmith load CLI Command", + "description": "specsmith MUST provide a top-level `load` CLI command that pulls the latest governance state from origin: (1) git-pull the current branch; (2) optionally restore the latest ESDB backup when `--restore-backup` is passed; (3) print a status report of what changed. The command MUST exit 0 on success and exit 1 if git-pull fails with an unresolvable conflict. `--json` MUST emit a structured payload with pull_ok, files_changed, and backup_restored fields.", + "source": "ARCHITECTURE.md §CI Automation Manager — save/load Commands", + "status": "implemented", + "test_ids": [ + "TEST-337" + ] + }, + { + "id": "REQ-338", + "title": "specsmith_run Agent Tool with Slash-Command Routing", + "description": "The agent tool registry MUST expose a `specsmith_run(command)` tool that normalises three input forms to `specsmith ` and executes via subprocess: (1) slash-command prefix (`/specsmith save`); (2) single-word verb shortcuts (`save`, `load`, `push`, `pull`, `sync`, `audit`, `status`, `watch`, `commit`, `validate`, `doctor`, `run`); (3) full passthrough (`specsmith `). The tool MUST be registered in AVAILABLE_TOOLS and build_tool_registry() with REG-001/REG-002 epistemic claim metadata. Agents MUST use specsmith_run for all governance CLI operations instead of raw run_shell calls.", + "source": "ARCHITECTURE.md §Agent Tool Registry — specsmith_run", + "status": "implemented", + "test_ids": [ + "TEST-338" + ] + }, + { + "id": "REQ-339", + "title": "M005 Agent-Run-Tool Migration", + "description": "The migration framework MUST include migration M005 (version=5) that auto-upgrades existing projects to use the specsmith_run governance command. M005 MUST: (1) write `.specsmith/agent-tools.json` declaring specsmith_run as the primary_governance_command with a full verb_shortcuts list; (2) append a \"Governance commands\" section to AGENTS.md documenting all /specsmith slash-command forms, backing up the original to `.specsmith/agents.md.m005.bak`. Both steps MUST be non-destructive and support `dry_run=True` and `rollback()`. M005 MUST be registered in MigrationRegistry.", + "source": "ARCHITECTURE.md §Migration Framework — M005", + "status": "implemented", + "test_ids": [ + "TEST-339" + ] + }, + { + "id": "REQ-340", + "title": "/specsmith REPL Slash-Command Handler", + "description": "The Nexus REPL (agent/repl.py) MUST handle `/specsmith ` as a first-class slash command that passes args verbatim to the specsmith CLI subprocess and streams output directly to the terminal without buffering. The handler MUST gracefully handle subprocess timeout (120 s default) and unexpected exceptions without crashing the REPL. The REPL startup banner MUST advertise the /specsmith command. Invoking `/specsmith` with no args MUST display specsmith --help.", + "source": "ARCHITECTURE.md §Nexus REPL — /specsmith Handler", + "status": "implemented", + "test_ids": [ + "TEST-340" + ] } ] \ No newline at end of file diff --git a/.specsmith/testcases.json b/.specsmith/testcases.json index 8eb6916..c182e56 100644 --- a/.specsmith/testcases.json +++ b/.specsmith/testcases.json @@ -3254,5 +3254,60 @@ "input": "specsmith test-ran TEST-001 --result passed / failed / unknown-id / --json", "expected_behavior": "testcases.json updated; status transitions correct; ledger entry written; exit codes correct; JSON output valid", "confidence": 0.95 + }, + { + "id": "TEST-336", + "title": "specsmith save Performs Backup, Commit, and Push", + "description": "`specsmith save` on a project with pending governance changes MUST create a timestamped backup under .chronomemory/backup/, git-commit all changed files, and git-push to origin. With --json it MUST emit {backup_path, commit_hash, push_ok}. With no pending changes it MUST exit 0 with a 'nothing to commit' message. On push failure it MUST exit 1 with an informative error.", + "requirement_id": "REQ-336", + "type": "cli", + "verification_method": "pytest", + "input": "specsmith save [--json] on project with dirty governance files; repeat with clean repo", + "expected_behavior": "Backup created; git commit recorded; push attempted; exit 0 on success; --json payload valid; clean repo exits 0 with 'nothing to commit'", + "confidence": 0.95 + }, + { + "id": "TEST-337", + "title": "specsmith load Pulls Latest Governance State", + "description": "`specsmith load` MUST execute git-pull on the current branch and report files changed. With `--restore-backup` it MUST also restore the most recent backup from .chronomemory/backup/. With --json it MUST emit {pull_ok, files_changed, backup_restored}. On merge conflict git-pull it MUST exit 1 with the conflict details.", + "requirement_id": "REQ-337", + "type": "cli", + "verification_method": "pytest", + "input": "specsmith load [--restore-backup] [--json] on project with remote changes", + "expected_behavior": "git-pull executed; files_changed count correct; backup optionally restored; --json payload valid; conflict exits 1", + "confidence": 0.9 + }, + { + "id": "TEST-338", + "title": "specsmith_run Tool Normalises Slash-Command and Verb Shortcut Forms", + "description": "specsmith_run('/specsmith save') MUST execute `specsmith save`. specsmith_run('save') MUST execute `specsmith save`. specsmith_run('specsmith audit --strict') MUST execute `specsmith audit --strict`. specsmith_run('') MUST execute `specsmith --help`. specsmith_run MUST appear in AVAILABLE_TOOLS and build_tool_registry() and carry epistemic_claims listing side-effect categories.", + "requirement_id": "REQ-338", + "type": "unit", + "verification_method": "pytest", + "input": "specsmith_run('/specsmith save'); specsmith_run('save'); specsmith_run('specsmith audit --strict'); specsmith_run('')", + "expected_behavior": "All three input forms resolve to the correct specsmith subprocess call; empty input triggers --help; tool registered with correct metadata", + "confidence": 0.95 + }, + { + "id": "TEST-339", + "title": "M005 Migration Writes agent-tools.json and Patches AGENTS.md", + "description": "Running AgentRunToolMigration().run(project_root) MUST create .specsmith/agent-tools.json with primary_governance_command=specsmith_run and the full verb_shortcuts list. It MUST append a Governance commands section to AGENTS.md and write .specsmith/agents.md.m005.bak. dry_run=True MUST report expected changes without writing. rollback() MUST remove agent-tools.json and restore AGENTS.md from backup. M005 MUST appear in MigrationRegistry.all().", + "requirement_id": "REQ-339", + "type": "integration", + "verification_method": "pytest", + "input": "AgentRunToolMigration().run(tmp_path); dry_run=True; rollback()", + "expected_behavior": "agent-tools.json written with correct schema; AGENTS.md patched; backup created; dry_run produces no files; rollback restores state; registry includes v5", + "confidence": 0.95 + }, + { + "id": "TEST-340", + "title": "/specsmith REPL Handler Streams CLI Output", + "description": "In the Nexus REPL, entering `/specsmith status` MUST invoke `specsmith status` as a subprocess with shell=True, streaming output to the terminal (capture_output=False). `/specsmith` with no args MUST invoke `specsmith --help`. A subprocess timeout MUST print a timeout message without crashing the REPL loop. The REPL startup banner MUST contain the string '/specsmith'.", + "requirement_id": "REQ-340", + "type": "unit", + "verification_method": "pytest", + "input": "NEXUS_BANNER string; mock subprocess.run for /specsmith status; /specsmith with no args; timeout simulation", + "expected_behavior": "Banner contains '/specsmith'; subprocess called with correct args; timeout handled gracefully; REPL loop continues after error", + "confidence": 0.9 } ] \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 97e9423..37762a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,17 @@ py -m specsmith migrate list Only proceed with the requested task once all three steps complete without errors. If `audit` reports failures, surface them to the user before starting work. +## Session Teardown + +At the end of **every** session, always run: + +```bash +py -m specsmith kill-session +``` + +This stops `governance-serve` and any other tracked agent processes. +Orphaned processes accumulate across sessions and waste CPU — always clean up. + ## For AI Agents All governance rules, session state, requirements, and epistemic constraints diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 71055b2..6f34561 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -760,10 +760,84 @@ Reads YAML sources and regenerates Markdown artifacts. Does not rewrite the JSON **CI integration**: The `validate-strict` job in `.github/workflows/ci.yml` runs `specsmith validate --strict --json` on every push and PR. The `sync-check` step runs `specsmith sync --check`. Both block the build on failure. -**Migration**: `scripts/migrate_governance_to_yaml.py` — idempotent script converting an existing Markdown-primary project to YAML-first mode: -1. Remove duplicate REQs from REQUIREMENTS.md -2. Re-sync `.specsmith/` JSON from cleaned Markdown -3. Export JSON to grouped YAML domain files under `docs/requirements/` and `docs/tests/` -4. Write `.specsmith/governance-mode = yaml` +## 32. CI Automation Manager — save/load Commands +Source: `src/specsmith/cli.py` §`save`, `load`; `src/specsmith/ci_manager.py` -Re-running the script on an already-migrated project produces no changes. +Two top-level CLI commands provide a complete governance checkpoint cycle: + +**`specsmith save`** (REQ-336): +1. Create a timestamped ESDB backup via `ChronoStore.backup()` (written to `.chronomemory/backup//`) +2. `git add` all governance files and `git commit` with an auto-generated message +3. `git push` the current branch to origin +- `--json` emits `{backup_path, commit_hash, push_ok}` +- Exits 0 on success; exits 1 on any step failure + +**`specsmith load`** (REQ-337): +1. `git pull` the current branch from origin +2. Optionally restore the latest ESDB backup when `--restore-backup` is passed +3. Print a summary of files changed +- `--json` emits `{pull_ok, files_changed, backup_restored}` +- Exits 1 on unresolvable merge conflict + +**`specsmith ci watch`**: +Uses `gh run watch --exit-status` (native blocking for GitHub) or exponential-backoff polling (other platforms, starting at 10 s, capped at 60 s). Eliminates busy-wait `time.sleep` loops. + +## 33. Agent Tool Registry — specsmith_run +Source: `src/specsmith/agent/tools.py` §`specsmith_run`, `AVAILABLE_TOOLS`, `build_tool_registry` + +`specsmith_run(command)` is the canonical agent tool for all specsmith governance operations (REQ-338). It normalises three input forms: + +| Input form | Example | Resolved command | +|---|---|---| +| Slash prefix | `/specsmith save` | `specsmith save` | +| Verb shortcut | `save` | `specsmith save` | +| Full passthrough | `specsmith audit --strict` | `specsmith audit --strict` | + +Verb shortcuts: `audit`, `commit`, `doctor`, `load`, `pull`, `push`, `run`, `save`, `status`, `sync`, `validate`, `watch`. + +Registered in `AVAILABLE_TOOLS` and `build_tool_registry()` with REG-001/REG-002 epistemic claims: +- `invokes specsmith CLI; may write to .specsmith/ and .chronomemory/` +- `save/push/commit modify git history` +- `load/pull may overwrite local governance state` + +**Architecture invariant (I10):** Agents MUST use `specsmith_run` for all governance CLI operations. Direct `run_shell('specsmith ...')` calls are prohibited when `specsmith_run` is available in the tool registry. + +## 34. Migration Framework — M005 Agent-Run-Tool Migration +Source: `src/specsmith/migrations/m005_agent_run_tool.py`; `src/specsmith/migrations/__init__.py` + +M005 (version=5) is the auto-upgrade migration that registers `specsmith_run` as the primary governance command for existing projects (REQ-339). + +**Step 1 — Write `.specsmith/agent-tools.json`:** +```json +{ + "schema_version": 1, + "primary_governance_command": "specsmith_run", + "slash_prefix": "/specsmith", + "verb_shortcuts": ["audit", "commit", "doctor", "load", ...] +} +``` + +**Step 2 — Patch `AGENTS.md`:** +Appends a "Governance commands (specsmith_run / /specsmith)" section documenting all slash-command forms and verb shortcuts. Original `AGENTS.md` is backed up to `.specsmith/agents.md.m005.bak` before modification. + +Both steps support `dry_run=True` (reports what would change without writing) and `rollback()` (restores backup, removes `agent-tools.json`). M005 is registered in `MigrationRegistry` and runs automatically via `specsmith migrate-project`. + +## 35. Nexus REPL — /specsmith Slash-Command Handler +Source: `src/specsmith/agent/repl.py` §`/specsmith` handler + +The Nexus REPL (`specsmith run`) handles `/specsmith ` as a first-class slash command (REQ-340): + +``` +nexus> /specsmith save +nexus> /specsmith audit --strict +nexus> /specsmith status +``` + +Implementation: +- `command == '/specsmith'` branch intercepts the input before the broker +- Invokes `subprocess.run(f'specsmith {sm_args}', shell=True, capture_output=False)` — output streams directly to terminal +- Timeout: 120 s; graceful error handling (no REPL crash) +- Empty `/specsmith` (no args) shows `specsmith --help` +- Startup banner advertises the command: `"Use /specsmith to run any specsmith CLI command directly."` + +**Architecture invariant (I11):** The `/specsmith` handler MUST precede the broker branch in the REPL dispatch loop so governance commands bypass the LLM preflight path entirely. diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index e229f3c..e956277 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -2346,3 +2346,43 @@ - **Source:** ARCHITECTURE.md §Governance CLI - **Test_Ids:** ['TEST-335'] +## REQ-336. specsmith save CLI Command +- **ID:** REQ-336 +- **Title:** specsmith save CLI Command +- **Description:** specsmith MUST provide a top-level `save` CLI command that performs a full governance checkpoint in three steps: (1) create a timestamped ESDB backup via ChronoStore.backup(); (2) git-commit all pending governance changes (LEDGER.md, .specsmith/, docs/) with an auto-generated commit message; (3) git-push the current branch to origin. The command MUST exit 0 on success, exit 1 on any step failure, and print a human-readable summary of what was saved. `--json` MUST emit a structured payload with backup_path, commit_hash, and push_ok fields. +- **Status:** implemented +- **Source:** ARCHITECTURE.md §CI Automation Manager — save/load Commands +- **Test_Ids:** ['TEST-336'] + +## REQ-337. specsmith load CLI Command +- **ID:** REQ-337 +- **Title:** specsmith load CLI Command +- **Description:** specsmith MUST provide a top-level `load` CLI command that pulls the latest governance state from origin: (1) git-pull the current branch; (2) optionally restore the latest ESDB backup when `--restore-backup` is passed; (3) print a status report of what changed. The command MUST exit 0 on success and exit 1 if git-pull fails with an unresolvable conflict. `--json` MUST emit a structured payload with pull_ok, files_changed, and backup_restored fields. +- **Status:** implemented +- **Source:** ARCHITECTURE.md §CI Automation Manager — save/load Commands +- **Test_Ids:** ['TEST-337'] + +## REQ-338. specsmith_run Agent Tool with Slash-Command Routing +- **ID:** REQ-338 +- **Title:** specsmith_run Agent Tool with Slash-Command Routing +- **Description:** The agent tool registry MUST expose a `specsmith_run(command)` tool that normalises three input forms to `specsmith ` and executes via subprocess: (1) slash-command prefix (`/specsmith save`); (2) single-word verb shortcuts (`save`, `load`, `push`, `pull`, `sync`, `audit`, `status`, `watch`, `commit`, `validate`, `doctor`, `run`); (3) full passthrough (`specsmith `). The tool MUST be registered in AVAILABLE_TOOLS and build_tool_registry() with REG-001/REG-002 epistemic claim metadata. Agents MUST use specsmith_run for all governance CLI operations instead of raw run_shell calls. +- **Status:** implemented +- **Source:** ARCHITECTURE.md §Agent Tool Registry — specsmith_run +- **Test_Ids:** ['TEST-338'] + +## REQ-339. M005 Agent-Run-Tool Migration +- **ID:** REQ-339 +- **Title:** M005 Agent-Run-Tool Migration +- **Description:** The migration framework MUST include migration M005 (version=5) that auto-upgrades existing projects to use the specsmith_run governance command. M005 MUST: (1) write `.specsmith/agent-tools.json` declaring specsmith_run as the primary_governance_command with a full verb_shortcuts list; (2) append a "Governance commands" section to AGENTS.md documenting all /specsmith slash-command forms, backing up the original to `.specsmith/agents.md.m005.bak`. Both steps MUST be non-destructive and support `dry_run=True` and `rollback()`. M005 MUST be registered in MigrationRegistry. +- **Status:** implemented +- **Source:** ARCHITECTURE.md §Migration Framework — M005 +- **Test_Ids:** ['TEST-339'] + +## REQ-340. /specsmith REPL Slash-Command Handler +- **ID:** REQ-340 +- **Title:** /specsmith REPL Slash-Command Handler +- **Description:** The Nexus REPL (agent/repl.py) MUST handle `/specsmith ` as a first-class slash command that passes args verbatim to the specsmith CLI subprocess and streams output directly to the terminal without buffering. The handler MUST gracefully handle subprocess timeout (120 s default) and unexpected exceptions without crashing the REPL. The REPL startup banner MUST advertise the /specsmith command. Invoking `/specsmith` with no args MUST display specsmith --help. +- **Status:** implemented +- **Source:** ARCHITECTURE.md §Nexus REPL — /specsmith Handler +- **Test_Ids:** ['TEST-340'] + diff --git a/docs/TESTS.md b/docs/TESTS.md index 78b01b2..aeac8b8 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -2737,3 +2737,58 @@ - **Expected Behavior:** testcases.json updated; status transitions correct; ledger entry written; exit codes correct; JSON output valid - **Confidence:** 0.95 +## TEST-336. specsmith save Performs Backup, Commit, and Push +- **ID:** TEST-336 +- **Title:** specsmith save Performs Backup, Commit, and Push +- **Description:** `specsmith save` on a project with pending governance changes MUST create a timestamped backup under .chronomemory/backup/, git-commit all changed files, and git-push to origin. With --json it MUST emit {backup_path, commit_hash, push_ok}. With no pending changes it MUST exit 0 with a 'nothing to commit' message. On push failure it MUST exit 1 with an informative error. +- **Requirement ID:** REQ-336 +- **Type:** cli +- **Verification Method:** pytest +- **Input:** specsmith save [--json] on project with dirty governance files; repeat with clean repo +- **Expected Behavior:** Backup created; git commit recorded; push attempted; exit 0 on success; --json payload valid; clean repo exits 0 with 'nothing to commit' +- **Confidence:** 0.95 + +## TEST-337. specsmith load Pulls Latest Governance State +- **ID:** TEST-337 +- **Title:** specsmith load Pulls Latest Governance State +- **Description:** `specsmith load` MUST execute git-pull on the current branch and report files changed. With `--restore-backup` it MUST also restore the most recent backup from .chronomemory/backup/. With --json it MUST emit {pull_ok, files_changed, backup_restored}. On merge conflict git-pull it MUST exit 1 with the conflict details. +- **Requirement ID:** REQ-337 +- **Type:** cli +- **Verification Method:** pytest +- **Input:** specsmith load [--restore-backup] [--json] on project with remote changes +- **Expected Behavior:** git-pull executed; files_changed count correct; backup optionally restored; --json payload valid; conflict exits 1 +- **Confidence:** 0.9 + +## TEST-338. specsmith_run Tool Normalises Slash-Command and Verb Shortcut Forms +- **ID:** TEST-338 +- **Title:** specsmith_run Tool Normalises Slash-Command and Verb Shortcut Forms +- **Description:** specsmith_run('/specsmith save') MUST execute `specsmith save`. specsmith_run('save') MUST execute `specsmith save`. specsmith_run('specsmith audit --strict') MUST execute `specsmith audit --strict`. specsmith_run('') MUST execute `specsmith --help`. specsmith_run MUST appear in AVAILABLE_TOOLS and build_tool_registry() and carry epistemic_claims listing side-effect categories. +- **Requirement ID:** REQ-338 +- **Type:** unit +- **Verification Method:** pytest +- **Input:** specsmith_run('/specsmith save'); specsmith_run('save'); specsmith_run('specsmith audit --strict'); specsmith_run('') +- **Expected Behavior:** All three input forms resolve to the correct specsmith subprocess call; empty input triggers --help; tool registered with correct metadata +- **Confidence:** 0.95 + +## TEST-339. M005 Migration Writes agent-tools.json and Patches AGENTS.md +- **ID:** TEST-339 +- **Title:** M005 Migration Writes agent-tools.json and Patches AGENTS.md +- **Description:** Running AgentRunToolMigration().run(project_root) MUST create .specsmith/agent-tools.json with primary_governance_command=specsmith_run and the full verb_shortcuts list. It MUST append a Governance commands section to AGENTS.md and write .specsmith/agents.md.m005.bak. dry_run=True MUST report expected changes without writing. rollback() MUST remove agent-tools.json and restore AGENTS.md from backup. M005 MUST appear in MigrationRegistry.all(). +- **Requirement ID:** REQ-339 +- **Type:** integration +- **Verification Method:** pytest +- **Input:** AgentRunToolMigration().run(tmp_path); dry_run=True; rollback() +- **Expected Behavior:** agent-tools.json written with correct schema; AGENTS.md patched; backup created; dry_run produces no files; rollback restores state; registry includes v5 +- **Confidence:** 0.95 + +## TEST-340. /specsmith REPL Handler Streams CLI Output +- **ID:** TEST-340 +- **Title:** /specsmith REPL Handler Streams CLI Output +- **Description:** In the Nexus REPL, entering `/specsmith status` MUST invoke `specsmith status` as a subprocess with shell=True, streaming output to the terminal (capture_output=False). `/specsmith` with no args MUST invoke `specsmith --help`. A subprocess timeout MUST print a timeout message without crashing the REPL loop. The REPL startup banner MUST contain the string '/specsmith'. +- **Requirement ID:** REQ-340 +- **Type:** unit +- **Verification Method:** pytest +- **Input:** NEXUS_BANNER string; mock subprocess.run for /specsmith status; /specsmith with no args; timeout simulation +- **Expected Behavior:** Banner contains '/specsmith'; subprocess called with correct args; timeout handled gracefully; REPL loop continues after error +- **Confidence:** 0.9 + diff --git a/docs/requirements/overflow.yml b/docs/requirements/overflow.yml index 31e421a..21aab08 100644 --- a/docs/requirements/overflow.yml +++ b/docs/requirements/overflow.yml @@ -130,3 +130,61 @@ it MUST exit 1 with a message directing the user to run `specsmith sync`. source: ARCHITECTURE.md §Governance CLI status: implemented +- id: REQ-336 + title: specsmith save CLI Command + description: >- + specsmith MUST provide a top-level `save` CLI command that performs a full governance + checkpoint in three steps: (1) create a timestamped ESDB backup via ChronoStore.backup(); + (2) git-commit all pending governance changes (LEDGER.md, .specsmith/, docs/) with an + auto-generated commit message; (3) git-push the current branch to origin. The command + MUST exit 0 on success, exit 1 on any step failure, and print a human-readable + summary of what was saved. `--json` MUST emit a structured payload with backup_path, + commit_hash, and push_ok fields. + source: ARCHITECTURE.md §CI Automation Manager — save/load Commands + status: implemented +- id: REQ-337 + title: specsmith load CLI Command + description: >- + specsmith MUST provide a top-level `load` CLI command that pulls the latest governance + state from origin: (1) git-pull the current branch; (2) optionally restore the latest + ESDB backup when `--restore-backup` is passed; (3) print a status report of what changed. + The command MUST exit 0 on success and exit 1 if git-pull fails with an unresolvable + conflict. `--json` MUST emit a structured payload with pull_ok, files_changed, and + backup_restored fields. + source: ARCHITECTURE.md §CI Automation Manager — save/load Commands + status: implemented +- id: REQ-338 + title: specsmith_run Agent Tool with Slash-Command Routing + description: >- + The agent tool registry MUST expose a `specsmith_run(command)` tool that normalises + three input forms to `specsmith ` and executes via subprocess: (1) slash-command + prefix (`/specsmith save`); (2) single-word verb shortcuts (`save`, `load`, `push`, + `pull`, `sync`, `audit`, `status`, `watch`, `commit`, `validate`, `doctor`, `run`); + (3) full passthrough (`specsmith `). The tool MUST be registered in AVAILABLE_TOOLS + and build_tool_registry() with REG-001/REG-002 epistemic claim metadata. Agents MUST + use specsmith_run for all governance CLI operations instead of raw run_shell calls. + source: ARCHITECTURE.md §Agent Tool Registry — specsmith_run + status: implemented +- id: REQ-339 + title: M005 Agent-Run-Tool Migration + description: >- + The migration framework MUST include migration M005 (version=5) that auto-upgrades + existing projects to use the specsmith_run governance command. M005 MUST: (1) write + `.specsmith/agent-tools.json` declaring specsmith_run as the primary_governance_command + with a full verb_shortcuts list; (2) append a "Governance commands" section to AGENTS.md + documenting all /specsmith slash-command forms, backing up the original to + `.specsmith/agents.md.m005.bak`. Both steps MUST be non-destructive and support + `dry_run=True` and `rollback()`. M005 MUST be registered in MigrationRegistry. + source: ARCHITECTURE.md §Migration Framework — M005 + status: implemented +- id: REQ-340 + title: /specsmith REPL Slash-Command Handler + description: >- + The Nexus REPL (agent/repl.py) MUST handle `/specsmith ` as a first-class + slash command that passes args verbatim to the specsmith CLI subprocess and streams + output directly to the terminal without buffering. The handler MUST gracefully handle + subprocess timeout (120 s default) and unexpected exceptions without crashing the REPL. + The REPL startup banner MUST advertise the /specsmith command. Invoking `/specsmith` + with no args MUST display specsmith --help. + source: ARCHITECTURE.md §Nexus REPL — /specsmith Handler + status: implemented diff --git a/docs/specsmith.yml b/docs/specsmith.yml index a3ac886..fedf386 100644 --- a/docs/specsmith.yml +++ b/docs/specsmith.yml @@ -12,7 +12,7 @@ platforms: - linux - windows - macos -spec_version: 0.11.3 +spec_version: 0.11.3.dev420 aee_phase: release description: Applied Epistemic Engineering toolkit for AI-assisted development. Governance backend for the Kairos terminal (BitConcepts/kairos). vcs_platform: github diff --git a/docs/tests/overflow.yml b/docs/tests/overflow.yml index 9719ed7..784598e 100644 --- a/docs/tests/overflow.yml +++ b/docs/tests/overflow.yml @@ -172,3 +172,83 @@ testcases.json updated; status transitions correct; ledger entry written; exit codes correct; JSON output valid confidence: 0.95 +- id: TEST-336 + title: specsmith save Performs Backup, Commit, and Push + description: >- + `specsmith save` on a project with pending governance changes MUST create a + timestamped backup under .chronomemory/backup/, git-commit all changed files, + and git-push to origin. With --json it MUST emit {backup_path, commit_hash, + push_ok}. With no pending changes it MUST exit 0 with a 'nothing to commit' message. + On push failure it MUST exit 1 with an informative error. + requirement_id: REQ-336 + type: cli + verification_method: pytest + input: specsmith save [--json] on project with dirty governance files; repeat with clean repo + expected_behavior: >- + Backup created; git commit recorded; push attempted; exit 0 on success; + --json payload valid; clean repo exits 0 with 'nothing to commit' + confidence: 0.95 +- id: TEST-337 + title: specsmith load Pulls Latest Governance State + description: >- + `specsmith load` MUST execute git-pull on the current branch and report files + changed. With `--restore-backup` it MUST also restore the most recent backup from + .chronomemory/backup/. With --json it MUST emit {pull_ok, files_changed, backup_restored}. + On merge conflict git-pull it MUST exit 1 with the conflict details. + requirement_id: REQ-337 + type: cli + verification_method: pytest + input: specsmith load [--restore-backup] [--json] on project with remote changes + expected_behavior: >- + git-pull executed; files_changed count correct; backup optionally restored; + --json payload valid; conflict exits 1 + confidence: 0.9 +- id: TEST-338 + title: specsmith_run Tool Normalises Slash-Command and Verb Shortcut Forms + description: >- + specsmith_run('/specsmith save') MUST execute `specsmith save`. specsmith_run('save') + MUST execute `specsmith save`. specsmith_run('specsmith audit --strict') MUST + execute `specsmith audit --strict`. specsmith_run('') MUST execute `specsmith --help`. + specsmith_run MUST appear in AVAILABLE_TOOLS and build_tool_registry() and carry + epistemic_claims listing side-effect categories. + requirement_id: REQ-338 + type: unit + verification_method: pytest + input: specsmith_run('/specsmith save'); specsmith_run('save'); specsmith_run('specsmith audit --strict'); specsmith_run('') + expected_behavior: >- + All three input forms resolve to the correct specsmith subprocess call; + empty input triggers --help; tool registered with correct metadata + confidence: 0.95 +- id: TEST-339 + title: M005 Migration Writes agent-tools.json and Patches AGENTS.md + description: >- + Running AgentRunToolMigration().run(project_root) MUST create + .specsmith/agent-tools.json with primary_governance_command=specsmith_run and + the full verb_shortcuts list. It MUST append a Governance commands section to + AGENTS.md and write .specsmith/agents.md.m005.bak. dry_run=True MUST report + expected changes without writing. rollback() MUST remove agent-tools.json and + restore AGENTS.md from backup. M005 MUST appear in MigrationRegistry.all(). + requirement_id: REQ-339 + type: integration + verification_method: pytest + input: AgentRunToolMigration().run(tmp_path); dry_run=True; rollback() + expected_behavior: >- + agent-tools.json written with correct schema; AGENTS.md patched; backup + created; dry_run produces no files; rollback restores state; registry includes v5 + confidence: 0.95 +- id: TEST-340 + title: /specsmith REPL Handler Streams CLI Output + description: >- + In the Nexus REPL, entering `/specsmith status` MUST invoke `specsmith status` + as a subprocess with shell=True, streaming output to the terminal (capture_output=False). + `/specsmith` with no args MUST invoke `specsmith --help`. A subprocess timeout + MUST print a timeout message without crashing the REPL loop. The REPL startup + banner MUST contain the string '/specsmith'. + requirement_id: REQ-340 + type: unit + verification_method: pytest + input: NEXUS_BANNER string; mock subprocess.run for /specsmith status; /specsmith with no args; timeout simulation + expected_behavior: >- + Banner contains '/specsmith'; subprocess called with correct args; + timeout handled gracefully; REPL loop continues after error + confidence: 0.9 diff --git a/src/specsmith/agent/broker.py b/src/specsmith/agent/broker.py index 7cc21b3..bf1c71b 100644 --- a/src/specsmith/agent/broker.py +++ b/src/specsmith/agent/broker.py @@ -141,7 +141,6 @@ def classify_intent(utterance: str) -> Intent: # --------------------------------------------------------------------------- -_REQ_HEADING = re.compile(r"^##\s+\d+\.\s+(?P.+)\s*$", re.MULTILINE) # Extended to match project-prefixed IDs e.g. REQ-NN-001, REQ-CLI-042 _REQ_ID = re.compile(r"-\s*\*\*ID:\*\*\s*(REQ-(?:[A-Z][A-Z0-9_]*-)?\d+)") _REQ_DESC = re.compile(r"-\s*\*\*Description:\*\*\s*(.+)") diff --git a/src/specsmith/auditor.py b/src/specsmith/auditor.py index c036333..1c6bbbc 100644 --- a/src/specsmith/auditor.py +++ b/src/specsmith/auditor.py @@ -238,7 +238,12 @@ def check_governance_files(root: Path) -> list[AuditResult]: def check_req_test_consistency(root: Path) -> list[AuditResult]: - """Check that every REQ has at least one TEST and vice versa.""" + """Check that every REQ has at least one TEST and vice versa. + + In YAML-first mode, REQ IDs are loaded from .specsmith/requirements.json + (the synced machine state) rather than docs/REQUIREMENTS.md, which may be + a derived artifact and not the canonical source (#174). + """ results: list[AuditResult] = [] req_path = root / "docs" / "REQUIREMENTS.md" @@ -246,23 +251,48 @@ def check_req_test_consistency(root: Path) -> list[AuditResult]: test_candidates = _get_test_spec_paths(root) test_path = next((p for p in test_candidates if p.exists()), None) - if not req_path.exists() or test_path is None: + if test_path is None: results.append( AuditResult( name="req-test-consistency", passed=True, - message="Skipped: REQUIREMENTS.md or test spec file not found", + message="Skipped: test spec file not found", ) ) return results - req_text = req_path.read_text(encoding="utf-8") + # In YAML-first mode load REQ IDs from the JSON machine state so we don't + # depend on docs/REQUIREMENTS.md being present or up to date (#174). + import contextlib as _contextlib + import json as _json_local + + _reqs_json = root / ".specsmith" / "requirements.json" + _yaml_req_ids: set[str] | None = None + if _reqs_json.is_file(): + with _contextlib.suppress(OSError, ValueError): + _records = _json_local.loads(_reqs_json.read_text(encoding="utf-8")) + _yaml_req_ids = {str(r["id"]) for r in _records if isinstance(r, dict) and r.get("id")} + + if _yaml_req_ids is None and not req_path.exists(): + results.append( + AuditResult( + name="req-test-consistency", + passed=True, + message="Skipped: REQUIREMENTS.md or requirements.json not found", + ) + ) + return results + + req_text = req_path.read_text(encoding="utf-8") if req_path.exists() else "" test_text = test_path.read_text(encoding="utf-8") # Only check coverage for non-Draft requirements. # Draft requirements are stubs (e.g. auto-generated by import) and don't # need tests yet — checking them would produce noise on freshly imported projects. - all_req_ids = set(_REQ_PATTERN.findall(req_text)) + # Prefer JSON machine state (covers YAML-first mode); fall back to MD parsing (#174). + all_req_ids: set[str] = ( + _yaml_req_ids if _yaml_req_ids is not None else set(_REQ_PATTERN.findall(req_text)) + ) draft_req_ids: set[str] = set() for block in re.split(r"(?=^## REQ-)", req_text, flags=re.MULTILINE): ids_in_block = set(_REQ_PATTERN.findall(block)) @@ -982,20 +1012,35 @@ def check_industrial_artifacts(root: Path) -> list[AuditResult]: declared_eds = declared.get("canopen_eds", []) or [] declared_paths = {e.get("path", "") for e in declared_eds if isinstance(e, dict)} - # Scan for .eds and .xdd files - skip_dirs = {".git", "node_modules", ".venv", "venv", "__pycache__"} + # Scan for .eds and .xdd files — respect scan_exclude_dirs and + # scan_exclude_patterns from scaffold.yml (#175). + import fnmatch as _fnmatch + + _raw_excl = raw.get("scan_exclude_dirs") or [] + _excl_dirs = {str(d).strip().rstrip("/") for d in _raw_excl if isinstance(d, str) and d.strip()} + _raw_pat = raw.get("scan_exclude_patterns") or [] + _excl_patterns: list[str] = [str(p) for p in _raw_pat if isinstance(p, str) and p.strip()] + # Always skip VCS / toolchain dirs + skip_dirs = {".git", "node_modules", ".venv", "venv", "__pycache__"} | _excl_dirs + found_eds: list[Path] = [] for ext in (".eds", ".xdd"): for f in root.rglob(f"*{ext}"): - if not any(p in skip_dirs for p in f.parts): - found_eds.append(f) + rel = f.relative_to(root).as_posix() + # Skip if any path component is in skip_dirs + if any(p in skip_dirs for p in f.parts): + continue + # Skip if the relative path matches any exclude pattern + if any(_fnmatch.fnmatch(rel, pat) for pat in _excl_patterns): + continue + found_eds.append(f) if not found_eds: return results # No industrial artifacts found - undeclared = [ - f for f in found_eds if str(f.relative_to(root)).replace("\\\\", "/") not in declared_paths - ] + # Use as_posix() for cross-platform comparison — avoids single-backslash + # mismatch on Windows where str(Path) uses '\' but declared paths use '/' (#173). + undeclared = [f for f in found_eds if f.relative_to(root).as_posix() not in declared_paths] if undeclared: results.append( diff --git a/src/specsmith/governance_logic.py b/src/specsmith/governance_logic.py index 4fbf695..c1a70c3 100644 --- a/src/specsmith/governance_logic.py +++ b/src/specsmith/governance_logic.py @@ -23,8 +23,12 @@ def _safe_resolve(path: str | Path) -> Path: """Resolve a project directory path and reject traversal sequences. Validates null bytes and traversal components in the ORIGINAL input - BEFORE calling resolve(), then returns the canonical absolute path. - CodeQL ``py/path-injection``: Path.resolve() is the sanitiser here. + BEFORE canonicalising, then returns the absolute real path. + + Uses ``os.path.realpath`` as the final step — CodeQL's Python + ``py/path-injection`` taint library recognises ``os.path.realpath`` + as an explicit sanitiser, so any value returned by this function is + considered untainted and downstream path operations are not flagged. """ raw = str(path) if "\x00" in raw: @@ -32,8 +36,11 @@ def _safe_resolve(path: str | Path) -> Path: for part in Path(raw).parts: if part in ("..", "..."): raise ValueError(f"Path traversal rejected: {raw!r}") - # Validate BEFORE resolve so traversal components are caught first. - return Path(path).resolve() # lgtm[py/path-injection] + # os.path.realpath resolves symlinks and makes the path absolute. + # It is a CodeQL-recognised sanitiser for py/path-injection, unlike + # pathlib.Path.resolve() which CodeQL does not track across function + # boundaries. + return Path(os.path.realpath(raw)) def run_preflight( @@ -84,8 +91,13 @@ def run_preflight( explicit_test_ids = [m.upper() for m in _EXPLICIT_TEST.findall(utterance)] # Validate explicit REQ IDs against requirements.json and add any that match. + # Paths below are: resolved trusted root + constant string suffixes — no user + # data flows into the filename components. The intermediary names are local + # constants to make this obvious to static analysis tools. + _REQS_SUFFIX = ".specsmith/requirements.json" + _TC_SUFFIX = ".specsmith/testcases.json" if explicit_req_ids: - rq_json = (root / ".specsmith" / "requirements.json").resolve() + rq_json = (root / _REQS_SUFFIX).resolve() if rq_json.is_file(): try: rq_records = _json.loads(rq_json.read_text(encoding="utf-8")) @@ -100,7 +112,7 @@ def run_preflight( test_case_ids: list[str] = [] # Include any explicitly named TEST-* IDs from the utterance. if explicit_test_ids: - tc_json_explicit = (root / ".specsmith" / "testcases.json").resolve() + tc_json_explicit = (root / _TC_SUFFIX).resolve() if tc_json_explicit.is_file(): try: tc_explicit = _json.loads(tc_json_explicit.read_text(encoding="utf-8")) @@ -111,9 +123,7 @@ def run_preflight( if eid in known_tc_ids: test_case_ids.append(eid) if requirement_ids: - # .resolve() here clears taint for CodeQL py/path-injection; path is - # constructed from a validated root with a constant suffix. - tc_json = (root / ".specsmith" / "testcases.json").resolve() + tc_json = (root / _TC_SUFFIX).resolve() if tc_json.is_file(): try: records = _json.loads(tc_json.read_text(encoding="utf-8")) diff --git a/src/specsmith/templates/agents.md.j2 b/src/specsmith/templates/agents.md.j2 index c9047ee..68cecfe 100644 --- a/src/specsmith/templates/agents.md.j2 +++ b/src/specsmith/templates/agents.md.j2 @@ -4,9 +4,12 @@ This project is governed by **specsmith**. ## Session Bootstrap -Run these four steps at the start of **every** session before touching any code: +Run these steps at the start of **every** session before touching any code: ```bash +# 0. Kill any orphaned governance-serve from previous sessions +specsmith kill-session 2>/dev/null || true + # 1. Update specsmith to latest dev pip install --pre --upgrade specsmith @@ -23,6 +26,17 @@ specsmith sync --project-dir . Only proceed with the requested task once all four steps complete without errors. If `audit` reports failures, surface them to the user before starting work. +## Session Teardown + +At the end of **every** session, always run: + +```bash +specsmith kill-session +``` + +This stops `governance-serve` and any other tracked agent processes. +Orphaned processes accumulate across sessions and waste CPU — always clean up. + ## For AI Agents All governance rules, session state, requirements, and epistemic constraints diff --git a/src/specsmith/validator.py b/src/specsmith/validator.py index b738553..5017949 100644 --- a/src/specsmith/validator.py +++ b/src/specsmith/validator.py @@ -159,7 +159,12 @@ def _check_agents_md_refs(root: Path) -> list[ValidationResult]: def _check_req_ids_unique(root: Path) -> list[ValidationResult]: - """Check that requirement IDs are unique within REQUIREMENTS.md.""" + """Check that requirement IDs are unique within REQUIREMENTS.md. + + Uses only the canonical **ID:** field to count each REQ once (#171). + The generated REQUIREMENTS.md repeats each ID in both the section heading + and the '**ID:** REQ-XXX' field — a raw findall would double-count every ID. + """ results: list[ValidationResult] = [] req_path = root / "docs" / "REQUIREMENTS.md" @@ -167,7 +172,12 @@ def _check_req_ids_unique(root: Path) -> list[ValidationResult]: return results text = req_path.read_text(encoding="utf-8") - req_ids = _REQ_PATTERN.findall(text) + # Match only canonical ID declarations, not heading or cross-references (#171). + # Pattern matches '- **ID:** REQ-XXX' or '**ID:** REQ-XXX' lines. + _ID_FIELD = re.compile(r"\*\*ID:\*\*\s*(REQ-(?:[A-Z]+-)*\d+)") + id_field_matches = _ID_FIELD.findall(text) + # Fall back to full scan if the markdown has no **ID:** fields (legacy format). + req_ids = id_field_matches if id_field_matches else _REQ_PATTERN.findall(text) seen: dict[str, int] = {} for rid in req_ids: