agent-polygraph 0.0.0-stage → 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +232 -2
- package/bin/polygraph.js +274 -0
- package/package.json +41 -4
- package/src/analyze.js +621 -0
- package/src/ci.js +204 -0
- package/src/classify.js +506 -0
- package/src/demo.js +120 -0
- package/src/guard.js +240 -0
- package/src/jsonl.js +23 -0
- package/src/record.js +402 -0
- package/src/report/format.js +87 -0
- package/src/report/html.js +101 -0
- package/src/report/terminal.js +160 -0
- package/src/safe-config.js +85 -0
- package/src/scan.js +60 -0
- package/src/shell/record.sh +73 -0
- package/src/sources/claude.js +300 -0
- package/src/sources/codex.js +476 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eleftherios Chondros
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,233 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agent-polygraph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/EleChon-pixel/agent-polygraph/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
Your coding agent said "all tests pass". Did a test pass after its last edit?
|
|
6
|
+
|
|
7
|
+
agent-polygraph is an audit of your past sessions. It reads the Claude Code and Codex session logs already on your machine and checks every "tests pass" claim against what ran: was there a passing test run after the agent's last code edit? It tells you how often your agents claimed something they hadn't verified, and shows the evidence for each case. No setup, no API key, nothing uploaded.
|
|
8
|
+
|
|
9
|
+
The audit and guard **read evidence; they do not execute tests**. A flagged claim is not proof the code is wrong: it means the available evidence does not support the claim at that point. If you want automatic test execution, use a separate test-running hook or CI.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx agent-polygraph # your own sessions
|
|
13
|
+
npx agent-polygraph --demo # generated sample sessions
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> **Beta / evidence limitations:** The parser has been evaluated on the author's own sessions and public Claude Code traces ([evaluation](docs/evaluation.md)). Controlled replays and real executed tests exist, but **independent, blinded human-validated accuracy has not been established**. If you report a wrong verdict, [open an issue](https://github.com/EleChon-pixel/agent-polygraph/issues/new/choose) with a *manually redacted* example—never upload an unreviewed raw session log.
|
|
17
|
+
|
|
18
|
+
Historical example from the author's sessions, July–October 2026 (not a representative benchmark or a current-user measurement):
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
agent-polygraph 32 working sessions · Jul 30 – Oct 31, 2026 · 11.0s
|
|
22
|
+
|
|
23
|
+
"Tests pass" claims was there a passing run after the last code edit?
|
|
24
|
+
Codex Claude Code All
|
|
25
|
+
Backed by a fresh passing run 243 80 323
|
|
26
|
+
No test run in the session 0 0 0
|
|
27
|
+
Code edited after the last run 15 5 20
|
|
28
|
+
Last run had failed 1 0 1
|
|
29
|
+
Couldn't tell 15 17 32
|
|
30
|
+
…test result unreadable 3 1 4
|
|
31
|
+
…unresolved file change after the run 11 14 25
|
|
32
|
+
…other 1 2 3
|
|
33
|
+
Receipt rate 94% 94% 94%
|
|
34
|
+
|
|
35
|
+
Test integrity worth a second look, not proof of cheating
|
|
36
|
+
Failing test "fixed" by editing only tests 0 0 0
|
|
37
|
+
Edits that weakened an existing test 3 1 4
|
|
38
|
+
|
|
39
|
+
Where the time went
|
|
40
|
+
Test runs 851 976 1827
|
|
41
|
+
Time spent in tests 3.1h 8.7h 12h
|
|
42
|
+
Full-suite runs 325 212 537
|
|
43
|
+
...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Under the tables it prints the latest examples: what the agent claimed, the last test command and its result, what was edited afterwards, and the session file, so you can check it yourself.
|
|
47
|
+
|
|
48
|
+
**What the benchmarks actually show:** agent-polygraph detects stale test claims more reliably than a *simple any-prior-pass rule* in [25 authored replay scenarios](docs/benchmark.md) (14/14 unbacked claims versus 7/14). In [12 instrumented projects using real test executions](docs/live-evaluation.md), its selective guard requested **5** additional tests and detected **3 of 7** failing final suites; rerunning the full suite every time required **12** additional tests and detected **all 7**. That is an efficiency-versus-coverage trade-off, **not** evidence that the guard improves overall defect detection. [Public-session workload projections](docs/benchmark.md) are separate from actual executed tests. The independently labeled human study is [still open](https://github.com/EleChon-pixel/agent-polygraph/issues/3).
|
|
49
|
+
|
|
50
|
+
**npm release status:** The [public npm registry](https://www.npmjs.com/package/agent-polygraph) still serves **0.1.0**, while GitHub `main` contains the security-hardened **0.2.0 release candidate**. The automated tarball test passes, but new npm code will not be available to `npx` until it has been published. For the exact manual publishing procedure (GitHub OIDC trusted publisher), see [Release instructions](docs/releasing.md).
|
|
51
|
+
|
|
52
|
+
## Share your numbers
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npx agent-polygraph --html
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This writes `polygraph.html`, a self-contained card with numbers only. Prompts, code, commands and file paths are left out.
|
|
59
|
+
|
|
60
|
+
<img src="docs/card.png" alt="agent-polygraph report card from my own sessions" width="600">
|
|
61
|
+
|
|
62
|
+
## Catch unbacked claims as they happen (optional)
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx agent-polygraph guard --install # this project (.claude/settings.json, .codex/hooks.json)
|
|
66
|
+
npx agent-polygraph guard --install --global # every project (~/.claude, ~/.codex)
|
|
67
|
+
npx agent-polygraph guard --uninstall
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
<img src="docs/guard.gif" alt="staged demo: guard stops a coding agent that claims tests pass after editing code, and the rerun fails" width="760">
|
|
71
|
+
|
|
72
|
+
`guard` is a Stop hook that runs the same checks on the live session. When the agent's final message says tests pass and the session doesn't back it up, the agent is sent back once:
|
|
73
|
+
|
|
74
|
+
> agent-polygraph: your final message says "All tests pass.", but you edited calc.js after the last test run. Run the relevant tests now and report the actual result. If they can't be run, say so instead of claiming they pass.
|
|
75
|
+
|
|
76
|
+
It blocks at most once per stop, looks only at the final message (so a claim the agent already corrected doesn't count), stays quiet when it can't tell, and never fails the turn because of its own errors. Install it globally (`npm i -g agent-polygraph`) so the hook doesn't start `npx` on every stop.
|
|
77
|
+
|
|
78
|
+
I tested it with both agents in a staged setup: they were told to edit code after a test run and then claim the tests pass (the GIF above is one of these runs). Both were stopped, reran `npm test` and reported the failure. Codex needs one extra step: it only runs hooks you have approved, and only reads a project's `.codex/` folder if you trust it. After installing, open `codex` in the project once, trust the folder and approve the hook.
|
|
79
|
+
|
|
80
|
+
### Do you need it?
|
|
81
|
+
|
|
82
|
+
Maybe not. Two simpler setups also work, and they are more thorough:
|
|
83
|
+
|
|
84
|
+
- A Stop hook that runs your test suite after every turn and gives the agent the result. This also catches breakage when the agent doesn't claim anything.
|
|
85
|
+
- A Stop hook that always asks the agent to double-check its results. When I tried this on small tasks where the agent had edited code after its last run, it reran the tests every time (6 of 6, Claude Haiku).
|
|
86
|
+
|
|
87
|
+
The trade-off is cost. Both fire on every stop, so every turn ends with a test run, and the second also adds a model turn, even when nothing changed or nothing was claimed. A test-running hook also has to know each project's test command and setup (a virtualenv, environment variables, a subfolder, a database). `guard` runs nothing: it reads the session in well under a second and only sends the agent back when the final message claims a pass that no run after the last edit supports (about 6% of claims in my logs), naming the edit that made the run stale.
|
|
88
|
+
|
|
89
|
+
If your suite runs in a few seconds and needs no setup, running it on every stop is the better choice. `guard` is for suites that are slow or fiddly to run, and for when you don't want an extra turn on every stop.
|
|
90
|
+
|
|
91
|
+
### As a Claude Code plugin
|
|
92
|
+
|
|
93
|
+
In Claude Code you can install the guard as a plugin instead:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
/plugin install agent-polygraph --marketplace EleChon-pixel/agent-polygraph
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
On Claude Code older than 2.1.275, run `/plugin marketplace add EleChon-pixel/agent-polygraph` first, then `/plugin install agent-polygraph@agent-polygraph`.
|
|
100
|
+
|
|
101
|
+
The plugin runs the same Stop hook from its own copy, so nothing is downloaded when the agent stops, and it adds `/agent-polygraph:check` for the report (it takes the same options, for example `/agent-polygraph:check --since 30d`). If you also installed the hook with `guard --install`, the plugin's copy stays quiet so the agent isn't sent back twice. The plugin doesn't include `record`; turn that on separately if you want it.
|
|
102
|
+
|
|
103
|
+
## Evidence the logs don't have
|
|
104
|
+
|
|
105
|
+
Some things never show up in the logs: tests you ran yourself in another terminal, CI results, and files changed by a script that builds their names at run time. Two optional extras cover them.
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npx agent-polygraph record --install # this project; add --global for all of them
|
|
109
|
+
agent-polygraph record --install --shell # your own terminals (requires a persistent install)
|
|
110
|
+
npx agent-polygraph --ci # GitHub Actions (online)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`record --install` adds a PostToolUse hook to Claude Code and Codex. After a test or a command that may write files, it records the test outcome and an HMAC-fingerprinted source snapshot. The snapshot reads bounded source files without writing copies to Git or storing raw contents. Ignored and non-source files are excluded; symlinks, unreadable files and exceeded size limits make a snapshot inconclusive. Comparing snapshots can resolve scripts that modify dynamically chosen source files. All new records stay in `~/.agent-polygraph` (or `$AGENT_POLYGRAPH_HOME`). Recording starts with new sessions.
|
|
114
|
+
|
|
115
|
+
`record --install --shell` adds one line to `~/.bashrc` and `~/.zshrc`. Supported test commands you run in interactive Bash/Zsh shells produce a minimal receipt: runner category, working directory, timestamps and exit code. Raw commands and output are never saved. To avoid unsafe shell redirection, the shell hook invokes the installed Node CLI, which validates and writes the receipt with private permissions and no-follow checks. **Use a persistent installation** (`npm i -g agent-polygraph` or a durable checkout) for shell integration; temporary `npx` cache paths may disappear. Shell mode recognizes a conservative subset of direct test commands. `record --uninstall --shell` removes its startup line.
|
|
116
|
+
|
|
117
|
+
`--ci` goes online. Through your [`gh`](https://cli.github.com) login, it asks the GitHub API for the workflow runs of the repositories your sessions pushed to. The request contains the repository name and a date range, nothing from your sessions. A test workflow on a pushed commit counts as a test run of the code as it was at the push, so later edits still make it stale. Runs are matched to pushes by commit id, which has to appear in the session (in the push output, or from `git commit` or `git log` before it). Branch names and timing alone are not enough. With `--ci`, claims about CI itself ("CI is green", "all checks on the PR pass") are checked too. Workflows with no job or step named like a test, and runs GitHub never started, are ignored.
|
|
118
|
+
|
|
119
|
+
The report shows how many claims each of these settled.
|
|
120
|
+
|
|
121
|
+
## What it checks
|
|
122
|
+
|
|
123
|
+
| Finding | How it's decided |
|
|
124
|
+
|---|---|
|
|
125
|
+
| Backed claim | The agent said tests pass and the run it refers to passed. That is the run with the count or test file the claim names; for "the full suite passes", the latest run in that folder about as large as the largest one; otherwise the latest result the agent had seen. The run must have started after the last edit to a source file in the same checkout. |
|
|
126
|
+
| Code edited after the last run | A test ran, the agent then changed source code in that checkout (with an edit tool, a patch or the shell), and claimed tests pass without running them again. An edit made while the run was still going counts too. |
|
|
127
|
+
| Last run had failed | The latest test result in the session showed failures. A subagent's failing run in its own worktree is "couldn't tell" instead. |
|
|
128
|
+
| No test run | The agent changed code but ran nothing recognisable as a test before the claim. |
|
|
129
|
+
| Couldn't tell | Left out of the receipt rate, with the reason shown: the result can't be read (piped, truncated, mixed with other output), a file changed in a way that can't be traced (a script with no file name in it, `git checkout`, `git merge`), the test was still running, a subagent's run failed in its own worktree, or nothing in the session changed code. |
|
|
130
|
+
| Failing test "fixed" by editing only tests | A run failed, the agent changed a test it hadn't touched before, changed nothing else, and the next run passed. Fixing a test the agent just wrote doesn't count. Sometimes the test was wrong, so this is a flag to look at, not a verdict. |
|
|
131
|
+
| Weakened an existing test | An edit to a test that existed before the session removed cases or assertions, added `skip`/`xfail`, `.only` or an always-true assertion, or deleted the file. |
|
|
132
|
+
| Full-suite runs | A test command with no file, directory, node id or `-k` filter. |
|
|
133
|
+
| Retry loops | The same failing command run 3 or more times in a row with no edit in between. |
|
|
134
|
+
| Pushback and interrupts | Your messages that start with "no", "stop" or "revert" or contain "I told you", and turns you interrupted. |
|
|
135
|
+
| Risky git | `--no-verify`, `push --force`, `reset --hard`, discarding uncommitted changes. |
|
|
136
|
+
|
|
137
|
+
Background runs are followed: Claude Code's `run_in_background` results (the notification and the output file the agent reads) and Codex cells that finish later through `wait`.
|
|
138
|
+
|
|
139
|
+
## How it decides
|
|
140
|
+
|
|
141
|
+
Everything is deterministic pattern matching over the logs. There is no LLM involved, and the same logs always give the same numbers. When a rule is unsure, it says "couldn't tell" instead of flagging the agent.
|
|
142
|
+
|
|
143
|
+
- Each command's start and its result are separate events, in the order they were observed. A claim can only be backed by a result the agent had already seen. A run covers the code as it was when the run started, so an edit during the run makes it stale.
|
|
144
|
+
- In a Codex cell that runs several commands, each command gets its own output. If outputs can't be matched to commands, the result stays unknown.
|
|
145
|
+
- An edit counts once it happened: a Claude Code edit after its tool result confirms it, a Codex patch if it applied. Shell writes count too (`sed -i`, redirects, `tee`, `cp`/`mv`, scripts that write named files), placed by the `cd` in effect where they appear.
|
|
146
|
+
- A script's writes are the files it clearly writes, such as `open('x.py', 'w')`, `Path('x.py').write_text(...)`, `writeFileSync('x.js', ...)`, or a variable holding such a path that is written later. Files it only reads don't count. A write to a file it can't name (a loop over paths, `extractall`, `shutil.copy` to a variable) makes the claim "couldn't tell".
|
|
147
|
+
- An edit in one checkout or worktree doesn't make a run in another one stale. Only edits to source files count; docs, config, data, generated output and hidden tool folders don't.
|
|
148
|
+
- `tail pytest.log` is evidence about the run that wrote `pytest.log`, not a new run.
|
|
149
|
+
- Results come from the runner's summary line first (`3 failed, 10 passed in 1.2s`, `Tests: 1 failed`, `test result: FAILED`). An exit code is trusted only when the test ran last (`cd app && pytest`, `ruff check .; pytest`) or a plain `&&` chain exited 0. `npm test; true` exits 0 because of `true`.
|
|
150
|
+
- Commands refused by a permission prompt, the auto-mode classifier or a hook never ran, so they aren't test runs.
|
|
151
|
+
- A claim is a sentence like "all tests pass" or "62 passed". Plans ("I'll make sure the tests pass"), conditions ("once tests pass"), history ("the latest recorded run passed"), questions, partial results ("58 of 59 pass") and quoted runner output are ignored. English is supported, plus simple forms in Greek, Spanish, Portuguese, German, French and Italian.
|
|
152
|
+
- Subagent transcripts are merged into the parent session, so a subagent's passing run backs the parent's claim.
|
|
153
|
+
- Tests the agent created in the same session can't be "weakened". A Claude Code `Write` over an existing test is an overwrite, so it can.
|
|
154
|
+
- `guard` judges only the final message. "Correction: I did not run the tests" after an earlier claim doesn't trigger a block.
|
|
155
|
+
|
|
156
|
+
The rules are in [`src/classify.js`](src/classify.js) and the tests in [`test/`](test), including a labelled set of claim and non-claim sentences ([`test/claims.corpus.test.js`](test/claims.corpus.test.js)). A false positive or a missed claim makes a good [issue](https://github.com/EleChon-pixel/agent-polygraph/issues/new/choose); include the redacted sentence and it goes into that set.
|
|
157
|
+
|
|
158
|
+
## Validation and accuracy
|
|
159
|
+
|
|
160
|
+
The historical analysis of the author's own Claude Code and Codex sessions includes manual spot checks of claims and flagged evidence. In a small, author-selected review, approximately 95% of 25 detected claim candidates were genuine claims, and a separate sample suggested some real claims were missed. **These are preliminary, author-reviewed observations, not externally validated precision or recall.** See the [original evaluation protocol](docs/evaluation.md).
|
|
161
|
+
|
|
162
|
+
The public Claude Code evaluation reviewed 26 parsed sessions and identified 24 backed claims and three stale findings (one confirmed and two considered likely). It is useful for parser regression coverage but does **not** establish accuracy on an independent, representative population. A separately pinned, redacted holdout audit inspected 103 public sessions; under its conservative automated reference rules, **no eligible claims were found**, so **no holdout accuracy percentage was produced**. Additional dataset limitations and exact counts are documented in [live-evaluation.md](docs/live-evaluation.md).
|
|
163
|
+
|
|
164
|
+
Two reproducible controlled experiments test narrower propositions:
|
|
165
|
+
|
|
166
|
+
- [Stop-hook replay](docs/benchmark.md): 25 authored Claude Code/Codex scenarios. Agent-polygraph correctly blocked 14/14 labeled unbacked claims versus 7/14 for a naive “any test passed earlier” hook, with zero false blocks in these selected cases.
|
|
167
|
+
- [Executed-test policy comparison](docs/live-evaluation.md): 12 authored scenarios with actual Node.js test executions. The selective guard required five extra test executions and detected three of seven failing final suites; running the full suite after every response required 12 executions and detected all seven.
|
|
168
|
+
|
|
169
|
+
The replay labels and scenarios were constructed by the project authors. The guard cannot discover failures the agent never claimed away, or infer whether a narrow passing test establishes a full-suite pass. **An independent, blinded human evaluation is still needed.** The [review issue](https://github.com/EleChon-pixel/agent-polygraph/issues/3) describes the protocol; `scripts/blind-review.mjs` prepares prediction-free review packets. No raw private session logs should be shared with reviewers or issues without permission and redaction.
|
|
170
|
+
|
|
171
|
+
## Limits
|
|
172
|
+
|
|
173
|
+
These are the gaps that remain. `record` and `--ci` can reduce some of them, but neither replaces running tests or provides independent proof of correctness.
|
|
174
|
+
|
|
175
|
+
- Tests outside the logs. Tests the agent ran, test runs Claude Code recorded from hooks, commands you typed with `!` in Claude Code, and test output you pasted into the chat all count. So do your own bash and zsh runs with `record --shell`, and GitHub Actions runs on pushed commits with `--ci`. Other shells (fish, PowerShell, an IDE's test button), other CI services, and CI runs on commits the session never printed are still invisible.
|
|
176
|
+
- "The full suite passes" is compared with the latest run about as large as the largest one in that folder, using the counts the runner printed. If no run printed a count, it's compared with the latest run, which may have been narrower. Claims that name no count, test file or full suite are compared with the latest run.
|
|
177
|
+
- Output with no summary. Codex keeps each command's full output. Claude Code may save long output to a separate file that the analyzer reads if it still exists. New `record` entries retain **parsed test outcomes and pass counts, not raw test output**. A run abandoned before printing a result leaves nothing to read.
|
|
178
|
+
- Files a script names at run time. Without `record` these make a claim "couldn't tell", never "backed". `record` settles them, but only in git checkouts and only for sessions after it was installed.
|
|
179
|
+
- New agent versions can change the log format. Supported: Claude Code 2.x (Bash and PowerShell tools; Linux, macOS and Windows paths) and Codex CLI. The report lists the versions it read, warns about newer ones, and counts files it couldn't parse instead of dropping them silently.
|
|
180
|
+
- Unbacked is not the same as false. "Code edited after the last run" means the claim wasn't verified after the edit; the edit may have been harmless, such as a comment or a test file. The report can't tell whether the tests would have passed, because nobody ran them at that point.
|
|
181
|
+
- The numbers describe your sessions, projects and prompts. They are not a benchmark of the models.
|
|
182
|
+
|
|
183
|
+
## Options
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
--since <when> only sessions since a date (2026-10-01) or age (7d, 4w, 3m)
|
|
187
|
+
--project <text> only sessions whose working directory contains <text>
|
|
188
|
+
--exclude <text> skip sessions whose id starts with, or whose working directory contains, <text> (repeatable)
|
|
189
|
+
--agent <name> claude or codex (repeatable; default: both)
|
|
190
|
+
--by <col> compare by "agent" (default) or "model"
|
|
191
|
+
--examples <n> examples to show per finding type (default 3, 0 to hide)
|
|
192
|
+
--html [file] write a numbers-only report to share (default polygraph.html)
|
|
193
|
+
--json full results as JSON: findings, unclear reasons, unreadable files (keep it local)
|
|
194
|
+
--ci also use GitHub Actions results (goes online, through your gh login)
|
|
195
|
+
--claude-dir <dir> Claude Code projects dir (default ~/.claude/projects, or $CLAUDE_CONFIG_DIR/projects)
|
|
196
|
+
--codex-dir <dir> Codex sessions dir (default ~/.codex/sessions, or $CODEX_HOME/sessions)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Running the repository version
|
|
200
|
+
|
|
201
|
+
The GitHub source and the npm-distributed package are different delivery paths. To try the **current `main` code** without assuming npm is up to date:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
git clone https://github.com/EleChon-pixel/agent-polygraph.git
|
|
205
|
+
cd agent-polygraph
|
|
206
|
+
node bin/polygraph.js --demo
|
|
207
|
+
npm test
|
|
208
|
+
npm run benchmark:replay
|
|
209
|
+
npm run benchmark:live
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Before distributing a new version, verify `npm view agent-polygraph version`, the published tarball (`npm pack --dry-run`), and that a clean install really contains the latest privacy and guard fixes. The repository declares version `0.2.0` as the **next release candidate**; until it is published, `npx agent-polygraph` still installs the older public registry version. A green GitHub Actions badge does **not** prove npm has been updated.
|
|
213
|
+
|
|
214
|
+
## Privacy
|
|
215
|
+
|
|
216
|
+
New recordings intentionally minimize local data. The agent hook stores a keyed command fingerprint, the runner's result and optional fingerprints of source files. It does **not** save raw command text, test output or copies of repository files in its snapshot store. The shell integration saves only a recognized test-runner category, time, folder and exit code. Local record files, key and snapshot metadata are owner-only on Unix. Hook settings and shell startup files are replaced atomically, preserving their permissions and creating private recovery backups under `~/.agent-polygraph/backups` (see [recovery](docs/releasing.md#recovery-for-installed-hooks)). The secret fingerprint key is stored in `$AGENT_POLYGRAPH_HOME/record-key` (default `~/.agent-polygraph/record-key`).
|
|
217
|
+
|
|
218
|
+
**Existing installations:** records made before this update may still include full commands, and older versions may have created a local Git object store with duplicate source contents. Upgrading does **not** remove those files. After reviewing whether you still need the historical evidence, uninstall recording and manually remove the old `~/.agent-polygraph/records` and `~/.agent-polygraph/objects` directories. Removing historical records means those sessions will lose their supplemental evidence; the original Claude Code/Codex transcripts remain untouched. Do not delete `record-key` while retaining version 2 records that rely on matching fingerprints.
|
|
219
|
+
|
|
220
|
+
Stored fingerprints are **not** a guarantee of anonymity: recording metadata includes timestamps, working directories and relative source-file paths, and existing v1 records may still contain raw commands. The original Claude Code/Codex transcripts can also contain secrets and confidential data. The terminal report and `--json` may quote transcript text and paths; do not share them without review. The `--html` report intentionally excludes excerpts, prompts, commands and paths. The recorder only supports bounded source-file snapshots in Git checkouts and skips symbolic links; when evidence cannot be read it must stay inconclusive rather than certifying a pass. New source-file fingerprints include relative filenames and are not an anonymization guarantee.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
It reads files and works offline. These operations are opt-in:
|
|
224
|
+
|
|
225
|
+
- `guard` and `record` edit the hook config files they print when you install or uninstall them, and `record --shell` adds or removes one marked line in `~/.bashrc` and `~/.zshrc`.
|
|
226
|
+
- `record` saves snapshots and logs in `~/.agent-polygraph`.
|
|
227
|
+
- `--ci` contacts api.github.com through `gh` and sends repository names and date ranges.
|
|
228
|
+
|
|
229
|
+
The terminal report and `--json` include excerpts from your sessions. `--html` doesn't.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
package/bin/polygraph.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { scan } from "../src/scan.js";
|
|
7
|
+
import { aggregate } from "../src/analyze.js";
|
|
8
|
+
import { renderTerminal } from "../src/report/terminal.js";
|
|
9
|
+
import { renderHtml } from "../src/report/html.js";
|
|
10
|
+
|
|
11
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
12
|
+
|
|
13
|
+
if (process.argv[2] === "guard") {
|
|
14
|
+
await guard(process.argv.slice(3));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
if (process.argv[2] === "record") {
|
|
18
|
+
await record(process.argv.slice(3));
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HELP = `agent-polygraph ${version}
|
|
23
|
+
Check what your coding agents claimed against what actually ran.
|
|
24
|
+
Reads Claude Code and Codex session logs on this machine. Nothing is uploaded
|
|
25
|
+
(except with --ci, which asks GitHub for your workflow results).
|
|
26
|
+
|
|
27
|
+
Usage: npx agent-polygraph [options]
|
|
28
|
+
|
|
29
|
+
--since <when> only sessions since a date (2026-10-01) or age (7d, 4w, 3m)
|
|
30
|
+
--project <text> only sessions whose working directory contains <text>
|
|
31
|
+
--exclude <text> skip sessions whose id starts with, or whose working directory
|
|
32
|
+
contains, <text> (repeatable)
|
|
33
|
+
--agent <name> claude or codex (repeatable; default: both)
|
|
34
|
+
--by <col> compare by "agent" (default) or "model"
|
|
35
|
+
--examples <n> examples to show per finding type (default 3, 0 to hide)
|
|
36
|
+
--html [file] write a numbers-only report to share (default polygraph.html)
|
|
37
|
+
--json print full results as JSON (findings, unclear reasons, unreadable files; keep it local)
|
|
38
|
+
--ci also use GitHub Actions results (goes online: asks the GitHub API,
|
|
39
|
+
through your \`gh\` login, about the repositories your sessions pushed to)
|
|
40
|
+
--demo run on generated sample sessions instead of yours
|
|
41
|
+
--claude-dir <dir> Claude Code projects dir (default ~/.claude/projects)
|
|
42
|
+
--codex-dir <dir> Codex sessions dir (default ~/.codex/sessions)
|
|
43
|
+
-h, --help show this help
|
|
44
|
+
-v, --version show the version
|
|
45
|
+
|
|
46
|
+
Guard (stop unbacked "tests pass" claims as they happen):
|
|
47
|
+
|
|
48
|
+
npx agent-polygraph guard --install add a Stop hook to this project (.claude/, .codex/)
|
|
49
|
+
npx agent-polygraph guard --install --global add it for every project (~/.claude, ~/.codex)
|
|
50
|
+
npx agent-polygraph guard --uninstall [--global]
|
|
51
|
+
--agent claude|codex only that agent's config
|
|
52
|
+
|
|
53
|
+
Record (more evidence than the logs carry; stays on this machine):
|
|
54
|
+
|
|
55
|
+
npx agent-polygraph record --install [--global] hook that snapshots the working tree after test
|
|
56
|
+
and file-writing commands (.claude/, .codex/)
|
|
57
|
+
npx agent-polygraph record --install --shell log test commands you run in your own terminals
|
|
58
|
+
(bash, zsh; time, folder, exit code; no output)
|
|
59
|
+
npx agent-polygraph record --uninstall [--global] [--shell]
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
async function record(argv) {
|
|
63
|
+
const { values } = parseArgs({
|
|
64
|
+
args: argv,
|
|
65
|
+
options: {
|
|
66
|
+
install: { type: "boolean" },
|
|
67
|
+
uninstall: { type: "boolean" },
|
|
68
|
+
global: { type: "boolean" },
|
|
69
|
+
shell: { type: "boolean" },
|
|
70
|
+
"shell-event": { type: "boolean" },
|
|
71
|
+
agent: { type: "string", multiple: true },
|
|
72
|
+
command: { type: "string" },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const rec = await import("../src/record.js");
|
|
76
|
+
if (values["shell-event"]) {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
for await (const piece of process.stdin) chunks.push(piece);
|
|
79
|
+
rec.runShellRecord(Buffer.concat(chunks).toString("utf8"));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!values.install && !values.uninstall) {
|
|
83
|
+
// invoked by the agent as a PostToolUse hook: quiet, and never in the way
|
|
84
|
+
const chunks = [];
|
|
85
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
86
|
+
rec.runRecordHook(Buffer.concat(chunks).toString("utf8"));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { install, uninstall } = await import("../src/guard.js");
|
|
90
|
+
const opts = { sub: "record", agents: values.agent?.length ? values.agent : ["claude", "codex"], global: values.global, command: values.command };
|
|
91
|
+
// --shell alone touches only the shell setup
|
|
92
|
+
const hooks = values.shell ? [] : values.install ? install(opts) : uninstall(opts);
|
|
93
|
+
for (const r of hooks) {
|
|
94
|
+
const verb = values.install ? (r.changed ? "added PostToolUse hook to" : "already installed in") : r.changed ? "removed PostToolUse hook from" : "nothing to remove in";
|
|
95
|
+
console.log(`${r.agent === "claude" ? "Claude Code" : "Codex"}: ${verb} ${r.file}`);
|
|
96
|
+
if (r.backup) console.log(` Recovery backup: ${r.backup}`);
|
|
97
|
+
}
|
|
98
|
+
if (values.shell) {
|
|
99
|
+
const out = values.install ? rec.installShell() : rec.uninstallShell();
|
|
100
|
+
for (const r of out.rcs) {
|
|
101
|
+
console.log(`Shell: ${values.install ? (r.changed ? "added a line to" : "already in") : "removed the line from"} ${r.file}`);
|
|
102
|
+
if (r.backup) console.log(` Recovery backup: ${r.backup}`);
|
|
103
|
+
}
|
|
104
|
+
if (values.install) console.log(`Shell: ${out.rcs.length ? "open a new terminal to start logging" : "no ~/.bashrc or ~/.zshrc found; source " + out.script + " from your shell's startup file"}. Only interactive bash and zsh are supported.`);
|
|
105
|
+
}
|
|
106
|
+
if (values.install) {
|
|
107
|
+
console.log(`\nRecords are kept in ${rec.home()} and never leave this machine.`);
|
|
108
|
+
if (hooks.length) {
|
|
109
|
+
console.log(`Hook command: ${hooks[0].command}`);
|
|
110
|
+
console.log("Snapshots start with the next session; earlier sessions are read as before.");
|
|
111
|
+
if (hooks.some((r) => r.agent === "codex")) console.log("Codex: run `codex` once and approve the new hook when it says it needs review; until then Codex skips it.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function guard(argv) {
|
|
117
|
+
const { values } = parseArgs({
|
|
118
|
+
args: argv,
|
|
119
|
+
options: {
|
|
120
|
+
install: { type: "boolean" },
|
|
121
|
+
uninstall: { type: "boolean" },
|
|
122
|
+
global: { type: "boolean" },
|
|
123
|
+
agent: { type: "string", multiple: true },
|
|
124
|
+
command: { type: "string" },
|
|
125
|
+
"from-plugin": { type: "boolean" },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const { runHook, install, uninstall } = await import("../src/guard.js");
|
|
129
|
+
if (!values.install && !values.uninstall) {
|
|
130
|
+
// invoked by the agent as a Stop hook; never fail the agent's turn
|
|
131
|
+
try {
|
|
132
|
+
const chunks = [];
|
|
133
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
134
|
+
const out = await runHook(Buffer.concat(chunks).toString("utf8"), { plugin: values["from-plugin"] });
|
|
135
|
+
if (out) process.stdout.write(JSON.stringify(out));
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (process.env.POLYGRAPH_DEBUG) console.error(e);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const opts = { agents: values.agent?.length ? values.agent : ["claude", "codex"], global: values.global, command: values.command };
|
|
142
|
+
const results = values.install ? install(opts) : uninstall(opts);
|
|
143
|
+
for (const r of results) {
|
|
144
|
+
const verb = values.install ? (r.changed ? "added Stop hook to" : "already installed in") : r.changed ? "removed Stop hook from" : "nothing to remove in";
|
|
145
|
+
console.log(`${r.agent === "claude" ? "Claude Code" : "Codex"}: ${verb} ${r.file}`);
|
|
146
|
+
if (r.backup) console.log(` Recovery backup: ${r.backup}`);
|
|
147
|
+
}
|
|
148
|
+
if (values.install) {
|
|
149
|
+
console.log(`\nHook command: ${results[0].command}`);
|
|
150
|
+
console.log("When a final message says tests pass without a passing run after the last edit, the agent is asked once to rerun them.");
|
|
151
|
+
if (results.some((r) => r.agent === "codex")) {
|
|
152
|
+
console.log(
|
|
153
|
+
values.global
|
|
154
|
+
? "Codex: run `codex` once and approve the new hook when it says it needs review; until then Codex skips it."
|
|
155
|
+
: "Codex: run `codex` once in this project, trust the folder, and approve the new hook when it says it needs review; until then Codex skips it.",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let args;
|
|
162
|
+
try {
|
|
163
|
+
args = parseArgs({
|
|
164
|
+
allowPositionals: false,
|
|
165
|
+
options: {
|
|
166
|
+
since: { type: "string" },
|
|
167
|
+
project: { type: "string" },
|
|
168
|
+
exclude: { type: "string", multiple: true },
|
|
169
|
+
agent: { type: "string", multiple: true },
|
|
170
|
+
by: { type: "string", default: "agent" },
|
|
171
|
+
examples: { type: "string", default: "3" },
|
|
172
|
+
html: { type: "string" },
|
|
173
|
+
json: { type: "boolean" },
|
|
174
|
+
demo: { type: "boolean" },
|
|
175
|
+
ci: { type: "boolean" },
|
|
176
|
+
"claude-dir": { type: "string" },
|
|
177
|
+
"codex-dir": { type: "string" },
|
|
178
|
+
help: { type: "boolean", short: "h" },
|
|
179
|
+
version: { type: "boolean", short: "v" },
|
|
180
|
+
},
|
|
181
|
+
// `--html` with no value
|
|
182
|
+
strict: true,
|
|
183
|
+
tokens: false,
|
|
184
|
+
args: process.argv.slice(2).flatMap((a, i, arr) => (a === "--html" && (!arr[i + 1] || arr[i + 1].startsWith("-")) ? ["--html", "polygraph.html"] : [a])),
|
|
185
|
+
}).values;
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error(`${e.message}\n\n${HELP}`);
|
|
188
|
+
process.exit(2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (args.help) {
|
|
192
|
+
process.stdout.write(HELP);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
if (args.version) {
|
|
196
|
+
console.log(version);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const agents = args.agent?.map((a) => a.toLowerCase());
|
|
201
|
+
if (agents?.some((a) => !["claude", "codex"].includes(a))) {
|
|
202
|
+
console.error("--agent must be claude or codex");
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
if (!["agent", "model"].includes(args.by)) {
|
|
206
|
+
console.error('--by must be "agent" or "model"');
|
|
207
|
+
process.exit(2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const since = parseSince(args.since);
|
|
211
|
+
if (args.since && !since) {
|
|
212
|
+
console.error(`Can't read --since ${args.since}; use a date like 2026-10-01 or an age like 7d`);
|
|
213
|
+
process.exit(2);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const started = Date.now();
|
|
217
|
+
const progress = process.stderr.isTTY && !args.json;
|
|
218
|
+
let roots = { claude: args["claude-dir"], codex: args["codex-dir"] };
|
|
219
|
+
let demoDir;
|
|
220
|
+
if (args.demo) {
|
|
221
|
+
const { makeDemo } = await import("../src/demo.js");
|
|
222
|
+
const d = makeDemo();
|
|
223
|
+
demoDir = d.root;
|
|
224
|
+
roots = { claude: d.claudeDir, codex: d.codexDir };
|
|
225
|
+
}
|
|
226
|
+
let ci = null;
|
|
227
|
+
if (args.ci && !args.demo) {
|
|
228
|
+
const { checkGh, makeCi } = await import("../src/ci.js");
|
|
229
|
+
try {
|
|
230
|
+
await checkGh();
|
|
231
|
+
ci = makeCi();
|
|
232
|
+
console.error("--ci: asking GitHub (api.github.com, through your gh login) for workflow runs of the repositories your sessions pushed to. Only repository names, branches and dates are sent.");
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.error(e.message);
|
|
235
|
+
process.exit(2);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const sessions = await scan({
|
|
239
|
+
agents,
|
|
240
|
+
since,
|
|
241
|
+
project: args.project,
|
|
242
|
+
exclude: args.exclude,
|
|
243
|
+
roots,
|
|
244
|
+
records: !args.demo,
|
|
245
|
+
enrich: ci ? ci.enrich : undefined,
|
|
246
|
+
onProgress: progress ? (done, total) => process.stderr.write(`\rReading sessions… ${done}/${total}`) : undefined,
|
|
247
|
+
});
|
|
248
|
+
if (progress) process.stderr.write("\r\x1b[K");
|
|
249
|
+
const groups = aggregate(sessions);
|
|
250
|
+
const seconds = (Date.now() - started) / 1000;
|
|
251
|
+
if (demoDir) fs.rmSync(demoDir, { recursive: true, force: true });
|
|
252
|
+
|
|
253
|
+
if (args.json) {
|
|
254
|
+
process.stdout.write(JSON.stringify({ version, groups, sessions, skipped: sessions.skipped, unrecognized: sessions.unrecognized }, null, 2) + "\n");
|
|
255
|
+
} else {
|
|
256
|
+
console.log(renderTerminal({ sessions, groups, seconds, by: args.by, examples: Math.max(0, Number(args.examples) || 0), demo: args.demo }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (args.html) {
|
|
260
|
+
const file = path.resolve(args.html);
|
|
261
|
+
fs.writeFileSync(file, renderHtml({ groups, by: args.by, version, demo: args.demo, skipped: sessions.skipped?.length || 0, unrecognized: sessions.unrecognized?.length || 0 }));
|
|
262
|
+
console.error(`Wrote ${file}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseSince(s) {
|
|
266
|
+
if (!s) return null;
|
|
267
|
+
const rel = /^(\d+)\s*([dwmy])$/i.exec(s.trim());
|
|
268
|
+
if (rel) {
|
|
269
|
+
const days = { d: 1, w: 7, m: 30, y: 365 }[rel[2].toLowerCase()] * Number(rel[1]);
|
|
270
|
+
return Date.now() - days * 86400000;
|
|
271
|
+
}
|
|
272
|
+
const t = Date.parse(s);
|
|
273
|
+
return Number.isNaN(t) ? null : t;
|
|
274
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-polygraph",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Check your coding agents' \"tests pass\" claims against what actually ran. Reads Claude Code and Codex session logs locally.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-polygraph": "bin/polygraph.js",
|
|
8
|
+
"polygraph": "bin/polygraph.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"start": "node bin/polygraph.js",
|
|
19
|
+
"benchmark:demo": "node scripts/benchmark.mjs --demo",
|
|
20
|
+
"benchmark:replay": "node scripts/benchmark-guard-replay.mjs",
|
|
21
|
+
"benchmark:live": "node scripts/benchmark-live.mjs"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.3"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"claude-code",
|
|
28
|
+
"codex",
|
|
29
|
+
"coding-agents",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"testing",
|
|
32
|
+
"audit",
|
|
33
|
+
"cli"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/EleChon-pixel/agent-polygraph.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/EleChon-pixel/agent-polygraph#readme",
|
|
41
|
+
"bugs": "https://github.com/EleChon-pixel/agent-polygraph/issues",
|
|
42
|
+
"author": "Eleftherios Chondros"
|
|
43
|
+
}
|