@alizain/doublecheck 2.0.0 → 2.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/Dockerfile.guest +8 -2
- package/README.md +64 -18
- package/dist/cli.mjs +204 -42
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -1
package/Dockerfile.guest
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
#
|
|
4
4
|
# ./scripts/build-guest-image.sh
|
|
5
5
|
#
|
|
6
|
-
# Baking the
|
|
7
|
-
#
|
|
6
|
+
# Baking the agent CLIs here (instead of `npm i -g` at every guest boot) pins
|
|
7
|
+
# their versions and cuts ~30s off each check run. Rebuild to pick up newer
|
|
8
|
+
# CLIs. One image serves both --agent values.
|
|
8
9
|
# git is for check-authored scoping ("run `git diff main`" lives in check
|
|
9
10
|
# prose); ripgrep because inspectors grep; curl/wget because checks may fetch
|
|
10
11
|
# from the internet (node:*-slim ships none of these).
|
|
@@ -15,3 +16,8 @@ RUN apt-get update \
|
|
|
15
16
|
&& rm -rf /var/lib/apt/lists/*
|
|
16
17
|
|
|
17
18
|
RUN npm install -g @anthropic-ai/claude-code
|
|
19
|
+
|
|
20
|
+
# codex ships a static musl binary via platform optionalDependencies — never
|
|
21
|
+
# add --omit=optional here or npm silently installs the JS launcher with no
|
|
22
|
+
# binary in it.
|
|
23
|
+
RUN npm install -g @openai/codex
|
package/README.md
CHANGED
|
@@ -47,9 +47,9 @@ Per check: a scratch workdir gets `prompt.txt` (environment preamble +
|
|
|
47
47
|
operator run context, when given + check body + report contract). A [microsandbox](https://github.com/superradcompany/microsandbox)
|
|
48
48
|
microVM boots from a locally built image with the project bind-mounted
|
|
49
49
|
**read-only at its real host path** and the scratch dir mounted rw as the
|
|
50
|
-
guest cwd.
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
guest cwd. The selected agent CLI (`--agent`: headless `claude -p`, or
|
|
51
|
+
`codex exec`) runs inside with its full toolkit and no permission gates —
|
|
52
|
+
the microVM is the safety boundary, the ro mount is the "don't touch
|
|
53
53
|
my repo" guarantee. The agent writes `report.md`; the host copies it to
|
|
54
54
|
`$OUTPUT/<run-timestamp>/<check>.md` (with `--save-jsonl`, alongside
|
|
55
55
|
`<check>.stream.jsonl` — the inspector's raw stream, kept so a run can be
|
|
@@ -69,13 +69,24 @@ missing token / checks / image abort loudly up front.
|
|
|
69
69
|
npm install -g @alizain/doublecheck # installs the `doublecheck` command; or, from a clone: pnpm install
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
Guests boot from
|
|
73
|
-
|
|
72
|
+
Guests boot from an image with both agent CLIs baked in. An installed release
|
|
73
|
+
needs no image step: the runner resolves
|
|
74
|
+
`ghcr.io/alizain/doublecheck-guest:<its own version>` and pulls it on first
|
|
75
|
+
run (~1 GB, once per version — the release workflow publishes it for
|
|
76
|
+
amd64+arm64 alongside the npm package).
|
|
77
|
+
|
|
78
|
+
From a clone (version `0.0.0-development`) guests use a locally built image
|
|
79
|
+
instead. Build it once (needs a running Docker daemon; rebuild to pick up
|
|
80
|
+
newer agent CLIs):
|
|
74
81
|
|
|
75
82
|
```bash
|
|
76
83
|
./scripts/build-guest-image.sh
|
|
77
84
|
```
|
|
78
85
|
|
|
86
|
+
`DOUBLECHECK_GUEST_IMAGE=<ref>` overrides the image for any install — the
|
|
87
|
+
named ref must already be in the microsandbox cache (it is never pulled);
|
|
88
|
+
useful offline or for custom guest images.
|
|
89
|
+
|
|
79
90
|
## Usage
|
|
80
91
|
|
|
81
92
|
```bash
|
|
@@ -83,15 +94,26 @@ CLAUDE_CODE_OAUTH_TOKEN=... doublecheck check \
|
|
|
83
94
|
--target DIR # tree under inspection, mounted read-only; default: cwd
|
|
84
95
|
--checks-dir DIR # repeatable; default: $TARGET/.agents/checks
|
|
85
96
|
--context FILE # run brief: intent, nuances, sanctioned exceptions, scope
|
|
86
|
-
--
|
|
97
|
+
--agent NAME # claude (default) or codex
|
|
98
|
+
--model MODEL # default per agent: claude haiku, codex gpt-5.5
|
|
87
99
|
--parallel N # default: 4 concurrent guests
|
|
88
100
|
--output DIR # default: $TARGET/.doublecheck
|
|
89
101
|
--check NAME # repeatable; default: every check in the checks dirs
|
|
90
102
|
--save-jsonl # persist each inspector's raw stream beside its report
|
|
91
103
|
```
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
Credentials per agent, gathered on the host before any guest boots:
|
|
106
|
+
|
|
107
|
+
- **claude**: `CLAUDE_CODE_OAUTH_TOKEN` is required in the environment and
|
|
108
|
+
injected into each guest; where it comes from is the operator's business.
|
|
109
|
+
- **codex**: no env var — each guest gets a fresh copy of the host's
|
|
110
|
+
`~/.codex/auth.json` (run `codex login` once). For ChatGPT-plan auth the
|
|
111
|
+
run **hard-fails if the tokens were last refreshed over 7 days ago**:
|
|
112
|
+
codex self-refreshes at ~8 days, refresh tokens are single-use, and a
|
|
113
|
+
refresh inside a throwaway guest would rotate the token family and force
|
|
114
|
+
the host to re-login. Running any codex command on the host refreshes.
|
|
115
|
+
Residual risk: a mid-run 401 can still trigger a guest-side refresh.
|
|
116
|
+
Guests never write auth state back.
|
|
95
117
|
|
|
96
118
|
### Mining your Claude history into an observation catalog
|
|
97
119
|
|
|
@@ -101,13 +123,19 @@ turns) to extract durable engineering preferences into
|
|
|
101
123
|
`~/.doublecheck/catalog`, mirroring the transcript tree — one
|
|
102
124
|
`<project>/<session>/observations.md` per conversation, frontmatter recording
|
|
103
125
|
the source hash so re-runs only mine new or grown sessions. Mining guests get
|
|
104
|
-
egress to
|
|
126
|
+
egress to the selected agent's own API domains only (`claude`:
|
|
127
|
+
`*.anthropic.com`; `codex`: `*.chatgpt.com` + `*.openai.com`) — the one
|
|
128
|
+
reachable destination is the service the agent already sends its context to.
|
|
129
|
+
Note what that means for `--agent codex`: your Claude Code transcript
|
|
130
|
+
content flows to OpenAI. Design: `docs/2026-07-05-mine-design.md`.
|
|
105
131
|
|
|
106
132
|
```bash
|
|
107
133
|
CLAUDE_CODE_OAUTH_TOKEN=... doublecheck mine \
|
|
108
134
|
--projects DIR # default: ~/.claude/projects
|
|
109
135
|
--catalog DIR # default: ~/.doublecheck/catalog
|
|
110
|
-
--
|
|
136
|
+
--agent NAME # claude (default) or codex
|
|
137
|
+
--model MODEL # default per agent: claude opus, codex gpt-5.5
|
|
138
|
+
# (a bad-model mine pollutes a durable asset)
|
|
111
139
|
--parallel N # default: 4
|
|
112
140
|
--min-turns N # default: 2
|
|
113
141
|
--limit N # mine at most N pending conversations
|
|
@@ -120,18 +148,25 @@ end-to-end:
|
|
|
120
148
|
|
|
121
149
|
```bash
|
|
122
150
|
CLAUDE_CODE_OAUTH_TOKEN=... doublecheck check --target fixtures/planted --model haiku
|
|
151
|
+
doublecheck check --target fixtures/planted --agent codex # auth from ~/.codex/auth.json
|
|
123
152
|
```
|
|
124
153
|
|
|
125
154
|
## What the inspector sees
|
|
126
155
|
|
|
127
156
|
The facts of the agent's world, so you can decide how to author checks:
|
|
128
157
|
|
|
129
|
-
- **The agent** is one headless
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
- **The agent** is one headless CLI process per check, running inside its
|
|
159
|
+
own microVM with all approval/permission gates bypassed — the VM is the
|
|
160
|
+
boundary. Check agents have unrestricted internet egress.
|
|
161
|
+
- `--agent claude` (default): `claude -p` (model from `--model`, default
|
|
162
|
+
haiku) with the pinned toolkit Task (it can spawn subagents), Bash,
|
|
163
|
+
Read, Write, Edit, Glob, Grep, WebSearch, WebFetch.
|
|
164
|
+
- `--agent codex`: `codex exec` (default model gpt-5.5, reasoning effort
|
|
165
|
+
pinned to xhigh in the staged guest config) with codex's own toolset:
|
|
166
|
+
shell, apply_patch, plan/todo, subagent spawning (multi_agent), and
|
|
167
|
+
server-side web search. The staged config disables codex's ambient
|
|
168
|
+
inputs — no AGENTS.md pickup, no plugin/marketplace fetch — so the
|
|
169
|
+
piped prompt is its only input, same as claude.
|
|
135
170
|
- **Its only input is the prompt**: a short environment preamble (verbatim in
|
|
136
171
|
`src/contract.ts`) + the operator's run context, when `--context` was given
|
|
137
172
|
+ the check body + the report contract. No session, no conversation history
|
|
@@ -156,7 +191,7 @@ pnpm typecheck # tsc --noEmit
|
|
|
156
191
|
pnpm check # biome
|
|
157
192
|
```
|
|
158
193
|
|
|
159
|
-
Design records: `docs/2026-07-05-doublecheck-design.md`, `docs/2026-07-05-run-context-and-decomposed-flags-design.md`. For how a driving agent should use this tool — above all, what a good `--context` brief contains — see `SKILL.md`.
|
|
194
|
+
Design records: `docs/2026-07-05-doublecheck-design.md`, `docs/2026-07-05-run-context-and-decomposed-flags-design.md`, `docs/2026-07-06-codex-agent-design.md`. For how a driving agent should use this tool — above all, what a good `--context` brief contains — see `SKILL.md`.
|
|
160
195
|
|
|
161
196
|
## Releasing
|
|
162
197
|
|
|
@@ -166,9 +201,20 @@ pushing to main.
|
|
|
166
201
|
|
|
167
202
|
```bash
|
|
168
203
|
gh workflow run release -f dry_run=true # preview next version + notes, publish nothing
|
|
169
|
-
gh workflow run release -f dry_run=false # ship: npm publish + GitHub Release + tag
|
|
204
|
+
gh workflow run release -f dry_run=false # ship: npm publish + GitHub Release + tag + guest image
|
|
205
|
+
gh workflow run release -f image_version=X.Y.Z # no release: (re)build + push the guest image
|
|
206
|
+
# for an existing version (recovery, or refreshing
|
|
207
|
+
# the baked-in agent CLIs)
|
|
170
208
|
```
|
|
171
209
|
|
|
210
|
+
A real release also builds `Dockerfile.guest` for amd64+arm64 and pushes
|
|
211
|
+
`ghcr.io/alizain/doublecheck-guest:<version>` (+ `:latest`) — the image
|
|
212
|
+
installed CLIs pull at runtime. The ghcr package must be **public** for those
|
|
213
|
+
unauthenticated pulls (one-time visibility flip in the package settings after
|
|
214
|
+
the very first push). `image_version` mutates an existing tag in place;
|
|
215
|
+
machines that already pulled it keep their cached copy (the runner pulls
|
|
216
|
+
if-missing and never re-checks).
|
|
217
|
+
|
|
172
218
|
- **Only `feat:` / `fix:` / `BREAKING CHANGE:` commits trigger a release** and
|
|
173
219
|
decide the bump (minor / patch / major). Other commit styles ride along
|
|
174
220
|
unreleased — use `docs:`/`chore:`/plain messages for work that shouldn't ship
|
package/dist/cli.mjs
CHANGED
|
@@ -9,15 +9,17 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
//#region src/contract.ts
|
|
10
10
|
const PROMPT_FILE = "prompt.txt";
|
|
11
11
|
const REPORT_FILE = "report.md";
|
|
12
|
-
function reportContract(activity, deliverable, workdir) {
|
|
12
|
+
function reportContract(activity, deliverable, workdir, writeTool) {
|
|
13
|
+
const tool = writeTool ? ` ${writeTool}` : "";
|
|
13
14
|
return `## Report — the only output that counts
|
|
14
15
|
|
|
15
16
|
Your final chat reply is discarded. The only thing read back is the file \`${workdir}/${REPORT_FILE}\` — if it does not exist when you exit, this run FAILS. Always use that absolute path: if you \`cd\` elsewhere (into the inspected tree, say), a relative \`./${REPORT_FILE}\` lands in the wrong place, or in a read-only mount where the write fails.
|
|
16
17
|
|
|
17
|
-
So: create \`${workdir}/${REPORT_FILE}
|
|
18
|
+
So: create \`${workdir}/${REPORT_FILE}\`${tool} BEFORE you start ${activity} (a title line is enough), and update it as you work. The final state of the file when you finish is ${deliverable}.`;
|
|
18
19
|
}
|
|
19
|
-
function firstActionLine(workdir) {
|
|
20
|
-
|
|
20
|
+
function firstActionLine(workdir, writeTool) {
|
|
21
|
+
const tool = writeTool ? ` ${writeTool}` : "";
|
|
22
|
+
return `Your FIRST action, before anything else: create \`${workdir}/${REPORT_FILE}\`${tool} (a title line is enough). The full report contract is at the end of this prompt.`;
|
|
21
23
|
}
|
|
22
24
|
function composePrompt(opts) {
|
|
23
25
|
const contextSection = opts.runContext ? `## Run context (from the operator)
|
|
@@ -37,7 +39,7 @@ You are a code inspector running inside a sandboxed microVM with full permission
|
|
|
37
39
|
- \`git\` and \`rg\` are installed.
|
|
38
40
|
- Your current working directory is a writable scratch workspace: \`${opts.workdir}\`.
|
|
39
41
|
|
|
40
|
-
${firstActionLine(opts.workdir)}
|
|
42
|
+
${firstActionLine(opts.workdir, opts.writeTool)}
|
|
41
43
|
|
|
42
44
|
## Inspection discipline
|
|
43
45
|
|
|
@@ -53,16 +55,16 @@ ${contextSection}${opts.checkBody}
|
|
|
53
55
|
|
|
54
56
|
---
|
|
55
57
|
|
|
56
|
-
${reportContract("inspecting", "the check's report", opts.workdir)}
|
|
58
|
+
${reportContract("inspecting", "the check's report", opts.workdir, opts.writeTool)}
|
|
57
59
|
|
|
58
60
|
Every single line of your report will be read in full by the operator's agent — nothing is skimmed, so every line costs attention. Verify each finding against the actual code before writing it down. A report that says "no findings" is a perfectly good report; an unverified finding is not.`;
|
|
59
61
|
}
|
|
60
|
-
function composeMinePrompt(digest, workdir) {
|
|
62
|
+
function composeMinePrompt(digest, workdir, writeTool) {
|
|
61
63
|
return `## Environment
|
|
62
64
|
|
|
63
65
|
You are running inside a sandboxed microVM with no network access. The machine's Claude Code transcripts are mounted read-only at their real paths; your current working directory is a writable scratch workspace: \`${workdir}\`.
|
|
64
66
|
|
|
65
|
-
${firstActionLine(workdir)}
|
|
67
|
+
${firstActionLine(workdir, writeTool)}
|
|
66
68
|
|
|
67
69
|
## Task
|
|
68
70
|
|
|
@@ -106,29 +108,29 @@ If the conversation carries no durable preference signal, the report is exactly
|
|
|
106
108
|
No durable preference signal.
|
|
107
109
|
<one sentence: what the conversation was about instead>
|
|
108
110
|
|
|
109
|
-
${reportContract("mining", "the mined observations", workdir)}`;
|
|
111
|
+
${reportContract("mining", "the mined observations", workdir, writeTool)}`;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
//#endregion
|
|
113
115
|
//#region src/claude.ts
|
|
114
|
-
const GUEST_HOME = "/root";
|
|
116
|
+
const GUEST_HOME$1 = "/root";
|
|
115
117
|
function claudeAgent(opts) {
|
|
116
118
|
return {
|
|
117
119
|
command: `claude -p --no-session-persistence --dangerously-skip-permissions --output-format stream-json --verbose --tools Task Bash Read Write Edit Glob Grep WebSearch WebFetch < ${PROMPT_FILE}`,
|
|
118
120
|
env: {
|
|
119
|
-
HOME: GUEST_HOME,
|
|
121
|
+
HOME: GUEST_HOME$1,
|
|
120
122
|
IS_SANDBOX: "1",
|
|
121
123
|
GIT_OPTIONAL_LOCKS: "0",
|
|
122
124
|
ANTHROPIC_MODEL: opts.model,
|
|
123
125
|
CLAUDE_CODE_OAUTH_TOKEN: opts.token
|
|
124
126
|
},
|
|
125
127
|
files: [{
|
|
126
|
-
path: `${GUEST_HOME}/.claude.json`,
|
|
128
|
+
path: `${GUEST_HOME$1}/.claude.json`,
|
|
127
129
|
content: `${JSON.stringify({ projects: { [opts.workdir]: { hasTrustDialogAccepted: true } } }, null, " ")}\n`
|
|
128
130
|
}]
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
|
-
function
|
|
133
|
+
function describeClaudeStreamLine(line) {
|
|
132
134
|
let obj;
|
|
133
135
|
try {
|
|
134
136
|
obj = JSON.parse(line);
|
|
@@ -146,21 +148,172 @@ function describeStreamLine(line) {
|
|
|
146
148
|
} else if (obj.type === "result") detail = ` ${obj.subtype}, ${((obj.duration_ms ?? 0) / 1e3).toFixed(1)}s`;
|
|
147
149
|
return `[${label}]${detail}`;
|
|
148
150
|
}
|
|
149
|
-
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/codex.ts
|
|
154
|
+
const GUEST_HOME = "/root";
|
|
155
|
+
function guestConfig(model) {
|
|
156
|
+
return `model = "${model}"
|
|
157
|
+
approval_policy = "never"
|
|
158
|
+
sandbox_mode = "danger-full-access"
|
|
159
|
+
model_reasoning_effort = "xhigh"
|
|
160
|
+
web_search = "live"
|
|
161
|
+
project_doc_max_bytes = 0
|
|
162
|
+
cli_auth_credentials_store = "file"
|
|
163
|
+
|
|
164
|
+
[features]
|
|
165
|
+
plugins = false
|
|
166
|
+
apps = false
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
function codexAgent(opts) {
|
|
170
|
+
return {
|
|
171
|
+
command: `codex exec --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox --ephemeral - < ${PROMPT_FILE}`,
|
|
172
|
+
env: {
|
|
173
|
+
HOME: GUEST_HOME,
|
|
174
|
+
GIT_OPTIONAL_LOCKS: "0"
|
|
175
|
+
},
|
|
176
|
+
files: [{
|
|
177
|
+
path: `${GUEST_HOME}/.codex/auth.json`,
|
|
178
|
+
content: opts.authJson
|
|
179
|
+
}, {
|
|
180
|
+
path: `${GUEST_HOME}/.codex/config.toml`,
|
|
181
|
+
content: guestConfig(opts.model)
|
|
182
|
+
}]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const CODEX_AUTH_MAX_AGE_DAYS = 7;
|
|
186
|
+
function validateCodexAuth(content, now, where) {
|
|
187
|
+
let auth;
|
|
188
|
+
try {
|
|
189
|
+
auth = JSON.parse(content);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error(`${where} is not valid JSON — run \`codex login\` on the host`);
|
|
192
|
+
}
|
|
193
|
+
if (auth.tokens?.refresh_token) {
|
|
194
|
+
if (!((now.getTime() - Date.parse(auth.last_refresh ?? "")) / 864e5 <= 7)) throw new Error(`${where} tokens were last refreshed over ${7} days ago (codex refreshes at ~8 days, and a refresh inside a guest would rotate the single-use refresh token and poison this host's session) — run any codex command on the host to refresh, then retry`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (auth.OPENAI_API_KEY) return;
|
|
198
|
+
throw new Error(`${where} has neither ChatGPT tokens nor an API key — run \`codex login\` on the host`);
|
|
199
|
+
}
|
|
200
|
+
const clip = (s, max = 60) => s.length > max ? `${s.slice(0, max)}…` : s;
|
|
201
|
+
function describeCodexStreamLine(line) {
|
|
202
|
+
let obj;
|
|
203
|
+
try {
|
|
204
|
+
obj = JSON.parse(line);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const item = obj.item;
|
|
209
|
+
const label = (obj.type ?? "") + (item?.type ? `:${item.type}` : "");
|
|
210
|
+
let detail = "";
|
|
211
|
+
if (item?.type === "agent_message") detail = ` ${item.text?.length ?? 0} chars`;
|
|
212
|
+
else if (item?.type === "command_execution" && item.command) detail = ` ${clip(item.command)}`;
|
|
213
|
+
else if (item?.type === "file_change") detail = ` ${(item.changes ?? []).length} file(s)`;
|
|
214
|
+
else if (obj.type === "turn.completed") detail = ` ${obj.usage?.input_tokens ?? 0} in, ${obj.usage?.output_tokens ?? 0} out tokens`;
|
|
215
|
+
else if (obj.type === "turn.failed") detail = ` ${clip(obj.error?.message ?? "")}`;
|
|
216
|
+
else if (obj.type === "error") detail = ` ${clip(obj.message ?? "")}`;
|
|
217
|
+
return `[${label}]${detail}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/agent-cli.ts
|
|
222
|
+
const AGENT_CLIS = {
|
|
223
|
+
claude: {
|
|
224
|
+
name: "claude",
|
|
225
|
+
defaultModel: {
|
|
226
|
+
check: "haiku",
|
|
227
|
+
mine: "opus"
|
|
228
|
+
},
|
|
229
|
+
writeToolPhrase: "with the Write tool",
|
|
230
|
+
egressDomains: ["anthropic.com"],
|
|
231
|
+
credentials: () => {
|
|
232
|
+
const token = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
233
|
+
if (!token) throw new Error("CLAUDE_CODE_OAUTH_TOKEN is required in the environment (it is injected into each agent's sandbox)");
|
|
234
|
+
return token;
|
|
235
|
+
},
|
|
236
|
+
agent: ({ credentials, model, workdir }) => claudeAgent({
|
|
237
|
+
token: credentials,
|
|
238
|
+
model,
|
|
239
|
+
workdir
|
|
240
|
+
}),
|
|
241
|
+
describeStreamLine: describeClaudeStreamLine
|
|
242
|
+
},
|
|
243
|
+
codex: {
|
|
244
|
+
name: "codex",
|
|
245
|
+
defaultModel: {
|
|
246
|
+
check: "gpt-5.5",
|
|
247
|
+
mine: "gpt-5.5"
|
|
248
|
+
},
|
|
249
|
+
writeToolPhrase: null,
|
|
250
|
+
egressDomains: ["chatgpt.com", "openai.com"],
|
|
251
|
+
credentials: () => {
|
|
252
|
+
const authPath = join(homedir(), ".codex", "auth.json");
|
|
253
|
+
let content;
|
|
254
|
+
try {
|
|
255
|
+
content = readFileSync(authPath, "utf-8");
|
|
256
|
+
} catch {
|
|
257
|
+
throw new Error(`no readable ${authPath} — run \`codex login\` on the host`);
|
|
258
|
+
}
|
|
259
|
+
validateCodexAuth(content, /* @__PURE__ */ new Date(), authPath);
|
|
260
|
+
return content;
|
|
261
|
+
},
|
|
262
|
+
agent: ({ credentials, model, workdir }) => codexAgent({
|
|
263
|
+
authJson: credentials,
|
|
264
|
+
model,
|
|
265
|
+
workdir
|
|
266
|
+
}),
|
|
267
|
+
describeStreamLine: describeCodexStreamLine
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function resolveAgentCli(name) {
|
|
271
|
+
const cli = AGENT_CLIS[name];
|
|
272
|
+
if (!cli) throw new Error(`unknown agent "${name}" (have: ${Object.keys(AGENT_CLIS).join(", ")})`);
|
|
273
|
+
return cli;
|
|
274
|
+
}
|
|
275
|
+
function resolveAgentRun(name, model, workflow) {
|
|
276
|
+
const cli = resolveAgentCli(name);
|
|
277
|
+
return {
|
|
278
|
+
cli,
|
|
279
|
+
credentials: cli.credentials(),
|
|
280
|
+
model: model ?? cli.defaultModel[workflow]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function progressSink(label, describe) {
|
|
150
284
|
return (kind, line) => {
|
|
151
285
|
if (kind === "stderr") {
|
|
152
286
|
process.stderr.write(`[${label}] ! ${line}\n`);
|
|
153
287
|
return;
|
|
154
288
|
}
|
|
155
|
-
const described =
|
|
289
|
+
const described = describe(line);
|
|
156
290
|
if (described) process.stderr.write(`[${label}] ${described}\n`);
|
|
157
291
|
};
|
|
158
292
|
}
|
|
159
293
|
|
|
160
294
|
//#endregion
|
|
161
295
|
//#region src/runner.ts
|
|
162
|
-
const
|
|
296
|
+
const GUEST_IMAGE_REPO = "ghcr.io/alizain/doublecheck-guest";
|
|
297
|
+
const LOCAL_GUEST_IMAGE = "doublecheck-guest:latest";
|
|
298
|
+
const DEV_VERSION = "0.0.0-development";
|
|
163
299
|
const GUEST_MEMORY_MIB = 2048;
|
|
300
|
+
function decideGuestImage(version, override) {
|
|
301
|
+
if (override) return {
|
|
302
|
+
ref: override,
|
|
303
|
+
pullPolicy: "never"
|
|
304
|
+
};
|
|
305
|
+
if (version === "0.0.0-development") return {
|
|
306
|
+
ref: LOCAL_GUEST_IMAGE,
|
|
307
|
+
pullPolicy: "never"
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
ref: `${GUEST_IMAGE_REPO}:${version}`,
|
|
311
|
+
pullPolicy: "if-missing"
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function packageVersion() {
|
|
315
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
316
|
+
}
|
|
164
317
|
function feedLines(carry, chunk) {
|
|
165
318
|
const parts = (carry + chunk).split("\n");
|
|
166
319
|
const rest = parts.pop() ?? "";
|
|
@@ -188,11 +341,17 @@ function decideOutcome(exitCode, report, workdir) {
|
|
|
188
341
|
async function runAgent(opts) {
|
|
189
342
|
const microsandbox = await import("microsandbox");
|
|
190
343
|
const name = `doublecheck-${basename(opts.workdir)}`;
|
|
191
|
-
const
|
|
344
|
+
const image = decideGuestImage(packageVersion(), process.env.DOUBLECHECK_GUEST_IMAGE);
|
|
345
|
+
const network = opts.network;
|
|
346
|
+
const policy = network === "all" ? microsandbox.NetworkPolicy.allowAll() : microsandbox.NetworkPolicy.builder().defaultDeny().egress((rb) => {
|
|
347
|
+
let rules = rb;
|
|
348
|
+
for (const domain of network.onlyDomains) rules = rules.allow((d) => d.domainSuffix(domain));
|
|
349
|
+
return rules;
|
|
350
|
+
}).build();
|
|
192
351
|
let sandbox = null;
|
|
193
352
|
try {
|
|
194
353
|
try {
|
|
195
|
-
sandbox = await microsandbox.Sandbox.builder(name).image(
|
|
354
|
+
sandbox = await microsandbox.Sandbox.builder(name).image(image.ref).memory(GUEST_MEMORY_MIB).pullPolicy(image.pullPolicy).replace().workdir(opts.workdir).envs(opts.spec.env).volume(opts.mount, (mb) => mb.bind(opts.mount).readonly()).volume(opts.workdir, (mb) => mb.bind(opts.workdir)).patch((pb) => {
|
|
196
355
|
let p = pb;
|
|
197
356
|
for (const f of opts.spec.files) p = p.text(f.path, f.content, {
|
|
198
357
|
mode: 384,
|
|
@@ -201,7 +360,8 @@ async function runAgent(opts) {
|
|
|
201
360
|
return p;
|
|
202
361
|
}).network((nb) => nb.policy(policy)).create();
|
|
203
362
|
} catch (e) {
|
|
204
|
-
if (String(e).includes("not cached")) throw new Error(`guest image ${
|
|
363
|
+
if (image.pullPolicy === "never" && String(e).includes("not cached")) throw new Error(`guest image ${image.ref} is not in the microsandbox cache — run scripts/build-guest-image.sh (${String(e)})`);
|
|
364
|
+
if (image.pullPolicy === "if-missing") throw new Error(`guest ${image.ref} failed to boot — if this is a pull failure, check network access to ghcr.io and that the package is public; a locally built image can be forced with DOUBLECHECK_GUEST_IMAGE=doublecheck-guest:latest (${String(e)})`);
|
|
205
365
|
throw e;
|
|
206
366
|
}
|
|
207
367
|
const handle = await sandbox.execStreamWith("bash", (e) => e.args(["-c", opts.spec.command]));
|
|
@@ -286,20 +446,21 @@ function runTimestamp(d) {
|
|
|
286
446
|
function failureReport(reason, partialReport) {
|
|
287
447
|
return `# CHECK FAILED\n\n${reason}\n${partialReport ? `\n---\n\nPartial ${REPORT_FILE} left by the agent:\n\n${partialReport}` : ""}`;
|
|
288
448
|
}
|
|
289
|
-
async function runOneCheck(check, opts,
|
|
449
|
+
async function runOneCheck(check, opts, run, runContext, outDir) {
|
|
290
450
|
const workdir = mkdtempSync(join(tmpdir(), `doublecheck-${check.name}-`));
|
|
291
451
|
writeFileSync(join(workdir, PROMPT_FILE), composePrompt({
|
|
292
452
|
checkBody: check.body,
|
|
293
453
|
target: opts.target,
|
|
294
454
|
workdir,
|
|
295
|
-
runContext
|
|
455
|
+
runContext,
|
|
456
|
+
writeTool: run.cli.writeToolPhrase
|
|
296
457
|
}));
|
|
297
|
-
const spec =
|
|
298
|
-
|
|
299
|
-
model:
|
|
458
|
+
const spec = run.cli.agent({
|
|
459
|
+
credentials: run.credentials,
|
|
460
|
+
model: run.model,
|
|
300
461
|
workdir
|
|
301
462
|
});
|
|
302
|
-
const sink = progressSink(check.name);
|
|
463
|
+
const sink = progressSink(check.name, run.cli.describeStreamLine);
|
|
303
464
|
let onLine = sink;
|
|
304
465
|
if (opts.saveJsonl) {
|
|
305
466
|
const streamPath = join(outDir, `${check.name}.stream.jsonl`);
|
|
@@ -326,8 +487,7 @@ async function runOneCheck(check, opts, token, runContext, outDir) {
|
|
|
326
487
|
return false;
|
|
327
488
|
}
|
|
328
489
|
async function runChecks(opts) {
|
|
329
|
-
const
|
|
330
|
-
if (!token) throw new Error("CLAUDE_CODE_OAUTH_TOKEN is required in the environment (it is injected into each check's sandbox)");
|
|
490
|
+
const run = resolveAgentRun(opts.agent, opts.model, "check");
|
|
331
491
|
const checks = discoverChecks(opts.checksDirs, opts.only);
|
|
332
492
|
let runContext = null;
|
|
333
493
|
if (opts.contextFile) try {
|
|
@@ -337,9 +497,9 @@ async function runChecks(opts) {
|
|
|
337
497
|
}
|
|
338
498
|
const outDir = join(opts.output, runTimestamp(/* @__PURE__ */ new Date()));
|
|
339
499
|
mkdirSync(outDir, { recursive: true });
|
|
340
|
-
process.stderr.write(`${checks.length} check(s) against ${opts.target} (model ${
|
|
500
|
+
process.stderr.write(`${checks.length} check(s) against ${opts.target} (agent ${run.cli.name}, model ${run.model}, parallel ${opts.parallel})\n`);
|
|
341
501
|
const queue = new PQueue({ concurrency: opts.parallel });
|
|
342
|
-
return (await Promise.all(checks.map((check) => queue.add(() => runOneCheck(check, opts,
|
|
502
|
+
return (await Promise.all(checks.map((check) => queue.add(() => runOneCheck(check, opts, run, runContext, outDir))))).every((ok) => ok === true);
|
|
343
503
|
}
|
|
344
504
|
|
|
345
505
|
//#endregion
|
|
@@ -476,13 +636,13 @@ function summarizeMinePlan(plan, minTurns) {
|
|
|
476
636
|
function dryRunRow({ unit, status }) {
|
|
477
637
|
return `${status.reason.padEnd(7)} ${status.turns.toString().padStart(4)} turns ${unit.project}/${unit.session}`;
|
|
478
638
|
}
|
|
479
|
-
async function mineOneUnit({ unit, status }, opts,
|
|
639
|
+
async function mineOneUnit({ unit, status }, opts, run) {
|
|
480
640
|
const tag = unit.session.slice(0, 8);
|
|
481
641
|
const workdir = mkdtempSync(join(tmpdir(), `doublecheck-mine-${tag}-`));
|
|
482
|
-
writeFileSync(join(workdir, PROMPT_FILE), composeMinePrompt(status.digest, workdir));
|
|
483
|
-
const spec =
|
|
484
|
-
|
|
485
|
-
model:
|
|
642
|
+
writeFileSync(join(workdir, PROMPT_FILE), composeMinePrompt(status.digest, workdir, run.cli.writeToolPhrase));
|
|
643
|
+
const spec = run.cli.agent({
|
|
644
|
+
credentials: run.credentials,
|
|
645
|
+
model: run.model,
|
|
486
646
|
workdir
|
|
487
647
|
});
|
|
488
648
|
process.stderr.write(`[${tag}] mining ${unit.project}/${unit.session} (${status.turns} turns, ${status.reason})\n`);
|
|
@@ -490,8 +650,8 @@ async function mineOneUnit({ unit, status }, opts, token) {
|
|
|
490
650
|
mount: opts.projects,
|
|
491
651
|
workdir,
|
|
492
652
|
spec,
|
|
493
|
-
network:
|
|
494
|
-
onLine: progressSink(tag)
|
|
653
|
+
network: { onlyDomains: run.cli.egressDomains },
|
|
654
|
+
onLine: progressSink(tag, run.cli.describeStreamLine)
|
|
495
655
|
});
|
|
496
656
|
if (!outcome.ok) {
|
|
497
657
|
process.stderr.write(`[${tag}] FAILED (${outcome.reason})\n`);
|
|
@@ -503,13 +663,14 @@ async function mineOneUnit({ unit, status }, opts, token) {
|
|
|
503
663
|
source: unit.jsonlPath,
|
|
504
664
|
sourceSha256: status.sourceSha256,
|
|
505
665
|
minedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
506
|
-
model:
|
|
666
|
+
model: run.model,
|
|
507
667
|
humanTurns: status.turns
|
|
508
668
|
}, outcome.report));
|
|
509
669
|
process.stderr.write(`[${tag}] observations: ${obsPath}\n`);
|
|
510
670
|
return true;
|
|
511
671
|
}
|
|
512
672
|
async function runMine(opts) {
|
|
673
|
+
resolveAgentCli(opts.agent);
|
|
513
674
|
const plan = planMine(enumerateUnits(opts.projects).map((unit) => ({
|
|
514
675
|
unit,
|
|
515
676
|
status: unitStatus(unit, opts.catalog, opts.minTurns)
|
|
@@ -521,10 +682,9 @@ async function runMine(opts) {
|
|
|
521
682
|
return true;
|
|
522
683
|
}
|
|
523
684
|
if (plan.todo.length === 0) return true;
|
|
524
|
-
const
|
|
525
|
-
if (!token) throw new Error("CLAUDE_CODE_OAUTH_TOKEN is required in the environment (it is injected into each miner's sandbox)");
|
|
685
|
+
const run = resolveAgentRun(opts.agent, opts.model, "mine");
|
|
526
686
|
const queue = new PQueue({ concurrency: opts.parallel });
|
|
527
|
-
const results = await Promise.all(plan.todo.map((p) => queue.add(() => mineOneUnit(p, opts,
|
|
687
|
+
const results = await Promise.all(plan.todo.map((p) => queue.add(() => mineOneUnit(p, opts, run))));
|
|
528
688
|
const failed = results.filter((ok) => ok !== true).length;
|
|
529
689
|
process.stderr.write(`mined ${results.length - failed}/${results.length}${failed ? `, ${failed} FAILED` : ""}\n`);
|
|
530
690
|
return failed === 0;
|
|
@@ -541,12 +701,13 @@ function parsePositiveInt(flag) {
|
|
|
541
701
|
}
|
|
542
702
|
const collect = (v, prev) => [...prev, v];
|
|
543
703
|
const program = new Command().name("doublecheck").description("Run self-authored LLM code-inspectors (checks) against a target tree, one sandboxed agent per check");
|
|
544
|
-
program.command("check").option("--target <dir>", "tree under inspection, mounted read-only", process.cwd()).option("--checks-dir <dir>", "checks directory (repeatable; default: $TARGET/.agents/checks)", collect, []).option("--context <file>", "run-context file spliced into every inspector's prompt (intent, nuances, sanctioned exceptions, scope)").option("--model <model>", "model for the inspector agents
|
|
704
|
+
program.command("check").option("--target <dir>", "tree under inspection, mounted read-only", process.cwd()).option("--checks-dir <dir>", "checks directory (repeatable; default: $TARGET/.agents/checks)", collect, []).option("--context <file>", "run-context file spliced into every inspector's prompt (intent, nuances, sanctioned exceptions, scope)").option("--agent <name>", "agent CLI that runs the inspectors: claude or codex", "claude").option("--model <model>", "model for the inspector agents (default per agent: claude haiku, codex gpt-5.5)").option("--parallel <n>", "max concurrent checks", parsePositiveInt("--parallel"), 4).option("--output <dir>", "reports root (default: $TARGET/.doublecheck)").option("--check <name>", "run only this check (repeatable)", collect, []).option("--save-jsonl", "persist each inspector's raw stream-json beside its report (audit trail)").action(async (opts) => {
|
|
545
705
|
const target = resolve(opts.target);
|
|
546
706
|
if (!await runChecks({
|
|
547
707
|
target,
|
|
548
708
|
checksDirs: opts.checksDir.length > 0 ? opts.checksDir.map((d) => resolve(d)) : [join(target, ".agents", "checks")],
|
|
549
709
|
contextFile: opts.context ? resolve(opts.context) : null,
|
|
710
|
+
agent: opts.agent,
|
|
550
711
|
model: opts.model,
|
|
551
712
|
parallel: opts.parallel,
|
|
552
713
|
output: opts.output ? resolve(opts.output) : join(target, ".doublecheck"),
|
|
@@ -554,10 +715,11 @@ program.command("check").option("--target <dir>", "tree under inspection, mounte
|
|
|
554
715
|
saveJsonl: !!opts.saveJsonl
|
|
555
716
|
})) process.exitCode = 1;
|
|
556
717
|
});
|
|
557
|
-
program.command("mine").option("--projects <dir>", "Claude Code transcripts root", join(homedir(), ".claude", "projects")).option("--catalog <dir>", "observation catalog root", join(homedir(), ".doublecheck", "catalog")).option("--model <model>", "model for the mining agents
|
|
718
|
+
program.command("mine").option("--projects <dir>", "Claude Code transcripts root", join(homedir(), ".claude", "projects")).option("--catalog <dir>", "observation catalog root", join(homedir(), ".doublecheck", "catalog")).option("--agent <name>", "agent CLI that runs the miners: claude or codex", "claude").option("--model <model>", "model for the mining agents (default per agent: claude opus, codex gpt-5.5 — a bad-model mine pollutes a durable asset)").option("--parallel <n>", "max concurrent miners", parsePositiveInt("--parallel"), 4).option("--min-turns <n>", "min genuine human turns for a real conversation", parsePositiveInt("--min-turns"), 2).option("--limit <n>", "mine at most N pending units", parsePositiveInt("--limit")).option("--dry-run", "list what would be mined without booting anything").action(async (opts) => {
|
|
558
719
|
if (!await runMine({
|
|
559
720
|
projects: resolve(opts.projects),
|
|
560
721
|
catalog: resolve(opts.catalog),
|
|
722
|
+
agent: opts.agent,
|
|
561
723
|
model: opts.model,
|
|
562
724
|
parallel: opts.parallel,
|
|
563
725
|
minTurns: opts.minTurns,
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/contract.ts","../src/claude.ts","../src/runner.ts","../src/check.ts","../src/transcript.ts","../src/catalog.ts","../src/mine.ts","../src/cli.ts"],"sourcesContent":["// The agent contract: what the harness stages for the agent and what the\n// agent must leave behind. Any adapter (claude today, codex someday) runs\n// under this same contract, which is what keeps the harness CLI-agnostic.\n\n// Staged into the scratch workdir before boot; the rw bind mount delivers it.\nexport const PROMPT_FILE = \"prompt.txt\"\n// The agent writes its findings here (relative to its cwd); the host reads it\n// back after exit. Missing report = failure.\nexport const REPORT_FILE = \"report.md\"\n\n// Weak models reliably skip a report contract stated once at the prompt's\n// tail unless it leads with the stakes and orders the file created first\n// (measured: haiku went from ~50% to 3/3 with this wording). Two hard-won\n// additions (2026-07-05, all three cutover inspectors lost their reports):\n// the path is ABSOLUTE — agents cd into the inspected tree to run git, and a\n// relative ./report.md then lands in the read-only mount — and the\n// create-first order is repeated near the top of the prompt (see\n// firstActionLine), because on long prompts the tail alone loses its grip.\nfunction reportContract(activity: string, deliverable: string, workdir: string): string {\n\treturn `## Report — the only output that counts\n\nYour final chat reply is discarded. The only thing read back is the file \\`${workdir}/${REPORT_FILE}\\` — if it does not exist when you exit, this run FAILS. Always use that absolute path: if you \\`cd\\` elsewhere (into the inspected tree, say), a relative \\`./${REPORT_FILE}\\` lands in the wrong place, or in a read-only mount where the write fails.\n\nSo: create \\`${workdir}/${REPORT_FILE}\\` with the Write tool BEFORE you start ${activity} (a title line is enough), and update it as you work. The final state of the file when you finish is ${deliverable}.`\n}\n\nfunction firstActionLine(workdir: string): string {\n\treturn `Your FIRST action, before anything else: create \\`${workdir}/${REPORT_FILE}\\` with the Write tool (a title line is enough). The full report contract is at the end of this prompt.`\n}\n\n// Environment preamble + optional run context + check body + report\n// contract. Checks are timeless standards; everything run-specific (intent,\n// sanctioned exceptions, scope) arrives in the operator's run context — the\n// harness knows nothing about git.\nexport function composePrompt(opts: {\n\tcheckBody: string\n\ttarget: string\n\tworkdir: string\n\trunContext: string | null\n}): string {\n\tconst contextSection = opts.runContext\n\t\t? `## Run context (from the operator)\n\nThe operator's brief for this run: the intent behind the work under review, known nuances, sanctioned exceptions, and possibly a Scope naming exactly what is under review. Judge requestedness against it — what the brief explicitly sanctions is not a finding. If it names a Scope, the Scope defines the change under review: findings must concern that change, and the rest of the tree is reference for judgment — with one exception. For concepts the scoped change retires or renames, surviving references anywhere in the tree are reportable findings: the Scope bounds what counts as the change, not where survivors may hide.\n\n${opts.runContext}\n\n---\n\n`\n\t\t: \"\"\n\treturn `## Environment\n\nYou are a code inspector running inside a sandboxed microVM with full permissions and unrestricted internet access.\n\n- The code under inspection is mounted read-only at \\`${opts.target}\\`. You cannot modify it — inspect, don't fix.\n- \\`git\\` and \\`rg\\` are installed.\n- Your current working directory is a writable scratch workspace: \\`${opts.workdir}\\`.\n\n${firstActionLine(opts.workdir)}\n\n## Inspection discipline\n\nMeasured on real runs: inspectors that skip these rules produce reports whose verdicts are right but whose evidence is partly fabricated — which is worse than no report.\n\n- Before inspecting, enumerate what is under review (the Run context's Scope list if one is given; otherwise your own enumeration of the target) and keep a ledger as you work: **examined** (content actually opened), **name-only**, **not examined**. End your report with that ledger, with a one-line reason for anything not examined.\n- Use verification verbs — \"verified\", \"confirmed\", \"checked\", \"read\", \"diffed\", \"grepped\", \"spot-checked\" — ONLY for actions you actually performed in this session on content you actually opened. Everything else must be labeled as inference: \"assumed identical by generation — not opened.\"\n- Never truncate output you will base a claim on — no \\`| head\\` / \\`| tail\\` on a diff or search you intend to cite. Examine files one at a time instead.\n\n---\n\n${contextSection}${opts.checkBody}\n\n---\n\n${reportContract(\"inspecting\", \"the check's report\", opts.workdir)}\n\nEvery single line of your report will be read in full by the operator's agent — nothing is skimmed, so every line costs attention. Verify each finding against the actual code before writing it down. A report that says \"no findings\" is a perfectly good report; an unverified finding is not.`\n}\n\n// The mining prompt: digest of one conversation + what counts as a durable\n// preference observation + the block format the catalog accumulates.\nexport function composeMinePrompt(digest: string, workdir: string): string {\n\treturn `## Environment\n\nYou are running inside a sandboxed microVM with no network access. The machine's Claude Code transcripts are mounted read-only at their real paths; your current working directory is a writable scratch workspace: \\`${workdir}\\`.\n\n${firstActionLine(workdir)}\n\n## Task\n\nYou are mining ONE Claude Code conversation for the operator's durable preferences — how they want engineering done, in any conversation, not what they wanted built in this one.\n\nBelow is the conversation digest: every genuine human turn, numbered, in order. When a turn is a terse correction or interruption (\"no\", \"stop\", \"don't\", \"why did you…\", \"[Request interrupted]\"), recover its context: the \"# source:\" header names the full session transcript — Grep that turn's text in it to find its line, then Read the assistant turns IMMEDIATELY BEFORE it with offset/limit. The preference lives in the contrast between what the assistant did and what the operator demanded. NEVER read a transcript whole — some are tens of MB.\n\n---\n\n${digest}\n\n---\n\n## What counts as an observation (strict)\n\nA durable preference, backed by a verbatim quote from the digest or transcript:\n\n- **kind: code** — how code should be written/structured/shaped (\"no silent fallbacks\", \"reuse the existing helper instead of hand-rolling\").\n- **kind: workflow** — how work should proceed (\"verify before claiming done\", \"ask before making scope decisions\").\n- **kind: style** — how prose/output/docs should read.\n\nExclude task-of-the-moment directives with no durable signal (\"add a tab to home.tsx\", \"kill the dev server\"). Exclude anything you cannot quote. Be conservative: a weak or speculative observation is worse than none.\n\n## Observation format\n\nThe report is observation blocks and NOTHING else — no title, no preamble, no summary or conclusion sections. One block per observation:\n\n## <kebab-case-name>\n- **observation:** <one sentence: the durable preference>\n- **kind:** code | workflow | style\n- **evidence:** \"<verbatim quote>\" — turn <N>\n\nFor **code** observations that an LLM reviewer could check against a diff or file tree (a concrete change could flip PASS↔FAIL), and ONLY for those — never on workflow or style blocks — ALSO add:\n\n- **pass/fail:** PASS when <…>; FAIL when <…>\n- **why-LLM:** <why a deterministic linter cannot enforce this>\n- **scope:** diff-only | needs-tree\n\nIf the conversation carries no durable preference signal, the report is exactly these two lines and nothing more:\n\nNo durable preference signal.\n<one sentence: what the conversation was about instead>\n\n${reportContract(\"mining\", \"the mined observations\", workdir)}`\n}\n","import { PROMPT_FILE } from \"./contract.ts\"\nimport type { AgentSpec } from \"./runner.ts\"\n\nconst GUEST_HOME = \"/root\"\n\n// The claude adapter: produce the AgentSpec that runs `claude -p` in the guest.\nexport function claudeAgent(opts: {\n\ttoken: string\n\tmodel: string\n\tworkdir: string\n}): AgentSpec {\n\treturn {\n\t\t// Headless claude -p cannot answer permission prompts, so in \"default\"\n\t\t// mode every tool is auto-denied and the agent produces nothing. The\n\t\t// microVM is the safety boundary here, so bypass the per-tool gate;\n\t\t// IS_SANDBOX=1 is what lets that run as root without claude's refusal.\n\t\tcommand:\n\t\t\t\"claude -p --no-session-persistence \" +\n\t\t\t\"--dangerously-skip-permissions \" +\n\t\t\t// --verbose is required by claude -p with stream-json output; it only\n\t\t\t// widens what lands on stdout, which describeStreamLine already labels.\n\t\t\t\"--output-format stream-json --verbose \" +\n\t\t\t// Not a permission gate — a contract guard. The default headless tool\n\t\t\t// set omits Glob/Grep and includes harness tools (ReportFindings,\n\t\t\t// TaskCreate, …) that hijack the report: haiku \"reports findings\"\n\t\t\t// through them and never writes report.md (verified live). This pins\n\t\t\t// the inspector's full toolkit and nothing else.\n\t\t\t`--tools Task Bash Read Write Edit Glob Grep WebSearch WebFetch < ${PROMPT_FILE}`,\n\t\tenv: {\n\t\t\tHOME: GUEST_HOME,\n\t\t\tIS_SANDBOX: \"1\",\n\t\t\tGIT_OPTIONAL_LOCKS: \"0\",\n\t\t\tANTHROPIC_MODEL: opts.model,\n\t\t\tCLAUDE_CODE_OAUTH_TOKEN: opts.token,\n\t\t},\n\t\t// Minimal trust-accepted entry keyed to the guest cwd so headless claude\n\t\t// skips the trust prompt. Deliberately NOT a copy of any host .claude.json.\n\t\tfiles: [\n\t\t\t{\n\t\t\t\tpath: `${GUEST_HOME}/.claude.json`,\n\t\t\t\tcontent: `${JSON.stringify(\n\t\t\t\t\t{ projects: { [opts.workdir]: { hasTrustDialogAccepted: true } } },\n\t\t\t\t\tnull,\n\t\t\t\t\t\"\\t\",\n\t\t\t\t)}\\n`,\n\t\t\t},\n\t\t],\n\t}\n}\n\ninterface StreamLine {\n\ttype?: string\n\tsubtype?: string\n\tmessage?: { content?: Array<{ type?: string; text?: string; name?: string }> }\n\tduration_ms?: number\n}\n\n// One human-readable label per stream-json stdout line; null for lines that\n// aren't claude's JSON (dropped from the progress stream).\nexport function describeStreamLine(line: string): string | null {\n\tlet obj: StreamLine\n\ttry {\n\t\tobj = JSON.parse(line) as StreamLine\n\t} catch {\n\t\treturn null\n\t}\n\tconst label = (obj.type ?? \"\") + (obj.subtype ? `:${obj.subtype}` : \"\")\n\t// A once-per-second heartbeat with no content — pure noise in the stream.\n\tif (label === \"system:thinking_tokens\") return null\n\tlet detail = \"\"\n\tif (obj.type === \"assistant\") {\n\t\tconst content = obj.message?.content ?? []\n\t\tconst tools = content\n\t\t\t.filter((b) => b.type === \"tool_use\" && b.name)\n\t\t\t.map((b) => b.name)\n\t\tconst chars = content.reduce((n, b) => n + (b.text?.length ?? 0), 0)\n\t\tdetail = tools.length ? ` ${tools.join(\",\")}` : ` ${chars} chars`\n\t} else if (obj.type === \"result\") {\n\t\tdetail = ` ${obj.subtype}, ${((obj.duration_ms ?? 0) / 1000).toFixed(1)}s`\n\t}\n\treturn `[${label}]${detail}`\n}\n\n// The per-unit progress sink both workflow shells hang on runAgent: guest\n// stderr passes through as `[label] ! line`, stdout is claude's stream-json,\n// labelled by describeStreamLine (non-JSON and heartbeat lines dropped).\nexport function progressSink(\n\tlabel: string,\n): (kind: \"stdout\" | \"stderr\", line: string) => void {\n\treturn (kind, line) => {\n\t\tif (kind === \"stderr\") {\n\t\t\tprocess.stderr.write(`[${label}] ! ${line}\\n`)\n\t\t\treturn\n\t\t}\n\t\tconst described = describeStreamLine(line)\n\t\tif (described) process.stderr.write(`[${label}] ${described}\\n`)\n\t}\n}\n","import { existsSync, readFileSync } from \"node:fs\"\nimport { basename, join } from \"node:path\"\nimport type { ExecEvent } from \"microsandbox\"\nimport { REPORT_FILE } from \"./contract.ts\"\n\n// microsandbox exports its fluent builders as value-only consts; recover their\n// instance types via a type-only import (erased at build) so the native module\n// only loads behind the `await import` in runAgent.\ntype MountBuilder = InstanceType<typeof import(\"microsandbox\").MountBuilder>\ntype PatchBuilder = InstanceType<typeof import(\"microsandbox\").PatchBuilder>\ntype ExecOptionsBuilder = InstanceType<typeof import(\"microsandbox\").ExecOptionsBuilder>\n// The SDK types `.network(cb)` as `(b: any) => any`, and we call the JS-shim\n// `.policy(...)`, absent from the native builder's types.\n// biome-ignore lint/suspicious/noExplicitAny: SDK types this builder callback as any\ntype NetworkBuilder = any\n\n// Locally built (Dockerfile.guest): node 24 slim + git/ripgrep/curl/wget + the\n// claude CLI baked in. Side-loaded into the microsandbox cache by\n// ./scripts/build-guest-image.sh — it exists nowhere else, hence pullPolicy\n// \"never\": a cache miss means \"run the build script\", not \"try Docker Hub\".\nconst GUEST_IMAGE = \"doublecheck-guest:latest\"\nconst GUEST_MEMORY_MIB = 2048\n\n// Staged into the guest before boot (e.g. claude's trust-accepted config).\nexport interface GuestFile {\n\tpath: string\n\tcontent: string\n}\n\n// What it takes to run one agent in a booted guest: a bash command executed in\n// the scratch cwd that must leave REPORT_FILE there, the env it needs, and\n// files staged into the guest. Plain data — adapters produce it, this runner\n// consumes it, tests fake it.\nexport interface AgentSpec {\n\tcommand: string\n\tenv: Record<string, string>\n\tfiles: GuestFile[]\n}\n\nexport type AgentOutcome =\n\t| { ok: true; report: string }\n\t| { ok: false; reason: string; partialReport: string | null }\n\n// Pure line buffering: fold a chunk into the carry, emit complete non-blank\n// lines, keep the unterminated tail.\nexport function feedLines(\n\tcarry: string,\n\tchunk: string,\n): { lines: string[]; carry: string } {\n\tconst parts = (carry + chunk).split(\"\\n\")\n\tconst rest = parts.pop() ?? \"\"\n\treturn { lines: parts.filter((l) => l.trim()), carry: rest }\n}\n\n// Pure outcome decision: what the exec left behind → what it means.\nexport function decideOutcome(\n\texitCode: number | null,\n\treport: string | null,\n\tworkdir: string,\n): AgentOutcome {\n\tif (exitCode !== 0) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\treason: `agent process exited ${exitCode}`,\n\t\t\tpartialReport: report,\n\t\t}\n\t}\n\tif (report === null) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\treason: `agent exited 0 but wrote no ${REPORT_FILE} (work dir: ${workdir})`,\n\t\t\tpartialReport: null,\n\t\t}\n\t}\n\treturn { ok: true, report }\n}\n\nexport interface RunAgentOpts {\n\t// Host dir the agent inspects, bind-mounted READ-ONLY at its real host\n\t// path: the target for `check`, the transcripts corpus for `mine`.\n\tmount: string\n\t// Scratch dir with PROMPT_FILE already inside; bind-mounted rw as the guest\n\t// cwd at its identical host path, so the agent's report lands back on the host.\n\tworkdir: string\n\tspec: AgentSpec\n\t// \"all\" for checks (inspectors may research); \"anthropic-only\" for miners:\n\t// the personal corpus is mounted, so the only reachable destination is the\n\t// API the agent already sends its context to. NetworkPolicy.none() is not\n\t// an option here — it kills DNS entirely and the adapter can't reach its\n\t// own model API (measured: claude retries ~180s then exits 1).\n\tnetwork: \"all\" | \"anthropic-only\"\n\tonLine: (kind: \"stdout\" | \"stderr\", line: string) => void\n}\n\n// Boot a guest (ro mount + scratch rw as cwd), exec the agent command, stream\n// its output line-by-line, read the report back off the scratch dir, tear\n// down. Agent-level failures (exit ≠ 0, no report) return ok:false;\n// harness-level failures (image missing, boot error) throw.\nexport async function runAgent(opts: RunAgentOpts): Promise<AgentOutcome> {\n\tconst microsandbox = await import(\"microsandbox\")\n\tconst name = `doublecheck-${basename(opts.workdir)}`\n\tconst policy =\n\t\topts.network === \"all\"\n\t\t\t? microsandbox.NetworkPolicy.allowAll()\n\t\t\t: microsandbox.NetworkPolicy.builder()\n\t\t\t\t\t.defaultDeny()\n\t\t\t\t\t.egress((rb) => rb.allow((d) => d.domainSuffix(\"anthropic.com\")))\n\t\t\t\t\t.build()\n\tlet sandbox: InstanceType<typeof microsandbox.Sandbox> | null = null\n\ttry {\n\t\ttry {\n\t\t\tsandbox = await microsandbox.Sandbox.builder(name)\n\t\t\t\t.image(GUEST_IMAGE)\n\t\t\t\t.memory(GUEST_MEMORY_MIB)\n\t\t\t\t.pullPolicy(\"never\")\n\t\t\t\t.replace()\n\t\t\t\t.workdir(opts.workdir)\n\t\t\t\t.envs(opts.spec.env)\n\t\t\t\t.volume(opts.mount, (mb: MountBuilder) => mb.bind(opts.mount).readonly())\n\t\t\t\t.volume(opts.workdir, (mb: MountBuilder) => mb.bind(opts.workdir))\n\t\t\t\t.patch((pb: PatchBuilder) => {\n\t\t\t\t\tlet p = pb\n\t\t\t\t\tfor (const f of opts.spec.files) {\n\t\t\t\t\t\tp = p.text(f.path, f.content, { mode: 0o600, replace: true })\n\t\t\t\t\t}\n\t\t\t\t\treturn p\n\t\t\t\t})\n\t\t\t\t.network((nb: NetworkBuilder) => nb.policy(policy))\n\t\t\t\t.create()\n\t\t} catch (e) {\n\t\t\tif (String(e).includes(\"not cached\")) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`guest image ${GUEST_IMAGE} is not in the microsandbox cache — run scripts/build-guest-image.sh (${String(e)})`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow e\n\t\t}\n\n\t\tconst handle = await sandbox.execStreamWith(\"bash\", (e: ExecOptionsBuilder) =>\n\t\t\te.args([\"-c\", opts.spec.command]),\n\t\t)\n\n\t\tconst carry: Record<\"stdout\" | \"stderr\", string> = { stdout: \"\", stderr: \"\" }\n\t\tlet exitCode: number | null = null\n\t\tfor (;;) {\n\t\t\tconst ev: ExecEvent | null = await handle.recv()\n\t\t\tif (ev === null) break\n\t\t\tif (ev.kind === \"stdout\" || ev.kind === \"stderr\") {\n\t\t\t\tconst fed = feedLines(carry[ev.kind], Buffer.from(ev.data).toString())\n\t\t\t\tcarry[ev.kind] = fed.carry\n\t\t\t\tfor (const line of fed.lines) opts.onLine(ev.kind, line)\n\t\t\t} else if (ev.kind === \"exited\") exitCode = ev.code\n\t\t}\n\t\tfor (const kind of [\"stdout\", \"stderr\"] as const) {\n\t\t\tif (carry[kind].trim()) opts.onLine(kind, carry[kind])\n\t\t}\n\n\t\tconst reportPath = join(opts.workdir, REPORT_FILE)\n\t\tconst report = existsSync(reportPath) ? readFileSync(reportPath, \"utf-8\") : null\n\t\treturn decideOutcome(exitCode, report, opts.workdir)\n\t} finally {\n\t\tif (sandbox) {\n\t\t\ttry {\n\t\t\t\tawait sandbox.stop()\n\t\t\t} catch (e) {\n\t\t\t\topts.onLine(\"stderr\", `sandbox stop failed: ${String(e)}`)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait microsandbox.Sandbox.remove(name)\n\t\t\t} catch (e) {\n\t\t\t\topts.onLine(\"stderr\", `sandbox remove failed: ${String(e)}`)\n\t\t\t}\n\t\t}\n\t}\n}\n","import {\n\tappendFileSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\twriteFileSync,\n} from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport PQueue from \"p-queue\"\nimport { claudeAgent, progressSink } from \"./claude.ts\"\nimport { composePrompt, PROMPT_FILE, REPORT_FILE } from \"./contract.ts\"\nimport { runAgent } from \"./runner.ts\"\n\n// A check is a plain markdown file — no frontmatter, no schema. The body is\n// the inspector's instructions; the name is the filename minus .md. Checks\n// come from one or more --checks-dir directories (default:\n// $TARGET/.agents/checks).\nexport interface Check {\n\tname: string\n\tbody: string\n}\n\n// Pure selection: `only` (from repeated --check flags) filters, in the order\n// named; naming a check that doesn't exist is an error, not an empty run.\nexport function selectChecks(all: Check[], only: string[], where: string): Check[] {\n\tif (all.length === 0) throw new Error(`no checks (*.md) in ${where}`)\n\tif (only.length === 0) return all\n\tconst byName = new Map(all.map((c) => [c.name, c]))\n\treturn only.map((name) => {\n\t\tconst check = byName.get(name)\n\t\tif (!check) {\n\t\t\tconst have = all.map((c) => c.name).join(\", \")\n\t\t\tthrow new Error(`no check named \"${name}\" in ${where} (have: ${have})`)\n\t\t}\n\t\treturn check\n\t})\n}\n\n// Shell: read every check file from every checks dir, then select purely.\n// A missing dir is an error; the same check name in two dirs is an error —\n// no precedence rules, rename one.\nexport function discoverChecks(dirs: string[], only: string[]): Check[] {\n\tconst all: Array<Check & { dir: string }> = []\n\tfor (const dir of dirs) {\n\t\tlet entries: string[]\n\t\ttry {\n\t\t\tentries = readdirSync(dir)\n\t\t} catch {\n\t\t\tthrow new Error(`no checks directory at ${dir}`)\n\t\t}\n\t\tfor (const f of entries.filter((e) => e.endsWith(\".md\")).sort()) {\n\t\t\tconst name = f.slice(0, -\".md\".length)\n\t\t\tconst clash = all.find((c) => c.name === name)\n\t\t\tif (clash) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`check \"${name}\" exists in both ${clash.dir} and ${dir} — rename one`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tall.push({ name, body: readFileSync(join(dir, f), \"utf-8\"), dir })\n\t\t}\n\t}\n\tconst checks = all\n\t\t.map(({ name, body }) => ({ name, body }))\n\t\t.sort((a, b) => a.name.localeCompare(b.name))\n\treturn selectChecks(checks, only, dirs.join(\", \"))\n}\n\nexport interface CheckWorkflowOpts {\n\ttarget: string\n\tchecksDirs: string[]\n\tcontextFile: string | null\n\tmodel: string\n\tparallel: number\n\toutput: string\n\tonly: string[]\n\tsaveJsonl: boolean\n}\n\n// fs-safe local timestamp, one dir per run shared by all its checks:\n// 2026-07-05-143000\nexport function runTimestamp(d: Date): string {\n\tconst p = (n: number) => String(n).padStart(2, \"0\")\n\treturn `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`\n}\n\n// The report written when the agent failed: the failure is the report, plus\n// whatever partial report the agent managed to leave.\nexport function failureReport(reason: string, partialReport: string | null): string {\n\tconst partial = partialReport\n\t\t? `\\n---\\n\\nPartial ${REPORT_FILE} left by the agent:\\n\\n${partialReport}`\n\t\t: \"\"\n\treturn `# CHECK FAILED\\n\\n${reason}\\n${partial}`\n}\n\nasync function runOneCheck(\n\tcheck: Check,\n\topts: CheckWorkflowOpts,\n\ttoken: string,\n\trunContext: string | null,\n\toutDir: string,\n): Promise<boolean> {\n\tconst workdir = mkdtempSync(join(tmpdir(), `doublecheck-${check.name}-`))\n\twriteFileSync(\n\t\tjoin(workdir, PROMPT_FILE),\n\t\tcomposePrompt({\n\t\t\tcheckBody: check.body,\n\t\t\ttarget: opts.target,\n\t\t\tworkdir,\n\t\t\trunContext,\n\t\t}),\n\t)\n\tconst spec = claudeAgent({ token, model: opts.model, workdir })\n\t// The report is the verdict; with --save-jsonl the raw stream is kept\n\t// beside it so a run can be audited after the ephemeral guest is gone\n\t// (which files the inspector read, what it skipped, whether report claims\n\t// trace to actual observations).\n\tconst sink = progressSink(check.name)\n\tlet onLine = sink\n\tif (opts.saveJsonl) {\n\t\tconst streamPath = join(outDir, `${check.name}.stream.jsonl`)\n\t\tonLine = (kind, line) => {\n\t\t\tif (kind === \"stdout\") appendFileSync(streamPath, `${line}\\n`)\n\t\t\tsink(kind, line)\n\t\t}\n\t}\n\tconst outcome = await runAgent({\n\t\tmount: opts.target,\n\t\tworkdir,\n\t\tspec,\n\t\tnetwork: \"all\",\n\t\tonLine,\n\t})\n\tconst reportPath = join(outDir, `${check.name}.md`)\n\tif (outcome.ok) {\n\t\twriteFileSync(reportPath, outcome.report)\n\t\tprocess.stderr.write(`[${check.name}] report: ${reportPath}\\n`)\n\t\treturn true\n\t}\n\twriteFileSync(reportPath, failureReport(outcome.reason, outcome.partialReport))\n\tprocess.stderr.write(`[${check.name}] FAILED (${outcome.reason}): ${reportPath}\\n`)\n\treturn false\n}\n\n// The check workflow: one sandboxed agent per check, at most `parallel`\n// guests at once. Returns true only when every check produced a report; agent\n// failures still write a failure-record report and flip the run to false.\nexport async function runChecks(opts: CheckWorkflowOpts): Promise<boolean> {\n\tconst token = process.env.CLAUDE_CODE_OAUTH_TOKEN\n\tif (!token) {\n\t\tthrow new Error(\n\t\t\t\"CLAUDE_CODE_OAUTH_TOKEN is required in the environment (it is injected into each check's sandbox)\",\n\t\t)\n\t}\n\tconst checks = discoverChecks(opts.checksDirs, opts.only)\n\tlet runContext: string | null = null\n\tif (opts.contextFile) {\n\t\ttry {\n\t\t\trunContext = readFileSync(opts.contextFile, \"utf-8\")\n\t\t} catch (e) {\n\t\t\tthrow new Error(\n\t\t\t\t`--context file not readable: ${opts.contextFile} (${String(e)})`,\n\t\t\t)\n\t\t}\n\t}\n\tconst outDir = join(opts.output, runTimestamp(new Date()))\n\tmkdirSync(outDir, { recursive: true })\n\tprocess.stderr.write(\n\t\t`${checks.length} check(s) against ${opts.target} (model ${opts.model}, parallel ${opts.parallel})\\n`,\n\t)\n\tconst queue = new PQueue({ concurrency: opts.parallel })\n\tconst results = await Promise.all(\n\t\tchecks.map((check) =>\n\t\t\tqueue.add(() => runOneCheck(check, opts, token, runContext, outDir)),\n\t\t),\n\t)\n\treturn results.every((ok) => ok === true)\n}\n","// Claude Code transcript (.jsonl) → the genuine human turns → a flat digest.\n// Port of the June-2026 jq extraction, verified against the same corpus: a\n// \"genuine human turn\" is type:user, not meta, not sidechain, text content\n// only — which excludes the three things that are also type:user in the JSONL\n// (tool_results, slash-command stdout wrappers, injected system-reminders).\n\n// Per-turn char cap in the digest; the mining agent greps the source jsonl\n// for full text when it needs context around a turn.\nconst TURN_CHAR_CAP = 2000\n\ninterface RawLine {\n\ttype?: string\n\tisMeta?: boolean\n\tisSidechain?: boolean\n\tmessage?: { content?: unknown }\n}\n\nconst NON_HUMAN_TEXT =\n\t/^\\s*<command-name>|<local-command-stdout>|^\\s*<local-command|^\\s*<system-reminder>|Caveat: The messages below/\n\n// Throws on an unparseable line — the caller decides what an unreadable\n// transcript means for its run (mine counts and reports them, visibly).\nexport function humanTurns(jsonl: string): string[] {\n\tconst turns: string[] = []\n\tfor (const line of jsonl.split(\"\\n\")) {\n\t\tif (!line.trim()) continue\n\t\tconst raw = JSON.parse(line) as RawLine\n\t\tif (raw.type !== \"user\" || raw.isMeta || raw.isSidechain) continue\n\t\tconst content = raw.message?.content\n\t\tconst text =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? content\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? content\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(item): item is { type: \"text\"; text: string } =>\n\t\t\t\t\t\t\t\t\ttypeof item === \"object\" &&\n\t\t\t\t\t\t\t\t\titem !== null &&\n\t\t\t\t\t\t\t\t\t(item as { type?: string }).type === \"text\" &&\n\t\t\t\t\t\t\t\t\ttypeof (item as { text?: unknown }).text === \"string\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.map((item) => item.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\"\n\t\tif (!text) continue\n\t\tif (NON_HUMAN_TEXT.test(text)) continue\n\t\tturns.push(text.replace(/[\\n\\r]+/g, \" ⏎ \").slice(0, TURN_CHAR_CAP))\n\t}\n\treturn turns\n}\n\nexport interface DigestMeta {\n\tsource: string\n\tproject: string\n\tsession: string\n}\n\nexport function renderDigest(meta: DigestMeta, turns: string[]): string {\n\tconst numbered = turns\n\t\t.map((t, i) => `${String(i + 1).padStart(3, \" \")} | ${t}`)\n\t\t.join(\"\\n\")\n\treturn `# source: ${meta.source}\n# project: ${meta.project}\n# session: ${meta.session}\n# human_turns: ${turns.length}\n# To understand WHY a terse turn or interruption happened, grep its text in the\n# source file above and read the assistant turn(s) immediately before it.\n\n${numbered}\n`\n}\n","import { createHash } from \"node:crypto\"\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\"\nimport { basename, join } from \"node:path\"\nimport { humanTurns, renderDigest } from \"./transcript.ts\"\n\n// One minable unit = one top-level transcript $PROJECTS/<project>/<session>.jsonl.\n// Its catalog home mirrors that path: $CATALOG/<project>/<session>/observations.md.\nexport interface Unit {\n\tproject: string\n\tsession: string\n\tjsonlPath: string\n}\n\nexport function enumerateUnits(projectsDir: string): Unit[] {\n\tlet projects: string[]\n\ttry {\n\t\tprojects = readdirSync(projectsDir).sort()\n\t} catch {\n\t\tthrow new Error(`no transcripts directory at ${projectsDir}`)\n\t}\n\tconst units: Unit[] = []\n\tfor (const project of projects) {\n\t\tconst dir = join(projectsDir, project)\n\t\tif (!statSync(dir).isDirectory()) continue\n\t\tfor (const f of readdirSync(dir).sort()) {\n\t\t\tif (!f.endsWith(\".jsonl\")) continue\n\t\t\tunits.push({\n\t\t\t\tproject,\n\t\t\t\tsession: basename(f, \".jsonl\"),\n\t\t\t\tjsonlPath: join(dir, f),\n\t\t\t})\n\t\t}\n\t}\n\treturn units\n}\n\nexport function observationsPath(catalogDir: string, unit: Unit): string {\n\treturn join(catalogDir, unit.project, unit.session, \"observations.md\")\n}\n\nexport type UnitStatus =\n\t// hash matches the recorded source_sha256 — skipped without parsing\n\t| { kind: \"mined\" }\n\t| { kind: \"below-threshold\"; turns: number }\n\t| {\n\t\t\tkind: \"pending\"\n\t\t\treason: \"new\" | \"changed\"\n\t\t\tturns: number\n\t\t\tdigest: string\n\t\t\tsourceSha256: string\n\t }\n\t// transcript has an unparseable line; reported visibly, never mined\n\t| { kind: \"unreadable\"; error: string }\n\n// Pure decision core: everything already read, nothing left but judgment.\nexport function decideUnitStatus(input: {\n\tunit: Unit\n\tjsonl: Buffer\n\trecordedSha256: string | null\n\tminTurns: number\n}): UnitStatus {\n\tconst sourceSha256 = createHash(\"sha256\").update(input.jsonl).digest(\"hex\")\n\tif (input.recordedSha256 === sourceSha256) return { kind: \"mined\" }\n\tlet turns: string[]\n\ttry {\n\t\tturns = humanTurns(input.jsonl.toString(\"utf-8\"))\n\t} catch (e) {\n\t\treturn { kind: \"unreadable\", error: String(e) }\n\t}\n\tif (turns.length < input.minTurns)\n\t\treturn { kind: \"below-threshold\", turns: turns.length }\n\treturn {\n\t\tkind: \"pending\",\n\t\treason: input.recordedSha256 === null ? \"new\" : \"changed\",\n\t\tturns: turns.length,\n\t\tdigest: renderDigest(\n\t\t\t{\n\t\t\t\tsource: input.unit.jsonlPath,\n\t\t\t\tproject: input.unit.project,\n\t\t\t\tsession: input.unit.session,\n\t\t\t},\n\t\t\tturns,\n\t\t),\n\t\tsourceSha256,\n\t}\n}\n\n// Shell: read the unit's inputs, then decide purely.\nexport function unitStatus(unit: Unit, catalogDir: string, minTurns: number): UnitStatus {\n\tconst obsPath = observationsPath(catalogDir, unit)\n\treturn decideUnitStatus({\n\t\tunit,\n\t\tjsonl: readFileSync(unit.jsonlPath),\n\t\trecordedSha256: existsSync(obsPath)\n\t\t\t? readSourceSha256(readFileSync(obsPath, \"utf-8\"))\n\t\t\t: null,\n\t\tminTurns,\n\t})\n}\n\nexport interface ObservationsMeta {\n\tsource: string\n\tsourceSha256: string\n\tminedAt: string\n\tmodel: string\n\thumanTurns: number\n}\n\n// The host composes the final file: frontmatter it alone can vouch for, then\n// the agent's report verbatim.\nexport function composeObservationsFile(meta: ObservationsMeta, body: string): string {\n\treturn `---\nsource: ${meta.source}\nsource_sha256: ${meta.sourceSha256}\nmined_at: ${meta.minedAt}\nmodel: ${meta.model}\nhuman_turns: ${meta.humanTurns}\n---\n\n${body.trimEnd()}\n`\n}\n\nexport function readSourceSha256(observationsMd: string): string | null {\n\treturn observationsMd.match(/^source_sha256: ([0-9a-f]{64})$/m)?.[1] ?? null\n}\n","import { mkdirSync, mkdtempSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport PQueue from \"p-queue\"\nimport {\n\tcomposeObservationsFile,\n\tenumerateUnits,\n\tobservationsPath,\n\ttype Unit,\n\ttype UnitStatus,\n\tunitStatus,\n} from \"./catalog.ts\"\nimport { claudeAgent, progressSink } from \"./claude.ts\"\nimport { composeMinePrompt, PROMPT_FILE } from \"./contract.ts\"\nimport { runAgent } from \"./runner.ts\"\n\nexport interface MineOpts {\n\tprojects: string\n\tcatalog: string\n\tmodel: string\n\tparallel: number\n\tminTurns: number\n\tlimit?: number\n\tdryRun: boolean\n}\n\nexport interface Pending {\n\tunit: Unit\n\tstatus: Extract<UnitStatus, { kind: \"pending\" }>\n}\n\n// What one mine invocation will and won't do, decided purely from the\n// per-unit statuses.\nexport interface MinePlan {\n\ttodo: Pending[]\n\tpendingTotal: number\n\tmined: number\n\tbelowThreshold: number\n\tunreadable: { unit: Unit; error: string }[]\n}\n\nexport function planMine(\n\tentries: { unit: Unit; status: UnitStatus }[],\n\tlimit?: number,\n): MinePlan {\n\tconst pending = entries.filter((e): e is Pending => e.status.kind === \"pending\")\n\treturn {\n\t\ttodo: limit ? pending.slice(0, limit) : pending,\n\t\tpendingTotal: pending.length,\n\t\tmined: entries.filter((e) => e.status.kind === \"mined\").length,\n\t\tbelowThreshold: entries.filter((e) => e.status.kind === \"below-threshold\").length,\n\t\tunreadable: entries.flatMap((e) =>\n\t\t\te.status.kind === \"unreadable\"\n\t\t\t\t? [{ unit: e.unit, error: e.status.error }]\n\t\t\t\t: [],\n\t\t),\n\t}\n}\n\nexport function summarizeMinePlan(plan: MinePlan, minTurns: number): string {\n\tconst total =\n\t\tplan.pendingTotal + plan.mined + plan.belowThreshold + plan.unreadable.length\n\tconst limited =\n\t\tplan.todo.length !== plan.pendingTotal ? ` (mining ${plan.todo.length})` : \"\"\n\treturn `${total} transcripts: ${plan.pendingTotal} pending${limited}, ${plan.mined} mined, ${plan.belowThreshold} below ${minTurns} turns, ${plan.unreadable.length} unreadable`\n}\n\nexport function dryRunRow({ unit, status }: Pending): string {\n\treturn `${status.reason.padEnd(7)} ${status.turns.toString().padStart(4)} turns ${unit.project}/${unit.session}`\n}\n\nasync function mineOneUnit(\n\t{ unit, status }: Pending,\n\topts: MineOpts,\n\ttoken: string,\n): Promise<boolean> {\n\tconst tag = unit.session.slice(0, 8)\n\tconst workdir = mkdtempSync(join(tmpdir(), `doublecheck-mine-${tag}-`))\n\twriteFileSync(join(workdir, PROMPT_FILE), composeMinePrompt(status.digest, workdir))\n\tconst spec = claudeAgent({ token, model: opts.model, workdir })\n\tprocess.stderr.write(\n\t\t`[${tag}] mining ${unit.project}/${unit.session} (${status.turns} turns, ${status.reason})\\n`,\n\t)\n\tconst outcome = await runAgent({\n\t\tmount: opts.projects,\n\t\tworkdir,\n\t\tspec,\n\t\tnetwork: \"anthropic-only\",\n\t\tonLine: progressSink(tag),\n\t})\n\t// A failed mine writes NOTHING — the catalog is a durable asset; the next\n\t// run retries this unit because no hash gets recorded.\n\tif (!outcome.ok) {\n\t\tprocess.stderr.write(`[${tag}] FAILED (${outcome.reason})\\n`)\n\t\treturn false\n\t}\n\tconst obsPath = observationsPath(opts.catalog, unit)\n\tmkdirSync(dirname(obsPath), { recursive: true })\n\twriteFileSync(\n\t\tobsPath,\n\t\tcomposeObservationsFile(\n\t\t\t{\n\t\t\t\tsource: unit.jsonlPath,\n\t\t\t\tsourceSha256: status.sourceSha256,\n\t\t\t\tminedAt: new Date().toISOString(),\n\t\t\t\tmodel: opts.model,\n\t\t\t\thumanTurns: status.turns,\n\t\t\t},\n\t\t\toutcome.report,\n\t\t),\n\t)\n\tprocess.stderr.write(`[${tag}] observations: ${obsPath}\\n`)\n\treturn true\n}\n\n// The mine workflow shell: gather statuses (I/O), plan purely, then either\n// print the plan (dry-run) or run one sandboxed agent per pending unit.\n// Returns true when nothing it attempted failed.\nexport async function runMine(opts: MineOpts): Promise<boolean> {\n\tconst entries = enumerateUnits(opts.projects).map((unit) => ({\n\t\tunit,\n\t\tstatus: unitStatus(unit, opts.catalog, opts.minTurns),\n\t}))\n\tconst plan = planMine(entries, opts.limit)\n\tfor (const { unit, error } of plan.unreadable) {\n\t\tprocess.stderr.write(`UNREADABLE ${unit.project}/${unit.session}: ${error}\\n`)\n\t}\n\tprocess.stderr.write(`${summarizeMinePlan(plan, opts.minTurns)}\\n`)\n\n\tif (opts.dryRun) {\n\t\tfor (const pending of plan.todo) console.log(dryRunRow(pending))\n\t\treturn true\n\t}\n\tif (plan.todo.length === 0) return true\n\n\tconst token = process.env.CLAUDE_CODE_OAUTH_TOKEN\n\tif (!token) {\n\t\tthrow new Error(\n\t\t\t\"CLAUDE_CODE_OAUTH_TOKEN is required in the environment (it is injected into each miner's sandbox)\",\n\t\t)\n\t}\n\tconst queue = new PQueue({ concurrency: opts.parallel })\n\tconst results = await Promise.all(\n\t\tplan.todo.map((p) => queue.add(() => mineOneUnit(p, opts, token))),\n\t)\n\tconst failed = results.filter((ok) => ok !== true).length\n\tprocess.stderr.write(\n\t\t`mined ${results.length - failed}/${results.length}${failed ? `, ${failed} FAILED` : \"\"}\\n`,\n\t)\n\treturn failed === 0\n}\n","#!/usr/bin/env node\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { Command } from \"commander\"\nimport { runChecks } from \"./check.ts\"\nimport { runMine } from \"./mine.ts\"\n\nfunction parsePositiveInt(flag: string) {\n\treturn (v: string): number => {\n\t\tconst n = Number.parseInt(v, 10)\n\t\tif (!Number.isInteger(n) || n < 1)\n\t\t\tthrow new Error(`${flag} must be a positive integer, got \"${v}\"`)\n\t\treturn n\n\t}\n}\n\nconst collect = (v: string, prev: string[]): string[] => [...prev, v]\n\nconst program = new Command()\n\t.name(\"doublecheck\")\n\t.description(\n\t\t\"Run self-authored LLM code-inspectors (checks) against a target tree, one sandboxed agent per check\",\n\t)\n\nprogram\n\t.command(\"check\")\n\t.option(\"--target <dir>\", \"tree under inspection, mounted read-only\", process.cwd())\n\t.option(\n\t\t\"--checks-dir <dir>\",\n\t\t\"checks directory (repeatable; default: $TARGET/.agents/checks)\",\n\t\tcollect,\n\t\t[] as string[],\n\t)\n\t.option(\n\t\t\"--context <file>\",\n\t\t\"run-context file spliced into every inspector's prompt (intent, nuances, sanctioned exceptions, scope)\",\n\t)\n\t.option(\"--model <model>\", \"model for the inspector agents\", \"haiku\")\n\t.option(\"--parallel <n>\", \"max concurrent checks\", parsePositiveInt(\"--parallel\"), 4)\n\t.option(\"--output <dir>\", \"reports root (default: $TARGET/.doublecheck)\")\n\t.option(\"--check <name>\", \"run only this check (repeatable)\", collect, [] as string[])\n\t.option(\n\t\t\"--save-jsonl\",\n\t\t\"persist each inspector's raw stream-json beside its report (audit trail)\",\n\t)\n\t.action(async (opts) => {\n\t\tconst target = resolve(opts.target)\n\t\tconst checksDirs: string[] =\n\t\t\topts.checksDir.length > 0\n\t\t\t\t? opts.checksDir.map((d: string) => resolve(d))\n\t\t\t\t: [join(target, \".agents\", \"checks\")]\n\t\tconst ok = await runChecks({\n\t\t\ttarget,\n\t\t\tchecksDirs,\n\t\t\tcontextFile: opts.context ? resolve(opts.context) : null,\n\t\t\tmodel: opts.model,\n\t\t\tparallel: opts.parallel,\n\t\t\toutput: opts.output ? resolve(opts.output) : join(target, \".doublecheck\"),\n\t\t\tonly: opts.check,\n\t\t\tsaveJsonl: !!opts.saveJsonl,\n\t\t})\n\t\tif (!ok) process.exitCode = 1\n\t})\n\nprogram\n\t.command(\"mine\")\n\t.option(\n\t\t\"--projects <dir>\",\n\t\t\"Claude Code transcripts root\",\n\t\tjoin(homedir(), \".claude\", \"projects\"),\n\t)\n\t.option(\n\t\t\"--catalog <dir>\",\n\t\t\"observation catalog root\",\n\t\tjoin(homedir(), \".doublecheck\", \"catalog\"),\n\t)\n\t.option(\"--model <model>\", \"model for the mining agents\", \"opus\")\n\t.option(\"--parallel <n>\", \"max concurrent miners\", parsePositiveInt(\"--parallel\"), 4)\n\t.option(\n\t\t\"--min-turns <n>\",\n\t\t\"min genuine human turns for a real conversation\",\n\t\tparsePositiveInt(\"--min-turns\"),\n\t\t2,\n\t)\n\t.option(\"--limit <n>\", \"mine at most N pending units\", parsePositiveInt(\"--limit\"))\n\t.option(\"--dry-run\", \"list what would be mined without booting anything\")\n\t.action(async (opts) => {\n\t\tconst ok = await runMine({\n\t\t\tprojects: resolve(opts.projects),\n\t\t\tcatalog: resolve(opts.catalog),\n\t\t\tmodel: opts.model,\n\t\t\tparallel: opts.parallel,\n\t\t\tminTurns: opts.minTurns,\n\t\t\tlimit: opts.limit,\n\t\t\tdryRun: !!opts.dryRun,\n\t\t})\n\t\tif (!ok) process.exitCode = 1\n\t})\n\nprogram.parseAsync().catch((e: unknown) => {\n\tconsole.error(e instanceof Error ? e.message : String(e))\n\tprocess.exit(1)\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["GUEST_HOME"],"sources":["../src/contract.ts","../src/claude.ts","../src/codex.ts","../src/agent-cli.ts","../src/runner.ts","../src/check.ts","../src/transcript.ts","../src/catalog.ts","../src/mine.ts","../src/cli.ts"],"sourcesContent":["// The agent contract: what the harness stages for the agent and what the\n// agent must leave behind. Any adapter (claude, codex) runs under this same\n// contract, which is what keeps the harness CLI-agnostic. The one per-agent\n// piece is `writeTool`: the phrase naming how THIS agent creates a file\n// (claude has a literal Write tool; codex is just told to create the file —\n// naming a tool it doesn't have would invite it to distrust its brief).\n\n// Staged into the scratch workdir before boot; the rw bind mount delivers it.\nexport const PROMPT_FILE = \"prompt.txt\"\n// The agent writes its findings here (relative to its cwd); the host reads it\n// back after exit. Missing report = failure.\nexport const REPORT_FILE = \"report.md\"\n\n// Weak models reliably skip a report contract stated once at the prompt's\n// tail unless it leads with the stakes and orders the file created first\n// (measured: haiku went from ~50% to 3/3 with this wording). Two hard-won\n// additions (2026-07-05, all three cutover inspectors lost their reports):\n// the path is ABSOLUTE — agents cd into the inspected tree to run git, and a\n// relative ./report.md then lands in the read-only mount — and the\n// create-first order is repeated near the top of the prompt (see\n// firstActionLine), because on long prompts the tail alone loses its grip.\nfunction reportContract(\n\tactivity: string,\n\tdeliverable: string,\n\tworkdir: string,\n\twriteTool: string | null,\n): string {\n\tconst tool = writeTool ? ` ${writeTool}` : \"\"\n\treturn `## Report — the only output that counts\n\nYour final chat reply is discarded. The only thing read back is the file \\`${workdir}/${REPORT_FILE}\\` — if it does not exist when you exit, this run FAILS. Always use that absolute path: if you \\`cd\\` elsewhere (into the inspected tree, say), a relative \\`./${REPORT_FILE}\\` lands in the wrong place, or in a read-only mount where the write fails.\n\nSo: create \\`${workdir}/${REPORT_FILE}\\`${tool} BEFORE you start ${activity} (a title line is enough), and update it as you work. The final state of the file when you finish is ${deliverable}.`\n}\n\nfunction firstActionLine(workdir: string, writeTool: string | null): string {\n\tconst tool = writeTool ? ` ${writeTool}` : \"\"\n\treturn `Your FIRST action, before anything else: create \\`${workdir}/${REPORT_FILE}\\`${tool} (a title line is enough). The full report contract is at the end of this prompt.`\n}\n\n// Environment preamble + optional run context + check body + report\n// contract. Checks are timeless standards; everything run-specific (intent,\n// sanctioned exceptions, scope) arrives in the operator's run context — the\n// harness knows nothing about git.\nexport function composePrompt(opts: {\n\tcheckBody: string\n\ttarget: string\n\tworkdir: string\n\trunContext: string | null\n\twriteTool: string | null\n}): string {\n\tconst contextSection = opts.runContext\n\t\t? `## Run context (from the operator)\n\nThe operator's brief for this run: the intent behind the work under review, known nuances, sanctioned exceptions, and possibly a Scope naming exactly what is under review. Judge requestedness against it — what the brief explicitly sanctions is not a finding. If it names a Scope, the Scope defines the change under review: findings must concern that change, and the rest of the tree is reference for judgment — with one exception. For concepts the scoped change retires or renames, surviving references anywhere in the tree are reportable findings: the Scope bounds what counts as the change, not where survivors may hide.\n\n${opts.runContext}\n\n---\n\n`\n\t\t: \"\"\n\treturn `## Environment\n\nYou are a code inspector running inside a sandboxed microVM with full permissions and unrestricted internet access.\n\n- The code under inspection is mounted read-only at \\`${opts.target}\\`. You cannot modify it — inspect, don't fix.\n- \\`git\\` and \\`rg\\` are installed.\n- Your current working directory is a writable scratch workspace: \\`${opts.workdir}\\`.\n\n${firstActionLine(opts.workdir, opts.writeTool)}\n\n## Inspection discipline\n\nMeasured on real runs: inspectors that skip these rules produce reports whose verdicts are right but whose evidence is partly fabricated — which is worse than no report.\n\n- Before inspecting, enumerate what is under review (the Run context's Scope list if one is given; otherwise your own enumeration of the target) and keep a ledger as you work: **examined** (content actually opened), **name-only**, **not examined**. End your report with that ledger, with a one-line reason for anything not examined.\n- Use verification verbs — \"verified\", \"confirmed\", \"checked\", \"read\", \"diffed\", \"grepped\", \"spot-checked\" — ONLY for actions you actually performed in this session on content you actually opened. Everything else must be labeled as inference: \"assumed identical by generation — not opened.\"\n- Never truncate output you will base a claim on — no \\`| head\\` / \\`| tail\\` on a diff or search you intend to cite. Examine files one at a time instead.\n\n---\n\n${contextSection}${opts.checkBody}\n\n---\n\n${reportContract(\"inspecting\", \"the check's report\", opts.workdir, opts.writeTool)}\n\nEvery single line of your report will be read in full by the operator's agent — nothing is skimmed, so every line costs attention. Verify each finding against the actual code before writing it down. A report that says \"no findings\" is a perfectly good report; an unverified finding is not.`\n}\n\n// The mining prompt: digest of one conversation + what counts as a durable\n// preference observation + the block format the catalog accumulates.\nexport function composeMinePrompt(\n\tdigest: string,\n\tworkdir: string,\n\twriteTool: string | null,\n): string {\n\treturn `## Environment\n\nYou are running inside a sandboxed microVM with no network access. The machine's Claude Code transcripts are mounted read-only at their real paths; your current working directory is a writable scratch workspace: \\`${workdir}\\`.\n\n${firstActionLine(workdir, writeTool)}\n\n## Task\n\nYou are mining ONE Claude Code conversation for the operator's durable preferences — how they want engineering done, in any conversation, not what they wanted built in this one.\n\nBelow is the conversation digest: every genuine human turn, numbered, in order. When a turn is a terse correction or interruption (\"no\", \"stop\", \"don't\", \"why did you…\", \"[Request interrupted]\"), recover its context: the \"# source:\" header names the full session transcript — Grep that turn's text in it to find its line, then Read the assistant turns IMMEDIATELY BEFORE it with offset/limit. The preference lives in the contrast between what the assistant did and what the operator demanded. NEVER read a transcript whole — some are tens of MB.\n\n---\n\n${digest}\n\n---\n\n## What counts as an observation (strict)\n\nA durable preference, backed by a verbatim quote from the digest or transcript:\n\n- **kind: code** — how code should be written/structured/shaped (\"no silent fallbacks\", \"reuse the existing helper instead of hand-rolling\").\n- **kind: workflow** — how work should proceed (\"verify before claiming done\", \"ask before making scope decisions\").\n- **kind: style** — how prose/output/docs should read.\n\nExclude task-of-the-moment directives with no durable signal (\"add a tab to home.tsx\", \"kill the dev server\"). Exclude anything you cannot quote. Be conservative: a weak or speculative observation is worse than none.\n\n## Observation format\n\nThe report is observation blocks and NOTHING else — no title, no preamble, no summary or conclusion sections. One block per observation:\n\n## <kebab-case-name>\n- **observation:** <one sentence: the durable preference>\n- **kind:** code | workflow | style\n- **evidence:** \"<verbatim quote>\" — turn <N>\n\nFor **code** observations that an LLM reviewer could check against a diff or file tree (a concrete change could flip PASS↔FAIL), and ONLY for those — never on workflow or style blocks — ALSO add:\n\n- **pass/fail:** PASS when <…>; FAIL when <…>\n- **why-LLM:** <why a deterministic linter cannot enforce this>\n- **scope:** diff-only | needs-tree\n\nIf the conversation carries no durable preference signal, the report is exactly these two lines and nothing more:\n\nNo durable preference signal.\n<one sentence: what the conversation was about instead>\n\n${reportContract(\"mining\", \"the mined observations\", workdir, writeTool)}`\n}\n","import { PROMPT_FILE } from \"./contract.ts\"\nimport type { AgentSpec } from \"./runner.ts\"\n\nconst GUEST_HOME = \"/root\"\n\n// The claude adapter: produce the AgentSpec that runs `claude -p` in the guest.\nexport function claudeAgent(opts: {\n\ttoken: string\n\tmodel: string\n\tworkdir: string\n}): AgentSpec {\n\treturn {\n\t\t// Headless claude -p cannot answer permission prompts, so in \"default\"\n\t\t// mode every tool is auto-denied and the agent produces nothing. The\n\t\t// microVM is the safety boundary here, so bypass the per-tool gate;\n\t\t// IS_SANDBOX=1 is what lets that run as root without claude's refusal.\n\t\tcommand:\n\t\t\t\"claude -p --no-session-persistence \" +\n\t\t\t\"--dangerously-skip-permissions \" +\n\t\t\t// --verbose is required by claude -p with stream-json output; it only\n\t\t\t// widens what lands on stdout, which describeStreamLine already labels.\n\t\t\t\"--output-format stream-json --verbose \" +\n\t\t\t// Not a permission gate — a contract guard. The default headless tool\n\t\t\t// set omits Glob/Grep and includes harness tools (ReportFindings,\n\t\t\t// TaskCreate, …) that hijack the report: haiku \"reports findings\"\n\t\t\t// through them and never writes report.md (verified live). This pins\n\t\t\t// the inspector's full toolkit and nothing else.\n\t\t\t`--tools Task Bash Read Write Edit Glob Grep WebSearch WebFetch < ${PROMPT_FILE}`,\n\t\tenv: {\n\t\t\tHOME: GUEST_HOME,\n\t\t\tIS_SANDBOX: \"1\",\n\t\t\tGIT_OPTIONAL_LOCKS: \"0\",\n\t\t\tANTHROPIC_MODEL: opts.model,\n\t\t\tCLAUDE_CODE_OAUTH_TOKEN: opts.token,\n\t\t},\n\t\t// Minimal trust-accepted entry keyed to the guest cwd so headless claude\n\t\t// skips the trust prompt. Deliberately NOT a copy of any host .claude.json.\n\t\tfiles: [\n\t\t\t{\n\t\t\t\tpath: `${GUEST_HOME}/.claude.json`,\n\t\t\t\tcontent: `${JSON.stringify(\n\t\t\t\t\t{ projects: { [opts.workdir]: { hasTrustDialogAccepted: true } } },\n\t\t\t\t\tnull,\n\t\t\t\t\t\"\\t\",\n\t\t\t\t)}\\n`,\n\t\t\t},\n\t\t],\n\t}\n}\n\ninterface StreamLine {\n\ttype?: string\n\tsubtype?: string\n\tmessage?: { content?: Array<{ type?: string; text?: string; name?: string }> }\n\tduration_ms?: number\n}\n\n// One human-readable label per stream-json stdout line; null for lines that\n// aren't claude's JSON (dropped from the progress stream).\nexport function describeClaudeStreamLine(line: string): string | null {\n\tlet obj: StreamLine\n\ttry {\n\t\tobj = JSON.parse(line) as StreamLine\n\t} catch {\n\t\treturn null\n\t}\n\tconst label = (obj.type ?? \"\") + (obj.subtype ? `:${obj.subtype}` : \"\")\n\t// A once-per-second heartbeat with no content — pure noise in the stream.\n\tif (label === \"system:thinking_tokens\") return null\n\tlet detail = \"\"\n\tif (obj.type === \"assistant\") {\n\t\tconst content = obj.message?.content ?? []\n\t\tconst tools = content\n\t\t\t.filter((b) => b.type === \"tool_use\" && b.name)\n\t\t\t.map((b) => b.name)\n\t\tconst chars = content.reduce((n, b) => n + (b.text?.length ?? 0), 0)\n\t\tdetail = tools.length ? ` ${tools.join(\",\")}` : ` ${chars} chars`\n\t} else if (obj.type === \"result\") {\n\t\tdetail = ` ${obj.subtype}, ${((obj.duration_ms ?? 0) / 1000).toFixed(1)}s`\n\t}\n\treturn `[${label}]${detail}`\n}\n","import { PROMPT_FILE } from \"./contract.ts\"\nimport type { AgentSpec } from \"./runner.ts\"\n\nconst GUEST_HOME = \"/root\"\n\n// Guest-only codex config — deliberately NOT a copy of any host config.toml\n// (which carries MCP API keys, personality, plugins, a trust table). Each key\n// is load-bearing:\n// - model rides here rather than as a `-m` flag so nothing operator-supplied\n// is ever interpolated into the guest's bash command string.\n// - approval_policy/sandbox_mode double the bypass flag on the command line;\n// either alone is a documented trust-prompt suppressor, some builds leak\n// with only one (openai/codex#14547).\n// - model_reasoning_effort: operator decision — plan billing is flat-rate,\n// so there is no haiku-style cost gradient to encode in a cheaper setting.\n// - web_search is a server-side tool (the guest never fetches pages itself);\n// \"live\" pins it rather than relying on the full-access auto-upgrade.\n// - project_doc_max_bytes = 0 disables all AGENTS.md loading: the piped\n// prompt is the agent's only input.\n// - features.plugins/apps = false stop codex's boot-time marketplace clone\n// and apps fetch (verified live): under restricted egress they hang, and\n// under any egress they inject tools (e.g. a git-push skill) the contract\n// never sanctioned.\nfunction guestConfig(model: string): string {\n\treturn `model = \"${model}\"\napproval_policy = \"never\"\nsandbox_mode = \"danger-full-access\"\nmodel_reasoning_effort = \"xhigh\"\nweb_search = \"live\"\nproject_doc_max_bytes = 0\ncli_auth_credentials_store = \"file\"\n\n[features]\nplugins = false\napps = false\n`\n}\n\n// The codex adapter: produce the AgentSpec that runs `codex exec` in the guest.\nexport function codexAgent(opts: {\n\t// Byte-for-byte content of the operator's ~/.codex/auth.json; staleness is\n\t// the caller's preflight (a guest-side token refresh would rotate the\n\t// single-use refresh token and poison the host session).\n\tauthJson: string\n\tmodel: string\n\tworkdir: string\n}): AgentSpec {\n\treturn {\n\t\t// Headless codex cannot answer approval prompts, and its own bwrap\n\t\t// sandbox needs user namespaces the guest may not grant — the microVM is\n\t\t// the safety boundary, so bypass both (OpenAI's documented container\n\t\t// guidance). --ephemeral writes no session rollout files;\n\t\t// --skip-git-repo-check because the scratch cwd is not a git repo.\n\t\tcommand:\n\t\t\t\"codex exec --json --skip-git-repo-check \" +\n\t\t\t\"--dangerously-bypass-approvals-and-sandbox --ephemeral \" +\n\t\t\t`- < ${PROMPT_FILE}`,\n\t\tenv: {\n\t\t\t// CODEX_HOME defaults to $HOME/.codex, where both staged files land.\n\t\t\tHOME: GUEST_HOME,\n\t\t\tGIT_OPTIONAL_LOCKS: \"0\",\n\t\t},\n\t\tfiles: [\n\t\t\t{ path: `${GUEST_HOME}/.codex/auth.json`, content: opts.authJson },\n\t\t\t{\n\t\t\t\tpath: `${GUEST_HOME}/.codex/config.toml`,\n\t\t\t\tcontent: guestConfig(opts.model),\n\t\t\t},\n\t\t],\n\t}\n}\n\n// The freshness window for a staged ChatGPT-auth auth.json. Codex refreshes\n// tokens when last_refresh is ~8 days old (or on a 401), refresh tokens are\n// single-use, and a refresh performed INSIDE a guest rotates the token family\n// — the host's copy then dies with \"refresh token was already used\" (forced\n// re-login). 7 days leaves a day of margin below codex's own threshold.\nexport const CODEX_AUTH_MAX_AGE_DAYS = 7\n\ninterface CodexAuth {\n\ttokens?: { refresh_token?: string }\n\tlast_refresh?: string\n\tOPENAI_API_KEY?: string\n}\n\n// Pure preflight: throw (with the operator's next action) unless this\n// auth.json is safe to stage into guests. An api-key-mode auth.json (no\n// tokens, an OPENAI_API_KEY field) carries no refresh semantics, so no\n// staleness guard applies.\nexport function validateCodexAuth(content: string, now: Date, where: string): void {\n\tlet auth: CodexAuth\n\ttry {\n\t\tauth = JSON.parse(content) as CodexAuth\n\t} catch {\n\t\tthrow new Error(`${where} is not valid JSON — run \\`codex login\\` on the host`)\n\t}\n\tif (auth.tokens?.refresh_token) {\n\t\tconst ageDays = (now.getTime() - Date.parse(auth.last_refresh ?? \"\")) / 86_400_000\n\t\tif (!(ageDays <= CODEX_AUTH_MAX_AGE_DAYS)) {\n\t\t\tthrow new Error(\n\t\t\t\t`${where} tokens were last refreshed over ${CODEX_AUTH_MAX_AGE_DAYS} days ago (codex refreshes at ~8 days, and a refresh inside a guest would rotate the single-use refresh token and poison this host's session) — run any codex command on the host to refresh, then retry`,\n\t\t\t)\n\t\t}\n\t\treturn\n\t}\n\tif (auth.OPENAI_API_KEY) return\n\tthrow new Error(\n\t\t`${where} has neither ChatGPT tokens nor an API key — run \\`codex login\\` on the host`,\n\t)\n}\n\ninterface CodexStreamLine {\n\ttype?: string\n\titem?: {\n\t\ttype?: string\n\t\ttext?: string\n\t\tcommand?: string\n\t\tchanges?: Array<{ path?: string }>\n\t}\n\tusage?: { input_tokens?: number; output_tokens?: number }\n\terror?: { message?: string }\n\tmessage?: string\n}\n\nconst clip = (s: string, max = 60): string => (s.length > max ? `${s.slice(0, max)}…` : s)\n\n// One human-readable label per --json stdout line; null for lines that aren't\n// codex's JSON (dropped from the progress stream). Event taxonomy:\n// thread.started / turn.started / turn.completed / turn.failed /\n// item.{started,updated,completed} (item.type = agent_message,\n// command_execution, file_change, reasoning, web_search, todo_list,\n// mcp_tool_call, error) / error.\nexport function describeCodexStreamLine(line: string): string | null {\n\tlet obj: CodexStreamLine\n\ttry {\n\t\tobj = JSON.parse(line) as CodexStreamLine\n\t} catch {\n\t\treturn null\n\t}\n\tconst item = obj.item\n\tconst label = (obj.type ?? \"\") + (item?.type ? `:${item.type}` : \"\")\n\tlet detail = \"\"\n\tif (item?.type === \"agent_message\") {\n\t\tdetail = ` ${item.text?.length ?? 0} chars`\n\t} else if (item?.type === \"command_execution\" && item.command) {\n\t\tdetail = ` ${clip(item.command)}`\n\t} else if (item?.type === \"file_change\") {\n\t\tdetail = ` ${(item.changes ?? []).length} file(s)`\n\t} else if (obj.type === \"turn.completed\") {\n\t\tdetail = ` ${obj.usage?.input_tokens ?? 0} in, ${obj.usage?.output_tokens ?? 0} out tokens`\n\t} else if (obj.type === \"turn.failed\") {\n\t\tdetail = ` ${clip(obj.error?.message ?? \"\")}`\n\t} else if (obj.type === \"error\") {\n\t\tdetail = ` ${clip(obj.message ?? \"\")}`\n\t}\n\treturn `[${label}]${detail}`\n}\n","import { readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { claudeAgent, describeClaudeStreamLine } from \"./claude.ts\"\nimport { codexAgent, describeCodexStreamLine, validateCodexAuth } from \"./codex.ts\"\nimport type { AgentSpec } from \"./runner.ts\"\n\n// The agent CLI: the brand of agent (claude, codex) the operator picks with\n// --agent, and everything the workflows need from it. Not to be confused with\n// the harness (doublecheck's own mechanics) or an agent (one running\n// inspector instance, described by an AgentSpec).\nexport interface AgentCli {\n\tname: string\n\t// Applied only when --model is absent.\n\tdefaultModel: { check: string; mine: string }\n\t// The phrase the report contract uses to tell this agent how to create a\n\t// file; null when the prompt should just say \"create <path>\".\n\twriteToolPhrase: string | null\n\t// Domain suffixes for restricted-egress workflows (mine): the CLI's own\n\t// API endpoints, so the only reachable destination is the service the\n\t// agent already sends its context to.\n\tegressDomains: string[]\n\t// Host-side preflight: the secret material staged into every guest, or an\n\t// actionable error. Runs once per invocation, before any guest boots.\n\tcredentials(): string\n\tagent(opts: { credentials: string; model: string; workdir: string }): AgentSpec\n\tdescribeStreamLine(line: string): string | null\n}\n\nconst claude: AgentCli = {\n\tname: \"claude\",\n\tdefaultModel: { check: \"haiku\", mine: \"opus\" },\n\twriteToolPhrase: \"with the Write tool\",\n\tegressDomains: [\"anthropic.com\"],\n\tcredentials: () => {\n\t\tconst token = process.env.CLAUDE_CODE_OAUTH_TOKEN\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t\"CLAUDE_CODE_OAUTH_TOKEN is required in the environment (it is injected into each agent's sandbox)\",\n\t\t\t)\n\t\t}\n\t\treturn token\n\t},\n\tagent: ({ credentials, model, workdir }) =>\n\t\tclaudeAgent({ token: credentials, model, workdir }),\n\tdescribeStreamLine: describeClaudeStreamLine,\n}\n\nconst codex: AgentCli = {\n\tname: \"codex\",\n\t// No haiku-style cheap default: plan billing is flat-rate, and the staged\n\t// guest config already pins xhigh reasoning effort (operator decision).\n\tdefaultModel: { check: \"gpt-5.5\", mine: \"gpt-5.5\" },\n\twriteToolPhrase: null,\n\t// ChatGPT-plan inference lives at chatgpt.com/backend-api/codex; the\n\t// openai.com suffix covers auth.openai.com, where a 401-forced token\n\t// refresh would have to go.\n\tegressDomains: [\"chatgpt.com\", \"openai.com\"],\n\tcredentials: () => {\n\t\tconst authPath = join(homedir(), \".codex\", \"auth.json\")\n\t\tlet content: string\n\t\ttry {\n\t\t\tcontent = readFileSync(authPath, \"utf-8\")\n\t\t} catch {\n\t\t\tthrow new Error(`no readable ${authPath} — run \\`codex login\\` on the host`)\n\t\t}\n\t\tvalidateCodexAuth(content, new Date(), authPath)\n\t\treturn content\n\t},\n\tagent: ({ credentials, model, workdir }) =>\n\t\tcodexAgent({ authJson: credentials, model, workdir }),\n\tdescribeStreamLine: describeCodexStreamLine,\n}\n\nconst AGENT_CLIS: Record<string, AgentCli> = { claude, codex }\n\nexport function resolveAgentCli(name: string): AgentCli {\n\tconst cli = AGENT_CLIS[name]\n\tif (!cli) {\n\t\tthrow new Error(\n\t\t\t`unknown agent \"${name}\" (have: ${Object.keys(AGENT_CLIS).join(\", \")})`,\n\t\t)\n\t}\n\treturn cli\n}\n\n// One resolved, preflighted agent selection, shared by every unit of a run.\nexport interface AgentRun {\n\tcli: AgentCli\n\tcredentials: string\n\tmodel: string\n}\n\n// Resolve --agent/--model into what a workflow's units consume: the CLI, its\n// preflighted credentials, and the model (explicit flag, else the CLI's\n// default for this workflow).\nexport function resolveAgentRun(\n\tname: string,\n\tmodel: string | undefined,\n\tworkflow: \"check\" | \"mine\",\n): AgentRun {\n\tconst cli = resolveAgentCli(name)\n\treturn {\n\t\tcli,\n\t\tcredentials: cli.credentials(),\n\t\tmodel: model ?? cli.defaultModel[workflow],\n\t}\n}\n\n// The per-unit progress sink both workflow shells hang on runAgent: guest\n// stderr passes through as `[label] ! line`, stdout is the agent CLI's JSON\n// stream, labelled by its describeStreamLine (non-JSON lines dropped).\nexport function progressSink(\n\tlabel: string,\n\tdescribe: (line: string) => string | null,\n): (kind: \"stdout\" | \"stderr\", line: string) => void {\n\treturn (kind, line) => {\n\t\tif (kind === \"stderr\") {\n\t\t\tprocess.stderr.write(`[${label}] ! ${line}\\n`)\n\t\t\treturn\n\t\t}\n\t\tconst described = describe(line)\n\t\tif (described) process.stderr.write(`[${label}] ${described}\\n`)\n\t}\n}\n","import { existsSync, readFileSync } from \"node:fs\"\nimport { basename, join } from \"node:path\"\nimport type { ExecEvent } from \"microsandbox\"\nimport { REPORT_FILE } from \"./contract.ts\"\n\n// microsandbox exports its fluent builders as value-only consts; recover their\n// instance types via a type-only import (erased at build) so the native module\n// only loads behind the `await import` in runAgent.\ntype MountBuilder = InstanceType<typeof import(\"microsandbox\").MountBuilder>\ntype PatchBuilder = InstanceType<typeof import(\"microsandbox\").PatchBuilder>\ntype ExecOptionsBuilder = InstanceType<typeof import(\"microsandbox\").ExecOptionsBuilder>\n// The SDK types `.network(cb)` as `(b: any) => any`, and we call the JS-shim\n// `.policy(...)`, absent from the native builder's types.\n// biome-ignore lint/suspicious/noExplicitAny: SDK types this builder callback as any\ntype NetworkBuilder = any\n\n// The guest image (Dockerfile.guest): node 24 slim + git/ripgrep/curl/wget +\n// both agent CLIs baked in. Released versions pull the multi-arch image the\n// release workflow pushed to ghcr (tag = the package's own version, so CLI\n// and image always match); a dev tree (version 0.0.0-development) uses the\n// locally built image side-loaded by ./scripts/build-guest-image.sh, which\n// exists nowhere else — hence pullPolicy \"never\": a cache miss means \"run the\n// build script\", not \"try a registry\".\nconst GUEST_IMAGE_REPO = \"ghcr.io/alizain/doublecheck-guest\"\nconst LOCAL_GUEST_IMAGE = \"doublecheck-guest:latest\"\nexport const DEV_VERSION = \"0.0.0-development\"\nconst GUEST_MEMORY_MIB = 2048\n\nexport interface GuestImage {\n\tref: string\n\tpullPolicy: \"never\" | \"if-missing\"\n}\n\n// Pure: which image this doublecheck runs its guests from. An override (the\n// DOUBLECHECK_GUEST_IMAGE env var) is operator-managed: it must already be in\n// the msb cache, so it is never pulled.\nexport function decideGuestImage(version: string, override?: string): GuestImage {\n\tif (override) return { ref: override, pullPolicy: \"never\" }\n\tif (version === DEV_VERSION) return { ref: LOCAL_GUEST_IMAGE, pullPolicy: \"never\" }\n\treturn { ref: `${GUEST_IMAGE_REPO}:${version}`, pullPolicy: \"if-missing\" }\n}\n\n// Read at runtime, not import time: the dist bundle is built BEFORE\n// semantic-release stamps the real version into the published package.json,\n// so baking the version in at build time would pin every install to\n// 0.0.0-development. Works from both src/ (tsx) and dist/ (bundle) — each\n// sits one level below the package root.\nfunction packageVersion(): string {\n\tconst pkg = JSON.parse(\n\t\treadFileSync(new URL(\"../package.json\", import.meta.url), \"utf-8\"),\n\t) as { version: string }\n\treturn pkg.version\n}\n\n// Staged into the guest before boot (e.g. claude's trust-accepted config).\nexport interface GuestFile {\n\tpath: string\n\tcontent: string\n}\n\n// What it takes to run one agent in a booted guest: a bash command executed in\n// the scratch cwd that must leave REPORT_FILE there, the env it needs, and\n// files staged into the guest. Plain data — adapters produce it, this runner\n// consumes it, tests fake it.\nexport interface AgentSpec {\n\tcommand: string\n\tenv: Record<string, string>\n\tfiles: GuestFile[]\n}\n\nexport type AgentOutcome =\n\t| { ok: true; report: string }\n\t| { ok: false; reason: string; partialReport: string | null }\n\n// Pure line buffering: fold a chunk into the carry, emit complete non-blank\n// lines, keep the unterminated tail.\nexport function feedLines(\n\tcarry: string,\n\tchunk: string,\n): { lines: string[]; carry: string } {\n\tconst parts = (carry + chunk).split(\"\\n\")\n\tconst rest = parts.pop() ?? \"\"\n\treturn { lines: parts.filter((l) => l.trim()), carry: rest }\n}\n\n// Pure outcome decision: what the exec left behind → what it means.\nexport function decideOutcome(\n\texitCode: number | null,\n\treport: string | null,\n\tworkdir: string,\n): AgentOutcome {\n\tif (exitCode !== 0) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\treason: `agent process exited ${exitCode}`,\n\t\t\tpartialReport: report,\n\t\t}\n\t}\n\tif (report === null) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\treason: `agent exited 0 but wrote no ${REPORT_FILE} (work dir: ${workdir})`,\n\t\t\tpartialReport: null,\n\t\t}\n\t}\n\treturn { ok: true, report }\n}\n\nexport interface RunAgentOpts {\n\t// Host dir the agent inspects, bind-mounted READ-ONLY at its real host\n\t// path: the target for `check`, the transcripts corpus for `mine`.\n\tmount: string\n\t// Scratch dir with PROMPT_FILE already inside; bind-mounted rw as the guest\n\t// cwd at its identical host path, so the agent's report lands back on the host.\n\tworkdir: string\n\tspec: AgentSpec\n\t// \"all\" for checks (inspectors may research); a domain-suffix allowlist for\n\t// miners: the personal corpus is mounted, so egress is pinned to the agent\n\t// CLI's own API domains — the only reachable destination is the service\n\t// the agent already sends its context to. NetworkPolicy.none() is not an\n\t// option here — it kills DNS entirely and the adapter can't reach its own\n\t// model API (measured: claude retries ~180s then exits 1).\n\tnetwork: \"all\" | { onlyDomains: string[] }\n\tonLine: (kind: \"stdout\" | \"stderr\", line: string) => void\n}\n\n// Boot a guest (ro mount + scratch rw as cwd), exec the agent command, stream\n// its output line-by-line, read the report back off the scratch dir, tear\n// down. Agent-level failures (exit ≠ 0, no report) return ok:false;\n// harness-level failures (image missing, boot error) throw.\nexport async function runAgent(opts: RunAgentOpts): Promise<AgentOutcome> {\n\tconst microsandbox = await import(\"microsandbox\")\n\tconst name = `doublecheck-${basename(opts.workdir)}`\n\tconst image = decideGuestImage(packageVersion(), process.env.DOUBLECHECK_GUEST_IMAGE)\n\tconst network = opts.network\n\tconst policy =\n\t\tnetwork === \"all\"\n\t\t\t? microsandbox.NetworkPolicy.allowAll()\n\t\t\t: microsandbox.NetworkPolicy.builder()\n\t\t\t\t\t.defaultDeny()\n\t\t\t\t\t.egress((rb) => {\n\t\t\t\t\t\tlet rules = rb\n\t\t\t\t\t\tfor (const domain of network.onlyDomains) {\n\t\t\t\t\t\t\trules = rules.allow((d) => d.domainSuffix(domain))\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn rules\n\t\t\t\t\t})\n\t\t\t\t\t.build()\n\tlet sandbox: InstanceType<typeof microsandbox.Sandbox> | null = null\n\ttry {\n\t\ttry {\n\t\t\tsandbox = await microsandbox.Sandbox.builder(name)\n\t\t\t\t.image(image.ref)\n\t\t\t\t.memory(GUEST_MEMORY_MIB)\n\t\t\t\t.pullPolicy(image.pullPolicy)\n\t\t\t\t.replace()\n\t\t\t\t.workdir(opts.workdir)\n\t\t\t\t.envs(opts.spec.env)\n\t\t\t\t.volume(opts.mount, (mb: MountBuilder) => mb.bind(opts.mount).readonly())\n\t\t\t\t.volume(opts.workdir, (mb: MountBuilder) => mb.bind(opts.workdir))\n\t\t\t\t.patch((pb: PatchBuilder) => {\n\t\t\t\t\tlet p = pb\n\t\t\t\t\tfor (const f of opts.spec.files) {\n\t\t\t\t\t\tp = p.text(f.path, f.content, { mode: 0o600, replace: true })\n\t\t\t\t\t}\n\t\t\t\t\treturn p\n\t\t\t\t})\n\t\t\t\t.network((nb: NetworkBuilder) => nb.policy(policy))\n\t\t\t\t.create()\n\t\t} catch (e) {\n\t\t\tif (image.pullPolicy === \"never\" && String(e).includes(\"not cached\")) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`guest image ${image.ref} is not in the microsandbox cache — run scripts/build-guest-image.sh (${String(e)})`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (image.pullPolicy === \"if-missing\") {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`guest ${image.ref} failed to boot — if this is a pull failure, check network access to ghcr.io and that the package is public; a locally built image can be forced with DOUBLECHECK_GUEST_IMAGE=doublecheck-guest:latest (${String(e)})`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow e\n\t\t}\n\n\t\tconst handle = await sandbox.execStreamWith(\"bash\", (e: ExecOptionsBuilder) =>\n\t\t\te.args([\"-c\", opts.spec.command]),\n\t\t)\n\n\t\tconst carry: Record<\"stdout\" | \"stderr\", string> = { stdout: \"\", stderr: \"\" }\n\t\tlet exitCode: number | null = null\n\t\tfor (;;) {\n\t\t\tconst ev: ExecEvent | null = await handle.recv()\n\t\t\tif (ev === null) break\n\t\t\tif (ev.kind === \"stdout\" || ev.kind === \"stderr\") {\n\t\t\t\tconst fed = feedLines(carry[ev.kind], Buffer.from(ev.data).toString())\n\t\t\t\tcarry[ev.kind] = fed.carry\n\t\t\t\tfor (const line of fed.lines) opts.onLine(ev.kind, line)\n\t\t\t} else if (ev.kind === \"exited\") exitCode = ev.code\n\t\t}\n\t\tfor (const kind of [\"stdout\", \"stderr\"] as const) {\n\t\t\tif (carry[kind].trim()) opts.onLine(kind, carry[kind])\n\t\t}\n\n\t\tconst reportPath = join(opts.workdir, REPORT_FILE)\n\t\tconst report = existsSync(reportPath) ? readFileSync(reportPath, \"utf-8\") : null\n\t\treturn decideOutcome(exitCode, report, opts.workdir)\n\t} finally {\n\t\tif (sandbox) {\n\t\t\ttry {\n\t\t\t\tawait sandbox.stop()\n\t\t\t} catch (e) {\n\t\t\t\topts.onLine(\"stderr\", `sandbox stop failed: ${String(e)}`)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait microsandbox.Sandbox.remove(name)\n\t\t\t} catch (e) {\n\t\t\t\topts.onLine(\"stderr\", `sandbox remove failed: ${String(e)}`)\n\t\t\t}\n\t\t}\n\t}\n}\n","import {\n\tappendFileSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\twriteFileSync,\n} from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport PQueue from \"p-queue\"\nimport { type AgentRun, progressSink, resolveAgentRun } from \"./agent-cli.ts\"\nimport { composePrompt, PROMPT_FILE, REPORT_FILE } from \"./contract.ts\"\nimport { runAgent } from \"./runner.ts\"\n\n// A check is a plain markdown file — no frontmatter, no schema. The body is\n// the inspector's instructions; the name is the filename minus .md. Checks\n// come from one or more --checks-dir directories (default:\n// $TARGET/.agents/checks).\nexport interface Check {\n\tname: string\n\tbody: string\n}\n\n// Pure selection: `only` (from repeated --check flags) filters, in the order\n// named; naming a check that doesn't exist is an error, not an empty run.\nexport function selectChecks(all: Check[], only: string[], where: string): Check[] {\n\tif (all.length === 0) throw new Error(`no checks (*.md) in ${where}`)\n\tif (only.length === 0) return all\n\tconst byName = new Map(all.map((c) => [c.name, c]))\n\treturn only.map((name) => {\n\t\tconst check = byName.get(name)\n\t\tif (!check) {\n\t\t\tconst have = all.map((c) => c.name).join(\", \")\n\t\t\tthrow new Error(`no check named \"${name}\" in ${where} (have: ${have})`)\n\t\t}\n\t\treturn check\n\t})\n}\n\n// Shell: read every check file from every checks dir, then select purely.\n// A missing dir is an error; the same check name in two dirs is an error —\n// no precedence rules, rename one.\nexport function discoverChecks(dirs: string[], only: string[]): Check[] {\n\tconst all: Array<Check & { dir: string }> = []\n\tfor (const dir of dirs) {\n\t\tlet entries: string[]\n\t\ttry {\n\t\t\tentries = readdirSync(dir)\n\t\t} catch {\n\t\t\tthrow new Error(`no checks directory at ${dir}`)\n\t\t}\n\t\tfor (const f of entries.filter((e) => e.endsWith(\".md\")).sort()) {\n\t\t\tconst name = f.slice(0, -\".md\".length)\n\t\t\tconst clash = all.find((c) => c.name === name)\n\t\t\tif (clash) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`check \"${name}\" exists in both ${clash.dir} and ${dir} — rename one`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tall.push({ name, body: readFileSync(join(dir, f), \"utf-8\"), dir })\n\t\t}\n\t}\n\tconst checks = all\n\t\t.map(({ name, body }) => ({ name, body }))\n\t\t.sort((a, b) => a.name.localeCompare(b.name))\n\treturn selectChecks(checks, only, dirs.join(\", \"))\n}\n\nexport interface CheckWorkflowOpts {\n\ttarget: string\n\tchecksDirs: string[]\n\tcontextFile: string | null\n\tagent: string\n\t// Absent = the agent CLI's default for checks.\n\tmodel?: string\n\tparallel: number\n\toutput: string\n\tonly: string[]\n\tsaveJsonl: boolean\n}\n\n// fs-safe local timestamp, one dir per run shared by all its checks:\n// 2026-07-05-143000\nexport function runTimestamp(d: Date): string {\n\tconst p = (n: number) => String(n).padStart(2, \"0\")\n\treturn `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`\n}\n\n// The report written when the agent failed: the failure is the report, plus\n// whatever partial report the agent managed to leave.\nexport function failureReport(reason: string, partialReport: string | null): string {\n\tconst partial = partialReport\n\t\t? `\\n---\\n\\nPartial ${REPORT_FILE} left by the agent:\\n\\n${partialReport}`\n\t\t: \"\"\n\treturn `# CHECK FAILED\\n\\n${reason}\\n${partial}`\n}\n\nasync function runOneCheck(\n\tcheck: Check,\n\topts: CheckWorkflowOpts,\n\trun: AgentRun,\n\trunContext: string | null,\n\toutDir: string,\n): Promise<boolean> {\n\tconst workdir = mkdtempSync(join(tmpdir(), `doublecheck-${check.name}-`))\n\twriteFileSync(\n\t\tjoin(workdir, PROMPT_FILE),\n\t\tcomposePrompt({\n\t\t\tcheckBody: check.body,\n\t\t\ttarget: opts.target,\n\t\t\tworkdir,\n\t\t\trunContext,\n\t\t\twriteTool: run.cli.writeToolPhrase,\n\t\t}),\n\t)\n\tconst spec = run.cli.agent({\n\t\tcredentials: run.credentials,\n\t\tmodel: run.model,\n\t\tworkdir,\n\t})\n\t// The report is the verdict; with --save-jsonl the raw stream is kept\n\t// beside it so a run can be audited after the ephemeral guest is gone\n\t// (which files the inspector read, what it skipped, whether report claims\n\t// trace to actual observations).\n\tconst sink = progressSink(check.name, run.cli.describeStreamLine)\n\tlet onLine = sink\n\tif (opts.saveJsonl) {\n\t\tconst streamPath = join(outDir, `${check.name}.stream.jsonl`)\n\t\tonLine = (kind, line) => {\n\t\t\tif (kind === \"stdout\") appendFileSync(streamPath, `${line}\\n`)\n\t\t\tsink(kind, line)\n\t\t}\n\t}\n\tconst outcome = await runAgent({\n\t\tmount: opts.target,\n\t\tworkdir,\n\t\tspec,\n\t\tnetwork: \"all\",\n\t\tonLine,\n\t})\n\tconst reportPath = join(outDir, `${check.name}.md`)\n\tif (outcome.ok) {\n\t\twriteFileSync(reportPath, outcome.report)\n\t\tprocess.stderr.write(`[${check.name}] report: ${reportPath}\\n`)\n\t\treturn true\n\t}\n\twriteFileSync(reportPath, failureReport(outcome.reason, outcome.partialReport))\n\tprocess.stderr.write(`[${check.name}] FAILED (${outcome.reason}): ${reportPath}\\n`)\n\treturn false\n}\n\n// The check workflow: one sandboxed agent per check, at most `parallel`\n// guests at once. Returns true only when every check produced a report; agent\n// failures still write a failure-record report and flip the run to false.\nexport async function runChecks(opts: CheckWorkflowOpts): Promise<boolean> {\n\tconst run = resolveAgentRun(opts.agent, opts.model, \"check\")\n\tconst checks = discoverChecks(opts.checksDirs, opts.only)\n\tlet runContext: string | null = null\n\tif (opts.contextFile) {\n\t\ttry {\n\t\t\trunContext = readFileSync(opts.contextFile, \"utf-8\")\n\t\t} catch (e) {\n\t\t\tthrow new Error(\n\t\t\t\t`--context file not readable: ${opts.contextFile} (${String(e)})`,\n\t\t\t)\n\t\t}\n\t}\n\tconst outDir = join(opts.output, runTimestamp(new Date()))\n\tmkdirSync(outDir, { recursive: true })\n\tprocess.stderr.write(\n\t\t`${checks.length} check(s) against ${opts.target} (agent ${run.cli.name}, model ${run.model}, parallel ${opts.parallel})\\n`,\n\t)\n\tconst queue = new PQueue({ concurrency: opts.parallel })\n\tconst results = await Promise.all(\n\t\tchecks.map((check) =>\n\t\t\tqueue.add(() => runOneCheck(check, opts, run, runContext, outDir)),\n\t\t),\n\t)\n\treturn results.every((ok) => ok === true)\n}\n","// Claude Code transcript (.jsonl) → the genuine human turns → a flat digest.\n// Port of the June-2026 jq extraction, verified against the same corpus: a\n// \"genuine human turn\" is type:user, not meta, not sidechain, text content\n// only — which excludes the three things that are also type:user in the JSONL\n// (tool_results, slash-command stdout wrappers, injected system-reminders).\n\n// Per-turn char cap in the digest; the mining agent greps the source jsonl\n// for full text when it needs context around a turn.\nconst TURN_CHAR_CAP = 2000\n\ninterface RawLine {\n\ttype?: string\n\tisMeta?: boolean\n\tisSidechain?: boolean\n\tmessage?: { content?: unknown }\n}\n\nconst NON_HUMAN_TEXT =\n\t/^\\s*<command-name>|<local-command-stdout>|^\\s*<local-command|^\\s*<system-reminder>|Caveat: The messages below/\n\n// Throws on an unparseable line — the caller decides what an unreadable\n// transcript means for its run (mine counts and reports them, visibly).\nexport function humanTurns(jsonl: string): string[] {\n\tconst turns: string[] = []\n\tfor (const line of jsonl.split(\"\\n\")) {\n\t\tif (!line.trim()) continue\n\t\tconst raw = JSON.parse(line) as RawLine\n\t\tif (raw.type !== \"user\" || raw.isMeta || raw.isSidechain) continue\n\t\tconst content = raw.message?.content\n\t\tconst text =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? content\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? content\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(item): item is { type: \"text\"; text: string } =>\n\t\t\t\t\t\t\t\t\ttypeof item === \"object\" &&\n\t\t\t\t\t\t\t\t\titem !== null &&\n\t\t\t\t\t\t\t\t\t(item as { type?: string }).type === \"text\" &&\n\t\t\t\t\t\t\t\t\ttypeof (item as { text?: unknown }).text === \"string\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.map((item) => item.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\"\n\t\tif (!text) continue\n\t\tif (NON_HUMAN_TEXT.test(text)) continue\n\t\tturns.push(text.replace(/[\\n\\r]+/g, \" ⏎ \").slice(0, TURN_CHAR_CAP))\n\t}\n\treturn turns\n}\n\nexport interface DigestMeta {\n\tsource: string\n\tproject: string\n\tsession: string\n}\n\nexport function renderDigest(meta: DigestMeta, turns: string[]): string {\n\tconst numbered = turns\n\t\t.map((t, i) => `${String(i + 1).padStart(3, \" \")} | ${t}`)\n\t\t.join(\"\\n\")\n\treturn `# source: ${meta.source}\n# project: ${meta.project}\n# session: ${meta.session}\n# human_turns: ${turns.length}\n# To understand WHY a terse turn or interruption happened, grep its text in the\n# source file above and read the assistant turn(s) immediately before it.\n\n${numbered}\n`\n}\n","import { createHash } from \"node:crypto\"\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\"\nimport { basename, join } from \"node:path\"\nimport { humanTurns, renderDigest } from \"./transcript.ts\"\n\n// One minable unit = one top-level transcript $PROJECTS/<project>/<session>.jsonl.\n// Its catalog home mirrors that path: $CATALOG/<project>/<session>/observations.md.\nexport interface Unit {\n\tproject: string\n\tsession: string\n\tjsonlPath: string\n}\n\nexport function enumerateUnits(projectsDir: string): Unit[] {\n\tlet projects: string[]\n\ttry {\n\t\tprojects = readdirSync(projectsDir).sort()\n\t} catch {\n\t\tthrow new Error(`no transcripts directory at ${projectsDir}`)\n\t}\n\tconst units: Unit[] = []\n\tfor (const project of projects) {\n\t\tconst dir = join(projectsDir, project)\n\t\tif (!statSync(dir).isDirectory()) continue\n\t\tfor (const f of readdirSync(dir).sort()) {\n\t\t\tif (!f.endsWith(\".jsonl\")) continue\n\t\t\tunits.push({\n\t\t\t\tproject,\n\t\t\t\tsession: basename(f, \".jsonl\"),\n\t\t\t\tjsonlPath: join(dir, f),\n\t\t\t})\n\t\t}\n\t}\n\treturn units\n}\n\nexport function observationsPath(catalogDir: string, unit: Unit): string {\n\treturn join(catalogDir, unit.project, unit.session, \"observations.md\")\n}\n\nexport type UnitStatus =\n\t// hash matches the recorded source_sha256 — skipped without parsing\n\t| { kind: \"mined\" }\n\t| { kind: \"below-threshold\"; turns: number }\n\t| {\n\t\t\tkind: \"pending\"\n\t\t\treason: \"new\" | \"changed\"\n\t\t\tturns: number\n\t\t\tdigest: string\n\t\t\tsourceSha256: string\n\t }\n\t// transcript has an unparseable line; reported visibly, never mined\n\t| { kind: \"unreadable\"; error: string }\n\n// Pure decision core: everything already read, nothing left but judgment.\nexport function decideUnitStatus(input: {\n\tunit: Unit\n\tjsonl: Buffer\n\trecordedSha256: string | null\n\tminTurns: number\n}): UnitStatus {\n\tconst sourceSha256 = createHash(\"sha256\").update(input.jsonl).digest(\"hex\")\n\tif (input.recordedSha256 === sourceSha256) return { kind: \"mined\" }\n\tlet turns: string[]\n\ttry {\n\t\tturns = humanTurns(input.jsonl.toString(\"utf-8\"))\n\t} catch (e) {\n\t\treturn { kind: \"unreadable\", error: String(e) }\n\t}\n\tif (turns.length < input.minTurns)\n\t\treturn { kind: \"below-threshold\", turns: turns.length }\n\treturn {\n\t\tkind: \"pending\",\n\t\treason: input.recordedSha256 === null ? \"new\" : \"changed\",\n\t\tturns: turns.length,\n\t\tdigest: renderDigest(\n\t\t\t{\n\t\t\t\tsource: input.unit.jsonlPath,\n\t\t\t\tproject: input.unit.project,\n\t\t\t\tsession: input.unit.session,\n\t\t\t},\n\t\t\tturns,\n\t\t),\n\t\tsourceSha256,\n\t}\n}\n\n// Shell: read the unit's inputs, then decide purely.\nexport function unitStatus(unit: Unit, catalogDir: string, minTurns: number): UnitStatus {\n\tconst obsPath = observationsPath(catalogDir, unit)\n\treturn decideUnitStatus({\n\t\tunit,\n\t\tjsonl: readFileSync(unit.jsonlPath),\n\t\trecordedSha256: existsSync(obsPath)\n\t\t\t? readSourceSha256(readFileSync(obsPath, \"utf-8\"))\n\t\t\t: null,\n\t\tminTurns,\n\t})\n}\n\nexport interface ObservationsMeta {\n\tsource: string\n\tsourceSha256: string\n\tminedAt: string\n\tmodel: string\n\thumanTurns: number\n}\n\n// The host composes the final file: frontmatter it alone can vouch for, then\n// the agent's report verbatim.\nexport function composeObservationsFile(meta: ObservationsMeta, body: string): string {\n\treturn `---\nsource: ${meta.source}\nsource_sha256: ${meta.sourceSha256}\nmined_at: ${meta.minedAt}\nmodel: ${meta.model}\nhuman_turns: ${meta.humanTurns}\n---\n\n${body.trimEnd()}\n`\n}\n\nexport function readSourceSha256(observationsMd: string): string | null {\n\treturn observationsMd.match(/^source_sha256: ([0-9a-f]{64})$/m)?.[1] ?? null\n}\n","import { mkdirSync, mkdtempSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport PQueue from \"p-queue\"\nimport {\n\ttype AgentRun,\n\tprogressSink,\n\tresolveAgentCli,\n\tresolveAgentRun,\n} from \"./agent-cli.ts\"\nimport {\n\tcomposeObservationsFile,\n\tenumerateUnits,\n\tobservationsPath,\n\ttype Unit,\n\ttype UnitStatus,\n\tunitStatus,\n} from \"./catalog.ts\"\nimport { composeMinePrompt, PROMPT_FILE } from \"./contract.ts\"\nimport { runAgent } from \"./runner.ts\"\n\nexport interface MineOpts {\n\tprojects: string\n\tcatalog: string\n\tagent: string\n\t// Absent = the agent CLI's default for mining.\n\tmodel?: string\n\tparallel: number\n\tminTurns: number\n\tlimit?: number\n\tdryRun: boolean\n}\n\nexport interface Pending {\n\tunit: Unit\n\tstatus: Extract<UnitStatus, { kind: \"pending\" }>\n}\n\n// What one mine invocation will and won't do, decided purely from the\n// per-unit statuses.\nexport interface MinePlan {\n\ttodo: Pending[]\n\tpendingTotal: number\n\tmined: number\n\tbelowThreshold: number\n\tunreadable: { unit: Unit; error: string }[]\n}\n\nexport function planMine(\n\tentries: { unit: Unit; status: UnitStatus }[],\n\tlimit?: number,\n): MinePlan {\n\tconst pending = entries.filter((e): e is Pending => e.status.kind === \"pending\")\n\treturn {\n\t\ttodo: limit ? pending.slice(0, limit) : pending,\n\t\tpendingTotal: pending.length,\n\t\tmined: entries.filter((e) => e.status.kind === \"mined\").length,\n\t\tbelowThreshold: entries.filter((e) => e.status.kind === \"below-threshold\").length,\n\t\tunreadable: entries.flatMap((e) =>\n\t\t\te.status.kind === \"unreadable\"\n\t\t\t\t? [{ unit: e.unit, error: e.status.error }]\n\t\t\t\t: [],\n\t\t),\n\t}\n}\n\nexport function summarizeMinePlan(plan: MinePlan, minTurns: number): string {\n\tconst total =\n\t\tplan.pendingTotal + plan.mined + plan.belowThreshold + plan.unreadable.length\n\tconst limited =\n\t\tplan.todo.length !== plan.pendingTotal ? ` (mining ${plan.todo.length})` : \"\"\n\treturn `${total} transcripts: ${plan.pendingTotal} pending${limited}, ${plan.mined} mined, ${plan.belowThreshold} below ${minTurns} turns, ${plan.unreadable.length} unreadable`\n}\n\nexport function dryRunRow({ unit, status }: Pending): string {\n\treturn `${status.reason.padEnd(7)} ${status.turns.toString().padStart(4)} turns ${unit.project}/${unit.session}`\n}\n\nasync function mineOneUnit(\n\t{ unit, status }: Pending,\n\topts: MineOpts,\n\trun: AgentRun,\n): Promise<boolean> {\n\tconst tag = unit.session.slice(0, 8)\n\tconst workdir = mkdtempSync(join(tmpdir(), `doublecheck-mine-${tag}-`))\n\twriteFileSync(\n\t\tjoin(workdir, PROMPT_FILE),\n\t\tcomposeMinePrompt(status.digest, workdir, run.cli.writeToolPhrase),\n\t)\n\tconst spec = run.cli.agent({\n\t\tcredentials: run.credentials,\n\t\tmodel: run.model,\n\t\tworkdir,\n\t})\n\tprocess.stderr.write(\n\t\t`[${tag}] mining ${unit.project}/${unit.session} (${status.turns} turns, ${status.reason})\\n`,\n\t)\n\tconst outcome = await runAgent({\n\t\tmount: opts.projects,\n\t\tworkdir,\n\t\tspec,\n\t\tnetwork: { onlyDomains: run.cli.egressDomains },\n\t\tonLine: progressSink(tag, run.cli.describeStreamLine),\n\t})\n\t// A failed mine writes NOTHING — the catalog is a durable asset; the next\n\t// run retries this unit because no hash gets recorded.\n\tif (!outcome.ok) {\n\t\tprocess.stderr.write(`[${tag}] FAILED (${outcome.reason})\\n`)\n\t\treturn false\n\t}\n\tconst obsPath = observationsPath(opts.catalog, unit)\n\tmkdirSync(dirname(obsPath), { recursive: true })\n\twriteFileSync(\n\t\tobsPath,\n\t\tcomposeObservationsFile(\n\t\t\t{\n\t\t\t\tsource: unit.jsonlPath,\n\t\t\t\tsourceSha256: status.sourceSha256,\n\t\t\t\tminedAt: new Date().toISOString(),\n\t\t\t\tmodel: run.model,\n\t\t\t\thumanTurns: status.turns,\n\t\t\t},\n\t\t\toutcome.report,\n\t\t),\n\t)\n\tprocess.stderr.write(`[${tag}] observations: ${obsPath}\\n`)\n\treturn true\n}\n\n// The mine workflow shell: gather statuses (I/O), plan purely, then either\n// print the plan (dry-run) or run one sandboxed agent per pending unit.\n// Returns true when nothing it attempted failed.\nexport async function runMine(opts: MineOpts): Promise<boolean> {\n\t// Validate the agent name before the dry-run/nothing-pending short-circuits\n\t// so a bad --agent errors on every invocation; the credentials preflight\n\t// stays below them (a dry run must not require credentials).\n\tresolveAgentCli(opts.agent)\n\tconst entries = enumerateUnits(opts.projects).map((unit) => ({\n\t\tunit,\n\t\tstatus: unitStatus(unit, opts.catalog, opts.minTurns),\n\t}))\n\tconst plan = planMine(entries, opts.limit)\n\tfor (const { unit, error } of plan.unreadable) {\n\t\tprocess.stderr.write(`UNREADABLE ${unit.project}/${unit.session}: ${error}\\n`)\n\t}\n\tprocess.stderr.write(`${summarizeMinePlan(plan, opts.minTurns)}\\n`)\n\n\tif (opts.dryRun) {\n\t\tfor (const pending of plan.todo) console.log(dryRunRow(pending))\n\t\treturn true\n\t}\n\tif (plan.todo.length === 0) return true\n\n\tconst run = resolveAgentRun(opts.agent, opts.model, \"mine\")\n\tconst queue = new PQueue({ concurrency: opts.parallel })\n\tconst results = await Promise.all(\n\t\tplan.todo.map((p) => queue.add(() => mineOneUnit(p, opts, run))),\n\t)\n\tconst failed = results.filter((ok) => ok !== true).length\n\tprocess.stderr.write(\n\t\t`mined ${results.length - failed}/${results.length}${failed ? `, ${failed} FAILED` : \"\"}\\n`,\n\t)\n\treturn failed === 0\n}\n","#!/usr/bin/env node\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { Command } from \"commander\"\nimport { runChecks } from \"./check.ts\"\nimport { runMine } from \"./mine.ts\"\n\nfunction parsePositiveInt(flag: string) {\n\treturn (v: string): number => {\n\t\tconst n = Number.parseInt(v, 10)\n\t\tif (!Number.isInteger(n) || n < 1)\n\t\t\tthrow new Error(`${flag} must be a positive integer, got \"${v}\"`)\n\t\treturn n\n\t}\n}\n\nconst collect = (v: string, prev: string[]): string[] => [...prev, v]\n\nconst program = new Command()\n\t.name(\"doublecheck\")\n\t.description(\n\t\t\"Run self-authored LLM code-inspectors (checks) against a target tree, one sandboxed agent per check\",\n\t)\n\nprogram\n\t.command(\"check\")\n\t.option(\"--target <dir>\", \"tree under inspection, mounted read-only\", process.cwd())\n\t.option(\n\t\t\"--checks-dir <dir>\",\n\t\t\"checks directory (repeatable; default: $TARGET/.agents/checks)\",\n\t\tcollect,\n\t\t[] as string[],\n\t)\n\t.option(\n\t\t\"--context <file>\",\n\t\t\"run-context file spliced into every inspector's prompt (intent, nuances, sanctioned exceptions, scope)\",\n\t)\n\t.option(\n\t\t\"--agent <name>\",\n\t\t\"agent CLI that runs the inspectors: claude or codex\",\n\t\t\"claude\",\n\t)\n\t.option(\n\t\t\"--model <model>\",\n\t\t\"model for the inspector agents (default per agent: claude haiku, codex gpt-5.5)\",\n\t)\n\t.option(\"--parallel <n>\", \"max concurrent checks\", parsePositiveInt(\"--parallel\"), 4)\n\t.option(\"--output <dir>\", \"reports root (default: $TARGET/.doublecheck)\")\n\t.option(\"--check <name>\", \"run only this check (repeatable)\", collect, [] as string[])\n\t.option(\n\t\t\"--save-jsonl\",\n\t\t\"persist each inspector's raw stream-json beside its report (audit trail)\",\n\t)\n\t.action(async (opts) => {\n\t\tconst target = resolve(opts.target)\n\t\tconst checksDirs: string[] =\n\t\t\topts.checksDir.length > 0\n\t\t\t\t? opts.checksDir.map((d: string) => resolve(d))\n\t\t\t\t: [join(target, \".agents\", \"checks\")]\n\t\tconst ok = await runChecks({\n\t\t\ttarget,\n\t\t\tchecksDirs,\n\t\t\tcontextFile: opts.context ? resolve(opts.context) : null,\n\t\t\tagent: opts.agent,\n\t\t\tmodel: opts.model,\n\t\t\tparallel: opts.parallel,\n\t\t\toutput: opts.output ? resolve(opts.output) : join(target, \".doublecheck\"),\n\t\t\tonly: opts.check,\n\t\t\tsaveJsonl: !!opts.saveJsonl,\n\t\t})\n\t\tif (!ok) process.exitCode = 1\n\t})\n\nprogram\n\t.command(\"mine\")\n\t.option(\n\t\t\"--projects <dir>\",\n\t\t\"Claude Code transcripts root\",\n\t\tjoin(homedir(), \".claude\", \"projects\"),\n\t)\n\t.option(\n\t\t\"--catalog <dir>\",\n\t\t\"observation catalog root\",\n\t\tjoin(homedir(), \".doublecheck\", \"catalog\"),\n\t)\n\t.option(\"--agent <name>\", \"agent CLI that runs the miners: claude or codex\", \"claude\")\n\t.option(\n\t\t\"--model <model>\",\n\t\t\"model for the mining agents (default per agent: claude opus, codex gpt-5.5 — a bad-model mine pollutes a durable asset)\",\n\t)\n\t.option(\"--parallel <n>\", \"max concurrent miners\", parsePositiveInt(\"--parallel\"), 4)\n\t.option(\n\t\t\"--min-turns <n>\",\n\t\t\"min genuine human turns for a real conversation\",\n\t\tparsePositiveInt(\"--min-turns\"),\n\t\t2,\n\t)\n\t.option(\"--limit <n>\", \"mine at most N pending units\", parsePositiveInt(\"--limit\"))\n\t.option(\"--dry-run\", \"list what would be mined without booting anything\")\n\t.action(async (opts) => {\n\t\tconst ok = await runMine({\n\t\t\tprojects: resolve(opts.projects),\n\t\t\tcatalog: resolve(opts.catalog),\n\t\t\tagent: opts.agent,\n\t\t\tmodel: opts.model,\n\t\t\tparallel: opts.parallel,\n\t\t\tminTurns: opts.minTurns,\n\t\t\tlimit: opts.limit,\n\t\t\tdryRun: !!opts.dryRun,\n\t\t})\n\t\tif (!ok) process.exitCode = 1\n\t})\n\nprogram.parseAsync().catch((e: unknown) => {\n\tconsole.error(e instanceof Error ? e.message : String(e))\n\tprocess.exit(1)\n})\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alizain/doublecheck",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Run self-authored LLM code-inspectors against a project — one sandboxed microVM agent per check, one markdown report each",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.5.1",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"claude",
|
|
18
|
+
"codex",
|
|
19
|
+
"openai",
|
|
18
20
|
"code-review",
|
|
19
21
|
"llm",
|
|
20
22
|
"agent",
|