assertion-cli 0.1.1__tar.gz → 0.4.0__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.
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/PKG-INFO +19 -6
- assertion_cli-0.4.0/README.md +47 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/assertion_cli.egg-info/PKG-INFO +19 -6
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/assertion_cli.egg-info/SOURCES.txt +1 -2
- assertion_cli-0.4.0/bundle.py +42 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/git.py +14 -78
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/main.py +210 -133
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/models.py +12 -1
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/pyproject.toml +1 -1
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/session.py +35 -51
- assertion_cli-0.4.0/templates/ACTIVATION.md +14 -0
- assertion_cli-0.4.0/templates/SKILL.md +184 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_bundle.py +5 -1
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_git.py +44 -10
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_init.py +63 -7
- assertion_cli-0.4.0/tests/test_main.py +82 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_session.py +64 -8
- assertion_cli-0.1.1/README.md +0 -34
- assertion_cli-0.1.1/bundle.py +0 -26
- assertion_cli-0.1.1/templates/ACTIVATION.md +0 -14
- assertion_cli-0.1.1/templates/SKILL.md +0 -177
- assertion_cli-0.1.1/tests/test_main.py +0 -23
- assertion_cli-0.1.1/tests/test_stack_resolve.py +0 -120
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/api.py +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/assertion_cli.egg-info/dependency_links.txt +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/assertion_cli.egg-info/entry_points.txt +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/assertion_cli.egg-info/requires.txt +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/assertion_cli.egg-info/top_level.txt +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/link.py +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/setup.cfg +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/templates/__init__.py +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_api.py +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_decision.py +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_link.py +0 -0
- {assertion_cli-0.1.1 → assertion_cli-0.4.0}/tests/test_prompt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertion-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI for the Assertion API
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -26,14 +26,15 @@ Run locally from the workspace:
|
|
|
26
26
|
uv run --package assertion-cli asrt --help
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Install
|
|
29
|
+
Install as a `uv` tool from PyPI:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
uv tool install
|
|
32
|
+
uv tool install assertion-cli # latest
|
|
33
|
+
uv tool install --reinstall assertion-cli # upgrade in place
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
The CLI package declares all of its direct runtime dependencies. At the moment
|
|
36
|
-
that set is `httpx`, `pydantic`, and `typer`.
|
|
37
|
+
that set is `httpx`, `pydantic`, `python-dotenv`, and `typer`.
|
|
37
38
|
|
|
38
39
|
After installation:
|
|
39
40
|
|
|
@@ -42,7 +43,19 @@ asrt stacks
|
|
|
42
43
|
asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
|
|
43
44
|
asrt checkpoint --continue "Implemented Y"
|
|
44
45
|
asrt decision --yes <checkpoint-id> # optional, only after a failed checkpoint
|
|
45
|
-
asrt verify
|
|
46
|
+
asrt verify # submit final verification (non-blocking)
|
|
47
|
+
asrt verify-status # one-shot poll; loop with your own sleep until terminal
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
## Publishing a new version
|
|
51
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Assertion CLI
|
|
2
|
+
|
|
3
|
+
CLI for the Assertion API.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
The CLI currently targets a local backend at `http://localhost:8000`.
|
|
8
|
+
|
|
9
|
+
Run locally from the workspace:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv run --package assertion-cli asrt --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install as a `uv` tool from PyPI:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install assertion-cli # latest
|
|
19
|
+
uv tool install --reinstall assertion-cli # upgrade in place
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The CLI package declares all of its direct runtime dependencies. At the moment
|
|
23
|
+
that set is `httpx`, `pydantic`, `python-dotenv`, and `typer`.
|
|
24
|
+
|
|
25
|
+
After installation:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
asrt stacks
|
|
29
|
+
asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
|
|
30
|
+
asrt checkpoint --continue "Implemented Y"
|
|
31
|
+
asrt decision --yes <checkpoint-id> # optional, only after a failed checkpoint
|
|
32
|
+
asrt verify # submit final verification (non-blocking)
|
|
33
|
+
asrt verify-status # one-shot poll; loop with your own sleep until terminal
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Publishing a new version
|
|
37
|
+
|
|
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
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertion-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI for the Assertion API
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -26,14 +26,15 @@ Run locally from the workspace:
|
|
|
26
26
|
uv run --package assertion-cli asrt --help
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Install
|
|
29
|
+
Install as a `uv` tool from PyPI:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
uv tool install
|
|
32
|
+
uv tool install assertion-cli # latest
|
|
33
|
+
uv tool install --reinstall assertion-cli # upgrade in place
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
The CLI package declares all of its direct runtime dependencies. At the moment
|
|
36
|
-
that set is `httpx`, `pydantic`, and `typer`.
|
|
37
|
+
that set is `httpx`, `pydantic`, `python-dotenv`, and `typer`.
|
|
37
38
|
|
|
38
39
|
After installation:
|
|
39
40
|
|
|
@@ -42,7 +43,19 @@ asrt stacks
|
|
|
42
43
|
asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
|
|
43
44
|
asrt checkpoint --continue "Implemented Y"
|
|
44
45
|
asrt decision --yes <checkpoint-id> # optional, only after a failed checkpoint
|
|
45
|
-
asrt verify
|
|
46
|
+
asrt verify # submit final verification (non-blocking)
|
|
47
|
+
asrt verify-status # one-shot poll; loop with your own sleep until terminal
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
## Publishing a new version
|
|
51
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import zipfile
|
|
4
|
+
|
|
5
|
+
from models import MetadataPayload
|
|
6
|
+
|
|
7
|
+
ASSERTION_DIR_NAME = ".assertion"
|
|
8
|
+
DIFF_ARCHIVE_PATH = "git.diff"
|
|
9
|
+
METADATA_ARCHIVE_PATH = f"{ASSERTION_DIR_NAME}/metadata.json"
|
|
10
|
+
PROMPTS_ARCHIVE_PATH = f"{ASSERTION_DIR_NAME}/prompts"
|
|
11
|
+
CHECKPOINT_ARCHIVE_PATH = f"{ASSERTION_DIR_NAME}/checkpoint"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _metadata_wire_json(metadata: MetadataPayload) -> str:
|
|
15
|
+
"""Serialize metadata for the bundle, remapping `base_sha` → `head_sha`.
|
|
16
|
+
|
|
17
|
+
The CLI stores the diff base as `base_sha` because that matches what the
|
|
18
|
+
field means from the CLI's perspective (the foundation the diff builds
|
|
19
|
+
on). The backend reads it as `head_sha` because after its `git checkout`
|
|
20
|
+
that SHA literally is the clone's HEAD. Same value, different name per
|
|
21
|
+
side; the remap lives here so neither side has to know about the other's
|
|
22
|
+
perspective.
|
|
23
|
+
"""
|
|
24
|
+
payload = metadata.model_dump()
|
|
25
|
+
payload["head_sha"] = payload.pop("base_sha", None)
|
|
26
|
+
return json.dumps(payload, indent=2) + "\n"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_bundle(
|
|
30
|
+
*,
|
|
31
|
+
metadata: MetadataPayload,
|
|
32
|
+
diff_text: str,
|
|
33
|
+
prompts_text: str,
|
|
34
|
+
checkpoint_text: str,
|
|
35
|
+
) -> bytes:
|
|
36
|
+
buf = io.BytesIO()
|
|
37
|
+
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
38
|
+
zf.writestr(METADATA_ARCHIVE_PATH, _metadata_wire_json(metadata))
|
|
39
|
+
zf.writestr(PROMPTS_ARCHIVE_PATH, prompts_text)
|
|
40
|
+
zf.writestr(CHECKPOINT_ARCHIVE_PATH, checkpoint_text)
|
|
41
|
+
zf.writestr(DIFF_ARCHIVE_PATH, diff_text)
|
|
42
|
+
return buf.getvalue()
|
|
@@ -37,41 +37,6 @@ def find_git_root(start_path: Path) -> Path:
|
|
|
37
37
|
return Path(completed.stdout.strip())
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def require_head_pushed(repo_root: Path) -> None:
|
|
41
|
-
try:
|
|
42
|
-
remote_refs = run_git_command(
|
|
43
|
-
repo_root, ["for-each-ref", "--format=%(refname:short)", "refs/remotes"]
|
|
44
|
-
)
|
|
45
|
-
except RuntimeError as exc:
|
|
46
|
-
exit_with_error(f"Failed to inspect remote refs: {exc}")
|
|
47
|
-
|
|
48
|
-
refs = [
|
|
49
|
-
ref for ref in remote_refs.splitlines() if ref and not ref.endswith("/HEAD")
|
|
50
|
-
]
|
|
51
|
-
if not refs:
|
|
52
|
-
exit_with_error(
|
|
53
|
-
"Current HEAD commit is not present on any remote-tracking branch."
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
for ref in refs:
|
|
57
|
-
completed = subprocess.run(
|
|
58
|
-
["git", "merge-base", "--is-ancestor", "HEAD", ref],
|
|
59
|
-
cwd=repo_root,
|
|
60
|
-
capture_output=True,
|
|
61
|
-
text=True,
|
|
62
|
-
check=False,
|
|
63
|
-
)
|
|
64
|
-
if completed.returncode == 0:
|
|
65
|
-
return
|
|
66
|
-
if completed.returncode != 1:
|
|
67
|
-
message = (
|
|
68
|
-
completed.stderr.strip() or completed.stdout.strip() or "git failed"
|
|
69
|
-
)
|
|
70
|
-
exit_with_error(f"Failed to verify remote commit state: {message}")
|
|
71
|
-
|
|
72
|
-
exit_with_error("Current HEAD commit is not present on any remote-tracking branch.")
|
|
73
|
-
|
|
74
|
-
|
|
75
40
|
def get_head_sha(repo_root: Path) -> str:
|
|
76
41
|
try:
|
|
77
42
|
return run_git_command(repo_root, ["rev-parse", "HEAD"])
|
|
@@ -88,40 +53,6 @@ def get_head_branch(repo_root: Path) -> str | None:
|
|
|
88
53
|
return name if name and name != "HEAD" else None
|
|
89
54
|
|
|
90
55
|
|
|
91
|
-
def get_origin_github_repo(repo_root: Path) -> str | None:
|
|
92
|
-
"""Return the current repo's GitHub `owner/name` from origin, or None if not parseable.
|
|
93
|
-
|
|
94
|
-
Accepts the common remote URL forms:
|
|
95
|
-
git@github.com:owner/name(.git)?
|
|
96
|
-
https://github.com/owner/name(.git)?
|
|
97
|
-
ssh://git@github.com/owner/name(.git)?
|
|
98
|
-
"""
|
|
99
|
-
try:
|
|
100
|
-
url = run_git_command(repo_root, ["remote", "get-url", "origin"]).strip()
|
|
101
|
-
except RuntimeError:
|
|
102
|
-
return None
|
|
103
|
-
if not url:
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
if url.startswith("git@github.com:"):
|
|
107
|
-
path = url[len("git@github.com:") :]
|
|
108
|
-
elif url.startswith("ssh://git@github.com/"):
|
|
109
|
-
path = url[len("ssh://git@github.com/") :]
|
|
110
|
-
elif url.startswith("https://github.com/"):
|
|
111
|
-
path = url[len("https://github.com/") :]
|
|
112
|
-
elif url.startswith("http://github.com/"):
|
|
113
|
-
path = url[len("http://github.com/") :]
|
|
114
|
-
else:
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
path = path.rstrip("/")
|
|
118
|
-
if path.endswith(".git"):
|
|
119
|
-
path = path[: -len(".git")]
|
|
120
|
-
if path.count("/") != 1 or not all(path.split("/")):
|
|
121
|
-
return None
|
|
122
|
-
return path
|
|
123
|
-
|
|
124
|
-
|
|
125
56
|
def _build_untracked_diff(repo_root: Path, rel_path: str) -> str:
|
|
126
57
|
completed = subprocess.run(
|
|
127
58
|
[
|
|
@@ -148,9 +79,9 @@ def _build_untracked_diff(repo_root: Path, rel_path: str) -> str:
|
|
|
148
79
|
# Paths the assertion-cli owns end-to-end. Excluded from the diff bundle so
|
|
149
80
|
# reviewers don't flag our own bootstrap files as "unrelated changes" — they're
|
|
150
81
|
# generated/refreshed by `asrt init` and `asrt checkpoint` and have nothing to
|
|
151
|
-
# do with the
|
|
152
|
-
# NOT in this list: those are
|
|
153
|
-
# block into, so the
|
|
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.
|
|
154
85
|
_ASSERTION_EXCLUDED_PATHSPECS = [
|
|
155
86
|
":(exclude).assertion",
|
|
156
87
|
":(exclude).claude/skills/assertion-cli",
|
|
@@ -158,13 +89,18 @@ _ASSERTION_EXCLUDED_PATHSPECS = [
|
|
|
158
89
|
]
|
|
159
90
|
|
|
160
91
|
|
|
161
|
-
def get_uncommitted_diff(repo_root: Path) -> str:
|
|
92
|
+
def get_uncommitted_diff(repo_root: Path, base_sha: str) -> str:
|
|
93
|
+
"""Build a unified diff from `base_sha` to the current working tree.
|
|
94
|
+
|
|
95
|
+
`git diff <base_sha>` compares the working tree directly to the base
|
|
96
|
+
commit, which subsumes both committed-since-base and unstaged tweaks in
|
|
97
|
+
one shot — so a single call covers all tracked changes regardless of
|
|
98
|
+
whether the agent committed locally. Untracked files are still added
|
|
99
|
+
separately as new-file diffs.
|
|
100
|
+
"""
|
|
162
101
|
try:
|
|
163
102
|
tracked = run_git_command(
|
|
164
|
-
repo_root, ["diff", "--", *_ASSERTION_EXCLUDED_PATHSPECS]
|
|
165
|
-
)
|
|
166
|
-
staged = run_git_command(
|
|
167
|
-
repo_root, ["diff", "--cached", "--", *_ASSERTION_EXCLUDED_PATHSPECS]
|
|
103
|
+
repo_root, ["diff", base_sha, "--", *_ASSERTION_EXCLUDED_PATHSPECS]
|
|
168
104
|
)
|
|
169
105
|
|
|
170
106
|
untracked_output = run_git_command(
|
|
@@ -183,7 +119,7 @@ def get_uncommitted_diff(repo_root: Path) -> str:
|
|
|
183
119
|
if rel_path
|
|
184
120
|
]
|
|
185
121
|
|
|
186
|
-
parts = [p for p in [tracked,
|
|
122
|
+
parts = [p for p in [tracked, "\n".join(untracked_diffs)] if p]
|
|
187
123
|
return "\n".join(parts)
|
|
188
124
|
except RuntimeError as exc:
|
|
189
125
|
exit_with_error(f"Failed to collect git diff: {exc}")
|