devin-qa-pack 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {devin_qa_pack-0.2.0/src/devin_qa_pack.egg-info → devin_qa_pack-0.3.0}/PKG-INFO +93 -14
  2. devin_qa_pack-0.2.0/PKG-INFO → devin_qa_pack-0.3.0/README.md +90 -27
  3. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/pyproject.toml +3 -2
  4. devin_qa_pack-0.3.0/src/devin_qa_pack/__init__.py +1 -0
  5. devin_qa_pack-0.3.0/src/devin_qa_pack/adapters/aider.py +138 -0
  6. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/adapters/claude_code.py +22 -1
  7. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/adapters/mcp.py +23 -1
  8. devin_qa_pack-0.3.0/src/devin_qa_pack/claims.py +340 -0
  9. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/cli.py +111 -2
  10. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/html_report.py +16 -6
  11. devin_qa_pack-0.3.0/src/devin_qa_pack/install.py +84 -0
  12. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/mcp_server.py +4 -1
  13. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/report.py +24 -7
  14. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/session_end.py +69 -7
  15. devin_qa_pack-0.3.0/src/devin_qa_pack/sweep.py +93 -0
  16. devin_qa_pack-0.3.0/src/devin_qa_pack/verify.py +877 -0
  17. devin_qa_pack-0.2.0/README.md → devin_qa_pack-0.3.0/src/devin_qa_pack.egg-info/PKG-INFO +106 -12
  18. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack.egg-info/SOURCES.txt +6 -0
  19. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack.egg-info/requires.txt +1 -0
  20. devin_qa_pack-0.3.0/tests/test_action_contract.py +242 -0
  21. devin_qa_pack-0.3.0/tests/test_adapters.py +670 -0
  22. devin_qa_pack-0.3.0/tests/test_auto_approve_workflow.py +39 -0
  23. devin_qa_pack-0.3.0/tests/test_claims.py +350 -0
  24. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_cli.py +15 -2
  25. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_html_report.py +14 -0
  26. devin_qa_pack-0.3.0/tests/test_install.py +338 -0
  27. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_mcp.py +10 -0
  28. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_offline_core.py +1 -1
  29. devin_qa_pack-0.3.0/tests/test_release_pypi_workflow.py +49 -0
  30. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_report.py +60 -1
  31. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_session_end.py +118 -0
  32. devin_qa_pack-0.3.0/tests/test_verify.py +724 -0
  33. devin_qa_pack-0.2.0/src/devin_qa_pack/__init__.py +0 -1
  34. devin_qa_pack-0.2.0/src/devin_qa_pack/adapters/aider.py +0 -91
  35. devin_qa_pack-0.2.0/src/devin_qa_pack/claims.py +0 -204
  36. devin_qa_pack-0.2.0/src/devin_qa_pack/verify.py +0 -459
  37. devin_qa_pack-0.2.0/tests/test_adapters.py +0 -315
  38. devin_qa_pack-0.2.0/tests/test_claims.py +0 -131
  39. devin_qa_pack-0.2.0/tests/test_verify.py +0 -291
  40. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/setup.cfg +0 -0
  41. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/adapters/__init__.py +0 -0
  42. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/adapters/base.py +0 -0
  43. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/adapters/devin.py +0 -0
  44. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/evidence/__init__.py +0 -0
  45. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/evidence/codex_cli.py +0 -0
  46. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/evidence/copilot_cli.py +0 -0
  47. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/evidence/devin_cli.py +0 -0
  48. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/evidence/vscode_chat.py +0 -0
  49. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/intent.py +0 -0
  50. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack/paths.py +0 -0
  51. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack.egg-info/dependency_links.txt +0 -0
  52. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack.egg-info/entry_points.txt +0 -0
  53. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/src/devin_qa_pack.egg-info/top_level.txt +0 -0
  54. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_evidence.py +0 -0
  55. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_evidence_corpus.py +0 -0
  56. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_intent.py +0 -0
  57. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_paths.py +0 -0
  58. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_precommit_hook.py +0 -0
  59. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_release_workflow.py +0 -0
  60. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_skill.py +0 -0
  61. {devin_qa_pack-0.2.0 → devin_qa_pack-0.3.0}/tests/test_smoke.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devin-qa-pack
3
- Version: 0.2.0
4
- Summary: QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth.
3
+ Version: 0.3.0
4
+ Summary: QA audit for agent sessions: verifies deliverable claims (tests, commits, files, pushes) against recorded tool-call ground truth across Devin CLI, Copilot CLI, Codex CLI and VS Code chat stores.
5
5
  Author: Icaro0310
6
6
  License: MIT
7
7
  Requires-Python: >=3.10
8
8
  Description-Content-Type: text/markdown
9
9
  Requires-Dist: devin-internals-spec<0.4.0,>=0.3.0
10
+ Requires-Dist: devin-install-scheduler>=0.1.0
10
11
  Provides-Extra: dev
11
12
  Requires-Dist: pytest>=8; extra == "dev"
12
13
  Requires-Dist: pyyaml>=6; extra == "dev"
@@ -53,7 +54,8 @@ Part of the [awesome-devin](https://github.com/Icaro0310/awesome-devin) ecosyste
53
54
 
54
55
  Flagship QA audit for Devin sessions: checks that what a session *claims* it
55
56
  delivered is backed by what its tool calls *actually did* — tests run,
56
- commits created, files written, work pushed, HTTP statuses returned —
57
+ commits created, files written, work pushed, HTTP statuses returned,
58
+ eval scores and coverage numbers reported, work delegated to subagents —
57
59
  and prints a verdict per session: `PASS` / `PARTIAL` / `UNVERIFIED`.
58
60
 
59
61
  ## The problem
@@ -81,8 +83,11 @@ Devin persists every tool call in `sessions.db → tool_call_state`. This
81
83
  tool reads that table via
82
84
  [devin-internals-spec](https://github.com/Icaro0310/devin-internals-spec)
83
85
  and treats it as **ground truth**: a "tests passed" claim must be backed
84
- by a run/execute call that ran a test runner and completed; "committed
85
- `sha`" must show the hash in a call or in `git log`. Text-only auditors
86
+ by a run/execute call that ran a test runner and completed; "47 tests
87
+ passed" must match a recorded `47 passed` in the tool output; "committed
88
+ `sha`" must show the hash in a call or in `git log`; "coverage is at
89
+ 87%" or "the eval score was 0.92" must match a recorded percent/score
90
+ in tool-call output. Text-only auditors
86
91
  *cannot* do this — remove Devin's store and the check disappears.
87
92
 
88
93
  ## Install
@@ -109,14 +114,25 @@ curl -fsSL https://raw.githubusercontent.com/Icaro0310/devin-assure/main/install
109
114
  ship the wheel, sdist and CycloneDX SBOM per tag.
110
115
 
111
116
  **See it in action** — run the reproducible
112
- [Agent Assurance demo](examples/agent-assurance): one command generates a
117
+ [Agent Assurance demo](https://github.com/Icaro0310/devin-assure/tree/main/packages/qa-pack/examples/agent-assurance): one command generates a
113
118
  deliberately flawed agent session, then independently verifies what the
114
119
  agent actually did. No Devin install needed.
115
120
 
116
- <a href="examples/agent-assurance"><img src="examples/agent-assurance/demo.gif" alt="agent-assurance demo — UNVERIFIED, PARTIAL, PASS" width="80%"/></a>
121
+ <div align="center">
122
+ <a href="https://github.com/Icaro0310/devin-assure/tree/main/packages/qa-pack/examples/agent-assurance"><img src="https://raw.githubusercontent.com/Icaro0310/devin-assure/main/packages/qa-pack/examples/agent-assurance/demo.gif" alt="agent-assurance demo — UNVERIFIED, PARTIAL, PASS" width="80%"/></a>
123
+ </div>
117
124
 
118
125
  ## Usage
119
126
 
127
+ Every command reads local Devin data by default — `sessions.db`, the
128
+ `.devin/` config tree, transcript exports — and never writes back to
129
+ those stores. Two opt-ins do reach the network: `audit --source mcp`
130
+ fetches the session's cloud events, and `--online` probes live URLs.
131
+ Point `--sessions-db` at a copied database to keep reads fully off the
132
+ original store — `session-end` still writes its verdict under the Devin
133
+ data dir (`<data-dir>/qa/`; redirect with `--data-dir` or `--out`).
134
+ Reports are not auto-redacted: excerpt the verdict block before sharing.
135
+
120
136
  ```bash
121
137
  # audit one session (exact id or unique prefix)
122
138
  devin-qa-pack audit --session <id> --sessions-db path/to/sessions.db
@@ -132,6 +148,10 @@ devin-qa-pack report --sessions-db path/to/sessions.db --out report.html
132
148
  # (hook mode — writes a JSON side file, never touches the session)
133
149
  devin-qa-pack session-end
134
150
 
151
+ # schedule a daily audit sweep over every detected store
152
+ # (cron / Task Scheduler / elapsed registry — opt-in)
153
+ devin-qa-pack install --daily
154
+
135
155
  # intent vs. coverage (QA-4): did the agent touch what the prompt named?
136
156
  devin-qa-pack intent <session> --sessions-db path/to/sessions.db
137
157
 
@@ -148,9 +168,9 @@ devin-qa-pack audit --source mcp --session <cloud-session-id>
148
168
  Real output, on the synthetic fixture from the quick start:
149
169
 
150
170
  ```text
151
- UNVERIFIED sess-unverifiable Mystery session — 1 claim(s): 0 verified, 0 disputed, 1 unverifiable
171
+ UNVERIFIED sess-unverifiable Mystery session — 1 claim(s): 0 verified, 0 disputed, 1 unverifiable — uncovered: file/docs/spec.pdf
152
172
  [unverifiable] file/docs/spec.pdf: no matching tool call and working dir not on disk
153
- PARTIAL sess-disputed Disputed session — 2 claim(s): 0 verified, 2 disputed, 0 unverifiable
173
+ PARTIAL sess-disputed Disputed session — 2 claim(s): 0 verified, 2 disputed, 0 unverifiable — uncovered: tests/tests, commit/deadbee
154
174
  [disputed ] tests/tests: no execute call matching `tests` recorded
155
175
  [disputed ] commit/deadbee: no commit call recorded and no repo on disk to check
156
176
  PASS sess-verified Verified session — 4 claim(s): 4 verified, 0 disputed, 0 unverifiable
@@ -166,7 +186,10 @@ Recorded as an asciicast: [assets/demo.cast](assets/demo.cast) —
166
186
  The `report` subcommand audits all sessions (or one with `--session`,
167
187
  bounded with `--limit`) and writes a single deterministic HTML file:
168
188
  verdict counts, a per-session table and a per-claim breakdown with
169
- evidence and the source excerpt for every checked claim.
189
+ evidence and the source excerpt for every checked claim. Verdicts and
190
+ claim statuses render as labeled badges (color + text, never color
191
+ alone) — the report stays self-contained: inline CSS, zero JS, no
192
+ external assets.
170
193
 
171
194
  `--sessions-db` may be omitted. It auto-detects
172
195
  `%APPDATA%/devin/cli/sessions.db` on Windows and
@@ -257,6 +280,14 @@ when the session resolves. A one-line
257
280
  summary is also printed. `--limit N` bounds the number of claims
258
281
  verified.
259
282
 
283
+ Each run also appends one compact JSON line to `verdicts.jsonl` next to
284
+ the side file (`<data-dir>/qa/verdicts.jsonl` by default) — a
285
+ machine-readable ledger with `session_id`, `verdict`, `audited_at`,
286
+ claim counts, the `uncovered` claim names (disputed/unverifiable
287
+ `kind/detail` list, omitted when empty) and the skip `reason`, for
288
+ dashboards and triggers. The append is best-effort; the side file
289
+ remains the contract.
290
+
260
291
  **Fail-soft:** `session-end` always exits `0` once it ran — the verdict
261
292
  travels in the side file, so a hook can never fail the host session. A
262
293
  session that cannot be resolved produces a `SKIPPED` verdict (still
@@ -284,6 +315,41 @@ stdin and exports `DEVIN_SESSION_ID`):
284
315
  }
285
316
  ```
286
317
 
318
+ ## Scheduled audit sweep (`install --daily`)
319
+
320
+ The `SessionEnd` hook audits live; `devin-qa-pack install --daily` adds
321
+ the complementary **scheduled sweep** — a daily `devin-qa-pack audit
322
+ --all` over every detected surface (`--surface` narrows it) for
323
+ sessions the hook missed (Devin closed early, hook not wired, foreign
324
+ stores). It writes the full payload to
325
+ `<data-dir>/qa/audit-sweep.json` and appends one compact verdict line
326
+ per session (`origin: "sweep"`) to the same
327
+ `<data-dir>/qa/verdicts.jsonl` ledger `session-end` feeds. Read-only on
328
+ every store: the only writes are under the data dir's `qa/` folder, and
329
+ the job does not need Devin closed.
330
+
331
+ ```bash
332
+ devin-qa-pack install --daily # auto-detect backend
333
+ devin-qa-pack install --daily --backend cron # pin one backend
334
+ devin-qa-pack install --daily --data-dir /custom/devin --surface devin-cli
335
+ ```
336
+
337
+ Backend selection follows the shared `devin-install-scheduler`
338
+ convention (same as devin-state's `devin-janitor`/`devin-backup`): a
339
+ tagged `@daily` **crontab** line on Linux/macOS, a daily **Task
340
+ Scheduler** entry on Windows, or — on locked-down machines without
341
+ either — a job record in `<config-dir>/.devin-ecosystem/scheduled.json`
342
+ (the `elapsed` backend) that devin-powerups' `UserPromptSubmit` hook
343
+ ticks (`python tools/schedule.py check --run`). `--backend` pins a
344
+ backend, `--config-dir` relocates the registry, `--data-dir` reshapes
345
+ where the registered command writes, `--json` prints the job record.
346
+ Both `--sessions-db` and `--out` in the registered command are baked to
347
+ absolute paths at install time, so the job does not depend on
348
+ `DEVIN_DATA_DIR`/XDG env surviving into the scheduler's minimal
349
+ environment (re-run `install` if the data dir ever moves).
350
+ The sweep exits `1` when any session is `PARTIAL`/`UNVERIFIED` — the
351
+ verdicts land in the artifacts, the exit code is informational.
352
+
287
353
  ## Pre-commit gate
288
354
 
289
355
  The repo root ships `.pre-commit-hooks.yaml` — reference it from a
@@ -313,10 +379,15 @@ in the entry and the scope flags are mutually exclusive — run
313
379
  `audit --transcript` runs the same claim/evidence model over non-Devin
314
380
  transcripts via the adapters in `src/devin_qa_pack/adapters/`:
315
381
 
316
- - **`aider`** — `.aider.chat.history.md`. `> /run` and `> /test` records
317
- become execute calls with `unknown` status (the history does not store
318
- exit codes), so test claims resolve `unverifiable`; commit claims are
319
- checked against `git log` in the working directory.
382
+ - **`aider`** — `.aider.chat.history.md`. `#### /run`, `#### /test`,
383
+ `#### /git` and `#### !cmd` input lines become execute calls with
384
+ `unknown` status (the history does not store exit codes), so action
385
+ claims like "tests passed" resolve `unverifiable`; the `>`-blockquoted
386
+ tool output (`Applied edit to …`, `Commit <sha> …`, recorded test
387
+ output) is pooled as evidence, so value claims — pass counts, HTTP
388
+ statuses, coverage, eval scores — and file/commit details still
389
+ corroborate. Commit claims are also checked against `git log` in the
390
+ working directory.
320
391
  - **`claude-code`** — session `.jsonl` transcripts. `Bash` → execute,
321
392
  `Write`/`Edit`/`MultiEdit`/`NotebookEdit` → write, `tool_result`
322
393
  blocks mark calls completed/failed.
@@ -384,6 +455,14 @@ network is off by default and the audit is fully offline otherwise.
384
455
  output: a claim is `verified` when a recorded status matches,
385
456
  `disputed` when outputs record a different status and `unverifiable`
386
457
  when no output records any status. No request is ever replayed.
458
+ - Counted test claims work the same way: "47 tests passed" is `verified`
459
+ when recorded output reports `47 passed`, `disputed` when it reports
460
+ a different count and `unverifiable` when no output records a count.
461
+ So do coverage and eval-score claims: "coverage is at 87%" needs a
462
+ recorded 87% (coverage.py's `TOTAL` row, a `coverage`/`line-rate`
463
+ payload field, or a "coverage …87%" output string); "the eval score
464
+ was 0.92" needs a recorded `0.92`. Values compare on the scale
465
+ written — an `85%` claim matches a recorded `85`, never `0.85`.
387
466
  - File/commit checks use `sessions.working_directory` only when it
388
467
  exists on disk and is a git repo — otherwise they rely on tool-call
389
468
  evidence alone.
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: devin-qa-pack
3
- Version: 0.2.0
4
- Summary: QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth.
5
- Author: Icaro0310
6
- License: MIT
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: devin-internals-spec<0.4.0,>=0.3.0
10
- Provides-Extra: dev
11
- Requires-Dist: pytest>=8; extra == "dev"
12
- Requires-Dist: pyyaml>=6; extra == "dev"
13
- Provides-Extra: mcp
14
- Requires-Dist: mcp>=1.2; extra == "mcp"
15
-
16
1
  <div align="center">
17
2
 
18
3
  <img src="assets/banner.svg" alt="devin-qa-pack" width="100%"/>
@@ -53,7 +38,8 @@ Part of the [awesome-devin](https://github.com/Icaro0310/awesome-devin) ecosyste
53
38
 
54
39
  Flagship QA audit for Devin sessions: checks that what a session *claims* it
55
40
  delivered is backed by what its tool calls *actually did* — tests run,
56
- commits created, files written, work pushed, HTTP statuses returned —
41
+ commits created, files written, work pushed, HTTP statuses returned,
42
+ eval scores and coverage numbers reported, work delegated to subagents —
57
43
  and prints a verdict per session: `PASS` / `PARTIAL` / `UNVERIFIED`.
58
44
 
59
45
  ## The problem
@@ -81,8 +67,11 @@ Devin persists every tool call in `sessions.db → tool_call_state`. This
81
67
  tool reads that table via
82
68
  [devin-internals-spec](https://github.com/Icaro0310/devin-internals-spec)
83
69
  and treats it as **ground truth**: a "tests passed" claim must be backed
84
- by a run/execute call that ran a test runner and completed; "committed
85
- `sha`" must show the hash in a call or in `git log`. Text-only auditors
70
+ by a run/execute call that ran a test runner and completed; "47 tests
71
+ passed" must match a recorded `47 passed` in the tool output; "committed
72
+ `sha`" must show the hash in a call or in `git log`; "coverage is at
73
+ 87%" or "the eval score was 0.92" must match a recorded percent/score
74
+ in tool-call output. Text-only auditors
86
75
  *cannot* do this — remove Devin's store and the check disappears.
87
76
 
88
77
  ## Install
@@ -109,14 +98,25 @@ curl -fsSL https://raw.githubusercontent.com/Icaro0310/devin-assure/main/install
109
98
  ship the wheel, sdist and CycloneDX SBOM per tag.
110
99
 
111
100
  **See it in action** — run the reproducible
112
- [Agent Assurance demo](examples/agent-assurance): one command generates a
101
+ [Agent Assurance demo](https://github.com/Icaro0310/devin-assure/tree/main/packages/qa-pack/examples/agent-assurance): one command generates a
113
102
  deliberately flawed agent session, then independently verifies what the
114
103
  agent actually did. No Devin install needed.
115
104
 
116
- <a href="examples/agent-assurance"><img src="examples/agent-assurance/demo.gif" alt="agent-assurance demo — UNVERIFIED, PARTIAL, PASS" width="80%"/></a>
105
+ <div align="center">
106
+ <a href="https://github.com/Icaro0310/devin-assure/tree/main/packages/qa-pack/examples/agent-assurance"><img src="https://raw.githubusercontent.com/Icaro0310/devin-assure/main/packages/qa-pack/examples/agent-assurance/demo.gif" alt="agent-assurance demo — UNVERIFIED, PARTIAL, PASS" width="80%"/></a>
107
+ </div>
117
108
 
118
109
  ## Usage
119
110
 
111
+ Every command reads local Devin data by default — `sessions.db`, the
112
+ `.devin/` config tree, transcript exports — and never writes back to
113
+ those stores. Two opt-ins do reach the network: `audit --source mcp`
114
+ fetches the session's cloud events, and `--online` probes live URLs.
115
+ Point `--sessions-db` at a copied database to keep reads fully off the
116
+ original store — `session-end` still writes its verdict under the Devin
117
+ data dir (`<data-dir>/qa/`; redirect with `--data-dir` or `--out`).
118
+ Reports are not auto-redacted: excerpt the verdict block before sharing.
119
+
120
120
  ```bash
121
121
  # audit one session (exact id or unique prefix)
122
122
  devin-qa-pack audit --session <id> --sessions-db path/to/sessions.db
@@ -132,6 +132,10 @@ devin-qa-pack report --sessions-db path/to/sessions.db --out report.html
132
132
  # (hook mode — writes a JSON side file, never touches the session)
133
133
  devin-qa-pack session-end
134
134
 
135
+ # schedule a daily audit sweep over every detected store
136
+ # (cron / Task Scheduler / elapsed registry — opt-in)
137
+ devin-qa-pack install --daily
138
+
135
139
  # intent vs. coverage (QA-4): did the agent touch what the prompt named?
136
140
  devin-qa-pack intent <session> --sessions-db path/to/sessions.db
137
141
 
@@ -148,9 +152,9 @@ devin-qa-pack audit --source mcp --session <cloud-session-id>
148
152
  Real output, on the synthetic fixture from the quick start:
149
153
 
150
154
  ```text
151
- UNVERIFIED sess-unverifiable Mystery session — 1 claim(s): 0 verified, 0 disputed, 1 unverifiable
155
+ UNVERIFIED sess-unverifiable Mystery session — 1 claim(s): 0 verified, 0 disputed, 1 unverifiable — uncovered: file/docs/spec.pdf
152
156
  [unverifiable] file/docs/spec.pdf: no matching tool call and working dir not on disk
153
- PARTIAL sess-disputed Disputed session — 2 claim(s): 0 verified, 2 disputed, 0 unverifiable
157
+ PARTIAL sess-disputed Disputed session — 2 claim(s): 0 verified, 2 disputed, 0 unverifiable — uncovered: tests/tests, commit/deadbee
154
158
  [disputed ] tests/tests: no execute call matching `tests` recorded
155
159
  [disputed ] commit/deadbee: no commit call recorded and no repo on disk to check
156
160
  PASS sess-verified Verified session — 4 claim(s): 4 verified, 0 disputed, 0 unverifiable
@@ -166,7 +170,10 @@ Recorded as an asciicast: [assets/demo.cast](assets/demo.cast) —
166
170
  The `report` subcommand audits all sessions (or one with `--session`,
167
171
  bounded with `--limit`) and writes a single deterministic HTML file:
168
172
  verdict counts, a per-session table and a per-claim breakdown with
169
- evidence and the source excerpt for every checked claim.
173
+ evidence and the source excerpt for every checked claim. Verdicts and
174
+ claim statuses render as labeled badges (color + text, never color
175
+ alone) — the report stays self-contained: inline CSS, zero JS, no
176
+ external assets.
170
177
 
171
178
  `--sessions-db` may be omitted. It auto-detects
172
179
  `%APPDATA%/devin/cli/sessions.db` on Windows and
@@ -257,6 +264,14 @@ when the session resolves. A one-line
257
264
  summary is also printed. `--limit N` bounds the number of claims
258
265
  verified.
259
266
 
267
+ Each run also appends one compact JSON line to `verdicts.jsonl` next to
268
+ the side file (`<data-dir>/qa/verdicts.jsonl` by default) — a
269
+ machine-readable ledger with `session_id`, `verdict`, `audited_at`,
270
+ claim counts, the `uncovered` claim names (disputed/unverifiable
271
+ `kind/detail` list, omitted when empty) and the skip `reason`, for
272
+ dashboards and triggers. The append is best-effort; the side file
273
+ remains the contract.
274
+
260
275
  **Fail-soft:** `session-end` always exits `0` once it ran — the verdict
261
276
  travels in the side file, so a hook can never fail the host session. A
262
277
  session that cannot be resolved produces a `SKIPPED` verdict (still
@@ -284,6 +299,41 @@ stdin and exports `DEVIN_SESSION_ID`):
284
299
  }
285
300
  ```
286
301
 
302
+ ## Scheduled audit sweep (`install --daily`)
303
+
304
+ The `SessionEnd` hook audits live; `devin-qa-pack install --daily` adds
305
+ the complementary **scheduled sweep** — a daily `devin-qa-pack audit
306
+ --all` over every detected surface (`--surface` narrows it) for
307
+ sessions the hook missed (Devin closed early, hook not wired, foreign
308
+ stores). It writes the full payload to
309
+ `<data-dir>/qa/audit-sweep.json` and appends one compact verdict line
310
+ per session (`origin: "sweep"`) to the same
311
+ `<data-dir>/qa/verdicts.jsonl` ledger `session-end` feeds. Read-only on
312
+ every store: the only writes are under the data dir's `qa/` folder, and
313
+ the job does not need Devin closed.
314
+
315
+ ```bash
316
+ devin-qa-pack install --daily # auto-detect backend
317
+ devin-qa-pack install --daily --backend cron # pin one backend
318
+ devin-qa-pack install --daily --data-dir /custom/devin --surface devin-cli
319
+ ```
320
+
321
+ Backend selection follows the shared `devin-install-scheduler`
322
+ convention (same as devin-state's `devin-janitor`/`devin-backup`): a
323
+ tagged `@daily` **crontab** line on Linux/macOS, a daily **Task
324
+ Scheduler** entry on Windows, or — on locked-down machines without
325
+ either — a job record in `<config-dir>/.devin-ecosystem/scheduled.json`
326
+ (the `elapsed` backend) that devin-powerups' `UserPromptSubmit` hook
327
+ ticks (`python tools/schedule.py check --run`). `--backend` pins a
328
+ backend, `--config-dir` relocates the registry, `--data-dir` reshapes
329
+ where the registered command writes, `--json` prints the job record.
330
+ Both `--sessions-db` and `--out` in the registered command are baked to
331
+ absolute paths at install time, so the job does not depend on
332
+ `DEVIN_DATA_DIR`/XDG env surviving into the scheduler's minimal
333
+ environment (re-run `install` if the data dir ever moves).
334
+ The sweep exits `1` when any session is `PARTIAL`/`UNVERIFIED` — the
335
+ verdicts land in the artifacts, the exit code is informational.
336
+
287
337
  ## Pre-commit gate
288
338
 
289
339
  The repo root ships `.pre-commit-hooks.yaml` — reference it from a
@@ -313,10 +363,15 @@ in the entry and the scope flags are mutually exclusive — run
313
363
  `audit --transcript` runs the same claim/evidence model over non-Devin
314
364
  transcripts via the adapters in `src/devin_qa_pack/adapters/`:
315
365
 
316
- - **`aider`** — `.aider.chat.history.md`. `> /run` and `> /test` records
317
- become execute calls with `unknown` status (the history does not store
318
- exit codes), so test claims resolve `unverifiable`; commit claims are
319
- checked against `git log` in the working directory.
366
+ - **`aider`** — `.aider.chat.history.md`. `#### /run`, `#### /test`,
367
+ `#### /git` and `#### !cmd` input lines become execute calls with
368
+ `unknown` status (the history does not store exit codes), so action
369
+ claims like "tests passed" resolve `unverifiable`; the `>`-blockquoted
370
+ tool output (`Applied edit to …`, `Commit <sha> …`, recorded test
371
+ output) is pooled as evidence, so value claims — pass counts, HTTP
372
+ statuses, coverage, eval scores — and file/commit details still
373
+ corroborate. Commit claims are also checked against `git log` in the
374
+ working directory.
320
375
  - **`claude-code`** — session `.jsonl` transcripts. `Bash` → execute,
321
376
  `Write`/`Edit`/`MultiEdit`/`NotebookEdit` → write, `tool_result`
322
377
  blocks mark calls completed/failed.
@@ -384,6 +439,14 @@ network is off by default and the audit is fully offline otherwise.
384
439
  output: a claim is `verified` when a recorded status matches,
385
440
  `disputed` when outputs record a different status and `unverifiable`
386
441
  when no output records any status. No request is ever replayed.
442
+ - Counted test claims work the same way: "47 tests passed" is `verified`
443
+ when recorded output reports `47 passed`, `disputed` when it reports
444
+ a different count and `unverifiable` when no output records a count.
445
+ So do coverage and eval-score claims: "coverage is at 87%" needs a
446
+ recorded 87% (coverage.py's `TOTAL` row, a `coverage`/`line-rate`
447
+ payload field, or a "coverage …87%" output string); "the eval score
448
+ was 0.92" needs a recorded `0.92`. Values compare on the scale
449
+ written — an `85%` claim matches a recorded `85`, never `0.85`.
387
450
  - File/commit checks use `sessions.working_directory` only when it
388
451
  exists on disk and is a git repo — otherwise they rely on tool-call
389
452
  evidence alone.
@@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devin-qa-pack"
7
- version = "0.2.0"
8
- description = "QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth."
7
+ version = "0.3.0"
8
+ description = "QA audit for agent sessions: verifies deliverable claims (tests, commits, files, pushes) against recorded tool-call ground truth across Devin CLI, Copilot CLI, Codex CLI and VS Code chat stores."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
12
12
  authors = [{ name = "Icaro0310" }]
13
13
  dependencies = [
14
14
  "devin-internals-spec>=0.3.0,<0.4.0",
15
+ "devin-install-scheduler>=0.1.0",
15
16
  ]
16
17
 
17
18
  [project.optional-dependencies]
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,138 @@
1
+ """Experimental Aider adapter — audits ``.aider.chat.history.md``.
2
+
3
+ Aider records its chat history as markdown (``io.append_chat_history``):
4
+ ``####`` lines are user input — natural-language prompts *and* slash
5
+ commands (``#### /run pytest -q``, ``#### !ls``, ``#### /git status``);
6
+ ``>`` lines are blockquoted tool/system output (``> Applied edit to
7
+ x.py``, ``> Commit abc1234 ...``, errors, confirmations); everything
8
+ else is assistant prose (the claim source).
9
+
10
+ Evidence is deliberately weak where the format is weak: a recorded
11
+ ``/run``/``/test``/``!``/``/git`` line becomes an execute call with
12
+ ``unknown`` status — the history does not persist exit codes — so
13
+ action claims like "tests passed" resolve ``unverifiable``, never a
14
+ fake ``verified``. The ``>`` output lines still corroborate
15
+ value-style claims: a recorded "47 passed" backs a ``tests_passed``
16
+ claim, a ``> Commit a1b2c3d`` line backs a ``commit`` claim even
17
+ without the repo on disk (``git log`` in the working directory stays
18
+ the stronger check). Commit claims also verify through the repo's own
19
+ ``git log`` whenever the working directory is on disk, since aider
20
+ auto-commits.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from pathlib import Path
26
+
27
+ from devin_qa_pack.adapters.base import SourceSession
28
+ from devin_qa_pack.claims import claims_from_pairs
29
+ from devin_qa_pack.verify import (
30
+ ParsedToolCall,
31
+ _coverage_values,
32
+ _eval_scores,
33
+ _http_statuses,
34
+ _passed_counts,
35
+ )
36
+
37
+ SOURCE = "aider"
38
+
39
+ # Slash commands that actually run a shell command — ``/run``/``/test``
40
+ # shell out and ``/git X`` runs ``git X``. Everything else (``/add``,
41
+ # ``/model``, ``/commit``…) does not execute anything checkable; its
42
+ # evidence, if any, lands on the ``>`` output lines instead.
43
+ _RUN_SLASH = ("/run", "/test", "/git")
44
+
45
+
46
+ def _command_of(user_input: str) -> str | None:
47
+ """Map a ``####`` input line to the shell command it ran, if any.
48
+
49
+ ``/run pytest``, ``/test`` and the ``!cmd`` shell shortcut are
50
+ commands; ``/git push`` records as ``git push``. A bare ``/test``
51
+ falls back to aider's configured ``test_cmd`` — unknown here — so
52
+ it records no command.
53
+ """
54
+ text = user_input.strip()
55
+ if text.startswith("!"):
56
+ command = text[1:].strip()
57
+ return command or None
58
+ for slash in _RUN_SLASH:
59
+ if text == slash or text.startswith(slash + " "):
60
+ rest = text[len(slash):].strip()
61
+ if not rest:
62
+ return None
63
+ return f"git {rest}" if slash == "/git" else rest
64
+ return None
65
+
66
+
67
+ def _parse(text: str) -> tuple[list[tuple[str, str]], list[ParsedToolCall]]:
68
+ pairs: list[tuple[str, str]] = []
69
+ calls: list[ParsedToolCall] = []
70
+ output: list[str] = [] # ">" blockquote lines — recorded tool output
71
+ for raw in text.splitlines():
72
+ line = raw.rstrip()
73
+ if not line.strip():
74
+ continue
75
+ if line.startswith("####"):
76
+ user_input = line[4:].strip()
77
+ command = _command_of(user_input)
78
+ if command is not None:
79
+ calls.append(
80
+ ParsedToolCall(
81
+ tool_call_id=f"aider-cmd-{len(calls) + 1}",
82
+ kind="execute",
83
+ status="unknown", # history records no exit code
84
+ commands=(command,),
85
+ search_text=command.lower().replace("\\", "/"),
86
+ http_statuses=(),
87
+ passed_counts=(), # no output on the command line
88
+ )
89
+ )
90
+ else:
91
+ pairs.append(("user", user_input))
92
+ continue
93
+ if line.startswith("#"):
94
+ continue # "# aider chat started at …" and section headers
95
+ if line.startswith(">"):
96
+ output.append(line[1:].strip())
97
+ continue
98
+ pairs.append(("assistant", line))
99
+ if output:
100
+ # Aider's tool output never ties back to a specific command
101
+ # (no ids, no exit codes) — pool it into one evidence call so
102
+ # value claims (counts, statuses, percents, scores) and
103
+ # file/commit details check against what was actually recorded.
104
+ blob = "\n".join(output)
105
+ calls.append(
106
+ ParsedToolCall(
107
+ tool_call_id="aider-output",
108
+ kind="other",
109
+ status="completed",
110
+ commands=(),
111
+ search_text=blob.lower().replace("\\", "/"),
112
+ http_statuses=tuple(sorted(set(_http_statuses(blob)))),
113
+ passed_counts=tuple(sorted(set(_passed_counts(blob)))),
114
+ coverage_values=tuple(sorted(set(_coverage_values(blob)))),
115
+ eval_scores=tuple(sorted(set(_eval_scores(blob)))),
116
+ )
117
+ )
118
+ return pairs, calls
119
+
120
+
121
+ def load(path: str | Path, working_directory: str | None = None) -> SourceSession:
122
+ """Load ``.aider.chat.history.md`` into the normalized audit view.
123
+
124
+ ``working_directory`` defaults to the directory containing the
125
+ history file — aider writes it at the repository root.
126
+ """
127
+ p = Path(path).expanduser()
128
+ pairs, calls = _parse(p.read_text(encoding="utf-8", errors="replace"))
129
+ session_id = f"aider:{p.name}"
130
+ return SourceSession(
131
+ source=SOURCE,
132
+ session_id=session_id,
133
+ title=p.name,
134
+ working_directory=working_directory or str(p.parent),
135
+ claims=tuple(claims_from_pairs(pairs, session_id)),
136
+ calls=tuple(calls),
137
+ raw_call_count=len(calls),
138
+ )
@@ -21,7 +21,14 @@ from typing import Any
21
21
 
22
22
  from devin_qa_pack.adapters.base import SourceSession
23
23
  from devin_qa_pack.claims import claims_from_pairs
24
- from devin_qa_pack.verify import ParsedToolCall, _http_statuses, _strings
24
+ from devin_qa_pack.verify import (
25
+ ParsedToolCall,
26
+ _coverage_values,
27
+ _eval_scores,
28
+ _http_statuses,
29
+ _passed_counts,
30
+ _strings,
31
+ )
25
32
 
26
33
  SOURCE = "claude-code"
27
34
 
@@ -63,7 +70,16 @@ def _result_text(content: Any) -> str:
63
70
  return "" if content is None else str(content)
64
71
 
65
72
 
73
+ # Claude Code's subagent-spawning tools — ``Task`` exactly, plus any
74
+ # custom subagent-named tool — so delegation claims verify against the
75
+ # transcript the same way they do against devin-cli evidence.
76
+ _SPAWN_TOOLS = {"task"}
77
+
78
+
66
79
  def _tool_kind(name: str) -> str:
80
+ low = name.lower()
81
+ if low in _SPAWN_TOOLS or "subagent" in low or "sub_agent" in low:
82
+ return "spawn"
67
83
  if name in _EXECUTE_TOOLS:
68
84
  return "execute"
69
85
  if name in _WRITE_TOOLS:
@@ -117,6 +133,11 @@ def load(path: str | Path, working_directory: str | None = None) -> SourceSessio
117
133
  http_statuses=tuple(sorted(
118
134
  {*_http_statuses(inp), *_http_statuses(output)}
119
135
  )),
136
+ # counts come from the tool_result, never the tool_use
137
+ # input (a Write's `content` is the request, not output)
138
+ passed_counts=tuple(sorted(set(_passed_counts(output)))),
139
+ coverage_values=tuple(sorted(set(_coverage_values(output)))),
140
+ eval_scores=tuple(sorted(set(_eval_scores(output)))),
120
141
  )
121
142
  )
122
143
 
@@ -45,7 +45,14 @@ from typing import Any
45
45
 
46
46
  from devin_qa_pack.adapters.base import SourceSession
47
47
  from devin_qa_pack.claims import claims_from_pairs
48
- from devin_qa_pack.verify import ParsedToolCall, _http_statuses, _strings
48
+ from devin_qa_pack.verify import (
49
+ ParsedToolCall,
50
+ _coverage_values,
51
+ _eval_scores,
52
+ _http_statuses,
53
+ _passed_counts,
54
+ _strings,
55
+ )
49
56
 
50
57
  SOURCE = "mcp"
51
58
 
@@ -404,10 +411,19 @@ def load(
404
411
  ]
405
412
  texts: list[str] = list(_strings(started)) + outputs
406
413
  statuses: set[int] = set(_http_statuses(started))
414
+ joined_output = "".join(outputs)
415
+ # Terminal chunks are one contiguous stdout stream — join before
416
+ # extracting, or a "47 passed" split across updates is missed.
417
+ counts: set[int] = set(_passed_counts(joined_output))
418
+ coverage: set[str] = set(_coverage_values(joined_output))
419
+ scores: set[str] = set(_eval_scores(joined_output))
407
420
  status = "unknown"
408
421
  if completed is not None:
409
422
  texts.extend(_strings(completed))
410
423
  statuses |= set(_http_statuses(completed))
424
+ counts |= set(_passed_counts(completed))
425
+ coverage |= set(_coverage_values(completed))
426
+ scores |= set(_eval_scores(completed))
411
427
  status = _status_for("shell_process_completed", completed)
412
428
  elif started:
413
429
  status = "pending"
@@ -422,6 +438,9 @@ def load(
422
438
  search_text=" ".join(t for t in texts if t).lower().replace(
423
439
  "\\", "/"),
424
440
  http_statuses=tuple(sorted(statuses)),
441
+ passed_counts=tuple(sorted(counts)),
442
+ coverage_values=tuple(sorted(coverage)),
443
+ eval_scores=tuple(sorted(scores)),
425
444
  ))
426
445
 
427
446
  for item in standalone:
@@ -434,6 +453,9 @@ def load(
434
453
  commands=_commands(contents),
435
454
  search_text=" ".join(texts).lower().replace("\\", "/"),
436
455
  http_statuses=tuple(sorted(set(_http_statuses(contents)))),
456
+ passed_counts=tuple(sorted(set(_passed_counts(contents)))),
457
+ coverage_values=tuple(sorted(set(_coverage_values(contents)))),
458
+ eval_scores=tuple(sorted(set(_eval_scores(contents)))),
437
459
  ))
438
460
 
439
461
  return SourceSession(