assertion-cli 0.5.3__tar.gz → 0.5.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/PKG-INFO +18 -11
  2. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/README.md +17 -10
  3. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/assertion_cli.egg-info/PKG-INFO +18 -11
  4. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/assertion_cli.egg-info/SOURCES.txt +1 -0
  5. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/main.py +60 -1
  6. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/pyproject.toml +1 -1
  7. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/templates/SKILL.md +3 -3
  8. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_api.py +1 -1
  9. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_main.py +28 -1
  10. assertion_cli-0.5.5/tests/test_new.py +121 -0
  11. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/api.py +0 -0
  12. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/assertion_cli.egg-info/dependency_links.txt +0 -0
  13. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/assertion_cli.egg-info/entry_points.txt +0 -0
  14. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/assertion_cli.egg-info/requires.txt +0 -0
  15. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/assertion_cli.egg-info/top_level.txt +0 -0
  16. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/bundle.py +0 -0
  17. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/git.py +0 -0
  18. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/link.py +0 -0
  19. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/models.py +0 -0
  20. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/session.py +0 -0
  21. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/setup.cfg +0 -0
  22. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/templates/ACTIVATION.md +0 -0
  23. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/templates/__init__.py +0 -0
  24. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_bundle.py +0 -0
  25. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_decision.py +0 -0
  26. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_git.py +0 -0
  27. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_init.py +0 -0
  28. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_link.py +0 -0
  29. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_prompt.py +0 -0
  30. {assertion_cli-0.5.3 → assertion_cli-0.5.5}/tests/test_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertion-cli
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: CLI for the Assertion API
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -39,6 +39,7 @@ that set is `httpx`, `pydantic`, `python-dotenv`, and `typer`.
39
39
  After installation:
40
40
 
