assertion-cli 0.5.4__tar.gz → 0.5.6__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.4 → assertion_cli-0.5.6}/PKG-INFO +17 -11
  2. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/README.md +16 -10
  3. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/assertion_cli.egg-info/PKG-INFO +17 -11
  4. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/assertion_cli.egg-info/SOURCES.txt +1 -1
  5. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/git.py +2 -2
  6. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/main.py +86 -105
  7. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/pyproject.toml +1 -1
  8. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/templates/SKILL.md +6 -3
  9. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_init.py +143 -38
  10. assertion_cli-0.5.6/tests/test_new.py +121 -0
  11. assertion_cli-0.5.4/templates/ACTIVATION.md +0 -50
  12. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/api.py +0 -0
  13. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/assertion_cli.egg-info/dependency_links.txt +0 -0
  14. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/assertion_cli.egg-info/entry_points.txt +0 -0
  15. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/assertion_cli.egg-info/requires.txt +0 -0
  16. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/assertion_cli.egg-info/top_level.txt +0 -0
  17. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/bundle.py +0 -0
  18. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/link.py +0 -0
  19. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/models.py +0 -0
  20. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/session.py +0 -0
  21. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/setup.cfg +0 -0
  22. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/templates/__init__.py +0 -0
  23. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_api.py +0 -0
  24. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_bundle.py +0 -0
  25. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_decision.py +0 -0
  26. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_git.py +0 -0
  27. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_link.py +0 -0
  28. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_main.py +0 -0
  29. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/tests/test_prompt.py +0 -0
  30. {assertion_cli-0.5.4 → assertion_cli-0.5.6}/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.4
3
+ Version: 0.5.6
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,18 @@ 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, and pushes a `cli-v<version>` tag.
59
+
60
+ If the version hasn't changed (e.g. you updated a dependency), the workflow
61
+ skips publishing only version bumps trigger a publish.
62
+
63
+ You can also trigger the workflow manually from the GitHub Actions UI — it
64
+ reads the version from the current `pyproject.toml` on `main`.
65
+
66
+ **Prerequisite**: `PYPI_TOKEN` must be set as a GitHub Actions secret — a
67
+ 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,18 @@ 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, and pushes a `cli-v<version>` tag.
45
+
46
+ If the version hasn't changed (e.g. you updated a dependency), the workflow
47
+ skips publishing only version bumps trigger a publish.
48
+
49
+ You can also trigger the workflow manually from the GitHub Actions UI — it
50
+ reads the version from the current `pyproject.toml` on `main`.
51
+
52
+ **Prerequisite**: `PYPI_TOKEN` must be set as a GitHub Actions secret — a
53
+ 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.4
3
+ Version: 0.5.6
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,18 @@ 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, and pushes a `cli-v<version>` tag.
59
+
60
+ If the version hasn't changed (e.g. you updated a dependency), the workflow
61
+ skips publishing only version bumps trigger a publish.
62
+
63
+ You can also trigger the workflow manually from the GitHub Actions UI — it
64
+ reads the version from the current `pyproject.toml` on `main`.
65
+
66
+ **Prerequisite**: `PYPI_TOKEN` must be set as a GitHub Actions secret — a
67
+ PyPI API token with upload scope for `assertion-cli`.
@@ -13,7 +13,6 @@ assertion_cli.egg-info/dependency_links.txt
13
13
  assertion_cli.egg-info/entry_points.txt
14
14
  assertion_cli.egg-info/requires.txt
15
15
  assertion_cli.egg-info/top_level.txt
16
- templates/ACTIVATION.md
17
16
  templates/SKILL.md
18
17
  templates/__init__.py
19
18
  tests/test_api.py
@@ -23,5 +22,6 @@ tests/test_git.py
23
22
  tests/test_init.py
24
23
  tests/test_link.py
25
24
  tests/test_main.py
25
+ tests/test_new.py
26
26
  tests/test_prompt.py
27
27
  tests/test_session.py
@@ -80,8 +80,8 @@ def _build_untracked_diff(repo_root: Path, rel_path: str) -> str:
80
80
  # reviewers don't flag our own bootstrap files as "unrelated changes" — they're
81
81
  # generated/refreshed by `asrt init` and `asrt checkpoint` and have nothing to
82
82
  # do with the user's feature work. CLAUDE.md / AGENTS.md are intentionally
83
- # NOT in this list: those are user-owned files we only patch a marked
84
- # block into, so the user's other edits to them should still flow through.
83
+ # NOT in this list: the CLI never writes them, so any edits there are the
84
+ # user's own and should flow through to reviewers.
85
85
  _ASSERTION_EXCLUDED_PATHSPECS = [
86
86
  ":(exclude).assertion",
87
87
  ":(exclude).claude/skills/assertion-cli",
@@ -2,34 +2,13 @@ import base64
2
2
  import importlib.metadata
3
3
  import importlib.resources
4
4
  import json
5
- import re
5
+ import shutil
6
6
  from datetime import datetime, timezone
7
7
  from pathlib import Path
8
8
 
9
9
  import httpx
10
10
  import typer
11
11
 
12
-
13
- _VERSION: str | None = None
14
-
15
-
16
- def _get_version() -> str:
17
- global _VERSION
18
- if _VERSION is not None:
19
- return _VERSION
20
- try:
21
- _VERSION = importlib.metadata.version("assertion-cli")
22
- except importlib.metadata.PackageNotFoundError:
23
- pyproject = Path(__file__).resolve().parent / "pyproject.toml"
24
- if pyproject.exists():
25
- import tomllib
26
-
27
- data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
28
- _VERSION = data["project"]["version"]
29
- else:
30
- _VERSION = "0.0.0"
31
- return _VERSION
32
-
33
12
  from api import AssertionClient
34
13
  from bundle import build_bundle
35
14
  from git import (
@@ -54,6 +33,27 @@ from session import (
54
33
  update_metadata_anchor,
55
34
  )
56
35
 
36
+ _VERSION: str | None = None
37
+
38
+
39
+ def _get_version() -> str:
40
+ global _VERSION
41
+ if _VERSION is not None:
42
+ return _VERSION
43
+ try:
44
+ _VERSION = importlib.metadata.version("assertion-cli")
45
+ except importlib.metadata.PackageNotFoundError:
46
+ pyproject = Path(__file__).resolve().parent / "pyproject.toml"
47
+ if pyproject.exists():
48
+ import tomllib
49
+
50
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
51
+ _VERSION = data["project"]["version"]
52
+ else:
53
+ _VERSION = "0.0.0"
54
+ return _VERSION
55
+
56
+
57
57
  app = typer.Typer(help="Assertion CLI")
58
58
 
59
59
 
@@ -100,10 +100,6 @@ def stacks() -> None:
100
100
  typer.echo(render_stack_list(stack_list))
101
101
 
102
102
 
103
- ASSERTION_MARKER_BEGIN = "<!-- assertion-cli:begin -->"
104
- ASSERTION_MARKER_END = "<!-- assertion-cli:end -->"
105
-
106
-
107
103
  def _load_template(name: str) -> str:
108
104
  return (
109
105
  importlib.resources.files("assertion_cli_templates")
@@ -124,45 +120,6 @@ def _write_full_file(path: Path, content: str, repo_root: Path) -> None:
124
120
  typer.echo(f"Wrote {path.relative_to(repo_root)}")
125
121
 
126
122
 
127
- def _apply_marked_block(path: Path, body: str, repo_root: Path) -> None:
128
- """Idempotently insert/update an assertion-marked block in a markdown file.
129
-
130
- Existing content outside the markers is preserved exactly.
131
- Silent on no-op; echoes only when a write actually happens.
132
- """
133
- marked_block = f"{ASSERTION_MARKER_BEGIN}\n{body}\n{ASSERTION_MARKER_END}\n"
134
- pattern = re.compile(
135
- re.escape(ASSERTION_MARKER_BEGIN)
136
- + r".*?"
137
- + re.escape(ASSERTION_MARKER_END)
138
- + r"\n?",
139
- re.DOTALL,
140
- )
141
-
142
- if path.exists():
143
- current = path.read_text(encoding="utf-8")
144
- if pattern.search(current):
145
- # Replace via a callable so re.sub does NOT interpret backslash
146
- # escapes (e.g. `\n`, `\g`) in the block — the activation template
147
- # contains literal shell snippets like `printf '%s\n\n...'`, and a
148
- # string replacement would mangle them, breaking init idempotency.
149
- new_content = pattern.sub(lambda _match: marked_block, current)
150
- else:
151
- sep = (
152
- ""
153
- if current.endswith("\n\n")
154
- else ("\n" if current.endswith("\n") else "\n\n")
155
- )
156
- new_content = current + sep + marked_block
157
- if new_content != current:
158
- path.write_text(new_content, encoding="utf-8")
159
- typer.echo(f"Updated {path.relative_to(repo_root)}")
160
- else:
161
- path.parent.mkdir(parents=True, exist_ok=True)
162
- path.write_text(marked_block, encoding="utf-8")
163
- typer.echo(f"Wrote {path.relative_to(repo_root)}")
164
-
165
-
166
123
  def _refresh_skill_files(repo_root: Path) -> None:
167
124
  """Write/refresh the assertion-owned skill files for each coding agent.
168
125
 
@@ -185,55 +142,55 @@ def _refresh_skill_files(repo_root: Path) -> None:
185
142
  )
186
143
 
187
144
 
188
- _GITIGNORE_ASSERTION_ENTRY = ".assertion/"
189
-
145
+ _ASSERTION_GITIGNORE_RULES = (".assertion/*", "!.assertion/seed.py")
190
146
 
191
- def _ensure_gitignore_excludes_assertion(repo_root: Path) -> None:
192
- """Append `.assertion/` to the repo's .gitignore if not already excluded.
193
147
 
194
- `.assertion/` holds per-session state (prompts, checkpoint message,
195
- metadata, screenshots) purely local working data. Without this entry it
196
- surfaces as untracked changes that `asrt checkpoint`'s diff collector
197
- would otherwise bundle and send to the reviewers.
198
- """
148
+ def _ensure_gitignore_limits_assertion_state(repo_root: Path) -> None:
149
+ """Ignore local Assertion state while allowing committed seed hooks."""
199
150
  gitignore = repo_root / ".gitignore"
200
151
  existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
201
- if any(
202
- line.strip() in {".assertion", ".assertion/"} for line in existing.splitlines()
203
- ):
152
+ lines = existing.splitlines()
153
+ stripped_lines = {line.strip() for line in lines}
154
+
155
+ missing_rules = [
156
+ rule for rule in _ASSERTION_GITIGNORE_RULES if rule not in stripped_lines
157
+ ]
158
+ if not missing_rules:
204
159
  return
205
- prefix = "" if not existing or existing.endswith("\n") else "\n"
206
- gitignore.write_text(
207
- existing + prefix + _GITIGNORE_ASSERTION_ENTRY + "\n", encoding="utf-8"
208
- )
209
- typer.echo(
210
- f"Updated {gitignore.relative_to(repo_root)} (added {_GITIGNORE_ASSERTION_ENTRY})"
211
- )
212
160
 
161
+ added_lines = []
162
+ if "# Assertion CLI local state" not in stripped_lines:
163
+ added_lines.append("# Assertion CLI local state")
164
+ added_lines.extend(missing_rules)
213
165
 
214
- def _apply_activation_blocks(repo_root: Path) -> None:
215
- """Insert/update the marked activation block in user-owned CLAUDE.md / AGENTS.md.
166
+ content = existing.rstrip()
167
+ if content:
168
+ content += "\n\n"
169
+ content += "\n".join(added_lines) + "\n"
216
170
 
217
- Only called by `asrt init`. We deliberately do NOT call this on every checkpoint —
218
- these files may be tracked in the user's git, and silently dirtying them on
219
- every checkpoint is hostile. The block is a stable pointer to the skill file, so
220
- re-running on every CLI upgrade isn't necessary.
221
- """
222
- activation_body = _load_template("ACTIVATION.md").strip()
223
- _apply_marked_block(repo_root / "CLAUDE.md", activation_body, repo_root)
224
- _apply_marked_block(repo_root / "AGENTS.md", activation_body, repo_root)
171
+ gitignore.write_text(content, encoding="utf-8")
172
+ typer.echo(
173
+ f"Updated {gitignore.relative_to(repo_root)} "
174
+ "(ignored .assertion/* except .assertion/seed.py)"
175
+ )
225
176
 
226
177
 
227
178
  @app.command("init")
228
179
  def init() -> None:
229
180
  """One-time bootstrap: install the assertion-cli skill files into this repo.
230
181
 
231
- Writes `.claude/skills/assertion-cli/SKILL.md`, `.agents/skills/assertion-cli/SKILL.md`,
232
- and the marked activation block in `CLAUDE.md` and `AGENTS.md`. Run this once after
233
- `uv tool install assertion-cli` so the coding agent (Claude Code, Codex, Cursor) loads
234
- the skill and knows to call `asrt`. `asrt checkpoint` refreshes the skill files on each
235
- run so they stay aligned with the installed CLI; `CLAUDE.md` / `AGENTS.md` are only
236
- touched here, never by checkpoint, so they're safe to track in git.
182
+ Writes `.claude/skills/assertion-cli/SKILL.md` and
183
+ `.agents/skills/assertion-cli/SKILL.md`. Run this once after
184
+ `uv tool install assertion-cli` so the coding agent (Claude Code, Codex, Cursor)
185
+ auto-discovers the skill from its skills directory and knows to call `asrt`.
186
+ `asrt checkpoint` refreshes these skill files on each run so they stay aligned
187
+ with the installed CLI.
188
+
189
+ `init` never touches `CLAUDE.md` or `AGENTS.md`. Those are user-owned files;
190
+ the skill files are auto-discovered without an activation pointer, and editing
191
+ user-tracked docs would dirty their working tree and leak the assertion
192
+ bootstrap into the verification diff (which is anchored to the HEAD recorded
193
+ below).
237
194
 
238
195
  Also records the current HEAD SHA as the diff base for all future checkpoint/verify
239
196
  calls in this repo. The base is what the backend `git checkout`s against before
@@ -242,8 +199,7 @@ def init() -> None:
242
199
  """
243
200
  repo_root = find_git_root(Path.cwd())
244
201
  _refresh_skill_files(repo_root)
245
- _apply_activation_blocks(repo_root)
246
- _ensure_gitignore_excludes_assertion(repo_root)
202
+ _ensure_gitignore_limits_assertion_state(repo_root)
247
203
 
248
204
  base_sha = get_head_sha(repo_root)
249
205
  assertion_dir = repo_root / ASSERTION_DIR_NAME
@@ -261,6 +217,31 @@ def init() -> None:
261
217
  typer.echo(" - asrt verify at completion, then PR")
262
218
 
263
219
 
220
+ @app.command("new")
221
+ def new() -> None:
222
+ """Reset local Assertion run state and record current HEAD as the diff base."""
223
+ repo_root = find_git_root(Path.cwd())
224
+ base_sha = get_head_sha(repo_root)
225
+ assertion_dir = repo_root / ASSERTION_DIR_NAME
226
+ assertion_dir.mkdir(exist_ok=True)
227
+
228
+ removed = 0
229
+ for child in assertion_dir.iterdir():
230
+ if child.is_dir():
231
+ shutil.rmtree(child)
232
+ else:
233
+ child.unlink()
234
+ removed += 1
235
+
236
+ base_sha_path = assertion_dir / BASE_SHA_FILE_NAME
237
+ base_sha_path.write_text(base_sha + "\n", encoding="utf-8")
238
+
239
+ typer.echo(f"Cleared {removed} .assertion entr{'y' if removed == 1 else 'ies'}.")
240
+ typer.echo(
241
+ f"Recorded diff base {base_sha[:12]} → {base_sha_path.relative_to(repo_root)}"
242
+ )
243
+
244
+
264
245
  @app.command("prompt")
265
246
  def prompt_cmd(
266
247
  text: str = typer.Argument(..., help="User prompt text to record."),
@@ -316,9 +297,9 @@ def checkpoint(
316
297
  repo_root = find_git_root(Path.cwd())
317
298
 
318
299
  # Refresh the assertion-owned skill files so the agent always loads the
319
- # version that ships with the installed CLI. We deliberately do NOT touch
320
- # CLAUDE.md / AGENTS.md here — those are user-owned and only updated
321
- # by `asrt init`.
300
+ # version that ships with the installed CLI. The CLI never writes
301
+ # CLAUDE.md / AGENTS.md — those are user-owned, and the skill files are
302
+ # auto-discovered without an activation pointer.
322
303
  _refresh_skill_files(repo_root)
323
304
 
324
305
  existing_metadata_path = repo_root / ASSERTION_DIR_NAME / METADATA_FILE_NAME
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "assertion-cli"
7
- version = "0.5.4"
7
+ version = "0.5.6"
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 work session, check whether this repo may have previous Assertion state in `.assertion/`, then ask the user whether they want to clean up the previous session and start fresh. If the user says yes, run `asrt new` before any checkpoint or verify command. 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. If the user says no, continue with the existing session state. Do not run `asrt new` without explicit user consent — 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. |
@@ -193,12 +193,15 @@ A missing marker is a **blocking error**, not a warning: do not consider the PR
193
193
 
194
194
  **Do not open a PR without a clean `asrt verify` / `asrt verify-status`, and do not open one whose body omits the session marker.** Do not end a session with a failed/dirty verify unless the verification service itself is unavailable.
195
195
 
196
+ **After the PR marker is confirmed:** ask the user whether they want to clean up the completed Assertion session and start fresh for the next session. If the user says yes, run `asrt new`. If the user says no, leave the existing `.assertion/` state intact.
197
+
196
198
  ---
197
199
 
198
200
  ## Quick reference
199
201
 
200
202
  ```
201
203
  asrt prompt "<message>" # every user turn, before anything else
204
+ asrt new # reset local run state only after user consent
202
205
  asrt stacks # list every stack — read once before first checkpoint
203
206
  asrt checkpoint --stack <id> "..." # first checkpoint of a session
204
207
  asrt checkpoint "..." # subsequent checkpoints — auto-continues
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from typer.testing import CliRunner
7
7
 
8
8
  import main
9
- from main import ASSERTION_MARKER_BEGIN, ASSERTION_MARKER_END
10
9
 
11
10
 
12
11
  runner = CliRunner()
@@ -53,12 +52,137 @@ def test_init_writes_all_agent_destinations(tmp_path: Path, monkeypatch) -> None
53
52
  assert agents_skill.exists()
54
53
  assert agents_skill.read_text() == claude_skill.read_text()
55
54
 
56
- # CLAUDE.md + AGENTS.md activation marker blocks
57
- for filename in ["CLAUDE.md", "AGENTS.md"]:
58
- body = (tmp_path / filename).read_text()
59
- assert ASSERTION_MARKER_BEGIN in body
60
- assert ASSERTION_MARKER_END in body
61
- assert "Assertion verification" in body
55
+ # init must never create or touch user-owned CLAUDE.md / AGENTS.md the
56
+ # skill files are auto-discovered, and editing user docs would leak the
57
+ # assertion bootstrap into the verification diff.
58
+ assert not (tmp_path / "CLAUDE.md").exists()
59
+ assert not (tmp_path / "AGENTS.md").exists()
60
+
61
+
62
+ def test_init_ignores_assertion_state_but_keeps_seed_trackable(
63
+ tmp_path: Path, monkeypatch
64
+ ) -> None:
65
+ _init_repo(tmp_path)
66
+ monkeypatch.chdir(tmp_path)
67
+
68
+ existing_gitignore = "*.pyc\n"
69
+ (tmp_path / ".gitignore").write_text(existing_gitignore)
70
+ (tmp_path / ".assertion").mkdir()
71
+ (tmp_path / ".assertion" / "base_sha").write_text("local state\n")
72
+ (tmp_path / ".assertion" / "seed.py").write_text("from assertion_seed import seed\n")
73
+
74
+ result = runner.invoke(main.app, ["init"])
75
+ assert result.exit_code == 0, result.stderr
76
+
77
+ assert (tmp_path / ".gitignore").read_text() == (
78
+ "*.pyc\n\n"
79
+ "# Assertion CLI local state\n"
80
+ ".assertion/*\n"
81
+ "!.assertion/seed.py\n"
82
+ )
83
+
84
+ ignored_state = subprocess.run(
85
+ ["git", "check-ignore", ".assertion/base_sha"],
86
+ cwd=tmp_path,
87
+ capture_output=True,
88
+ )
89
+ trackable_seed = subprocess.run(
90
+ ["git", "check-ignore", ".assertion/seed.py"],
91
+ cwd=tmp_path,
92
+ capture_output=True,
93
+ )
94
+ assert ignored_state.returncode == 0
95
+ assert trackable_seed.returncode == 1
96
+
97
+
98
+ def test_init_creates_gitignore_for_assertion_state(tmp_path: Path, monkeypatch) -> None:
99
+ _init_repo(tmp_path)
100
+ monkeypatch.chdir(tmp_path)
101
+
102
+ result = runner.invoke(main.app, ["init"])
103
+ assert result.exit_code == 0, result.stderr
104
+
105
+ assert (tmp_path / ".gitignore").read_text() == (
106
+ "# Assertion CLI local state\n"
107
+ ".assertion/*\n"
108
+ "!.assertion/seed.py\n"
109
+ )
110
+
111
+
112
+ def test_init_preserves_broad_assertion_gitignore_entry_while_adding_missing_rules(
113
+ tmp_path: Path, monkeypatch
114
+ ) -> None:
115
+ _init_repo(tmp_path)
116
+ monkeypatch.chdir(tmp_path)
117
+ (tmp_path / ".gitignore").write_text(".assertion/\n")
118
+
119
+ result = runner.invoke(main.app, ["init"])
120
+ assert result.exit_code == 0, result.stderr
121
+
122
+ assert (tmp_path / ".gitignore").read_text() == (
123
+ ".assertion/\n\n"
124
+ "# Assertion CLI local state\n"
125
+ ".assertion/*\n"
126
+ "!.assertion/seed.py\n"
127
+ )
128
+
129
+
130
+ def test_init_adds_only_missing_assertion_gitignore_rules(
131
+ tmp_path: Path, monkeypatch
132
+ ) -> None:
133
+ _init_repo(tmp_path)
134
+ monkeypatch.chdir(tmp_path)
135
+ (tmp_path / ".gitignore").write_text(
136
+ "# Assertion CLI local state\n"
137
+ ".assertion/*\n"
138
+ )
139
+
140
+ result = runner.invoke(main.app, ["init"])
141
+ assert result.exit_code == 0, result.stderr
142
+
143
+ assert (tmp_path / ".gitignore").read_text() == (
144
+ "# Assertion CLI local state\n"
145
+ ".assertion/*\n\n"
146
+ "!.assertion/seed.py\n"
147
+ )
148
+
149
+
150
+ def test_init_leaves_gitignore_unchanged_when_assertion_rules_exist(
151
+ tmp_path: Path, monkeypatch
152
+ ) -> None:
153
+ _init_repo(tmp_path)
154
+ monkeypatch.chdir(tmp_path)
155
+ existing_gitignore = (
156
+ "*.pyc\n\n"
157
+ "# Assertion CLI local state\n"
158
+ ".assertion/*\n"
159
+ "!.assertion/seed.py\n"
160
+ )
161
+ (tmp_path / ".gitignore").write_text(existing_gitignore)
162
+
163
+ result = runner.invoke(main.app, ["init"])
164
+ assert result.exit_code == 0, result.stderr
165
+
166
+ assert (tmp_path / ".gitignore").read_text() == existing_gitignore
167
+
168
+
169
+ def test_init_skill_prompts_before_new_session_cleanup(
170
+ tmp_path: Path, monkeypatch
171
+ ) -> None:
172
+ _init_repo(tmp_path)
173
+ monkeypatch.chdir(tmp_path)
174
+
175
+ result = runner.invoke(main.app, ["init"])
176
+ assert result.exit_code == 0, result.stderr
177
+
178
+ skill = (
179
+ tmp_path / ".agents" / "skills" / "assertion-cli" / "SKILL.md"
180
+ ).read_text()
181
+ assert "ask the user whether they want to clean up the previous session" in skill
182
+ assert "If the user says yes, run `asrt new` before any checkpoint" in skill
183
+ assert "Do not run `asrt new` without explicit user consent" in skill
184
+ assert "After the PR marker is confirmed" in skill
185
+ assert "clean up the completed Assertion session" in skill
62
186
 
63
187
 
64
188
  def test_init_is_idempotent(tmp_path: Path, monkeypatch) -> None:
@@ -71,8 +195,6 @@ def test_init_is_idempotent(tmp_path: Path, monkeypatch) -> None:
71
195
  for p in [
72
196
  ".claude/skills/assertion-cli/SKILL.md",
73
197
  ".agents/skills/assertion-cli/SKILL.md",
74
- "CLAUDE.md",
75
- "AGENTS.md",
76
198
  ]
77
199
  }
78
200
 
@@ -81,7 +203,16 @@ def test_init_is_idempotent(tmp_path: Path, monkeypatch) -> None:
81
203
  assert (tmp_path / rel).read_text() == before, rel
82
204
 
83
205
 
84
- def test_init_appends_to_existing_unmarked_files(tmp_path: Path, monkeypatch) -> None:
206
+ def test_init_leaves_existing_activation_files_untouched(
207
+ tmp_path: Path, monkeypatch
208
+ ) -> None:
209
+ """init must not modify user-owned CLAUDE.md / AGENTS.md, byte for byte.
210
+
211
+ Previously init patched a marked activation block into these files. Because
212
+ the base SHA is recorded at init time, that edit then surfaced in every
213
+ checkpoint/verify diff as unrelated bootstrap noise. The skill files are
214
+ auto-discovered, so the activation block is unnecessary.
215
+ """
85
216
  _init_repo(tmp_path)
86
217
  monkeypatch.chdir(tmp_path)
87
218
 
@@ -93,34 +224,8 @@ def test_init_appends_to_existing_unmarked_files(tmp_path: Path, monkeypatch) ->
93
224
  result = runner.invoke(main.app, ["init"])
94
225
  assert result.exit_code == 0
95
226
 
96
- claude_body = (tmp_path / "CLAUDE.md").read_text()
97
- agents_body = (tmp_path / "AGENTS.md").read_text()
98
- assert claude_body.startswith(existing_claude)
99
- assert agents_body.startswith(existing_agents)
100
- assert ASSERTION_MARKER_BEGIN in claude_body
101
- assert ASSERTION_MARKER_BEGIN in agents_body
102
-
103
-
104
- def test_init_replaces_stale_marked_section(tmp_path: Path, monkeypatch) -> None:
105
- _init_repo(tmp_path)
106
- monkeypatch.chdir(tmp_path)
107
-
108
- stale = (
109
- "# Existing\n\n"
110
- f"{ASSERTION_MARKER_BEGIN}\n"
111
- "outdated content\n"
112
- f"{ASSERTION_MARKER_END}\n\n"
113
- "Custom instructions after.\n"
114
- )
115
- (tmp_path / "CLAUDE.md").write_text(stale)
116
-
117
- runner.invoke(main.app, ["init"])
118
-
119
- body = (tmp_path / "CLAUDE.md").read_text()
120
- assert "outdated content" not in body
121
- assert "# Existing" in body
122
- assert "Custom instructions after." in body
123
- assert "Assertion verification" in body
227
+ assert (tmp_path / "CLAUDE.md").read_text() == existing_claude
228
+ assert (tmp_path / "AGENTS.md").read_text() == existing_agents
124
229
 
125
230
 
126
231
  def test_init_requires_git_repo(tmp_path: Path, monkeypatch) -> None:
@@ -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()
@@ -1,50 +0,0 @@
1
- # Assertion verification
2
-
3
- This repo uses the Assertion CLI (`asrt`) to track user intent and verify your work. The full workflow lives in the `assertion-cli` skill:
4
-
5
- - **Claude Code** — [`.claude/skills/assertion-cli/SKILL.md`](.claude/skills/assertion-cli/SKILL.md)
6
- - **Codex / Cursor** — [`.agents/skills/assertion-cli/SKILL.md`](.agents/skills/assertion-cli/SKILL.md)
7
-
8
- Both files are byte-identical. Read whichever your agent loads (or either one directly). In brief, follow these three rules for every conversation:
9
-
10
- 1. **Log every user message:** run `asrt prompt "<exact text>"` before anything else, on every turn — it's the verifier's record of intent.
11
- 2. **Checkpoint at trajectory-feedback moments**, not every change. First checkpoint: `asrt stacks`, pick the stack that fits, then `asrt checkpoint --stack <id> "<summary>"`; later ones drop `--stack` and auto-continue. A feature build is a handful of checkpoints.
12
- 3. **Verify at completion** with `asrt verify`. If feedback requires changes, iterate and re-verify — do **not** drop back into the checkpoint loop. Once verify is clean, open a PR — and that PR **must** carry the hidden Assertion session marker (see below).
13
-
14
- ---
15
-
16
- ## ⚠️ REQUIRED: every PR must embed the hidden session marker
17
-
18
- **This is the single most-missed step, and it is not optional.** A PR opened from an Assertion session that omits the marker cannot be linked back to its verification, so the work reads as unverified.
19
-
20
- The Assertion GitHub bot links a PR to its verification session by **branch name first**. When the branch name doesn't match the one you verified on (you renamed it, the head ref differs, etc.), the bot falls back to reading the marker. No marker → no fallback → no link.
21
-
22
- `asrt get-link` prints the marker as an HTML comment that GitHub hides from the rendered description but still delivers to the bot in the webhook payload:
23
-
24
- ```
25
- <!-- assertion-session: https://app.tryassertion.com/sessions/<id> -->
26
- ```
27
-
28
- **Open the PR with the marker embedded** — never paste the bare URL into the visible body; use the command so the format stays exact:
29
-
30
- ```
31
- gh pr create --title "<title>" --body "$(printf '%s\n\n%s\n' '<one-paragraph why>' "$(asrt get-link)")"
32
- ```
33
-
34
- If a PR already exists for the branch, add the marker instead of opening a new one:
35
-
36
- ```
37
- gh pr edit --body "$(printf '%s\n\n%s\n' '<existing summary>' "$(asrt get-link)")"
38
- ```
39
-
40
- **Then confirm it landed.** The marker won't show in the rendered description — that's expected — so verify it's in the raw body:
41
-
42
- ```
43
- gh pr view --json body --jq .body | grep -q 'assertion-session:' && echo "marker present" || echo "MARKER MISSING — fix before continuing"
44
- ```
45
-
46
- ---
47
-
48
- ## Hard gates (do not bypass)
49
- - **No PR whose body omits the session marker.** If `asrt get-link` has nothing to return, you haven't verified yet — go back to rule 3.
50
- - **Confirm the marker after creating/editing the PR** with the `gh pr view` check above. Treat a missing marker as a blocking error, not a warning.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes