devin-qa-pack 0.1.2__tar.gz → 0.2.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 (51) hide show
  1. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/PKG-INFO +25 -1
  2. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/README.md +24 -0
  3. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/pyproject.toml +1 -1
  4. devin_qa_pack-0.2.0/src/devin_qa_pack/__init__.py +1 -0
  5. devin_qa_pack-0.2.0/src/devin_qa_pack/adapters/base.py +46 -0
  6. devin_qa_pack-0.2.0/src/devin_qa_pack/adapters/devin.py +23 -0
  7. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/claims.py +36 -59
  8. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/cli.py +117 -15
  9. devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/__init__.py +364 -0
  10. devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/codex_cli.py +477 -0
  11. devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/copilot_cli.py +344 -0
  12. devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/devin_cli.py +162 -0
  13. devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/vscode_chat.py +546 -0
  14. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/html_report.py +27 -4
  15. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/intent.py +61 -59
  16. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/mcp_server.py +76 -4
  17. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/report.py +66 -2
  18. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/session_end.py +8 -4
  19. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/verify.py +19 -23
  20. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/PKG-INFO +25 -1
  21. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/SOURCES.txt +7 -0
  22. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_claims.py +20 -16
  23. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_cli.py +10 -0
  24. devin_qa_pack-0.2.0/tests/test_evidence.py +821 -0
  25. devin_qa_pack-0.2.0/tests/test_evidence_corpus.py +248 -0
  26. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_intent.py +42 -46
  27. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_mcp.py +18 -0
  28. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_verify.py +16 -14
  29. devin_qa_pack-0.1.2/src/devin_qa_pack/__init__.py +0 -1
  30. devin_qa_pack-0.1.2/src/devin_qa_pack/adapters/base.py +0 -26
  31. devin_qa_pack-0.1.2/src/devin_qa_pack/adapters/devin.py +0 -28
  32. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/setup.cfg +0 -0
  33. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/__init__.py +0 -0
  34. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/aider.py +0 -0
  35. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/claude_code.py +0 -0
  36. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/mcp.py +0 -0
  37. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/paths.py +0 -0
  38. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/dependency_links.txt +0 -0
  39. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/entry_points.txt +0 -0
  40. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/requires.txt +0 -0
  41. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/top_level.txt +0 -0
  42. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_adapters.py +0 -0
  43. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_html_report.py +0 -0
  44. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_offline_core.py +0 -0
  45. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_paths.py +0 -0
  46. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_precommit_hook.py +0 -0
  47. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_release_workflow.py +0 -0
  48. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_report.py +0 -0
  49. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_session_end.py +0 -0
  50. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_skill.py +0 -0
  51. {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devin-qa-pack
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth.
5
5
  Author: Icaro0310
6
6
  License: MIT
@@ -173,6 +173,30 @@ evidence and the source excerpt for every checked claim.
173
173
  `$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
174
174
  `~/.local/share/devin/cli/sessions.db`). Always read-only.
175
175
 
176
+ ### Surfaces (`--surface`)
177
+
178
+ `audit`/`report` are surface-agnostic. Without an explicit
179
+ `--sessions-db` the default `--surface auto` discovers **every**
180
+ supported agent store — Devin CLI plus Copilot CLI
181
+ (`$COPILOT_DIR`/`$COPILOT_HOME`/`~/.copilot` session-state), Codex CLI
182
+ (`$CODEX_HOME`/`~/.codex` state db + rollout JSONL) and VS Code chat
183
+ (`User/workspaceStorage|globalStorage` chat sessions, incl.
184
+ `.vscode-server`). A surface whose store is absent simply contributes
185
+ nothing — discovery never errors on a missing store.
186
+
187
+ ```bash
188
+ devin-qa-pack audit --all # every detected surface
189
+ devin-qa-pack audit --surface copilot-cli --all
190
+ devin-qa-pack audit --surface auto --session vscode-chat:abc123
191
+ ```
192
+
193
+ In provider mode session ids are namespaced `surface:native_id`
194
+ (e.g. `codex-cli:019ae…`); `--session` accepts a bare native prefix or a
195
+ `surface:`-scoped ref. Passing `--sessions-db` *alone* keeps the Devin
196
+ fast path (bare ids, no other surfaces) — pass `--surface` to combine
197
+ a pinned store with the rest. The `qa_audit` MCP tool takes the same
198
+ `surface` argument.
199
+
176
200
  No Devin installed? Try it on a synthetic fixture:
177
201
 
178
202
  ```bash
@@ -158,6 +158,30 @@ evidence and the source excerpt for every checked claim.
158
158
  `$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
159
159
  `~/.local/share/devin/cli/sessions.db`). Always read-only.
160
160
 
161
+ ### Surfaces (`--surface`)
162
+
163
+ `audit`/`report` are surface-agnostic. Without an explicit
164
+ `--sessions-db` the default `--surface auto` discovers **every**
165
+ supported agent store — Devin CLI plus Copilot CLI
166
+ (`$COPILOT_DIR`/`$COPILOT_HOME`/`~/.copilot` session-state), Codex CLI
167
+ (`$CODEX_HOME`/`~/.codex` state db + rollout JSONL) and VS Code chat
168
+ (`User/workspaceStorage|globalStorage` chat sessions, incl.
169
+ `.vscode-server`). A surface whose store is absent simply contributes
170
+ nothing — discovery never errors on a missing store.
171
+
172
+ ```bash
173
+ devin-qa-pack audit --all # every detected surface
174
+ devin-qa-pack audit --surface copilot-cli --all
175
+ devin-qa-pack audit --surface auto --session vscode-chat:abc123
176
+ ```
177
+
178
+ In provider mode session ids are namespaced `surface:native_id`
179
+ (e.g. `codex-cli:019ae…`); `--session` accepts a bare native prefix or a
180
+ `surface:`-scoped ref. Passing `--sessions-db` *alone* keeps the Devin
181
+ fast path (bare ids, no other surfaces) — pass `--surface` to combine
182
+ a pinned store with the rest. The `qa_audit` MCP tool takes the same
183
+ `surface` argument.
184
+
161
185
  No Devin installed? Try it on a synthetic fixture:
162
186
 
163
187
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devin-qa-pack"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,46 @@
1
+ """The normalized session view every adapter produces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from devin_qa_pack.claims import Claim, extract_claims
8
+ from devin_qa_pack.evidence import SessionEvidence
9
+ from devin_qa_pack.verify import ParsedToolCall, parse_tool_calls
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SourceSession:
14
+ """One session's claims plus its recorded tool calls.
15
+
16
+ ``raw_call_count`` is the number of evidence rows *before* decoding —
17
+ when rows exist but none parse, ground truth is unreadable and claims
18
+ resolve ``unverifiable`` rather than ``disputed``.
19
+ """
20
+
21
+ source: str # adapter name: "devin-cli" | "aider" | "claude-code" | ...
22
+ session_id: str
23
+ title: str | None
24
+ working_directory: str
25
+ claims: tuple[Claim, ...]
26
+ calls: tuple[ParsedToolCall, ...]
27
+ raw_call_count: int
28
+
29
+
30
+ def source_view(
31
+ evidence: SessionEvidence,
32
+ session_id: str | None = None,
33
+ title: str | None = None,
34
+ ) -> SourceSession:
35
+ """Bridge :class:`SessionEvidence` into the audit-side view: claims
36
+ extracted from ``messages``, payloads parsed from ``tool_calls``."""
37
+ sid = session_id or evidence.native_id
38
+ return SourceSession(
39
+ source=evidence.surface,
40
+ session_id=sid,
41
+ title=title,
42
+ working_directory=evidence.working_dir or "",
43
+ claims=tuple(extract_claims(evidence.messages, sid)),
44
+ calls=tuple(parse_tool_calls(evidence.tool_calls)),
45
+ raw_call_count=len(evidence.tool_calls),
46
+ )
@@ -0,0 +1,23 @@
1
+ """The Devin adapter — the native ``sessions.db`` source.
2
+
3
+ Claims come from ``message_nodes`` (agent-role text); ground truth comes
4
+ from ``tool_call_state`` rows decoded via devin-internals-spec parsers —
5
+ both through the devin-cli evidence provider.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import replace
11
+
12
+ from devin_internals.parsers import SessionsStore
13
+ from devin_internals.parsers.sessions import Session
14
+
15
+ from devin_qa_pack.adapters.base import SourceSession, source_view
16
+ from devin_qa_pack.evidence.devin_cli import session_evidence
17
+
18
+
19
+ def session_view(store: SessionsStore, session: Session) -> SourceSession:
20
+ """The store-backed view: bare session id + title (the legacy path —
21
+ provider audits namespace ids as ``devin-cli:<id>`` instead)."""
22
+ view = source_view(session_evidence(store, session), session.id)
23
+ return replace(view, title=session.title)
@@ -1,25 +1,22 @@
1
- """Extract deliverable claims from a session's ``message_nodes``.
1
+ """Extract deliverable claims from a session's evidence messages.
2
2
 
3
3
  A *claim* is an assertion the agent made about delivered work: tests
4
4
  passed, a commit exists, a file was created/changed, work was pushed.
5
5
  Only agent-role messages are scanned — a user prompt quoting the same
6
6
  words is not a delivery claim.
7
7
 
8
- ``chat_message`` payloads are marked *unstable* in devin-internals-spec
9
- SCHEMA.md, so decoding is defensive: JSON dicts are checked for the
10
- usual role/text/content keys (including content-block lists); anything
11
- that is not JSON at all is scanned as plain text.
8
+ The input is the canonical :class:`EvidenceMessage` — store-specific
9
+ payload decoding (``chat_message`` JSON for the Devin store) happens in
10
+ the evidence providers, so this module only ever sees plain role+text.
12
11
  """
13
12
 
14
13
  from __future__ import annotations
15
14
 
16
- import json
17
15
  import re
18
16
  from collections.abc import Iterable
19
17
  from dataclasses import dataclass
20
- from typing import Any
21
18
 
22
- from devin_internals.parsers.sessions import MessageNode
19
+ from devin_qa_pack.evidence import EvidenceMessage, normalize_role
23
20
 
24
21
  TESTS = "tests"
25
22
  COMMIT = "commit"
@@ -101,41 +98,10 @@ class Claim:
101
98
  kind: str
102
99
  detail: str
103
100
  session_id: str
104
- node_id: int
101
+ node_id: int | str
105
102
  excerpt: str
106
103
 
107
104
 
108
- def _decode_message(chat_message: str) -> tuple[str | None, str]:
109
- """Return ``(role, text)``. Non-JSON payloads become ``(None, raw)``."""
110
- try:
111
- data = json.loads(chat_message)
112
- except (json.JSONDecodeError, TypeError):
113
- return None, chat_message
114
- if isinstance(data, str):
115
- return None, data
116
- if not isinstance(data, dict):
117
- return None, ""
118
- role = data.get("role") or data.get("author") or data.get("sender")
119
- return (str(role).lower() if role is not None else None), _text_of(data)
120
-
121
-
122
- def _text_of(data: dict[str, Any]) -> str:
123
- for key in ("text", "content", "message", "body", "markdown"):
124
- value = data.get(key)
125
- if isinstance(value, str):
126
- return value
127
- if isinstance(value, list):
128
- parts = [
129
- block.get("text", "")
130
- for block in value
131
- if isinstance(block, dict)
132
- ]
133
- text = "\n".join(p for p in parts if p)
134
- if text:
135
- return text
136
- return ""
137
-
138
-
139
105
  def _excerpt(line: str) -> str:
140
106
  line = " ".join(line.split())
141
107
  return line if len(line) <= _MAX_EXCERPT else line[: _MAX_EXCERPT - 1] + "…"
@@ -176,7 +142,7 @@ def _claims_in_line(line: str) -> Iterable[tuple[str, str]]:
176
142
 
177
143
  def _collect(
178
144
  session_id: str,
179
- node_id: int,
145
+ node_id: int | str,
180
146
  text: str,
181
147
  claims: list[Claim],
182
148
  seen: set[tuple[str, str]],
@@ -197,15 +163,28 @@ def _collect(
197
163
  )
198
164
 
199
165
 
200
- def extract_claims(nodes: Iterable[MessageNode]) -> list[Claim]:
201
- """All claims across ``nodes``, deduplicated by ``(kind, detail)``."""
166
+ def extract_claims(
167
+ messages: Iterable[EvidenceMessage], session_id: str = ""
168
+ ) -> list[Claim]:
169
+ """All claims across ``messages``, deduplicated by ``(kind, detail)``.
170
+
171
+ ``Claim.node_id`` cites the surface's real node id
172
+ (``EvidenceMessage.source_id``, e.g. the devin-cli
173
+ ``message_nodes.node_id``) when the provider supplies one; otherwise
174
+ the message's positional index — same convention as
175
+ :func:`claims_from_pairs`.
176
+ """
202
177
  claims: list[Claim] = []
203
178
  seen: set[tuple[str, str]] = set()
204
- for node in nodes:
205
- role, text = _decode_message(node.chat_message)
206
- if role in _SKIP_ROLES:
179
+ for position, message in enumerate(messages, 1):
180
+ if message.role in _SKIP_ROLES:
207
181
  continue
208
- _collect(node.session_id, node.node_id, text, claims, seen)
182
+ node_id = (
183
+ message.source_id
184
+ if message.source_id is not None
185
+ else position
186
+ )
187
+ _collect(session_id, node_id, message.text, claims, seen)
209
188
  return claims
210
189
 
211
190
 
@@ -213,15 +192,13 @@ def claims_from_pairs(
213
192
  pairs: Iterable[tuple[str | None, str]],
214
193
  session_id: str,
215
194
  ) -> list[Claim]:
216
- """Claims over pre-decoded ``(role, text)`` pairs — the adapter input.
217
-
218
- ``node_id`` is synthesized per pair so excerpts remain traceable back
219
- to their position in the transcript.
220
- """
221
- claims: list[Claim] = []
222
- seen: set[tuple[str, str]] = set()
223
- for node_id, (role, text) in enumerate(pairs, 1):
224
- if role in _SKIP_ROLES:
225
- continue
226
- _collect(session_id, node_id, text, claims, seen)
227
- return claims
195
+ """Claims over pre-decoded ``(role, text)`` pairs — the adapter input."""
196
+ return extract_claims(
197
+ (
198
+ EvidenceMessage(
199
+ role=normalize_role(role), text=text, ts_ms=None
200
+ )
201
+ for role, text in pairs
202
+ ),
203
+ session_id,
204
+ )
@@ -1,11 +1,14 @@
1
1
  """Thin CLI wrapper — all logic lives in the library modules.
2
2
 
3
- ``devin-qa-pack audit`` reads a sessions.db (explicit or auto-detected),
4
- verifies deliverable claims against tool_call_state ground truth and
5
- prints a verdict per session. Read-only. Exit codes: 0 = every audited
6
- session PASS · 1 = audit ran, some session PARTIAL/UNVERIFIED · 2 = the
7
- audit could not run (missing/unknown store, unknown session, or no
8
- session matched the scope — e.g. an empty db or ``--within``).
3
+ ``devin-qa-pack audit`` reads session evidence through surface
4
+ providers (``--surface``, default ``auto`` = every detected store:
5
+ devin-cli sessions.db, copilot-cli, codex-cli, vscode-chat), verifies
6
+ deliverable claims against tool-call ground truth and prints a verdict
7
+ per session. An explicit ``--sessions-db`` alone keeps the Devin fast
8
+ path (bare ids, exactly the pre-surface behavior). Read-only. Exit
9
+ codes: 0 = every audited session PASS · 1 = audit ran, some session
10
+ PARTIAL/UNVERIFIED · 2 = the audit could not run (missing/unknown
11
+ store, unknown session, or no session matched the scope).
9
12
 
10
13
  ``devin-qa-pack session-end`` is the SessionEnd hook variant: it audits
11
14
  only the session that just ended, writes the verdict to a JSON side
@@ -30,6 +33,13 @@ from devin_internals.parsers import SessionsStore
30
33
  from devin_internals.parsers.sessions import Session
31
34
  from devin_internals.schema import SchemaError
32
35
 
36
+ from devin_qa_pack import __version__
37
+ from devin_qa_pack.evidence import (
38
+ Surface,
39
+ collect_evidences,
40
+ match_evidence,
41
+ )
42
+ from devin_qa_pack.evidence.devin_cli import session_evidence
33
43
  from devin_qa_pack.html_report import render_html
34
44
  from devin_qa_pack.intent import (
35
45
  FLAGGED,
@@ -37,14 +47,17 @@ from devin_qa_pack.intent import (
37
47
  audit_intent,
38
48
  intent_dict,
39
49
  render_intent,
50
+ workspace_dir_names,
40
51
  )
41
52
  from devin_qa_pack.paths import default_sessions_db
42
53
  from devin_qa_pack.report import (
43
54
  PASS,
44
55
  audit_all,
56
+ audit_evidence,
45
57
  audit_session,
46
58
  audit_source,
47
59
  audits_payload,
60
+ filter_evidences,
48
61
  render_text,
49
62
  )
50
63
  from devin_qa_pack.session_end import run_session_end, summary_line
@@ -56,12 +69,26 @@ def _build_parser() -> argparse.ArgumentParser:
56
69
  description="Audit Devin sessions: verify deliverable claims "
57
70
  "(tests, commits, files, pushes) against tool_call_state ground truth.",
58
71
  )
72
+ parser.add_argument(
73
+ "--version", action="version", version=f"%(prog)s {__version__}"
74
+ )
59
75
  sub = parser.add_subparsers(dest="command")
60
76
  audit = sub.add_parser("audit", help="verify claims per session")
61
77
  audit.add_argument(
62
78
  "--sessions-db",
63
79
  metavar="PATH",
64
- help="path to sessions.db (default: auto-detect Devin data dir)",
80
+ help="path to sessions.db (default: auto-detect Devin data dir); "
81
+ "on its own this stays the Devin fast path — pass --surface to "
82
+ "combine it with other surfaces",
83
+ )
84
+ audit.add_argument(
85
+ "--surface",
86
+ choices=Surface.CHOICES,
87
+ default=None,
88
+ metavar="S",
89
+ help="evidence surface: devin-cli | copilot-cli | codex-cli | "
90
+ "vscode-chat | auto (default: auto — every detected store; "
91
+ "provider-mode ids are namespaced as surface:native_id)",
65
92
  )
66
93
  scope = audit.add_mutually_exclusive_group(required=True)
67
94
  scope.add_argument("--session", metavar="ID", help="audit one session "
@@ -108,6 +135,15 @@ def _build_parser() -> argparse.ArgumentParser:
108
135
  metavar="PATH",
109
136
  help="path to sessions.db (default: auto-detect Devin data dir)",
110
137
  )
138
+ report.add_argument(
139
+ "--surface",
140
+ choices=Surface.CHOICES,
141
+ default=None,
142
+ metavar="S",
143
+ help="evidence surface to report on (default: auto — every "
144
+ "detected store; explicit --sessions-db alone keeps the Devin "
145
+ "fast path)",
146
+ )
111
147
  report.add_argument(
112
148
  "--out", metavar="PATH", default="qa-report.html",
113
149
  help="HTML output path (default: ./qa-report.html)")
@@ -223,6 +259,60 @@ def _audit_scope(store: SessionsStore, args: argparse.Namespace):
223
259
  return audit_all(store, limit=getattr(args, "limit", None), **kw)
224
260
 
225
261
 
262
+ def _provider_mode(args: argparse.Namespace) -> bool:
263
+ """True when audit goes through evidence providers rather than the
264
+ legacy sessions.db path: an explicit ``--surface``, or auto mode
265
+ without a pinned ``--sessions-db``."""
266
+ return getattr(args, "surface", None) is not None or (
267
+ not getattr(args, "sessions_db", None)
268
+ )
269
+
270
+
271
+ def _provider_audits(args: argparse.Namespace, kw: dict):
272
+ """``(audits, label)`` for provider mode; ``(None, None)`` after a
273
+ printed error."""
274
+ surface = args.surface or Surface.AUTO
275
+ evidences, warnings = collect_evidences(
276
+ surface, sessions_db=getattr(args, "sessions_db", None)
277
+ )
278
+ for warning in warnings:
279
+ print(f"warning: {warning}", file=sys.stderr)
280
+ session_arg = getattr(args, "session", None)
281
+ if session_arg:
282
+ found = match_evidence(evidences, session_arg)
283
+ if isinstance(found, list):
284
+ # ref (workspace-hash-bearing canonical id or ephemeral key)
285
+ # is selectable; surface:native_id alone can repeat.
286
+ refs = ", ".join(
287
+ e.ref or f"{e.surface}:{e.native_id}" for e in found
288
+ )
289
+ print(
290
+ f"error: session '{session_arg}' is ambiguous: {refs}",
291
+ file=sys.stderr,
292
+ )
293
+ return None, None
294
+ if found is None:
295
+ print(
296
+ f"error: unknown session '{session_arg}'", file=sys.stderr
297
+ )
298
+ return None, None
299
+ evidences = [found]
300
+ else:
301
+ evidences = filter_evidences(
302
+ evidences,
303
+ within=getattr(args, "within", None),
304
+ limit=getattr(args, "limit", None),
305
+ )
306
+ db_arg = getattr(args, "sessions_db", None)
307
+ label = (
308
+ str(Path(db_arg).expanduser())
309
+ if db_arg
310
+ else ("surfaces: auto" if surface == Surface.AUTO
311
+ else f"surface: {surface}")
312
+ )
313
+ return [audit_evidence(e, **kw) for e in evidences], label
314
+
315
+
226
316
  def _load_transcript(path_arg: str, fmt: str | None, cwd: str | None):
227
317
  """Resolve ``--transcript`` into a SourceSession via the adapters."""
228
318
  from devin_qa_pack.adapters import aider, claude_code
@@ -286,6 +376,12 @@ def _cmd_audit(args: argparse.Namespace) -> int:
286
376
  )
287
377
  ]
288
378
  db = Path(args.transcript).expanduser()
379
+ elif _provider_mode(args):
380
+ kw = {"online": args.online,
381
+ "allow_domains": tuple(args.allow_domain)}
382
+ audits, db = _provider_audits(args, kw)
383
+ if audits is None:
384
+ return 2
289
385
  else:
290
386
  db, store = _open_store(args.sessions_db)
291
387
  if store is None:
@@ -315,13 +411,20 @@ def _cmd_audit(args: argparse.Namespace) -> int:
315
411
 
316
412
 
317
413
  def _cmd_report(args: argparse.Namespace) -> int:
318
- db, store = _open_store(args.sessions_db)
319
- if store is None:
320
- return 2
321
- with store:
322
- audits = _audit_scope(store, args)
414
+ if _provider_mode(args):
415
+ kw = {"online": args.online,
416
+ "allow_domains": tuple(args.allow_domain)}
417
+ audits, db = _provider_audits(args, kw)
323
418
  if audits is None:
324
419
  return 2
420
+ else:
421
+ db, store = _open_store(args.sessions_db)
422
+ if store is None:
423
+ return 2
424
+ with store:
425
+ audits = _audit_scope(store, args)
426
+ if audits is None:
427
+ return 2
325
428
  out = Path(args.out).expanduser()
326
429
  try:
327
430
  out.write_text(render_html(audits, db), encoding="utf-8")
@@ -342,9 +445,8 @@ def _cmd_intent(args: argparse.Namespace) -> int:
342
445
  if session is None:
343
446
  return 2
344
447
  audit = audit_intent(
345
- session,
346
- store.message_nodes(session.id),
347
- store.tool_call_state(session.id),
448
+ session_evidence(store, session),
449
+ repo_names=workspace_dir_names(session),
348
450
  )
349
451
  if args.json:
350
452
  print(json.dumps(intent_dict(audit), indent=2))