41
41
  ```bash
42
+ asrt new # reset local run state for a fresh session
42
43
  asrt stacks
43
44
  asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
44
45
  asrt checkpoint --continue "Implemented Y"
@@ -49,13 +50,19 @@ asrt verify-status # one-shot poll; loop with your own sleep
49
50
 
50
51
  ## Publishing a new version
51
52
 
52
- ```bash
53
- # 1. Bump the version in cli/pyproject.toml.
54
- # 2. Build the sdist + wheel.
55
- uv build --package assertion-cli
56
- # 3. Upload (requires UV_PUBLISH_TOKEN, or `--token` on the command).
57
- uv publish dist/*
58
- # 4. Tag the release so consumers can correlate to git:
59
- git tag cli-v$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)
60
- git push origin --tags
61
- ```
53
+ 1. **Bump the version** in `cli/pyproject.toml`.
54
+ 2. **Commit and push** to `main`.
55
+
56
+ That's it. The `Publish CLI to PyPI` workflow auto-triggers on pushes that
57
+ change `cli/pyproject.toml`. It reads the version from the file, runs tests,
58
+ builds, publishes to PyPI, pushes a `cli-v<version>` tag, and creates a
59
+ GitHub Release with auto-generated notes.
60
+
61
+ If the version hasn't changed (e.g. you updated a dependency), the workflow
62
+ skips publishing — only version bumps trigger a release.
63
+
64
+ You can also trigger the workflow manually from the GitHub Actions UI — it
65
+ reads the version from the current `pyproject.toml` on `main`.
66
+
67
+ **Prerequisite**: `PYPI_TOKEN` must be set as a GitHub Actions secret — a
68
+ PyPI API token with upload scope for `assertion-cli`.
@@ -25,6 +25,7 @@ that set is `httpx`, `pydantic`, `python-dotenv`, and `typer`.
25
25
  After installation:
26
26
 
27
27
  ```bash
28
+ asrt new # reset local run state for a fresh session
28
29
  asrt stacks
29
30
  asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
30
31
  asrt checkpoint --continue "Implemented Y"
@@ -35,13 +36,19 @@ asrt verify-status # one-shot poll; loop with your own sleep
35
36
 
36
37
  ## Publishing a new version
37
38
 
38
- ```bash
39
- # 1. Bump the version in cli/pyproject.toml.
40
- # 2. Build the sdist + wheel.
41
- uv build --package assertion-cli
42
- # 3. Upload (requires UV_PUBLISH_TOKEN, or `--token` on the command).
43
- uv publish dist/*
44
- # 4. Tag the release so consumers can correlate to git:
45
- git tag cli-v$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)
46
- git push origin --tags
47
- ```
39
+ 1. **Bump the version** in `cli/pyproject.toml`.
40
+ 2. **Commit and push** to `main`.
41
+
42
+ That's it. The `Publish CLI to PyPI` workflow auto-triggers on pushes that
43
+ change `cli/pyproject.toml`. It reads the version from the file, runs tests,
44
+ builds, publishes to PyPI, pushes a `cli-v<version>` tag, and creates a
45
+ GitHub Release with auto-generated notes.
46
+
47
+ If the version hasn't changed (e.g. you updated a dependency), the workflow
48
+ skips publishing — only version bumps trigger a release.
49
+
50
+ You can also trigger the workflow manually from the GitHub Actions UI — it
51
+ reads the version from the current `pyproject.toml` on `main`.
52
+
53
+ **Prerequisite**: `PYPI_TOKEN` must be set as a GitHub Actions secret — a
54
+ PyPI API token with upload scope for `assertion-cli`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertion-cli
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: CLI for the Assertion API
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -39,6 +39,7 @@ that set is `httpx`, `pydantic`, `python-dotenv`, and `typer`.
39
39
  After installation:
40
40
 
41
41
  ```bash
42
+ asrt new # reset local run state for a fresh session
42
43
  asrt stacks
43
44
  asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
44
45
  asrt checkpoint --continue "Implemented Y"
@@ -49,13 +50,19 @@ asrt verify-status # one-shot poll; loop with your own sleep
49
50
 
50
51
  ## Publishing a new version
51
52
 
52
- ```bash
53
- # 1. Bump the version in cli/pyproject.toml.
54
- # 2. Build the sdist + wheel.
55
- uv build --package assertion-cli
56
- # 3. Upload (requires UV_PUBLISH_TOKEN, or `--token` on the command).
57
- uv publish dist/*
58
- # 4. Tag the release so consumers can correlate to git:
59
- git tag cli-v$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)
60
- git push origin --tags
61
- ```
53
+ 1. **Bump the version** in `cli/pyproject.toml`.
54
+ 2. **Commit and push** to `main`.
55
+
56
+ That's it. The `Publish CLI to PyPI` workflow auto-triggers on pushes that
57
+ change `cli/pyproject.toml`. It reads the version from the file, runs tests,
58
+ builds, publishes to PyPI, pushes a `cli-v<version>` tag, and creates a
59
+ GitHub Release with auto-generated notes.
60
+
61
+ If the version hasn't changed (e.g. you updated a dependency), the workflow
62
+ skips publishing — only version bumps trigger a release.
63
+
64
+ You can also trigger the workflow manually from the GitHub Actions UI — it
65
+ reads the version from the current `pyproject.toml` on `main`.
66
+
67
+ **Prerequisite**: `PYPI_TOKEN` must be set as a GitHub Actions secret — a
68
+ PyPI API token with upload scope for `assertion-cli`.
@@ -23,5 +23,6 @@ tests/test_git.py
23
23
  tests/test_init.py
24
24
  tests/test_link.py
25
25
  tests/test_main.py
26
+ tests/test_new.py
26
27
  tests/test_prompt.py
27
28
  tests/test_session.py
@@ -1,7 +1,9 @@
1
1
  import base64
2
+ import importlib.metadata
2
3
  import importlib.resources
3
4
  import json
4
5
  import re
6
+ import shutil
5
7
  from datetime import datetime, timezone
6
8
  from pathlib import Path
7
9
 
@@ -32,6 +34,27 @@ from session import (
32
34
  update_metadata_anchor,
33
35
  )
34
36
 
37
+ _VERSION: str | None = None
38
+
39
+
40
+ def _get_version() -> str:
41
+ global _VERSION
42
+ if _VERSION is not None:
43
+ return _VERSION
44
+ try:
45
+ _VERSION = importlib.metadata.version("assertion-cli")
46
+ except importlib.metadata.PackageNotFoundError:
47
+ pyproject = Path(__file__).resolve().parent / "pyproject.toml"
48
+ if pyproject.exists():
49
+ import tomllib
50
+
51
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
52
+ _VERSION = data["project"]["version"]
53
+ else:
54
+ _VERSION = "0.0.0"
55
+ return _VERSION
56
+
57
+
35
58
  app = typer.Typer(help="Assertion CLI")
36
59
 
37
60
 
@@ -52,8 +75,19 @@ def render_checkpoint_response(resp: CheckpointResponse) -> str:
52
75
 
53
76
 
54
77
  @app.callback(invoke_without_command=True)
55
- def main(ctx: typer.Context) -> None:
78
+ def main(
79
+ ctx: typer.Context,
80
+ version: bool = typer.Option(
81
+ False,
82
+ "--version",
83
+ "-v",
84
+ help="Show version and exit.",
85
+ ),
86
+ ) -> None:
56
87
  """Assertion command group."""
88
+ if version:
89
+ typer.echo(f"assertion-cli {_get_version()}")
90
+ raise typer.Exit()
57
91
  if ctx.invoked_subcommand is None:
58
92
  typer.echo(ctx.get_help())
59
93
  raise typer.Exit(code=0)
@@ -228,6 +262,31 @@ def init() -> None:
228
262
  typer.echo(" - asrt verify at completion, then PR")
229
263
 
230
264
 
265
+ @app.command("new")
266
+ def new() -> None:
267
+ """Reset local Assertion run state and record current HEAD as the diff base."""
268
+ repo_root = find_git_root(Path.cwd())
269
+ base_sha = get_head_sha(repo_root)
270
+ assertion_dir = repo_root / ASSERTION_DIR_NAME
271
+ assertion_dir.mkdir(exist_ok=True)
272
+
273
+ removed = 0
274
+ for child in assertion_dir.iterdir():
275
+ if child.is_dir():
276
+ shutil.rmtree(child)
277
+ else:
278
+ child.unlink()
279
+ removed += 1
280
+
281
+ base_sha_path = assertion_dir / BASE_SHA_FILE_NAME
282
+ base_sha_path.write_text(base_sha + "\n", encoding="utf-8")
283
+
284
+ typer.echo(f"Cleared {removed} .assertion entr{'y' if removed == 1 else 'ies'}.")
285
+ typer.echo(
286
+ f"Recorded diff base {base_sha[:12]} → {base_sha_path.relative_to(repo_root)}"
287
+ )
288
+
289
+
231
290
  @app.command("prompt")
232
291
  def prompt_cmd(
233
292
  text: str = typer.Argument(..., help="User prompt text to record."),
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "assertion-cli"
7
- version = "0.5.3"
7
+ version = "0.5.5"
8
8
  description = "CLI for the Assertion API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -16,9 +16,9 @@ Before any `asrt` call:
16
16
  - You are inside a git repository.
17
17
  - `asrt init` has been run in this repo. It records the current HEAD as the diff base that every checkpoint/verify diffs against, and installs the skill files you are reading right now. If `asrt checkpoint` or `asrt verify` errors with "No diff base recorded", run `asrt init` and retry.
18
18
 
19
- **Starting a fresh session:** when beginning a new session, delete the contents of `.assertion/` first. This clears the previous session's in-flight state (metadata, prompts log, link) so a new session starts clean and anchors to the stack you pick on the first checkpoint. Only the user should do this — it resets the session.
19
+ **Starting a fresh session:** when beginning a new session, run `asrt new` first. This clears the previous session's in-flight state (metadata, prompts log, link, verification status, screenshots) and records the current `HEAD` as the new diff base, so a new session starts clean on the current branch and anchors to the stack you pick on the first checkpoint. Only the user should do this — it resets the session.
20
20
 
21
- Treat `.assertion/metadata.json`, `.assertion/prompts`, and `.assertion/base_sha` as internal state owned by the CLI. Do not create, edit, or delete those files manually — use `asrt prompt` to append to the prompts log.
21
+ Treat `.assertion/metadata.json`, `.assertion/prompts`, and `.assertion/base_sha` as internal state owned by the CLI. Do not create, edit, or delete those files manually — use `asrt prompt` to append to the prompts log and `asrt new` to reset for a fresh session.
22
22
 
23
23
  `asrt stacks` is read-only and lists every verification stack in the user's workspace, along with the repo each is configured for. Run it once per session before your first checkpoint so you can pick a stack ID; do not hardcode IDs from memory.
24
24
 
@@ -105,7 +105,7 @@ When you pass `--stack`, briefly state which stack you chose and why ("Picked st
105
105
  | CLI error starts with | What it means | What to do |
106
106
  |---|---|---|
107
107
  | `No session exists yet and no --stack given.` | First checkpoint of a fresh `.assertion/`, but you didn't pick a stack. | Run `asrt stacks`, pick one using the signals above, pass `--stack <id>`. |
108
- | `This session is already anchored to a stack.` | You passed `--stack` after a session was already created. The CLI locks the stack at session creation. | Drop the `--stack` flag and re-run `asrt checkpoint "<message>"`. |
108
+ | `This session is already anchored to a stack.` | You passed `--stack` after a session was already created. The CLI locks the stack at session creation. | Drop the `--stack` flag and re-run `asrt checkpoint "<message>"`, or ask the user to run `asrt new` if this should be a fresh session. |
109
109
  | `ERROR: Unknown stack '<id>'.` | The `--stack` you passed is not in the workspace. | Re-run `asrt stacks` (the list may have changed) and pass a valid id. |
110
110
  | `No diff base recorded for this repo.` | `asrt init` wasn't run. | Run `asrt init` and retry. |
111
111
  | `ERROR: No active session to continue.` | You passed `--continue` but there is no session. | Drop `--continue` and pass `--stack <id>` to start one. |
@@ -22,7 +22,7 @@ def test_client_init() -> None:
22
22
  200,
23
23
  json={
24
24
  "session_id": "s-99",
25
- "stacks": [{"id": "stack-1", "name": "Browser", "description": "desc"}],
25
+ "stacks": [{"id": "stack-1", "name": "Browser", "description": "desc", "repo": "owner/repo"}],
26
26
  },
27
27
  )
28
28
  )
@@ -4,9 +4,14 @@ import base64
4
4
 
5
5
  import httpx
6
6
  from _pytest.monkeypatch import MonkeyPatch
7
+ from typer.testing import CliRunner
7
8
 
8
- from models import CheckpointResponse, ReviewDetail
9
+ import main
9
10
  from main import _decode_screenshot, _write_screenshots, render_checkpoint_response
11
+ from models import CheckpointResponse, ReviewDetail
12
+
13
+
14
+ runner = CliRunner()
10
15
 
11
16
 
12
17
  def test_decode_screenshot_data_uri() -> None:
@@ -80,3 +85,25 @@ def test_render_checkpoint_response() -> None:
80
85
  assert "checkpoint_id: cp-1" in output
81
86
  assert "outcome: failed" in output
82
87
  assert "Needs work" in output
88
+
89
+
90
+ def test_version_long_flag(monkeypatch: MonkeyPatch) -> None:
91
+ monkeypatch.setattr(main, "_get_version", lambda: "1.2.3")
92
+ result = runner.invoke(main.app, ["--version"])
93
+ assert result.exit_code == 0
94
+ assert "assertion-cli 1.2.3" in result.stdout
95
+
96
+
97
+ def test_version_short_flag(monkeypatch: MonkeyPatch) -> None:
98
+ monkeypatch.setattr(main, "_get_version", lambda: "1.2.3")
99
+ result = runner.invoke(main.app, ["-v"])
100
+ assert result.exit_code == 0
101
+ assert "assertion-cli 1.2.3" in result.stdout
102
+
103
+
104
+ def test_version_not_confused_with_help(monkeypatch: MonkeyPatch) -> None:
105
+ """--version should exit with version string, not show help."""
106
+ monkeypatch.setattr(main, "_get_version", lambda: "1.2.3")
107
+ result = runner.invoke(main.app, ["--version"])
108
+ assert result.exit_code == 0
109
+ assert result.stdout.strip() == "assertion-cli 1.2.3"
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from typer.testing import CliRunner
7
+
8
+ import main
9
+
10
+
11
+ runner = CliRunner()
12
+
13
+ _GIT_COMMIT_ENV = {
14
+ "GIT_AUTHOR_NAME": "T",
15
+ "GIT_AUTHOR_EMAIL": "t@t",
16
+ "GIT_COMMITTER_NAME": "T",
17
+ "GIT_COMMITTER_EMAIL": "t@t",
18
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin",
19
+ }
20
+
21
+
22
+ def _run_git(repo: Path, *args: str) -> str:
23
+ result = subprocess.run(
24
+ ["git", *args],
25
+ cwd=repo,
26
+ capture_output=True,
27
+ check=True,
28
+ env={**_GIT_COMMIT_ENV, "HOME": str(repo)},
29
+ text=True,
30
+ )
31
+ return result.stdout.strip()
32
+
33
+
34
+ def _init_repo(tmp_path: Path) -> str:
35
+ _run_git(tmp_path, "init")
36
+ _run_git(tmp_path, "commit", "--allow-empty", "-m", "init")
37
+ return _run_git(tmp_path, "rev-parse", "HEAD")
38
+
39
+
40
+ def test_new_clears_run_state_and_records_current_head(
41
+ tmp_path: Path,
42
+ monkeypatch,
43
+ ) -> None:
44
+ head = _init_repo(tmp_path)
45
+ monkeypatch.chdir(tmp_path)
46
+
47
+ assertion_dir = tmp_path / ".assertion"
48
+ (assertion_dir / "screenshots" / "verification-1").mkdir(parents=True)
49
+ (assertion_dir / "metadata.json").write_text("{}")
50
+ (assertion_dir / "prompts").write_text("old prompt")
51
+ (assertion_dir / "checkpoint").write_text("old checkpoint")
52
+ (assertion_dir / "link").write_text("old link")
53
+ (assertion_dir / "verify_session_id").write_text("old verification")
54
+ (assertion_dir / "base_sha").write_text("old-base")
55
+ (
56
+ assertion_dir / "screenshots" / "verification-1" / "screenshot_000.png"
57
+ ).write_bytes(b"png")
58
+
59
+ result = runner.invoke(main.app, ["new"])
60
+
61
+ assert result.exit_code == 0, result.stderr
62
+ assert (assertion_dir / "base_sha").read_text() == head + "\n"
63
+ assert not (assertion_dir / "metadata.json").exists()
64
+ assert not (assertion_dir / "prompts").exists()
65
+ assert not (assertion_dir / "checkpoint").exists()
66
+ assert not (assertion_dir / "link").exists()
67
+ assert not (assertion_dir / "verify_session_id").exists()
68
+ assert not (assertion_dir / "screenshots").exists()
69
+ assert "Recorded diff base" in result.stdout
70
+
71
+
72
+ def test_new_succeeds_when_assertion_dir_missing(
73
+ tmp_path: Path,
74
+ monkeypatch,
75
+ ) -> None:
76
+ head = _init_repo(tmp_path)
77
+ monkeypatch.chdir(tmp_path)
78
+
79
+ result = runner.invoke(main.app, ["new"])
80
+
81
+ assert result.exit_code == 0, result.stderr
82
+ assert (tmp_path / ".assertion" / "base_sha").read_text() == head + "\n"
83
+
84
+
85
+ def test_new_updates_base_sha_after_head_changes(
86
+ tmp_path: Path,
87
+ monkeypatch,
88
+ ) -> None:
89
+ first_head = _init_repo(tmp_path)
90
+ monkeypatch.chdir(tmp_path)
91
+
92
+ first_result = runner.invoke(main.app, ["new"])
93
+ assert first_result.exit_code == 0, first_result.stderr
94
+ assert (tmp_path / ".assertion" / "base_sha").read_text() == first_head + "\n"
95
+
96
+ _run_git(tmp_path, "commit", "--allow-empty", "-m", "second")
97
+ second_head = _run_git(tmp_path, "rev-parse", "HEAD")
98
+
99
+ second_result = runner.invoke(main.app, ["new"])
100
+
101
+ assert second_result.exit_code == 0, second_result.stderr
102
+ assert second_head != first_head
103
+ assert (tmp_path / ".assertion" / "base_sha").read_text() == second_head + "\n"
104
+
105
+
106
+ def test_new_leaves_next_checkpoint_requiring_stack(
107
+ tmp_path: Path,
108
+ monkeypatch,
109
+ ) -> None:
110
+ _init_repo(tmp_path)
111
+ monkeypatch.chdir(tmp_path)
112
+
113
+ assertion_dir = tmp_path / ".assertion"
114
+ assertion_dir.mkdir()
115
+ (assertion_dir / "metadata.json").write_text("{}")
116
+ (assertion_dir / "prompts").write_text("old prompt")
117
+
118
+ result = runner.invoke(main.app, ["new"])
119
+
120
+ assert result.exit_code == 0, result.stderr
121
+ assert not (assertion_dir / "metadata.json").exists()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes