Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .chronomemory/events.wal
Original file line number Diff line number Diff line change
Expand Up @@ -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}
3 changes: 2 additions & 1 deletion .specsmith/requirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -3236,7 +3236,8 @@
"status": "implemented",
"test_ids": [
"TEST-336",
"TEST-477"
"TEST-477",
"TEST-480"
]
},
{
Expand Down
12 changes: 12 additions & 0 deletions .specsmith/testcases.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<timestamp>/`)
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
Expand Down
2 changes: 1 addition & 1 deletion docs/COMPLIANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- **Type**: python
- **Language**: python
- **VCS Platform**: github
- **Spec Version**: 0.22.2
- **Spec Version**: 0.22.3

## Verification Tools

Expand Down
8 changes: 8 additions & 0 deletions docs/LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...`
2 changes: 1 addition & 1 deletion docs/SPECSMITH.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/tests/yaml_governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/specsmith/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 12 additions & 10 deletions src/specsmith/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/specsmith/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions tests/test_issue_305.py
Original file line number Diff line number Diff line change
@@ -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")
Loading