assertion-cli 0.1.0__py3-none-any.whl
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.
- api.py +211 -0
- assertion_cli-0.1.0.dist-info/METADATA +48 -0
- assertion_cli-0.1.0.dist-info/RECORD +15 -0
- assertion_cli-0.1.0.dist-info/WHEEL +5 -0
- assertion_cli-0.1.0.dist-info/entry_points.txt +2 -0
- assertion_cli-0.1.0.dist-info/top_level.txt +8 -0
- assertion_cli_templates/ACTIVATION.md +14 -0
- assertion_cli_templates/SKILL.md +177 -0
- assertion_cli_templates/__init__.py +0 -0
- bundle.py +26 -0
- git.py +189 -0
- link.py +21 -0
- main.py +499 -0
- models.py +86 -0
- session.py +220 -0
api.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
from dotenv import find_dotenv, load_dotenv
|
|
9
|
+
from pydantic import BaseModel, ValidationError
|
|
10
|
+
|
|
11
|
+
from models import (
|
|
12
|
+
CheckpointResponse,
|
|
13
|
+
DecisionResponse,
|
|
14
|
+
ErrorResponse,
|
|
15
|
+
InitResponse,
|
|
16
|
+
StatusResponse,
|
|
17
|
+
VerifyResponse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T", bound=BaseModel)
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "http://localhost:8000"
|
|
23
|
+
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS = 300.0
|
|
24
|
+
API_TOKEN_ENV = "ASSERTION_API_TOKEN"
|
|
25
|
+
|
|
26
|
+
# Walk up from cwd looking for a .env so the user can drop their token in the
|
|
27
|
+
# repo root once and forget it. override=False means a real shell `export`
|
|
28
|
+
# still wins over the file — useful for CI.
|
|
29
|
+
_dotenv_path = find_dotenv(usecwd=True)
|
|
30
|
+
if _dotenv_path:
|
|
31
|
+
load_dotenv(_dotenv_path, override=False)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _base_url() -> str:
|
|
35
|
+
return os.environ.get("ASSERTION_BASE_URL", DEFAULT_BASE_URL)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _auth_headers() -> dict[str, str]:
|
|
39
|
+
token = (os.environ.get(API_TOKEN_ENV) or "").strip()
|
|
40
|
+
if not token:
|
|
41
|
+
typer.echo(
|
|
42
|
+
f"Missing {API_TOKEN_ENV}. Generate a token at Settings → Authentication "
|
|
43
|
+
"in the dashboard, then save it in a .env at your repo root:\n\n"
|
|
44
|
+
f" echo '{API_TOKEN_ENV}=alk_...' >> .env\n\n"
|
|
45
|
+
"(Or export it in your shell.)",
|
|
46
|
+
err=True,
|
|
47
|
+
)
|
|
48
|
+
raise typer.Exit(code=1)
|
|
49
|
+
return {"Authorization": f"Bearer {token}"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_response(response: httpx.Response, model: type[T]) -> T:
|
|
53
|
+
try:
|
|
54
|
+
return model.model_validate_json(response.text)
|
|
55
|
+
except ValidationError as exc:
|
|
56
|
+
raise ValueError(f"Invalid API response: {exc}") from exc
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _summarize_error(response: httpx.Response) -> str:
|
|
60
|
+
try:
|
|
61
|
+
return ErrorResponse.model_validate_json(response.text).error
|
|
62
|
+
except ValidationError:
|
|
63
|
+
body = response.text.strip()
|
|
64
|
+
return body[:200] if body else "<empty response body>"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AssertionClient:
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
self.base_url = _base_url()
|
|
70
|
+
|
|
71
|
+
def _request(
|
|
72
|
+
self,
|
|
73
|
+
method: str,
|
|
74
|
+
path: str,
|
|
75
|
+
model: type[T],
|
|
76
|
+
*,
|
|
77
|
+
timeout: float = 30.0,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> T:
|
|
80
|
+
url = f"{self.base_url}{path}"
|
|
81
|
+
headers = {**_auth_headers(), **kwargs.pop("headers", {})}
|
|
82
|
+
try:
|
|
83
|
+
response = httpx.request(
|
|
84
|
+
method, url, timeout=timeout, headers=headers, **kwargs
|
|
85
|
+
)
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
except httpx.HTTPStatusError as exc:
|
|
88
|
+
summary = _summarize_error(exc.response)
|
|
89
|
+
typer.echo(
|
|
90
|
+
f"Request failed with status {exc.response.status_code}: {summary}",
|
|
91
|
+
err=True,
|
|
92
|
+
)
|
|
93
|
+
raise typer.Exit(code=1) from exc
|
|
94
|
+
except httpx.ReadTimeout as exc:
|
|
95
|
+
typer.echo(f"Request to {url} timed out after {timeout:.0f}s.", err=True)
|
|
96
|
+
raise typer.Exit(code=1) from exc
|
|
97
|
+
except httpx.HTTPError as exc:
|
|
98
|
+
typer.echo(f"Failed to connect to Assertion API at {url}: {exc}", err=True)
|
|
99
|
+
raise typer.Exit(code=1) from exc
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
return _parse_response(response, model)
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
typer.echo(str(exc), err=True)
|
|
105
|
+
raise typer.Exit(code=1) from exc
|
|
106
|
+
|
|
107
|
+
def init(self) -> InitResponse:
|
|
108
|
+
return self._request("POST", "/api/v0/agent/init", InitResponse, timeout=10.0)
|
|
109
|
+
|
|
110
|
+
def _request_no_content(
|
|
111
|
+
self,
|
|
112
|
+
method: str,
|
|
113
|
+
path: str,
|
|
114
|
+
*,
|
|
115
|
+
timeout: float = 30.0,
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> DecisionResponse:
|
|
118
|
+
url = f"{self.base_url}{path}"
|
|
119
|
+
headers = {**_auth_headers(), **kwargs.pop("headers", {})}
|
|
120
|
+
try:
|
|
121
|
+
response = httpx.request(
|
|
122
|
+
method, url, timeout=timeout, headers=headers, **kwargs
|
|
123
|
+
)
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
except httpx.HTTPStatusError as exc:
|
|
126
|
+
summary = _summarize_error(exc.response)
|
|
127
|
+
typer.echo(
|
|
128
|
+
f"Request failed with status {exc.response.status_code}: {summary}",
|
|
129
|
+
err=True,
|
|
130
|
+
)
|
|
131
|
+
raise typer.Exit(code=1) from exc
|
|
132
|
+
except httpx.HTTPError as exc:
|
|
133
|
+
typer.echo(f"Failed to connect to Assertion API at {url}: {exc}", err=True)
|
|
134
|
+
raise typer.Exit(code=1) from exc
|
|
135
|
+
|
|
136
|
+
return DecisionResponse()
|
|
137
|
+
|
|
138
|
+
def stacks(self) -> list:
|
|
139
|
+
"""Fetch available stacks (read-only, no session created)."""
|
|
140
|
+
from models import VerificationStack
|
|
141
|
+
from pydantic import TypeAdapter
|
|
142
|
+
|
|
143
|
+
url = f"{self.base_url}/api/v0/agent/stacks"
|
|
144
|
+
try:
|
|
145
|
+
response = httpx.get(url, timeout=10.0, headers=_auth_headers())
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
except httpx.HTTPStatusError as exc:
|
|
148
|
+
summary = _summarize_error(exc.response)
|
|
149
|
+
typer.echo(
|
|
150
|
+
f"Request failed with status {exc.response.status_code}: {summary}",
|
|
151
|
+
err=True,
|
|
152
|
+
)
|
|
153
|
+
raise typer.Exit(code=1) from exc
|
|
154
|
+
except httpx.HTTPError as exc:
|
|
155
|
+
typer.echo(f"Failed to connect to Assertion API at {url}: {exc}", err=True)
|
|
156
|
+
raise typer.Exit(code=1) from exc
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
adapter = TypeAdapter(list[VerificationStack])
|
|
160
|
+
return adapter.validate_json(response.text)
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
typer.echo(f"Invalid API response: {exc}", err=True)
|
|
163
|
+
raise typer.Exit(code=1) from exc
|
|
164
|
+
|
|
165
|
+
def verify(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
stack_id: str,
|
|
169
|
+
session_id: str,
|
|
170
|
+
bundle_bytes: bytes,
|
|
171
|
+
) -> VerifyResponse:
|
|
172
|
+
path = f"/api/v0/agent/verify/{stack_id}/{session_id}"
|
|
173
|
+
return self._request(
|
|
174
|
+
"POST",
|
|
175
|
+
path,
|
|
176
|
+
VerifyResponse,
|
|
177
|
+
files={"bundle": ("assertion_bundle.zip", bundle_bytes, "application/zip")},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def checkpoint(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
stack_id: str,
|
|
184
|
+
session_id: str,
|
|
185
|
+
bundle_bytes: bytes,
|
|
186
|
+
) -> CheckpointResponse:
|
|
187
|
+
path = f"/api/v0/agent/checkpoint/{stack_id}/{session_id}"
|
|
188
|
+
return self._request(
|
|
189
|
+
"POST",
|
|
190
|
+
path,
|
|
191
|
+
CheckpointResponse,
|
|
192
|
+
timeout=DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
|
193
|
+
files={"bundle": ("assertion_bundle.zip", bundle_bytes, "application/zip")},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def decision(self, *, checkpoint_id: str, decision: str) -> DecisionResponse:
|
|
197
|
+
return self._request_no_content(
|
|
198
|
+
"POST",
|
|
199
|
+
f"/api/v0/agent/decision/{checkpoint_id}",
|
|
200
|
+
json={"decision": decision},
|
|
201
|
+
timeout=10.0,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def status(self, *, session_id: str) -> StatusResponse:
|
|
205
|
+
return self._request(
|
|
206
|
+
"POST",
|
|
207
|
+
"/api/v0/agent/status",
|
|
208
|
+
StatusResponse,
|
|
209
|
+
json={"session_id": session_id},
|
|
210
|
+
timeout=10.0,
|
|
211
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assertion-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Assertion API
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0
|
|
10
|
+
Requires-Dist: typer>=0.24.1
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
13
|
+
Requires-Dist: respx>=0.22; extra == "test"
|
|
14
|
+
|
|
15
|
+
# Assertion CLI
|
|
16
|
+
|
|
17
|
+
CLI for the Assertion API.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
The CLI currently targets a local backend at `http://localhost:8000`.
|
|
22
|
+
|
|
23
|
+
Run locally from the workspace:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run --package assertion-cli asrt --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Install from Git as a `uv` tool:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install git+ssh://git@github.com/prooflayer-ai/backend.git#subdirectory=cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The CLI package declares all of its direct runtime dependencies. At the moment
|
|
36
|
+
that set is `httpx`, `pydantic`, and `typer`.
|
|
37
|
+
|
|
38
|
+
After installation:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
asrt stacks
|
|
42
|
+
asrt checkpoint --stack <stack-id> "Implemented X\nUpdated Y"
|
|
43
|
+
asrt checkpoint --continue "Implemented Y"
|
|
44
|
+
asrt decision --yes <checkpoint-id> # optional, only after a failed checkpoint
|
|
45
|
+
asrt verify
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This expects the installer to already have GitHub SSH access to `prooflayer-ai/backend`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
api.py,sha256=6RuyOoZrsBTY_51XA60iHwkYJ6nqME0CrmDgGaEbWzk,6901
|
|
2
|
+
bundle.py,sha256=e1-a0_hcKG0Xitjq0ac5u7ox3soAiodVbKIf4pRmI4w,828
|
|
3
|
+
git.py,sha256=qcPiQ01DO7joEqzmSIwRkiUekPTF52nwbRATAk5Qqj0,5977
|
|
4
|
+
link.py,sha256=bfH0MhkeTFGbOSJbSvuuw3ilGX3akyvjvCw25AP_fz8,643
|
|
5
|
+
main.py,sha256=Eof43tayPYuSwMiGGKZNPPcIaG_4FHmxdKS3gwakOSg,17308
|
|
6
|
+
models.py,sha256=PQ0q_kUUzRVme325TXB_8eQaNl4LPSuxY8CVy9HK2ls,1678
|
|
7
|
+
session.py,sha256=FfdQqVNhWLBefcdmbuz8xN5KIT4belIeYNX1VTTn1mw,7002
|
|
8
|
+
assertion_cli_templates/ACTIVATION.md,sha256=njAti5gFu3yn91D5dmO8vfiRJ2M337bHXybrxPNWlMk,1439
|
|
9
|
+
assertion_cli_templates/SKILL.md,sha256=Qi8lHaQni0KrjD8O4Bpg43ScXi5rG0b7URe0oDVimfY,12103
|
|
10
|
+
assertion_cli_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
assertion_cli-0.1.0.dist-info/METADATA,sha256=JWcTQeySngOLykBPQZvhgu08v-ihgGkYX2Zo9LXm1ew,1195
|
|
12
|
+
assertion_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
assertion_cli-0.1.0.dist-info/entry_points.txt,sha256=LgwuPLZEIk-4sD7ghGVRQOhm3PG8sPVozMRxrihwMgU,34
|
|
14
|
+
assertion_cli-0.1.0.dist-info/top_level.txt,sha256=xbZYH9xQOa99z-Vbkc2Xp6h8oUR97DVlyczy8H4ZuFc,64
|
|
15
|
+
assertion_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Assertion verification
|
|
2
|
+
|
|
3
|
+
This repo uses the Assertion CLI (`asrt`) to track customer 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. **Every customer message:** call `asrt prompt "<exact text of the message>"` before doing any other work for that turn. This is the verifier's source of truth for what the customer asked for and how their direction evolves. Apply to every customer turn, not just the first.
|
|
11
|
+
2. **Checkpoint at trajectory-feedback moments**, not on every small change. Use `asrt checkpoint "<summary>"` — the CLI auto-resolves the stack from this repo's GitHub origin on the first call and auto-continues the session on subsequent calls. A whole feature build is a handful of checkpoints, not one per file.
|
|
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 with `gh pr create` and include the session URL (from `asrt get-link`) in the body.
|
|
13
|
+
|
|
14
|
+
Do not open a PR without a clean `asrt verify`.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: assertion-cli
|
|
3
|
+
description: Use this skill in non-Plan mode whenever working in a repo that uses the Assertion CLI (`asrt`). It enforces the three-rule workflow — log every customer message with `asrt prompt`, checkpoint at trajectory-feedback moments (not on every change), and verify only at completion before opening a PR. Applies to any coding agent reading this skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Assertion CLI
|
|
7
|
+
|
|
8
|
+
You are operating in a repo that uses the Assertion CLI (`asrt`) to track customer intent and verify your work. Follow the three rules below without exception. They apply regardless of which coding agent harness you are running under (Claude Code, Codex, Cursor, etc.).
|
|
9
|
+
|
|
10
|
+
This skill is operational, not advisory. Treat each rule as a hard requirement unless the underlying service is unavailable.
|
|
11
|
+
|
|
12
|
+
## Preflight (one-time per session)
|
|
13
|
+
|
|
14
|
+
Before any `asrt` call:
|
|
15
|
+
|
|
16
|
+
- You are inside a git repository.
|
|
17
|
+
- `HEAD` is present on at least one remote-tracking branch (the CLI enforces this for checkpoint/verify; `asrt prompt` does not require it).
|
|
18
|
+
|
|
19
|
+
No `asrt init` is needed — the CLI creates `.assertion/` on first use. Treat `.assertion/metadata.json` and `.assertion/prompts` 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.
|
|
20
|
+
|
|
21
|
+
`asrt stacks` is read-only and lists available verification stack IDs. Run it once per session before your first checkpoint so you can pick a stack ID; do not hardcode IDs from memory.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Rule 1 — Log every customer message, immediately
|
|
26
|
+
|
|
27
|
+
**The first thing you do when a new customer message arrives — before reading any file, planning, or editing anything — is run:**
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
asrt prompt "<exact text of the customer's message>"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This applies to **every customer turn**, not just the first. It applies even when the message is a one-word direction change ("nope"), a clarification ("use Postgres, not SQLite"), or a follow-up after verify ("also add dark mode").
|
|
34
|
+
|
|
35
|
+
**Why:** The prompts log is the verifier's source of truth for what the customer asked for and how their direction evolved. It is shipped with every checkpoint and the final verify, and persisted in the verification database. A missed turn causes the verifier to flag legitimate work as off-spec because it cannot see the intent change that authorized the work.
|
|
36
|
+
|
|
37
|
+
**Edge cases:**
|
|
38
|
+
- Multi-line / multi-paragraph messages: pass the full text as one quoted argument. Use shell-quoting that preserves newlines (e.g., `$'first line\nsecond line'` in bash, or a heredoc piped via `xargs`).
|
|
39
|
+
- Empty / whitespace-only customer turns: skip — `asrt prompt` rejects them.
|
|
40
|
+
- You misrecorded a prompt: there is no undo. Append a correction as a normal prompt, e.g. `asrt prompt "Correction: previous message should have been '...'"`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Rule 2 — Checkpoint at trajectory-feedback moments, not on every change
|
|
45
|
+
|
|
46
|
+
`asrt checkpoint` is a request for a second opinion from the verifier on your trajectory so far. It is cheap but not free; the right cadence is **a handful of checkpoints across a feature build**, not one per file edit.
|
|
47
|
+
|
|
48
|
+
**Checkpoint when:**
|
|
49
|
+
- A meaningful slice of the feature is complete (e.g., the data model, the API layer, the UI scaffold).
|
|
50
|
+
- You made a non-trivial design choice and want it sanity-checked before building further on it.
|
|
51
|
+
- You hit ambiguity and want input on which of two reasonable paths to take.
|
|
52
|
+
- The customer's evolving direction (visible in the prompt log) could legitimately change what comes next.
|
|
53
|
+
|
|
54
|
+
**Do NOT checkpoint when:**
|
|
55
|
+
- You finished a single file edit.
|
|
56
|
+
- You fixed a typo, renamed a variable, or applied a small refactor inside one feature slice.
|
|
57
|
+
- The next step is mechanical and well-defined.
|
|
58
|
+
- You are stalling because verify is hard — verify is the right command, not more checkpoints.
|
|
59
|
+
|
|
60
|
+
**Form:**
|
|
61
|
+
|
|
62
|
+
Every checkpoint:
|
|
63
|
+
```
|
|
64
|
+
asrt checkpoint "<what you built and what you decided>"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You don't need a flag to distinguish "first" from "subsequent" — the CLI does the right thing:
|
|
68
|
+
- No session exists yet → start a new one. The CLI picks the stack by matching the repo's GitHub `owner/name` (from `git remote get-url origin`) against the stacks attached to this repo.
|
|
69
|
+
- A session already exists → continue it.
|
|
70
|
+
|
|
71
|
+
If no stack is attached to this repo on the very first checkpoint, the command fails with a clear message telling the customer to attach one in the Assertion web app. If multiple stacks are attached, it tells you to pass `--stack <id>` to disambiguate.
|
|
72
|
+
|
|
73
|
+
Flags exist for explicit intent, but you normally don't need them:
|
|
74
|
+
- `--continue` — fail fast if no session exists (otherwise behaves identically to no flag when a session does exist).
|
|
75
|
+
- `--stack <id>` — force a brand-new session against a specific stack, overwriting any existing one. Only pass this if you deliberately want to abandon the current session.
|
|
76
|
+
|
|
77
|
+
Use checkpoint messages to summarize substantive progress and decisions since the previous checkpoint — not a changelog of every line touched.
|
|
78
|
+
|
|
79
|
+
**After each checkpoint response:** read the result. Inspect `checkpoint_id`, the overall approval, and per-model feedback.
|
|
80
|
+
|
|
81
|
+
- If approved: continue to the next slice.
|
|
82
|
+
- If failed: treat the feedback as authoritative for the next slice of work, address it, then checkpoint again. Do not push toward verify with failing feedback unaddressed.
|
|
83
|
+
- Optionally, after a failed checkpoint, record your sentiment with `asrt decision --yes <checkpoint-id>` (you agree with the feedback) or `asrt decision --no <checkpoint-id>` (you disagree). Do not call `decision` for successful checkpoints.
|
|
84
|
+
|
|
85
|
+
### Stack selection — handled by the CLI
|
|
86
|
+
|
|
87
|
+
Stack selection is **enforced by the CLI**, not by you. When you run `asrt checkpoint "..."` (no `--stack` flag), the CLI:
|
|
88
|
+
|
|
89
|
+
1. Runs `git remote get-url origin` and normalizes to `owner/name`
|
|
90
|
+
2. Calls the backend, fetches every stack in the customer's workspace, and matches by the stack's `repo` field
|
|
91
|
+
3. Picks the single match → starts the session against it
|
|
92
|
+
4. If zero or multiple match, exits non-zero with a clear, customer-facing error
|
|
93
|
+
|
|
94
|
+
You don't need to do the matching yourself or pass `--stack`. Just call `asrt checkpoint "..."`.
|
|
95
|
+
|
|
96
|
+
### When `asrt checkpoint` fails because no stack is available
|
|
97
|
+
|
|
98
|
+
This is the most common failure. The CLI distinguishes three cases — your response is the same shape in all of them:
|
|
99
|
+
|
|
100
|
+
| CLI error starts with | Meaning |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `No verification stacks exist in this workspace yet.` | Customer has never set up any stacks. |
|
|
103
|
+
| `No verification stack is attached to this repo (...)` | Stacks exist but none are bound to this repo. |
|
|
104
|
+
| `Multiple stacks are attached to ...` | Two or more stacks match; CLI asks for `--stack <id>`. |
|
|
105
|
+
|
|
106
|
+
**For the first two:** the customer has to take an action in the Assertion web app. Your response:
|
|
107
|
+
|
|
108
|
+
1. Tell the customer the error verbatim. The CLI already wrote the right message.
|
|
109
|
+
2. **Keep the work moving on your side.** You can continue editing files and implementing what the customer asked for. Verification is the gate before opening a PR — not a gate on writing code.
|
|
110
|
+
3. Keep calling `asrt prompt "..."` on every new customer turn. The prompt log accumulates in `.assertion/prompts` and is *not* lost when checkpoint fails — once the customer attaches a stack, the next successful `asrt checkpoint "..."` ships every prompt logged so far in its bundle.
|
|
111
|
+
4. When the customer says they've attached a stack (or asks you to retry), call `asrt checkpoint "<what you built so far>"` again. The CLI will resolve the stack this time and start the session with the full prompt history already in the bundle.
|
|
112
|
+
|
|
113
|
+
**Do not** try to work around the missing stack by guessing a stack ID, hardcoding one from another repo, or picking the wrong one with `--stack`. Repo-binding is the customer's signal that the stack is configured for this codebase; bypassing it creates a verification session in the wrong context.
|
|
114
|
+
|
|
115
|
+
**For the third case (multiple matches):** the CLI's error lists the candidates. Pick the one whose name/description best fits the primary deliverable (code review, security, docs, perf) and pass `asrt checkpoint --stack <id> "..."`. State your choice briefly so the customer can correct you.
|
|
116
|
+
|
|
117
|
+
### Optional: confirm before you start
|
|
118
|
+
|
|
119
|
+
If you want to know which stack the CLI *would* pick before running checkpoint (e.g., the work is about to be long and you want to fail-fast on configuration), call `asrt stacks` and look for the line with `[repo: <current-repo-owner/name>]`. You don't have to — `asrt checkpoint` does this internally — but it's available as a read-only check.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Rule 3 — Verify at completion, then iterate or PR
|
|
124
|
+
|
|
125
|
+
`asrt verify` is the terminal validation gate. Call it when **all three** are true:
|
|
126
|
+
|
|
127
|
+
1. The feature is implemented.
|
|
128
|
+
2. The customer has signalled no further edits or redirections.
|
|
129
|
+
3. All in-flight checkpoint feedback has been addressed.
|
|
130
|
+
|
|
131
|
+
Do not call `verify` mid-task. It is not a substitute for checkpoints; it is the gate after them.
|
|
132
|
+
|
|
133
|
+
`verify` blocks until verification completes. It prints the final result, may print a summary, may save screenshots under `.assertion/screenshots/<verification_id>/`, prints a session URL, and saves the URL to `.assertion/link`. This may take several minutes — do not interrupt it.
|
|
134
|
+
|
|
135
|
+
`asrt get-link` prints the last session URL (e.g., for inclusion in PR descriptions).
|
|
136
|
+
|
|
137
|
+
**After verify returns, branch on the outcome:**
|
|
138
|
+
|
|
139
|
+
| Outcome | What you do next |
|
|
140
|
+
|---|---|
|
|
141
|
+
| **Clean** (succeeded, no required changes) | Open a PR with `gh pr create`. Include the session URL (printed by verify; also `asrt get-link`) in the PR body so reviewers can see the verification record. |
|
|
142
|
+
| **Feedback or failed** (changes required) | Treat the feedback as authoritative. Apply the changes it calls for, then re-run `asrt verify`. Repeat until clean. **Do not drop back into the checkpoint loop** — verify is the gate, not a checkpoint. |
|
|
143
|
+
| **Customer sends a new message during iteration** | Log it with `asrt prompt` first (Rule 1 still applies). Address the new direction. Re-verify before opening the PR. |
|
|
144
|
+
|
|
145
|
+
**Do not open a PR without a clean `asrt verify`.** Do not end a session with a failed/dirty verify unless the verification service itself is unavailable.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Quick reference
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
asrt prompt "<message>" # every customer turn, before anything else
|
|
153
|
+
asrt stacks # optional: list stacks attached to this repo
|
|
154
|
+
asrt checkpoint "..." # at trajectory moments — auto-starts or continues
|
|
155
|
+
asrt decision --yes|--no <ckpt-id> # optional, on failed checkpoints only
|
|
156
|
+
asrt verify # at feature completion; the gate before PR
|
|
157
|
+
asrt get-link # retrieve the last session URL
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Environment
|
|
161
|
+
|
|
162
|
+
- `ASSERTION_BASE_URL` — point the CLI at a non-default backend (default `http://localhost:8000`).
|
|
163
|
+
|
|
164
|
+
## Non-interactive / harness mode
|
|
165
|
+
|
|
166
|
+
When running under a non-interactive harness (CI, benchmark runner, scripts):
|
|
167
|
+
|
|
168
|
+
1. Pass `--json` to `asrt checkpoint` and `asrt verify`. Output is one JSON line per command — parse it; do not regex human text.
|
|
169
|
+
2. Checkpoint response shape: `checkpoint_id`, `outcome` (`succeeded` / `failed`), `reasoning`, `reviews[]`. A non-zero exit code is raised on failed checkpoints, so harnesses can short-circuit on exit status alone.
|
|
170
|
+
3. **Retry-with-feedback loop on a failed checkpoint:**
|
|
171
|
+
- Read `reasoning` and `reviews[].feedback` from the JSON.
|
|
172
|
+
- Apply changes targeted at the cited failure.
|
|
173
|
+
- Re-run `asrt checkpoint "<what you changed>" --json` (auto-continues the existing session).
|
|
174
|
+
- Bound retries: stop after 3 attempts on the same failure mode, or when the same `reasoning` is returned twice in a row.
|
|
175
|
+
4. Use `asrt verify --json --no-wait` to submit and exit, then poll `asrt get-link` / the session URL externally to detach the harness from the polling loop. Without `--no-wait`, `verify --json` still polls but emits a single JSON line on termination.
|
|
176
|
+
|
|
177
|
+
Do not mix `--json` output with human-rendered output in the same run. Pick one mode and stick with it.
|
|
File without changes
|
bundle.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import zipfile
|
|
3
|
+
|
|
4
|
+
from models import MetadataPayload
|
|
5
|
+
|
|
6
|
+
ASSERTION_DIR_NAME = ".assertion"
|
|
7
|
+
DIFF_ARCHIVE_PATH = "git.diff"
|
|
8
|
+
METADATA_ARCHIVE_PATH = f"{ASSERTION_DIR_NAME}/metadata.json"
|
|
9
|
+
PROMPTS_ARCHIVE_PATH = f"{ASSERTION_DIR_NAME}/prompts"
|
|
10
|
+
CHECKPOINT_ARCHIVE_PATH = f"{ASSERTION_DIR_NAME}/checkpoint"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_bundle(
|
|
14
|
+
*,
|
|
15
|
+
metadata: MetadataPayload,
|
|
16
|
+
diff_text: str,
|
|
17
|
+
prompts_text: str,
|
|
18
|
+
checkpoint_text: str,
|
|
19
|
+
) -> bytes:
|
|
20
|
+
buf = io.BytesIO()
|
|
21
|
+
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
22
|
+
zf.writestr(METADATA_ARCHIVE_PATH, metadata.model_dump_json(indent=2) + "\n")
|
|
23
|
+
zf.writestr(PROMPTS_ARCHIVE_PATH, prompts_text)
|
|
24
|
+
zf.writestr(CHECKPOINT_ARCHIVE_PATH, checkpoint_text)
|
|
25
|
+
zf.writestr(DIFF_ARCHIVE_PATH, diff_text)
|
|
26
|
+
return buf.getvalue()
|