diff --git a/.chronomemory/events.wal b/.chronomemory/events.wal index d1ee1b89..30544796 100644 --- a/.chronomemory/events.wal +++ b/.chronomemory/events.wal @@ -171,3 +171,7 @@ {"seq": 954, "ts": "2026-07-14T09:35:34Z", "op": "upsert", "record_id": "TEST-478", "record": {"id": "TEST-478", "kind": "testcase", "status": "active", "label": "ESDB migration repairs a verified legacy ChronoMemory WAL chain", "confidence": 1.0, "source_type": "observed", "evidence": ["synced from testcases.json"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"id": "TEST-478", "version": 1, "title": "ESDB migration repairs a verified legacy ChronoMemory WAL chain", "description": "Verify an invalid ChronoStore chain is backed up, compacted, revalidated, and recorded in the migration manifest.", "requirement_id": "REQ-305", "type": "unit", "verification_method": "pytest tests/test_esdb_migrate_wal_repair.py", "input": {}, "expected_behavior": {}, "confidence": 1.0}}, "prev_hash": "d64af022d79195f8d08806a4b47bb7446026a2c5e5184786b7108eae481f293f", "hash": "422df45e44f7cb33d04686f9b4b0f07dcec8213318137b45b23100473717b7fd", "recursion_depth": 0} {"seq": 955, "ts": "2026-07-14T09:35:52Z", "op": "upsert", "record_id": "LED-FEBAF78EA7FC", "record": {"id": "LED-FEBAF78EA7FC", "kind": "ledger_event", "status": "active", "label": "KILL SWITCH ACTIVATED: emergency stop", "confidence": 0.9, "source_type": "observed", "evidence": ["REG-005"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"description": "KILL SWITCH ACTIVATED: emergency stop", "entry_type": "kill-switch", "author": "specsmith-operator", "reqs": "REG-005", "status": "complete", "epistemic_status": "high", "belief_artifacts": "", "entry_hash": "71fb0a17514b368a94e3ea16db15d0e0f6780faad3b030b944076a894929dfe9", "timestamp": "2026-07-14T09:35:52.573379+00:00"}}, "prev_hash": "422df45e44f7cb33d04686f9b4b0f07dcec8213318137b45b23100473717b7fd", "hash": "4d9706487ded3e490e862d8e865c8e7c689a73960b6b39232406403a5a6cd514", "recursion_depth": 0} {"seq": 956, "ts": "2026-07-14T09:36:00Z", "op": "upsert", "record_id": "TEST-479", "record": {"id": "TEST-479", "kind": "testcase", "status": "active", "label": "Governance checkpoint anchor uses ASCII fixed-width rows", "confidence": 1.0, "source_type": "observed", "evidence": ["synced from testcases.json"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"id": "TEST-479", "version": 1, "title": "Governance checkpoint anchor uses ASCII fixed-width rows", "description": "Verify human checkpoint output uses ASCII borders and status words only, with every row the same fixed width.", "requirement_id": "REQ-351", "type": "cli", "verification_method": "pytest tests/test_cli.py", "input": {}, "expected_behavior": {}, "confidence": 1.0}}, "prev_hash": "4d9706487ded3e490e862d8e865c8e7c689a73960b6b39232406403a5a6cd514", "hash": "7859f1c70c27a47d0a27fe1fa970eba3f7f06a6d563b6b42bb2f2969b97786e5", "recursion_depth": 0} +{"seq": 957, "ts": "2026-07-14T11:02:16Z", "op": "upsert", "record_id": "LED-85BCF1C4DA6F", "record": {"id": "LED-85BCF1C4DA6F", "kind": "ledger_event", "status": "active", "label": "KILL SWITCH ACTIVATED: emergency stop", "confidence": 0.9, "source_type": "observed", "evidence": ["REG-005"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"description": "KILL SWITCH ACTIVATED: emergency stop", "entry_type": "kill-switch", "author": "specsmith-operator", "reqs": "REG-005", "status": "complete", "epistemic_status": "high", "belief_artifacts": "", "entry_hash": "6f5f68c50ef18586209265c5e12e126f14b6d9d61ce4b789058b8a677daa3ca5", "timestamp": "2026-07-14T11:02:16.856931+00:00"}}, "prev_hash": "7859f1c70c27a47d0a27fe1fa970eba3f7f06a6d563b6b42bb2f2969b97786e5", "hash": "0606129a9b66651c74375610b46b7ded371e5ae9c854478ee74f67204d08e3a2", "recursion_depth": 0} +{"seq": 958, "ts": "2026-07-14T11:03:03Z", "op": "upsert", "record_id": "WI-D99C5834", "record": {"id": "WI-D99C5834", "kind": "work_item", "status": "active", "label": "Investigate and fix GitHub issue #305, add a regression test, update release documentation and version to 0.22.3, then publish a verified production release [REQ-050 REQ-051]", "confidence": 0.8, "source_type": "observed", "evidence": ["REQ-050", "REQ-051", "TEST-050", "TEST-051"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"status": "open", "kind": "feature", "verified": false, "promoted_to_req": "", "blast_radius_estimate": "", "created_at": "2026-07-14T11:03:03Z", "updated_at": "2026-07-14T11:03:03Z", "closed_reason": ""}}, "prev_hash": "0606129a9b66651c74375610b46b7ded371e5ae9c854478ee74f67204d08e3a2", "hash": "ecfb88865ba462c51e0003db4d4c742fa29a7bfdbc0c79197828cde581037172", "recursion_depth": 0} +{"seq": 959, "ts": "2026-07-14T11:03:03Z", "op": "upsert", "record_id": "PF-WI-D99C5834", "record": {"id": "PF-WI-D99C5834", "kind": "preflight_decision", "status": "active", "label": "Change request matched existing governance scope. Proceed under Specsmith verification.", "confidence": 0.8, "source_type": "observed", "evidence": ["REQ-050", "REQ-051"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"decision": "accepted", "intent": "change", "work_item_id": "WI-D99C5834", "test_case_ids": ["TEST-050", "TEST-051"], "instruction": "Change request matched existing governance scope. Proceed under Specsmith verification.", "ai_disclosure": {"governed_by": "specsmith", "governance_gated": true, "provider": "local/heuristic", "model": "deterministic-broker", "spec_version": "0.22.2"}}}, "prev_hash": "ecfb88865ba462c51e0003db4d4c742fa29a7bfdbc0c79197828cde581037172", "hash": "82a6424b6350e759d3eddcb8b228ee783ba6be73cde6117be4e608724fe3eed3", "recursion_depth": 0} +{"seq": 960, "ts": "2026-07-14T11:07:37Z", "op": "upsert", "record_id": "TEST-480", "record": {"id": "TEST-480", "kind": "testcase", "status": "active", "label": "Save commits tracked ChronoMemory WAL metric before reporting success", "confidence": 1.0, "source_type": "observed", "evidence": ["synced from testcases.json"], "epistemic_boundary": [], "is_hypothesis": false, "model_assumptions": {}, "recursion_depth": 0, "data": {"id": "TEST-480", "version": 1, "title": "Save commits tracked ChronoMemory WAL metric before reporting success", "description": "Regression coverage for GitHub issue #305: the metrics dual-write must occur before save's commit boundary so events.wal is committed rather than left dirty.", "requirement_id": "REQ-336", "type": "integration", "verification_method": "Run save against a tracked ChronoMemory WAL fixture and assert git status is clean afterward", "input": {}, "expected_behavior": {}, "confidence": 1.0}}, "prev_hash": "82a6424b6350e759d3eddcb8b228ee783ba6be73cde6117be4e608724fe3eed3", "hash": "4a633af04a9dd994b4c30438007d2317b26136ce824e5213191ac67bf0170f36", "recursion_depth": 0} diff --git a/.specsmith/requirements.json b/.specsmith/requirements.json index 6a9135e3..eed8ccf0 100644 --- a/.specsmith/requirements.json +++ b/.specsmith/requirements.json @@ -3236,7 +3236,8 @@ "status": "implemented", "test_ids": [ "TEST-336", - "TEST-477" + "TEST-477", + "TEST-480" ] }, { diff --git a/.specsmith/testcases.json b/.specsmith/testcases.json index da0eea56..2cceb03c 100644 --- a/.specsmith/testcases.json +++ b/.specsmith/testcases.json @@ -5206,5 +5206,17 @@ "input": {}, "expected_behavior": {}, "confidence": 1.0 + }, + { + "id": "TEST-480", + "version": 1, + "title": "Save commits tracked ChronoMemory WAL metric before reporting success", + "description": "Regression coverage for GitHub issue #305: the metrics dual-write must occur before save's commit boundary so events.wal is committed rather than left dirty.", + "requirement_id": "REQ-336", + "type": "integration", + "verification_method": "Run save against a tracked ChronoMemory WAL fixture and assert git status is clean afterward", + "input": {}, + "expected_behavior": {}, + "confidence": 1.0 } ] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 276a179c..1614be7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ consolidated into the next published release. ## [Unreleased] +## [0.22.3] - 2026-07-14 + +### Fixed +- Issue #305 - `specsmith save` now writes its metrics/ChronoMemory WAL event before the commit boundary, so a successful save does not leave `.chronomemory/events.wal` modified afterward. + ## [0.22.2] - 2026-07-14 ### Fixed @@ -948,7 +953,8 @@ See git history for per-commit details on intermediate versions. --- -[Unreleased]: https://github.com/layer1labs/specsmith/compare/v0.22.2...HEAD +[Unreleased]: https://github.com/layer1labs/specsmith/compare/v0.22.3...HEAD +[0.22.3]: https://github.com/layer1labs/specsmith/compare/v0.22.2...v0.22.3 [0.22.2]: https://github.com/layer1labs/specsmith/compare/v0.22.1...v0.22.2 [0.22.1]: https://github.com/layer1labs/specsmith/compare/v0.22.0...v0.22.1 [0.22.0]: https://github.com/layer1labs/specsmith/compare/v0.20.1...v0.22.0 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 50267cb8..adb3e4d0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -887,10 +887,12 @@ Two top-level CLI commands provide a complete governance checkpoint cycle: **`specsmith save`** (REQ-336): 1. Create a timestamped ESDB backup via `ChronoStore.backup()` (ChronoMemory backend class; 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 +2. Persist the command's lifetime metric before staging, so any tracked ChronoMemory WAL event is included in this same save commit +3. `git add` all governance files and `git commit` with an auto-generated message +4. `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 +- A successful save leaves no tracked ESDB/WAL changes uncommitted. **`specsmith load`** (REQ-337): 1. `git pull` the current branch from origin diff --git a/docs/COMPLIANCE.md b/docs/COMPLIANCE.md index 56073ece..eac6e08c 100644 --- a/docs/COMPLIANCE.md +++ b/docs/COMPLIANCE.md @@ -8,7 +8,7 @@ - **Type**: python - **Language**: python - **VCS Platform**: github -- **Spec Version**: 0.22.2 +- **Spec Version**: 0.22.3 ## Verification Tools diff --git a/docs/LEDGER.md b/docs/LEDGER.md index ee04c676..a6e98a21 100644 --- a/docs/LEDGER.md +++ b/docs/LEDGER.md @@ -132,3 +132,11 @@ - **Type**: wi_archive - **Status**: complete - **Chain hash**: `45fd62de2f460885...` + +## 2026-07-14T07:02 — KILL SWITCH ACTIVATED: emergency stop +- **Author**: specsmith-operator +- **Type**: kill-switch +- **REQs affected**: REG-005 +- **Status**: complete +- **Epistemic status**: high +- **Chain hash**: `6f5f68c50ef18586...` diff --git a/docs/SPECSMITH.yml b/docs/SPECSMITH.yml index 59fd9e6a..6b6585a1 100644 --- a/docs/SPECSMITH.yml +++ b/docs/SPECSMITH.yml @@ -9,7 +9,7 @@ description: | through governance, traceability, and automated compliance checking. type: python -version: 0.22.2 +version: 0.22.3 license: MIT author: Layer1 Labs url: https://github.com/layer1labs/specsmith diff --git a/docs/tests/yaml_governance.yml b/docs/tests/yaml_governance.yml index 627f042d..2da49b37 100644 --- a/docs/tests/yaml_governance.yml +++ b/docs/tests/yaml_governance.yml @@ -1221,3 +1221,12 @@ verification_method: pytest tests/test_cli.py description: Verify human checkpoint output uses ASCII borders and status words only, with every row the same fixed width. +- id: TEST-480 + title: Save commits tracked ChronoMemory WAL metric before reporting success + requirement_id: REQ-336 + type: integration + verification_method: Run save against a tracked ChronoMemory WAL fixture and assert + git status is clean afterward + description: 'Regression coverage for GitHub issue #305: the metrics dual-write + must occur before save''s commit boundary so events.wal is committed rather than + left dirty.' diff --git a/pyproject.toml b/pyproject.toml index c443b9aa..3bbdfcff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "specsmith" -version = "0.22.2" +version = "0.22.3" description = "AEE governance toolkit for AI-assisted development — session preflight gates, multi-agent dispatch, requirements↔test traceability, ESDB persistence, MCP server, and skills for Warp, Cursor, Claude Code, Copilot, Windsurf, and Aider." readme = "README.md" license = "MIT" diff --git a/src/specsmith/__init__.py b/src/specsmith/__init__.py index 2acb5c3c..6e7023a2 100644 --- a/src/specsmith/__init__.py +++ b/src/specsmith/__init__.py @@ -8,4 +8,4 @@ try: __version__: str = _pkg_version("specsmith") except PackageNotFoundError: # running from source without install - __version__ = "0.22.2" # fallback: keep in sync with pyproject.toml + __version__ = "0.22.3" # fallback: keep in sync with pyproject.toml diff --git a/src/specsmith/cli.py b/src/specsmith/cli.py index c274dde9..19312d4f 100644 --- a/src/specsmith/cli.py +++ b/src/specsmith/cli.py @@ -3373,6 +3373,17 @@ def save_cmd(project_dir: str, message: str, no_push: bool, force: bool, as_json except Exception as exc: # noqa: BLE001 steps.append({"step": "esdb_backup", "ok": False, "error": str(exc)}) + # Persist the save metric before committing. ChronoStore appends this + # dual-write to its tracked WAL, so writing it after the commit leaves the + # worktree dirty even when save otherwise reports success. + try: + from specsmith.project_metrics import MetricsRecord, MetricsStore + + pre_commit_ok = all(step["ok"] for step in steps) + MetricsStore(root).append(MetricsRecord.new(command="save", passed=pre_commit_ok)) + except Exception: # noqa: BLE001 # metrics remain best-effort + pass + # 2. Commit if not has_uncommitted_changes(root): steps.append({"step": "commit", "ok": True, "note": "Nothing to commit"}) @@ -3436,15 +3447,6 @@ def save_cmd(project_dir: str, message: str, no_push: bool, force: bool, as_json note = step.get("message") or step.get("note") or step.get("error") or "" console.print(f" [{color}]{icon}[/{color}] {step['step']}: {note}") - # Auto-record a minimal metrics row for lifetime tracking - try: - from specsmith.project_metrics import MetricsRecord, MetricsStore - - _store = MetricsStore(root) - _store.append(MetricsRecord.new(command="save", passed=ok)) - except Exception: # noqa: BLE001 # intentional: metrics are best-effort; never block save - pass - if not ok: raise SystemExit(1) @@ -3855,7 +3857,7 @@ def channel_set_cmd(channel: str) -> None: if channel == "stable" and is_prerelease_version(__version__): raise click.UsageError( "Cannot select stable while a prerelease is installed. " - f"Install a stable build first: {version_mismatch_remediation('0.22.2')}" + f"Install a stable build first: {version_mismatch_remediation('0.22.3')}" ) set_persisted_channel(channel) diff --git a/src/specsmith/config.py b/src/specsmith/config.py index c1ba4985..d022da71 100644 --- a/src/specsmith/config.py +++ b/src/specsmith/config.py @@ -140,7 +140,7 @@ class ProjectConfig(BaseModel): ), ) language: str = Field(default="python", description="Primary language/runtime") - spec_version: str = Field(default="0.22.2", description="Spec version to scaffold from") + spec_version: str = Field(default="0.22.3", description="Spec version to scaffold from") description: str = Field(default="", description="Short project description") # Options diff --git a/tests/test_issue_305.py b/tests/test_issue_305.py new file mode 100644 index 00000000..485ee6fd --- /dev/null +++ b/tests/test_issue_305.py @@ -0,0 +1,78 @@ +# Copyright (c) 2026 Layer1Labs Silicon, Inc. All rights reserved. +"""Regression coverage for issue #305 (TEST-480).""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +from click.testing import CliRunner + +from specsmith.cli import main + + +def _git(root: Path, *args: str) -> str: + result = subprocess.run( + ["git", *args], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + return result.stdout + + +def _init_git_repo(root: Path) -> None: + _git(root, "init") + _git(root, "config", "user.name", "SpecSmith Test") + _git(root, "config", "user.email", "specsmith-test@example.invalid") + (root / ".gitignore").write_text( + ".chronomemory/*\n!.chronomemory/events.wal\n.specsmith/*.sqlite3\n", + encoding="utf-8", + ) + wal = root / ".chronomemory" / "events.wal" + wal.parent.mkdir() + wal.write_text("", encoding="utf-8") + (root / "README.md").write_text("fixture\n", encoding="utf-8") + (root / ".specsmith").mkdir() + _git(root, "add", "-A") + _git(root, "commit", "-m", "test: initialize fixture") + + +def test_save_commits_chronomemory_metric_wal_before_reporting_success( + tmp_path: Path, monkeypatch +) -> None: # type: ignore[no-untyped-def] + """A successful save must not leave its own tracked ChronoMemory WAL dirty.""" + _init_git_repo(tmp_path) + + class _Store: + def __enter__(self) -> _Store: + return self + + def __exit__(self, *_args: object) -> None: + return None + + def backup(self) -> Path: + backup = tmp_path / ".chronomemory" / "backup" / "save.wal" + backup.parent.mkdir(exist_ok=True) + backup.write_text("backup\n", encoding="utf-8") + return backup + + def _append_metric(self, _record: object) -> None: # type: ignore[no-untyped-def] + wal = self._root / ".chronomemory" / "events.wal" + with wal.open("a", encoding="utf-8") as stream: + stream.write('{"kind":"session_metric","command":"save"}\n') + + monkeypatch.setattr("specsmith.esdb.ESDB_BACKEND", "chronomemory") + monkeypatch.setattr("specsmith.esdb.open_default_store", lambda *_args, **_kwargs: _Store()) + monkeypatch.setattr("specsmith.project_metrics.MetricsStore.append", _append_metric) + + result = CliRunner().invoke( + main, ["save", "--project-dir", str(tmp_path), "--no-push", "--json"] + ) + + assert result.exit_code == 0, result.output + assert json.loads(result.output)["ok"] is True + assert _git(tmp_path, "status", "--short") == "" + assert ".chronomemory/events.wal" in _git(tmp_path, "show", "--format=", "--name-only", "HEAD")