custos-code 0.0.1__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.
- custos_code-0.0.1/.gitignore +24 -0
- custos_code-0.0.1/LICENSE +21 -0
- custos_code-0.0.1/PKG-INFO +138 -0
- custos_code-0.0.1/README.md +112 -0
- custos_code-0.0.1/pyproject.toml +58 -0
- custos_code-0.0.1/src/custos_code/__init__.py +6 -0
- custos_code-0.0.1/src/custos_code/adapters/__init__.py +194 -0
- custos_code-0.0.1/src/custos_code/adapters/claude_code.py +266 -0
- custos_code-0.0.1/src/custos_code/adapters/codex.py +437 -0
- custos_code-0.0.1/src/custos_code/adapters/copilot.py +158 -0
- custos_code-0.0.1/src/custos_code/adapters/devin.py +172 -0
- custos_code-0.0.1/src/custos_code/adapters/machine.py +379 -0
- custos_code-0.0.1/src/custos_code/adapters/otel.py +210 -0
- custos_code-0.0.1/src/custos_code/adapters/state.py +164 -0
- custos_code-0.0.1/src/custos_code/claims.py +319 -0
- custos_code-0.0.1/src/custos_code/cli.py +789 -0
- custos_code-0.0.1/src/custos_code/compress.py +113 -0
- custos_code-0.0.1/src/custos_code/cost.py +216 -0
- custos_code-0.0.1/src/custos_code/demo_fixtures/__init__.py +1 -0
- custos_code-0.0.1/src/custos_code/demo_fixtures/ok_tests_0.jsonl +8 -0
- custos_code-0.0.1/src/custos_code/demo_fixtures/trap_echo_0.jsonl +4 -0
- custos_code-0.0.1/src/custos_code/demo_fixtures/trap_ghost_0.jsonl +4 -0
- custos_code-0.0.1/src/custos_code/demo_fixtures/trap_piped_0.jsonl +4 -0
- custos_code-0.0.1/src/custos_code/feedback.py +93 -0
- custos_code-0.0.1/src/custos_code/hooks.py +648 -0
- custos_code-0.0.1/src/custos_code/judge.py +338 -0
- custos_code-0.0.1/src/custos_code/ledger.py +93 -0
- custos_code-0.0.1/src/custos_code/models.py +129 -0
- custos_code-0.0.1/src/custos_code/parsers.py +408 -0
- custos_code-0.0.1/src/custos_code/report.py +317 -0
- custos_code-0.0.1/src/custos_code/rerun.py +424 -0
- custos_code-0.0.1/src/custos_code/review.py +381 -0
- custos_code-0.0.1/src/custos_code/rules.py +464 -0
- custos_code-0.0.1/src/custos_code/scope.py +471 -0
- custos_code-0.0.1/src/custos_code/verdicts.py +296 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
.uv-cache/
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
.mypy_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.hypothesis/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.pyc
|
|
12
|
+
*.sqlite
|
|
13
|
+
*.duckdb
|
|
14
|
+
.env
|
|
15
|
+
.env.*
|
|
16
|
+
config.toml
|
|
17
|
+
~/.receipts/
|
|
18
|
+
eval/study/sessions/
|
|
19
|
+
bench/runs/
|
|
20
|
+
bench/fixtures/*/.venv/
|
|
21
|
+
.claude/
|
|
22
|
+
CLAUDE.md
|
|
23
|
+
.DS_Store
|
|
24
|
+
docs/research/raw/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oliver Zhang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: custos-code
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Checks a coding agent's final report against the log of what it actually did.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: agents,claude-code,hooks,observability,verification
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: anthropic>=0.40
|
|
14
|
+
Requires-Dist: duckdb>=1.0
|
|
15
|
+
Requires-Dist: openai>=1.40
|
|
16
|
+
Requires-Dist: pydantic>=2.7
|
|
17
|
+
Requires-Dist: rich>=13.7
|
|
18
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
19
|
+
Requires-Dist: typer>=0.12
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Custos Code
|
|
28
|
+
|
|
29
|
+
Checks a coding agent's final report against the log of what it actually did.
|
|
30
|
+
|
|
31
|
+
An agent finishes and says "implemented the feature, ran the tests, all passing." Custos Code reads the harness-written action log, splits the report into claims, and marks each one **confirmed**, **contradicted**, **unwitnessed**, **unrecorded**, or **qualified**, with the ledger lines that back the verdict. Contradictions go back to the agent before it is allowed to stop.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
custos-code session 4f2a… · 63 events
|
|
35
|
+
✓ confirmed edited auth/middleware.py tier 1 · #14 Edit, git diff agrees
|
|
36
|
+
✓ confirmed added tests/test_rate_limit.py tier 1 · #31 Write, file present
|
|
37
|
+
✗ contradicted ran the suite, all 12 passing tier 2 · #41 `pytest | tail -5` exit 0, "collected 0 items"
|
|
38
|
+
? unwitnessed ready to merge no CI, no git status after #41
|
|
39
|
+
stop blocked · 1 contradicted · evidence returned to agent
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Status
|
|
43
|
+
|
|
44
|
+
Pre-build. The design, research, plan, and evidence protocol are in `docs/`. Start with [AGENTS.md](AGENTS.md).
|
|
45
|
+
|
|
46
|
+
- [docs/DESIGN.md](docs/DESIGN.md) — problem, customers, verification ladder, feasibility, product sketches, benchmark, system design, prize strategy, adversarial review
|
|
47
|
+
- [docs/RESEARCH.md](docs/RESEARCH.md) — the evidence: prevalence with denominators, cost, current workarounds, tool landscape, 40 seed cases
|
|
48
|
+
- [docs/PLAN.md](docs/PLAN.md) — who owns what, phases, parallel tracks
|
|
49
|
+
- [docs/EVIDENCE_PLAN.md](docs/EVIDENCE_PLAN.md) — pre-registered study: accuracy, time saved, retention, usability
|
|
50
|
+
- [docs/OPEN_QUESTIONS.md](docs/OPEN_QUESTIONS.md) — every unresolved decision with an owner
|
|
51
|
+
|
|
52
|
+
## See it work
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv sync
|
|
56
|
+
export OPENAI_API_KEY=...
|
|
57
|
+
uv run custos-code demo # the whole loop on a known trap, live
|
|
58
|
+
uv run custos-code demo --scenario honest # the control: nothing blocks
|
|
59
|
+
uv run custos-code check --last # your own most recent session
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`demo` prints five things from the fixture's own tool log: what was asked, what the agent actually
|
|
63
|
+
did, what it said, the receipt, and the deterministic nudge that goes back. `--format html --out
|
|
64
|
+
card.html` writes a self-contained report card; `--format markdown` writes what the PR bot posts.
|
|
65
|
+
|
|
66
|
+
## Prototype
|
|
67
|
+
|
|
68
|
+
`docs/prototype/index.html` is an interactive, non-functional mock of the editor experience: marks on the agent's message, the evidence panel, editor decorations, the auto-mode loop, and the PR receipt. It is a single self-contained file:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
open docs/prototype/index.html # macOS
|
|
72
|
+
# or: python3 -m http.server -d docs/prototype 8765 → http://localhost:8765
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Local setup
|
|
76
|
+
|
|
77
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) once. On macOS
|
|
78
|
+
with Homebrew: `brew install uv`. From the repository directory:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv python install
|
|
82
|
+
make sync
|
|
83
|
+
make check
|
|
84
|
+
uv run custos-code check --last
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`.python-version` selects Python 3.12, independently of your shell's pyenv or
|
|
88
|
+
Conda default. `make sync` installs the project and developer tools from the
|
|
89
|
+
committed `uv.lock`; it fails if the lockfile needs updating. After an intentional
|
|
90
|
+
dependency change, run `uv lock` and include the lockfile in the same PR.
|
|
91
|
+
|
|
92
|
+
`make build` produces a wheel and source distribution in `dist/`. CI runs the
|
|
93
|
+
same checks, installs both distributions in clean environments, and checks the
|
|
94
|
+
installed CLI outside the source checkout. CI uses locked installs following
|
|
95
|
+
the [uv integration guide](https://docs.astral.sh/uv/guides/integration/github/).
|
|
96
|
+
|
|
97
|
+
## Bench container
|
|
98
|
+
|
|
99
|
+
With Docker installed and running, build from the repository root:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
docker build -t custos-code-bench .
|
|
103
|
+
docker run --rm --network none custos-code-bench
|
|
104
|
+
docker run --rm --network none custos-code-bench python -m pytest --version
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The image contains Python 3.12, uv 0.12.17, git, the installed Custos Code package,
|
|
108
|
+
developer dependencies, and scenario descriptions under `/app/bench/scenarios`.
|
|
109
|
+
It runs as a non-root user in writable `/workspace`; the default command shows
|
|
110
|
+
CLI help. The benchmark orchestration and fixture repos are not implemented
|
|
111
|
+
yet, so this is their execution environment, not a working benchmark command.
|
|
112
|
+
Agent CLIs and their credentials are not installed.
|
|
113
|
+
|
|
114
|
+
The Docker build context is an allowlist that excludes local session logs,
|
|
115
|
+
credentials, caches, and git history. For a local Python fixture, mount only
|
|
116
|
+
that fixture (including its git metadata when state checks need it):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker run --rm --network none \
|
|
120
|
+
--mount type=bind,src="$(pwd)/path/to/fixture",dst=/workspace,readonly \
|
|
121
|
+
custos-code-bench python -m pytest -p no:cacheprovider
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This read-only example suits tests that do not write into the fixture. Agent
|
|
125
|
+
bench runs will need a disposable writable checkout and explicit network and
|
|
126
|
+
credential configuration. Non-Python runners require additional toolchains.
|
|
127
|
+
|
|
128
|
+
## Why
|
|
129
|
+
|
|
130
|
+
Across 20,574 real coding-agent sessions, 22.58% of 16,118 validated misalignment episodes were the agent misreporting its own work, and only 2.99% of resolved episodes were self-corrected. Every agent vendor attaches an action log; none checks the report against it. Sources and denominators: `docs/RESEARCH.md`.
|
|
131
|
+
|
|
132
|
+
## Working in this repo
|
|
133
|
+
|
|
134
|
+
Read `AGENTS.md`. Flag anything undecided with `NEEDS-DECISION(owner):`. Local by default; secrets are redacted at ingest; fixtures are synthetic.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT (see `LICENSE`).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Custos Code
|
|
2
|
+
|
|
3
|
+
Checks a coding agent's final report against the log of what it actually did.
|
|
4
|
+
|
|
5
|
+
An agent finishes and says "implemented the feature, ran the tests, all passing." Custos Code reads the harness-written action log, splits the report into claims, and marks each one **confirmed**, **contradicted**, **unwitnessed**, **unrecorded**, or **qualified**, with the ledger lines that back the verdict. Contradictions go back to the agent before it is allowed to stop.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
custos-code session 4f2a… · 63 events
|
|
9
|
+
✓ confirmed edited auth/middleware.py tier 1 · #14 Edit, git diff agrees
|
|
10
|
+
✓ confirmed added tests/test_rate_limit.py tier 1 · #31 Write, file present
|
|
11
|
+
✗ contradicted ran the suite, all 12 passing tier 2 · #41 `pytest | tail -5` exit 0, "collected 0 items"
|
|
12
|
+
? unwitnessed ready to merge no CI, no git status after #41
|
|
13
|
+
stop blocked · 1 contradicted · evidence returned to agent
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Status
|
|
17
|
+
|
|
18
|
+
Pre-build. The design, research, plan, and evidence protocol are in `docs/`. Start with [AGENTS.md](AGENTS.md).
|
|
19
|
+
|
|
20
|
+
- [docs/DESIGN.md](docs/DESIGN.md) — problem, customers, verification ladder, feasibility, product sketches, benchmark, system design, prize strategy, adversarial review
|
|
21
|
+
- [docs/RESEARCH.md](docs/RESEARCH.md) — the evidence: prevalence with denominators, cost, current workarounds, tool landscape, 40 seed cases
|
|
22
|
+
- [docs/PLAN.md](docs/PLAN.md) — who owns what, phases, parallel tracks
|
|
23
|
+
- [docs/EVIDENCE_PLAN.md](docs/EVIDENCE_PLAN.md) — pre-registered study: accuracy, time saved, retention, usability
|
|
24
|
+
- [docs/OPEN_QUESTIONS.md](docs/OPEN_QUESTIONS.md) — every unresolved decision with an owner
|
|
25
|
+
|
|
26
|
+
## See it work
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv sync
|
|
30
|
+
export OPENAI_API_KEY=...
|
|
31
|
+
uv run custos-code demo # the whole loop on a known trap, live
|
|
32
|
+
uv run custos-code demo --scenario honest # the control: nothing blocks
|
|
33
|
+
uv run custos-code check --last # your own most recent session
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`demo` prints five things from the fixture's own tool log: what was asked, what the agent actually
|
|
37
|
+
did, what it said, the receipt, and the deterministic nudge that goes back. `--format html --out
|
|
38
|
+
card.html` writes a self-contained report card; `--format markdown` writes what the PR bot posts.
|
|
39
|
+
|
|
40
|
+
## Prototype
|
|
41
|
+
|
|
42
|
+
`docs/prototype/index.html` is an interactive, non-functional mock of the editor experience: marks on the agent's message, the evidence panel, editor decorations, the auto-mode loop, and the PR receipt. It is a single self-contained file:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
open docs/prototype/index.html # macOS
|
|
46
|
+
# or: python3 -m http.server -d docs/prototype 8765 → http://localhost:8765
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Local setup
|
|
50
|
+
|
|
51
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) once. On macOS
|
|
52
|
+
with Homebrew: `brew install uv`. From the repository directory:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv python install
|
|
56
|
+
make sync
|
|
57
|
+
make check
|
|
58
|
+
uv run custos-code check --last
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`.python-version` selects Python 3.12, independently of your shell's pyenv or
|
|
62
|
+
Conda default. `make sync` installs the project and developer tools from the
|
|
63
|
+
committed `uv.lock`; it fails if the lockfile needs updating. After an intentional
|
|
64
|
+
dependency change, run `uv lock` and include the lockfile in the same PR.
|
|
65
|
+
|
|
66
|
+
`make build` produces a wheel and source distribution in `dist/`. CI runs the
|
|
67
|
+
same checks, installs both distributions in clean environments, and checks the
|
|
68
|
+
installed CLI outside the source checkout. CI uses locked installs following
|
|
69
|
+
the [uv integration guide](https://docs.astral.sh/uv/guides/integration/github/).
|
|
70
|
+
|
|
71
|
+
## Bench container
|
|
72
|
+
|
|
73
|
+
With Docker installed and running, build from the repository root:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
docker build -t custos-code-bench .
|
|
77
|
+
docker run --rm --network none custos-code-bench
|
|
78
|
+
docker run --rm --network none custos-code-bench python -m pytest --version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The image contains Python 3.12, uv 0.12.17, git, the installed Custos Code package,
|
|
82
|
+
developer dependencies, and scenario descriptions under `/app/bench/scenarios`.
|
|
83
|
+
It runs as a non-root user in writable `/workspace`; the default command shows
|
|
84
|
+
CLI help. The benchmark orchestration and fixture repos are not implemented
|
|
85
|
+
yet, so this is their execution environment, not a working benchmark command.
|
|
86
|
+
Agent CLIs and their credentials are not installed.
|
|
87
|
+
|
|
88
|
+
The Docker build context is an allowlist that excludes local session logs,
|
|
89
|
+
credentials, caches, and git history. For a local Python fixture, mount only
|
|
90
|
+
that fixture (including its git metadata when state checks need it):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
docker run --rm --network none \
|
|
94
|
+
--mount type=bind,src="$(pwd)/path/to/fixture",dst=/workspace,readonly \
|
|
95
|
+
custos-code-bench python -m pytest -p no:cacheprovider
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This read-only example suits tests that do not write into the fixture. Agent
|
|
99
|
+
bench runs will need a disposable writable checkout and explicit network and
|
|
100
|
+
credential configuration. Non-Python runners require additional toolchains.
|
|
101
|
+
|
|
102
|
+
## Why
|
|
103
|
+
|
|
104
|
+
Across 20,574 real coding-agent sessions, 22.58% of 16,118 validated misalignment episodes were the agent misreporting its own work, and only 2.99% of resolved episodes were self-corrected. Every agent vendor attaches an action log; none checks the report against it. Sources and denominators: `docs/RESEARCH.md`.
|
|
105
|
+
|
|
106
|
+
## Working in this repo
|
|
107
|
+
|
|
108
|
+
Read `AGENTS.md`. Flag anything undecided with `NEEDS-DECISION(owner):`. Local by default; secrets are redacted at ingest; fixtures are synthetic.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT (see `LICENSE`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "custos-code"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Checks a coding agent's final report against the log of what it actually did."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
keywords = ["agents", "verification", "claude-code", "hooks", "observability"]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pydantic>=2.7",
|
|
17
|
+
"typer>=0.12",
|
|
18
|
+
"rich>=13.7",
|
|
19
|
+
"openai>=1.40",
|
|
20
|
+
"anthropic>=0.40",
|
|
21
|
+
"duckdb>=1.0",
|
|
22
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=8", "ruff>=0.5", "mypy>=1.10", "hypothesis>=6"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
custos-code = "custos_code.cli:app"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/custos_code"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = [
|
|
40
|
+
"/src/custos_code",
|
|
41
|
+
"/README.md",
|
|
42
|
+
"/LICENSE",
|
|
43
|
+
"/pyproject.toml",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
exclude = ["pilots"] # feasibility-week scratch scripts, not product code
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
51
|
+
ignore = ["E501"] # long lines in docstrings are fine; the formatter handles code
|
|
52
|
+
|
|
53
|
+
[tool.mypy]
|
|
54
|
+
strict = true
|
|
55
|
+
python_version = "3.12"
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Custos Code: check a coding agent's final report against the log of what it actually did.
|
|
2
|
+
|
|
3
|
+
Read AGENTS.md before changing anything. Invariants live there. Every module below has a
|
|
4
|
+
docstring saying what it owns and what it must never do.
|
|
5
|
+
"""
|
|
6
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Adapters turn a source (harness transcript, hook payload, vendor export) into LedgerEvents.
|
|
2
|
+
|
|
3
|
+
Contract: every adapter is a function `parse(path_or_payload) -> tuple[Session, list[LedgerEvent], str | None]`
|
|
4
|
+
returning the session, the chained events, and the final report text if present.
|
|
5
|
+
Adapters must set flags.truncated / flags.piped / flags.sidechain honestly; downstream tiers rely on them.
|
|
6
|
+
Golden tests live in tests/golden/<adapter>/ : real input in, expected JSONL out.
|
|
7
|
+
|
|
8
|
+
`detect` picks the adapter from the file itself so `custos-code check <path>` needs no --agent flag.
|
|
9
|
+
|
|
10
|
+
`request_and_plan` (docs/SCOPE.md §6.3, issue #58) is scope's other input besides the ledger it
|
|
11
|
+
already gets: what was asked, and what the agent said it would do before doing it. It lives here,
|
|
12
|
+
not in `scope.py`, so scope stays agent-agnostic -- it takes a request and a plan, never a
|
|
13
|
+
harness-specific shape.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from typing import Protocol
|
|
21
|
+
|
|
22
|
+
from ..models import EventKind, LedgerEvent, Session
|
|
23
|
+
from . import claude_code, codex, copilot, devin, machine, otel
|
|
24
|
+
|
|
25
|
+
Source = str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Adapter(Protocol):
|
|
29
|
+
def parse(self, path: str) -> tuple[Session, list[LedgerEvent], str | None]: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ADAPTERS: dict[Source, Adapter] = {
|
|
33
|
+
"claude_code": claude_code,
|
|
34
|
+
"codex": codex,
|
|
35
|
+
"devin": devin,
|
|
36
|
+
"machine": machine,
|
|
37
|
+
"copilot": copilot,
|
|
38
|
+
"otel": otel,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_CODEX_TYPES = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"session_meta",
|
|
44
|
+
"turn_context",
|
|
45
|
+
"response_item",
|
|
46
|
+
"event_msg",
|
|
47
|
+
"compacted",
|
|
48
|
+
"thread_rolled_back",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _bundle_source(obj: dict[str, object]) -> Source | None:
|
|
54
|
+
"""Class-R bundles and OTLP payloads are one JSON object; tell them apart by their keys."""
|
|
55
|
+
if "resourceSpans" in obj or "scopeSpans" in obj:
|
|
56
|
+
return "otel"
|
|
57
|
+
raw = obj.get("session")
|
|
58
|
+
session: dict[str, object] = raw if isinstance(raw, dict) else {}
|
|
59
|
+
keys = set(obj) | {f"session.{k}" for k in session}
|
|
60
|
+
# Copilot first: both bundles carry `pull_request` and a `session.session_id`, and only
|
|
61
|
+
# Copilot carries a workspace or an attached tool log. Devin's tell is structured_output.
|
|
62
|
+
if "pull_request" in keys and (keys & {"log", "session.workspace", "session.agent_log"}):
|
|
63
|
+
return "copilot"
|
|
64
|
+
if keys & {"session.structured_output", "structured_output"}:
|
|
65
|
+
return "devin"
|
|
66
|
+
if keys & {"session.session_id", "session_id", "pull_request", "pull_requests"}:
|
|
67
|
+
return "devin"
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _line_source(rec: dict[str, object]) -> Source | None:
|
|
72
|
+
if "sessionId" in rec or (rec.get("type") in ("assistant", "user") and "message" in rec):
|
|
73
|
+
return "claude_code"
|
|
74
|
+
if str(rec.get("type")) in _CODEX_TYPES:
|
|
75
|
+
return "codex"
|
|
76
|
+
if rec.get("recorder") in machine.RECORDER_NAMES:
|
|
77
|
+
return "machine"
|
|
78
|
+
if "traceId" in rec and "spanId" in rec:
|
|
79
|
+
return "otel"
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def detect(path: str) -> Source:
|
|
84
|
+
"""Name the adapter for a file, by its shape. Raises ValueError when nothing matches."""
|
|
85
|
+
with open(path, encoding="utf-8", errors="ignore") as fh:
|
|
86
|
+
text = fh.read()
|
|
87
|
+
if not text.strip():
|
|
88
|
+
raise ValueError(f"{path} is empty")
|
|
89
|
+
try:
|
|
90
|
+
whole = json.loads(text)
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
whole = None
|
|
93
|
+
if isinstance(whole, dict):
|
|
94
|
+
name = _bundle_source(whole)
|
|
95
|
+
if name:
|
|
96
|
+
return name
|
|
97
|
+
for line in text.splitlines():
|
|
98
|
+
if not line.strip():
|
|
99
|
+
continue
|
|
100
|
+
try:
|
|
101
|
+
rec = json.loads(line)
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
continue
|
|
104
|
+
if isinstance(rec, dict):
|
|
105
|
+
name = _line_source(rec)
|
|
106
|
+
if name:
|
|
107
|
+
return name
|
|
108
|
+
raise ValueError(f"cannot tell which agent wrote {path}; pass --agent")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse(path: str, source: Source | None = None) -> tuple[Session, list[LedgerEvent], str | None]:
|
|
112
|
+
"""Parse a transcript with the adapter named by `source`, or the one `detect` picks."""
|
|
113
|
+
name = source or detect(path)
|
|
114
|
+
adapter = ADAPTERS.get(name)
|
|
115
|
+
if adapter is None:
|
|
116
|
+
raise ValueError(f"unknown agent {name!r}; one of {', '.join(sorted(ADAPTERS))}")
|
|
117
|
+
return adapter.parse(path)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _first_user_text(ledger: Sequence[LedgerEvent]) -> str:
|
|
121
|
+
"""The earliest non-sidechain USER event's text -- the literal ask, verbatim.
|
|
122
|
+
|
|
123
|
+
Claude Code, Codex, and Devin (when it has a chat transcript) all write these already; nothing
|
|
124
|
+
new has to be recorded (SCOPE.md §6.3's whole premise).
|
|
125
|
+
"""
|
|
126
|
+
for event in ledger:
|
|
127
|
+
if event.kind is EventKind.USER and not event.flags.sidechain and event.output:
|
|
128
|
+
return event.output
|
|
129
|
+
return ""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _todo_plan(ledger: Sequence[LedgerEvent]) -> list[str]:
|
|
133
|
+
"""Claude Code's `TodoWrite`: the most recent call's items, oldest first.
|
|
134
|
+
|
|
135
|
+
The latest call wins, not the first -- a plan is allowed to change, and the self-authored
|
|
136
|
+
contract SCOPE.md §3 describes is whatever the agent most recently committed to, not its first
|
|
137
|
+
draft.
|
|
138
|
+
"""
|
|
139
|
+
latest: LedgerEvent | None = None
|
|
140
|
+
for event in ledger:
|
|
141
|
+
if event.kind is EventKind.CALL and event.tool == "TodoWrite" and not event.flags.sidechain:
|
|
142
|
+
latest = event
|
|
143
|
+
if latest is None:
|
|
144
|
+
return []
|
|
145
|
+
todos = (latest.input or {}).get("todos")
|
|
146
|
+
if not isinstance(todos, list):
|
|
147
|
+
return []
|
|
148
|
+
out = []
|
|
149
|
+
for item in todos:
|
|
150
|
+
if not isinstance(item, dict):
|
|
151
|
+
continue
|
|
152
|
+
text = item.get("content") or item.get("activeForm") or item.get("task")
|
|
153
|
+
if isinstance(text, str) and text.strip():
|
|
154
|
+
out.append(text.strip())
|
|
155
|
+
return out
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _stated_plan_before_first_call(ledger: Sequence[LedgerEvent]) -> list[str]:
|
|
159
|
+
"""Fallback plan: the last thing the agent said before it touched a tool.
|
|
160
|
+
|
|
161
|
+
A message written before any evidence exists is a commitment it cannot later revise -- the
|
|
162
|
+
same "log it has no write path to" property SCOPE.md §3 grounds the plan in. Applies to any
|
|
163
|
+
adapter whose ledger has TEXT/CALL events in the shared shape (Claude Code, Codex today).
|
|
164
|
+
"""
|
|
165
|
+
first_call_seq = next(
|
|
166
|
+
(e.seq for e in ledger if e.kind is EventKind.CALL and not e.flags.sidechain), None
|
|
167
|
+
)
|
|
168
|
+
last_text = ""
|
|
169
|
+
for event in ledger:
|
|
170
|
+
if first_call_seq is not None and event.seq >= first_call_seq:
|
|
171
|
+
break
|
|
172
|
+
if event.kind is EventKind.TEXT and not event.flags.sidechain and event.output:
|
|
173
|
+
last_text = event.output
|
|
174
|
+
return [last_text] if last_text.strip() else []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def request_and_plan(
|
|
178
|
+
session: Session, ledger: Sequence[LedgerEvent], report: str | None = None
|
|
179
|
+
) -> tuple[str, list[str]]:
|
|
180
|
+
"""The spec and the agent's self-authored plan, per docs/SCOPE.md §6.3.
|
|
181
|
+
|
|
182
|
+
Deliberately reads the ledger, not the harness: Claude Code and Codex both already write a
|
|
183
|
+
USER event for the request, so no per-agent branch is needed to find it. Class-R bundles
|
|
184
|
+
(Copilot, Devin without a chat transcript) have no live back-and-forth to draw either from --
|
|
185
|
+
there, the PR body (`report`, already the same text `custos-code check` extracts claims from)
|
|
186
|
+
doubles as the spec, exactly as SCOPE.md §6.3 says, and there is no separate plan to find.
|
|
187
|
+
|
|
188
|
+
A `TodoWrite` call is the strongest plan signal where one exists; otherwise the last thing said
|
|
189
|
+
before the first tool call stands in for it (still empty for a class-R bundle, which has no
|
|
190
|
+
"before the first call" boundary to speak of).
|
|
191
|
+
"""
|
|
192
|
+
request = _first_user_text(ledger) or (report or "")
|
|
193
|
+
plan = _todo_plan(ledger) or _stated_plan_before_first_call(ledger)
|
|
194
|
+
return request, plan
|