@redue/cli 0.1.0-alpha.31 → 0.1.0-alpha.33

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/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.33 — 2026-11-06
4
+
5
+ pytest checks follow the project's quick suite, and the hook starts for
6
+ every check form.
7
+
8
+ - **The pull-request workflow's narrowed suite.** For pytest, `init` now
9
+ reads `coverage run -m pytest`, quoted arguments (`-m "not slow"`) and
10
+ lines continued with a backslash, and leaves out options that only write
11
+ a report (`--junit-xml`, `--html`, `--durations`). When a workflow that
12
+ runs on pull requests narrows the suite (`-m`, `-k`, `--ignore`,
13
+ `--deselect`), its line wins. On project-lighter/lighter the test check is
14
+ now `pytest tests -m "not slow"` (4.7 s), not the full suite with its
15
+ CIFAR download: setup took 68 s instead of 10.5 minutes, and all three
16
+ checks qualify.
17
+ - **Trigger rules that Claude Code matches.** A hook trigger rule with a
18
+ quote in it never started the hook in a real session, so such a check's
19
+ runs were neither skipped nor recorded. A rule now ends before the first
20
+ character outside letters, digits, spaces and `_./:=,+@-`, and the hook
21
+ matches the whole command itself, spaces inside quotes included.
22
+ - **`redue agent doctor` checks the trigger rules.** `trigger_rules` says
23
+ whether the installed rules start the hook for every command form of
24
+ every configured check, as REDUE models Claude Code's matching, and names
25
+ the forms they miss.
26
+ - **README.** Setup can take longer with a large environment or a slow
27
+ suite; trial runs stop at 5 minutes and `redue init --check test
28
+ --no-time-limit` tries a check without the limit.
29
+
30
+ Measured: stranger sessions on lighter (alpha.32 code with these changes)
31
+ skipped mypy and pytest after a docs edit and ran both through REDUE after
32
+ a source change, 0 critical. Python product census: 0 critical; 17 test,
33
+ 15 typecheck and 22 lint checks qualify, as in alpha.32. `init` on the 13
34
+ slowest census repos chose the same checks as alpha.32.
35
+
36
+ ## 0.1.0-alpha.32 — 2026-11-05
37
+
38
+ REDUE's status is now the point: what is verified on the code as it is
39
+ now, what is stale, what is due.
40
+
41
+ - **`redue verify`.** Runs every check that is due (STALE, or UNVERIFIED
42
+ with a program that exists), starting the observer if needed, then shows
43
+ the full status. It exits 0 only when every check that can be verified
44
+ ends CURRENT / PASS. `--json` gives `verified`, the checks it ran and the
45
+ status. It runs what is due up to three rounds, since a run can make
46
+ another check due.
47
+ - **Not verifiable.** A check no run can confirm shows as NOT VERIFIABLE
48
+ with the reason: a recording-only check, or one whose last two passing
49
+ runs changed the same inputs while they ran (a test script that rebuilds
50
+ a tracked bundle). `verify`, its exit code, the end-of-turn check and the
51
+ status line's due count leave it out; the status line shows it ("✓ 3
52
+ verified · 1 not verifiable"). Any other change, or a failing run, makes
53
+ it due again. JSON: `"verifiable": false` and a `not_verifiable` count.
54
+ - **A status line in Claude Code.** `redue setup claude` adds one:
55
+ "REDUE ✓ 3 verified", "REDUE ✓ 1 verified · 2 due: typecheck, test",
56
+ "REDUE ✗ 1 failing: lint". A status line you already have stays; setup
57
+ prints the command instead.
58
+ - **End-of-turn check.** When the agent changed files during a turn and a
59
+ check is due, the hook asks it once to run `redue verify` before it
60
+ finishes. It never asks twice in a turn. `"endOfTurnCheck": false` in
61
+ the config turns it off.
62
+ - **Faster sandboxed runs.** Receipts and checkpoints keep each installed
63
+ tree (`.venv`, `node_modules`) as one digest, a run computes only its own
64
+ check's context, and Linux keeps the Landlock probe's answer. What `redue
65
+ run` adds to a sub-second check on GitHub's Linux runners fell from
66
+ 270-514 ms to 125-205 ms (lighter's ruff 514 to 155 ms); on macOS from
67
+ 390-710 ms to 310-435 ms, where an FSEvents history stall (150-190 ms in
68
+ about a third of checkpoints) remains. The Landlock launcher now refuses
69
+ a network-less run on a kernel below ABI 4.
70
+ - **Windows.** A status call before the first `redue start` no longer
71
+ leaves a file that makes `start` refuse the state directory.
72
+
73
+ Measured with the verification-gap study (21 tasks on 7 repositories, 2
74
+ runs, Claude Code headless): sessions that ended with every check verified
75
+ on the final code went from 5 of 42 to 41 of 42 with Sonnet 5.5, and from
76
+ 20 of 42 to 42 of 42 with Opus 5.5, with no check REDUE called CURRENT /
77
+ PASS failing on the final code. In the product census, 73% of 330 checks
78
+ had a definite answer (CURRENT or STALE) after their second run (alpha.29:
79
+ 71%); 97% of those on the sandboxed recipe with ignored files and
80
+ documentation hidden. `init` on the 13 slowest census repos took 3874 s on
81
+ Linux and 4797 s on macOS (alpha.31: 4139 s and 5517 s), with the same
82
+ checks chosen.
83
+
3
84
  ## 0.1.0-alpha.31 — 2026-11-03
4
85
 
5
86
  A check that runs longer than init's trial limit no longer holds up
package/README.md CHANGED
@@ -1,14 +1,36 @@
1
1
  # REDUE
2
2
 
3
- **Know which checks still hold. Redo what's due.**
3
+ **Know what is verified on the code you have now.**
4
4
 
5
- You or your coding agent ran the typecheck, the linter or the tests. Then the
6
- code changed, or a new agent session started with no memory of it. REDUE
7
- records each check's result with the exact inputs it ran on, and tells you and
8
- your agent whether that result still applies to the code you have now, so a
9
- check that still holds is not run again and one that is stale is.
5
+ Your typecheck, linter and tests passed at some point. Then the code
6
+ changed, or a new agent session started with no memory of what ran. REDUE
7
+ records each check's result with the exact inputs it ran on, and gives you
8
+ and your coding agent one dependable answer: which checks are verified on
9
+ the code as it is now, which are stale, and which are due. When it cannot
10
+ tell, it says so and why; it never guesses a result into place.
10
11
 
11
- ## Quickstart (about 5 minutes)
12
+ ## What it changes
13
+
14
+ Coding agents often finish without running every check on their final code.
15
+ In headless Claude Code sessions on realistic tasks whose prompts never
16
+ mention checks (bug fixes, small features and refactors; 21 tasks on 7
17
+ TypeScript and Python projects, two runs each), the share of sessions that
18
+ ended with every check verified on the final code:
19
+
20
+ | Model | Without REDUE | With REDUE |
21
+ |---|---|---|
22
+ | Sonnet 5.5 | 12% (5 of 42) | 98% (41 of 42) |
23
+ | Opus 5.5 | 48% (20 of 42) | 100% (42 of 42) |
24
+
25
+ A check counts as verified when the session ran all of it on the final code,
26
+ or REDUE said CURRENT for it at the end; counting only the agents' own check
27
+ commands, it was 90% and 98% with REDUE. REDUE never called a failing check
28
+ verified: in no session did it say CURRENT / PASS for a check that failed
29
+ when run plainly on the final code. Without REDUE, 4 Sonnet sessions ended
30
+ with a failing check they had not run; with it, none did. The harness and
31
+ tasks are in [`eval/`](eval/README.md).
32
+
33
+ ## Quickstart
12
34
 
13
35
  You need macOS or Linux, Node 22 or 24, Git, Claude Code, and either a
14
36
  JavaScript or TypeScript project with its dependencies installed
@@ -31,37 +53,86 @@ stops before writing anything if the dependencies are not installed. It:
31
53
  workflows, Makefile, justfile, tox, nox or scripts give them: mypy,
32
54
  pyright and ty (typecheck), ruff (lint) and pytest (test). It says which
33
55
  checks it left out and why, and which it can only record, not reuse;
34
- - connects Claude Code: a short section in `CLAUDE.md`, a skill, and two hooks
35
- in `.claude/settings.local.json`;
56
+ - connects Claude Code: a short section in `CLAUDE.md`, a skill, hooks in
57
+ `.claude/settings.local.json`, and a status line there. A status line you
58
+ already have stays;
36
59
  - starts REDUE's file observer, waits while it indexes the project (a minute
37
60
  or more in a large one), then runs each check that can qualify once, so
38
- there is a first result to reuse, and prints one line per run (the run's
39
- own output only when it fails);
61
+ there is a first result, and prints one line per run (the run's own output
62
+ only when it fails);
40
63
  - prints every file it writes. Running it again changes nothing.
41
64
 
42
- Then start a new Claude Code session in the project and work as usual. When
43
- the agent is about to rerun a check whose last pass still applies, the hook
44
- skips it and tells the agent why. After an edit, the hook runs the check
45
- through REDUE so the new result is recorded.
65
+ Setup takes a minute or two in most projects. It can take longer in a large
66
+ environment, such as a `.venv` with PyTorch or a workspace with many
67
+ packages, because the observer indexes every installed file first, and with
68
+ a slow test suite, because `init` runs each check once plainly and once in
69
+ the sandbox before it chooses how to run it. Each of those trial runs stops
70
+ after 5 minutes; a check that runs longer is left out of the sandbox, and
71
+ setup names it and prints the command that tries it without the limit, such
72
+ as `redue init --check test --no-time-limit`. For pytest, `init` takes the
73
+ arguments the project's own workflows run it with, and prefers a
74
+ pull-request workflow that leaves slow tests out (`-m "not slow"`).
46
75
 
47
- To look yourself:
76
+ Then start a new Claude Code session in the project and work as usual. The
77
+ status line shows where things stand, updated as the agent works:
78
+
79
+ ```text
80
+ REDUE ✓ 3 verified
81
+ REDUE ✓ 2 verified · 1 due: typecheck
82
+ REDUE ✓ 2 verified · 1 not verifiable
83
+ ```
84
+
85
+ When the agent runs a check whose last pass still applies, the hook skips
86
+ the rerun and tells it why; after an edit, the hook runs the check through
87
+ REDUE so the new result is recorded. When the agent is about to finish a
88
+ turn in which files changed while a check is due, the hook asks it once to
89
+ run `redue verify` first (`"endOfTurnCheck": false` in the config turns
90
+ this off).
91
+
92
+ To ask yourself:
48
93
 
49
94
  ```text
50
95
  $ redue status
51
96
  ✓ typecheck CURRENT / PASS
52
97
  ✓ lint CURRENT / PASS
98
+ ✓ test CURRENT / PASS
53
99
 
54
100
  # after editing src/main.ts
55
- $ redue explain typecheck
56
- typecheck: STALE / PASS
57
- src/main.ts changed
58
- Due again when verification is needed: redue run "typecheck"
101
+ $ redue status
102
+ ⚠ typecheck STALE / PASS
103
+ src/main.ts changed
104
+ ⚠ lint STALE / PASS
105
+ src/main.ts changed
106
+ ✓ test CURRENT / PASS
107
+
108
+ $ redue verify
109
+ redue verify: running typecheck (STALE: src/main.ts changed)
110
+ typecheck: PASS recorded.
111
+ Check redue status before reusing this evidence.
112
+ redue verify: running lint (STALE: src/main.ts changed)
113
+ lint: PASS recorded.
114
+ Check redue status before reusing this evidence.
115
+
116
+ ✓ typecheck CURRENT / PASS
117
+ ✓ lint CURRENT / PASS
118
+ ✓ test CURRENT / PASS
119
+ Verified: every check that can be verified (3) passed on the files as they are now.
59
120
  ```
60
121
 
61
122
  `redue status` checks the files and your environment before it answers, as
62
123
  the agent's hook does. `redue status --cached` gives the last answer the
63
124
  observer published without checking; it can say UNVERIFIED ("cannot confirm
64
125
  your execution context") where `redue status` would say CURRENT.
126
+ `redue explain CHECK` says why a check is in its state.
127
+
128
+ `redue verify` runs every check that is due (STALE, or UNVERIFIED with a
129
+ command it can run), then prints the status. It exits 0 only when every
130
+ check that can be verified ends CURRENT / PASS, so it answers "is everything
131
+ verified on the code as it is now?" in one command. A check no run can
132
+ confirm is shown as NOT VERIFIABLE with the reason, and left out: a
133
+ recording-only check, or one whose runs change its own inputs (two passing
134
+ runs in a row changed the same files, such as a test script that rebuilds a
135
+ tracked bundle). Any other change, or a failing run, makes it due again.
65
136
 
66
137
  `redue stats` shows, per check, the reruns the hook skipped, the time saved,
67
138
  the recorded runs and the top reasons a check was UNVERIFIED, from a log kept
@@ -69,6 +140,26 @@ on your machine.
69
140
 
70
141
  To undo everything setup did: `redue remove`.
71
142
 
143
+ ## Time saved, as a bonus
144
+
145
+ A check whose last pass still applies does not run again. Headless Claude
146
+ Code sessions (Sonnet 5.5, CI Linux runner) on the same tasks with and
147
+ without REDUE, three sessions per task. Session time, medians per task
148
+ summed over the projects:
149
+
150
+ - **Typecheck** (twilio-node, semiotic, Ultra-Card, SwitchHosts; four tasks;
151
+ 96 sessions; 0.1.0-alpha.12 code): 464 s without, 330 s with. REDUE skipped
152
+ 33 typechecks. Each was checked afterwards with plain `tsc` on the exact tree
153
+ it was skipped on, and all 33 agreed.
154
+ - **Unit tests** (cheerio with Vitest 4, SwitchHosts with Vitest 5, twilio-node
155
+ with Jest 29; four tasks; 72 sessions; 0.1.0-alpha.14 code): 515 s without,
156
+ 295 s with. REDUE skipped 21 test runs. Each was rerun plainly three times on
157
+ the exact tree, and all passed every time.
158
+
159
+ API cost was the same in both arms. The savings come only when an earlier
160
+ pass still applies: a task that changes source code saves nothing. A handful
161
+ of projects and three runs per task show direction, not a general rate.
162
+
72
163
  ## Still CURRENT after your build and tests
73
164
 
74
165
  Running the project's build or tests writes files (`dist/`, `.next/`,
@@ -122,26 +213,6 @@ into and the sandbox can't run (inside Claude Code's macOS sandbox, on
122
213
  Windows), the project uses Bun, or a check writes into its own inputs.
123
214
  [Supported projects](docs/user/supported-projects.md) has the details.
124
215
 
125
- ## Measured with Claude Code
126
-
127
- Headless Claude Code sessions (Sonnet 5.5, CI Linux runner) on the same tasks
128
- with and without REDUE, three sessions per task. Session time, medians per
129
- task summed over the projects:
130
-
131
- - **Typecheck** (twilio-node, semiotic, Ultra-Card, SwitchHosts; four tasks;
132
- 96 sessions; 0.1.0-alpha.12 code): 464 s without, 330 s with. REDUE skipped
133
- 33 typechecks. Each was checked afterwards with plain `tsc` on the exact tree
134
- it was skipped on, and all 33 agreed.
135
- - **Unit tests** (cheerio with Vitest 4, SwitchHosts with Vitest 5, twilio-node
136
- with Jest 29; four tasks; 72 sessions; 0.1.0-alpha.14 code): 515 s without,
137
- 295 s with. REDUE skipped 21 test runs. Each was rerun plainly three times on
138
- the exact tree, and all passed every time.
139
-
140
- API cost was the same in both arms. The savings come only when an earlier
141
- pass still applies: a task that changes source code saves nothing. A handful
142
- of projects and three runs per task show direction, not a general rate. The
143
- harness, tasks and raw results are in [`eval/`](eval/README.md).
144
-
145
216
  ## What REDUE won't do
146
217
 
147
218
  - **Prove your code is correct.** CURRENT means the inputs of the recorded
@@ -151,9 +222,10 @@ harness, tasks and raw results are in [`eval/`](eval/README.md).
151
222
  - **Trust what it cannot see.** A program it cannot account for, a file it
152
223
  does not observe, or a gap in observation makes the result UNVERIFIED and
153
224
  says why. It never turns that into CURRENT by guessing.
154
- - **Support yet:** TypeScript 7's native compiler, Bun, per-package checks in
155
- large monorepos, results shared across machines, and the general recipe on
156
- Windows (Windows has the strict recipes only).
225
+ - **Support yet:** Bun, per-package checks in large monorepos, results shared
226
+ across machines, and the general recipe on Windows (Windows has the strict
227
+ recipes only). TypeScript 7's native compiler is covered: a typecheck that
228
+ runs it gets the [sandboxed recipe](docs/user/sandbox.md).
157
229
 
158
230
  ## What the states mean
159
231
 
@@ -163,6 +235,7 @@ harness, tasks and raw results are in [`eval/`](eval/README.md).
163
235
  | STALE | An input changed since the recorded result. | Rerun when you need the answer. |
164
236
  | UNVERIFIED | REDUE cannot tell: no result yet, an input it cannot see, or observation not ready. | Read the reason; rerun if it matters. |
165
237
  | FAILED | The last run failed: the check ran and reported a failure, or could not run. | Fix and rerun. |
238
+ | NOT VERIFIABLE | No run can confirm a result: a recording-only check, or one whose runs change its own inputs. | Read the reason; `redue verify` leaves it out. |
166
239
 
167
240
  ## Without Claude Code, or step by step
168
241
 
@@ -171,6 +244,7 @@ redue init # write redue.config.json from your scripts
171
244
  redue start # start the file observer
172
245
  redue run typecheck # run a check through REDUE and record the result
173
246
  redue status # what still holds
247
+ redue verify # run what is due; exit 0 when everything is CURRENT / PASS
174
248
  ```
175
249
 
176
250
  Codex is supported too (`redue agent setup codex --apply`); see
package/bin/redue.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decision-profile.mjs';
4
4
  traceBase({...traceContext(),request:traceId(),work:'client'});
5
- mark('client.loaded');
5
+ mark('client.loaded',{sinceStartMs:Math.round(performance.now())});
6
6
  import {directCompiler,directQualification,directEnvironment,directInstalledInputs,directInterpretation} from '../src/direct-typescript.mjs';
7
7
  import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
8
8
  import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
@@ -49,6 +49,8 @@ Commands:
49
49
  detail [CHECK] [--json] Alias for explain
50
50
  run CHECK [--if-due] Execute one configured check and record its outcome; with --if-due,
51
51
  only when its last pass does not apply to the files as they are now
52
+ verify [--json] Run every check that is due (STALE, or UNVERIFIED with a runnable command),
53
+ then show the status; exit 0 only when every check is CURRENT / PASS
52
54
  stats [--json] Per check: skips, time saved, recorded runs, why checks were UNVERIFIED
53
55
  remove-state Stop and remove only this configuration's owned local state
54
56
 
@@ -66,6 +68,12 @@ if(args.length===3&&args[0]==='agent'&&args[1]==='hook'&&args[2]==='claude'){
66
68
  const {runClaudeHook}=await import('../src/claude-hook.mjs');
67
69
  await runClaudeHook(fileURLToPath(import.meta.url));process.exit(0);
68
70
  }
71
+ // Claude Code's status line, also before config discovery: it prints one
72
+ // line, or nothing where REDUE is not set up.
73
+ if(args.length===3&&args[0]==='agent'&&args[1]==='statusline'&&args[2]==='claude'){
74
+ const {runStatusLine}=await import('../src/claude-hook.mjs');
75
+ await runStatusLine(fileURLToPath(import.meta.url));await exit(0);
76
+ }
69
77
  if(args.length===1&&args[0]==='--version'){console.log(version);process.exit(0);}
70
78
  // Internal: the entry this command runs, so setup and the hook can tell
71
79
  // whether the `redue` on PATH (a link, a package manager's shim, a wrapper)
@@ -99,7 +107,7 @@ if(command==='agent'){
99
107
  catch(e){error(e.message);}
100
108
  await exit(0);
101
109
  }
102
- if(!['init','start','stop','status','detail','explain','run','stats','remove-state'].includes(command))
110
+ if(!['init','start','stop','status','detail','explain','run','verify','stats','remove-state'].includes(command))
103
111
  error(`unknown command ${command||'<none>'}; use --help`);
104
112
  const json=args.includes('--json');
105
113
  // run --if-due: run the check only when its last pass does not apply now.
@@ -319,7 +327,9 @@ if(publicConfig.schema!==1)error('config schema must be 1');
319
327
  if(!Array.isArray(publicConfig.checks)||!publicConfig.checks.length)
320
328
  error('config needs at least one explicitly defined check');
321
329
  for(const key of Object.keys(publicConfig))
322
- if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
330
+ if(!['schema','root','checks','packageManager','endOfTurnCheck'].includes(key))error(`unsupported config field ${key}`);
331
+ // The Claude Code hook's end-of-turn check; only the hook reads it.
332
+ if(publicConfig.endOfTurnCheck!==undefined&&typeof publicConfig.endOfTurnCheck!=='boolean')error('endOfTurnCheck must be true or false');
323
333
  if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
324
334
  const root=realObservedPath(path.resolve(path.dirname(configFile),publicConfig.root||'.'));
325
335
  const state=stateOverride||projectState(root,configFile);
@@ -507,6 +517,75 @@ if(!maintenance&&!created){
507
517
  }
508
518
  }
509
519
 
520
+ if(command==='verify'){
521
+ const self=(more,options)=>spawnSync(process.execPath,[fileURLToPath(import.meta.url),'--config',configFile,
522
+ ...(stateOverride?['--state-dir',stateOverride]:[]),...more],{env:process.env,...options});
523
+ const ask=()=>{const r=self(['status','--sync','--json'],{encoding:'utf8',stdio:['ignore','pipe','ignore'],timeout:600_000});
524
+ try{return JSON.parse(r.stdout);}catch{return null;}};
525
+ const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
526
+ let status=ask();
527
+ // Nothing is confirmed without the observer: start it as the session hook
528
+ // does, and wait while it indexes.
529
+ if(!status?.observation?.healthy){
530
+ let pid=null;try{pid=Number(fs.readFileSync(path.join(state,'observer.lock','pid'),'utf8'));}catch{}
531
+ if(!(Number.isSafeInteger(pid)&&pid>0&&processAlive(pid))){
532
+ if(!json)console.error('redue verify: starting the observer');
533
+ self(['start'],{stdio:['ignore','ignore','inherit']});status=ask();
534
+ }
535
+ for(const until=Date.now()+120_000;!status?.observation?.healthy&&
536
+ ['discovery','indexing','reconciling','validating','initializing'].includes(status?.observation?.phase)&&Date.now()<until;){
537
+ await sleep(1000);status=ask();}
538
+ }
539
+ // A check is due when its last result does not apply to the files as they
540
+ // are now, and it can run: its program is there.
541
+ const runnable=row=>{const command=checks.find(item=>item.name===row.name)?.command?.[0];
542
+ if(typeof command!=='string'||command.endsWith('<unavailable>'))return false;
543
+ if(command.includes('/'))return fs.existsSync(path.resolve(root,checks.find(item=>item.name===row.name).cwd||'.',command));
544
+ return Boolean(findExecutable(command));};
545
+ // A check no run can confirm (recording-only, or one that rewrites its own
546
+ // inputs: verifiable false) is never due. A run can make another check due
547
+ // (a suite that writes an input), and a check that rewrites its own inputs
548
+ // shows that on its second run: verify runs what is due again, three rounds
549
+ // at most.
550
+ // In a round, a check whose last run changed its inputs runs first, so the
551
+ // checks its run makes due run after it.
552
+ // A check that already ran here is due again only when it is STALE or its
553
+ // inputs changed during its run: another UNVERIFIED reason would not clear.
554
+ const changedOwn=row=>/declared input event occurred during execution/.test(row.reason||'');
555
+ const ranNames=new Set();
556
+ const dueOf=value=>(value?.checks||[]).filter(row=>row.verifiable!==false&&
557
+ (row.freshness==='STALE'||row.freshness==='UNVERIFIED'&&runnable(row)&&(!ranNames.has(row.name)||changedOwn(row))))
558
+ .sort((a,b)=>changedOwn(b)-changedOwn(a));
559
+ // While the observer indexes or reconciles (a run may have changed a manifest).
560
+ const settle=async()=>{for(const until=Date.now()+120_000;!status?.observation?.healthy&&
561
+ ['discovery','indexing','reconciling','validating','initializing'].includes(status?.observation?.phase)&&Date.now()<until;){
562
+ await sleep(1000);status=ask();}};
563
+ const {note,dueNote}=await import('../src/stats.mjs');
564
+ const ran=[];
565
+ for(let round=0,due=dueOf(status);round<3&&due.length&&status?.observation?.healthy;round++,due=dueOf(status)){
566
+ for(const row of due){
567
+ const entry=dueNote(status,row.name);if(entry)note(state,entry);
568
+ if(!json)console.error(`redue verify: running ${row.name} (${row.freshness}: ${row.reason})`);
569
+ // With --json, the check's output goes to stderr; stdout carries the answer.
570
+ const r=self(['run',row.name],{stdio:json?['ignore',2,2]:'inherit'});
571
+ ran.push({check:row.name,was:row.freshness,exit:r.status});ranNames.add(row.name);
572
+ }
573
+ status=ask();await settle();
574
+ }
575
+ const final=status;
576
+ const counted=(final?.checks||[]).filter(row=>row.verifiable!==false);
577
+ const verified=Boolean(final?.observation?.healthy)&&counted.length>0&&counted.every(row=>row.freshness==='CURRENT'&&row.result==='PASS');
578
+ if(json)console.log(JSON.stringify({schema:1,verified,ran,status:final}));
579
+ else{
580
+ if(ran.length)console.log('');
581
+ console.log(final?renderStatus(final):'UNVERIFIED — status did not answer.');
582
+ const other=final?.checks?.length-counted.length;
583
+ console.log((verified?`Verified: every check that can be verified (${counted.length}) passed on the files as they are now.`:
584
+ counted.length?'Not verified: some checks did not end CURRENT / PASS.':'Not verified: no check can be verified here.')+
585
+ (other?` ${other} not verifiable, left out.`:''));
586
+ }
587
+ await exit(verified?0:1);
588
+ }
510
589
  if(command==='status'&&!sync){
511
590
  const value=readCachedStatus(state,publicConfig.checks);
512
591
  console.log(json?JSON.stringify(value):renderStatus(value,{short}));
@@ -87,6 +87,32 @@ paths no longer exist. When the hook runs a check through REDUE for the
87
87
  agent, it likewise writes plain `redue run` only if its own PATH finds this
88
88
  install, else the absolute form.
89
89
 
90
+ The same command also runs on UserPromptSubmit and Stop, for the end-of-turn
91
+ check. At the start of a turn it notes the observer's count of file events
92
+ and which checks are CURRENT (on Windows, from a synchronized status). When
93
+ the agent stops, it asks status: if a check is due (STALE or UNVERIFIED) and
94
+ files changed during the turn (the count moved, a check CURRENT at the start
95
+ is due now, or the turn's transcript has an Edit, Write, MultiEdit or
96
+ NotebookEdit call), it
97
+ blocks the stop once with "Before you finish, run `redue verify` and report
98
+ what it says". It never asks twice in a turn, and never on the stop that
99
+ follows its reminder (`stop_hook_active`), so it cannot loop. A turn that
100
+ changed nothing ends quietly even when a check is due, and so does any turn
101
+ where the observer is not healthy. `"endOfTurnCheck": false` in the config
102
+ turns it off.
103
+
104
+ Setup also adds Claude Code's status line, `redue agent statusline claude`,
105
+ in the same file and the same form. It prints one line from a synchronized
106
+ status (the observer's cached one when that takes over 3 s): `REDUE ✓ 3
107
+ verified`, `REDUE ✓ 1 verified · 2 due: typecheck, test`, `REDUE ✗ 1 failing:
108
+ lint`, `REDUE ✓ 3 verified · 1 not verifiable`, `REDUE … checking files` while
109
+ the observer indexes, or `REDUE ? observer not running`. A check that is not
110
+ verifiable (see [the JSON interface](json.md)) is never due, here or for the
111
+ end-of-turn check. Where Claude Code already has a status line (in the
112
+ local or shared project settings, or your user settings), setup leaves it and
113
+ prints the command, so you can add REDUE's line to yours. `redue agent remove
114
+ claude --apply` and `redue remove` take out only REDUE's status line.
115
+
90
116
  Agents often chain an edit and a check (`printf … >> README.md && npm run
91
117
  lint`). In a simple chain (commands joined by `&&`, `||` or `;`, with no
92
118
  subshell, here-document, command substitution, background job or shell
@@ -281,7 +307,18 @@ config resolution, cached observer health, a bounded read-only control-access pr
281
307
  managed-file integrity, detected host
282
308
  version, and loading guidance. `behavior_verified: false` is deliberate: intact
283
309
  files do not prove the agent followed them. For Claude, doctor reports whether the
284
- typecheck hook is installed. Doctor neither
310
+ typecheck hook is installed, the status line, and `trigger_rules`: whether the
311
+ `if` rules in the settings start the hook for every command form of every
312
+ configured check (`covered`, or the forms they miss, which would run plainly,
313
+ neither skipped nor recorded), including the fan-out commands and the forms
314
+ after a `cd` into a check's directory. It applies the rules as REDUE models
315
+ Claude Code's matching, not by asking Claude Code: `Bash(PREFIX*)` (or the
316
+ legacy `Bash(PREFIX:*)`) matches a simple command that starts with PREFIX in
317
+ the characters known to match (letters, digits, spaces and `_./:=,+@-`), and a
318
+ hook with no `if` matches every command. Setup ends a rule before any other
319
+ character, such as the quote in `-m "not slow"`, and the hook then matches
320
+ the whole command itself, spaces inside quotes included; a rule that would
321
+ end before the program's first argument is not written. Doctor neither
285
322
  executes checks nor starts observers, reconciles inputs, or installs a host.
286
323
 
287
324
  Doctor separates missing/unreadable state, a missing or refused endpoint, an
@@ -33,6 +33,14 @@ For the explicit direct compiler choice and generated exact argv, see
33
33
  A general check (`"qualification": "node-script-v1"`) has a `script` and no
34
34
  `command`; see the [general recipe](node-script.md).
35
35
 
36
+ ## End-of-turn check
37
+
38
+ With Claude Code, REDUE's hook checks at the end of each agent turn: when the
39
+ turn changed files and a check is due, it asks the agent once to run
40
+ `redue verify` before it finishes ([agents](agents.md)). To turn this off,
41
+ add `"endOfTurnCheck": false` at the top level of the config. Only the hook
42
+ reads it; the observer does not need a restart.
43
+
36
44
  ## Arbitrary commands
37
45
 
38
46
  An unrecognized ecosystem can still record ordinary execution:
@@ -64,8 +64,9 @@ byte for byte, in one of two ways:
64
64
 
65
65
  CI runs the same contract suite against the last release of each minor from
66
66
  5.0 to 5.8 that the reviewed releases do not cover. Later minors (6.1 and up)
67
- wait until CI covers them. TypeScript 7 (the native compiler) is not
68
- supported.
67
+ wait until CI covers them. This recipe does not take TypeScript 7 (the
68
+ native compiler); a typecheck that runs it gets the
69
+ [sandboxed recipe](sandbox.md).
69
70
 
70
71
  ## Script forms
71
72
 
package/docs/user/json.md CHANGED
@@ -59,3 +59,21 @@ JSON summary. **Its stdout is not a JSON-only stream.** Query status/details aft
59
59
  completion for machine-readable evidence. Normal check exit codes are preserved;
60
60
  interruption/process-start failures are nonzero. REDUE retains metadata and receipts,
61
61
  not arbitrary terminal output. The CLI does not infer tests executed from prose.
62
+
63
+ A check no run can confirm has `"verifiable": false` (its `freshness` stays
64
+ UNVERIFIED and `reason` says why): a recording-only check (no qualification
65
+ contract, inputs it cannot resolve), or one whose last two runs passed while
66
+ the same inputs changed during them, with its plan, environment and every
67
+ other input unchanged since. Anything else changing, a failing run or a
68
+ clean run makes it verifiable (and due) again. Such checks are counted in
69
+ `not_verifiable`, not in the other counts, and do not set `state`. A cached
70
+ answer (`--cached`) never marks a check this way.
71
+
72
+ `verify --json` writes one JSON object to stdout: `verified` (true only when the
73
+ observer is healthy, at least one check is verifiable, and every verifiable
74
+ check is CURRENT / PASS), `ran` (each check it ran, in order: `check`, `was`
75
+ (STALE or UNVERIFIED) and the run's `exit`), and `status`, the synchronized
76
+ status after those runs. It runs what is due up to three rounds, since a run
77
+ can make another check due; a check whose last run changed its inputs runs
78
+ first in a round. It never runs a check that is not verifiable. The checks'
79
+ own output goes to stderr. It exits 0 when `verified` is true, else 1.
@@ -41,9 +41,13 @@ built declarations) gets the whole checkout. See
41
41
  `.venv` (`uv sync` makes one; Poetry does with `virtualenvs.in-project`).
42
42
  `init` uses the arguments your CI workflow, Makefile, tox, nox, justfile or
43
43
  shell scripts in `scripts/` run the tool with, through `uv run`, `poetry run`,
44
- `python -m` or `.venv/bin`. Without them it uses the tool's default:
45
- `ruff check`, `ty check`, `pyright`, and `mypy` (with `.` when mypy's
46
- configuration names no `files`).
44
+ `python -m`, `coverage run -m` or `.venv/bin`, including quoted arguments
45
+ (`-m "not slow"`) and lines continued with a backslash. It takes the first
46
+ such line, but a pull-request workflow's when it narrows the test suite
47
+ (`-m`, `-k`, `--ignore`, `--deselect`), and leaves out options that only
48
+ write a report (`--junit-xml`, `--html`, `--durations`). Without them it
49
+ uses the tool's default: `ruff check`, `ty check`, `pyright`, `pytest`, and
50
+ `mypy` (with `.` when mypy's configuration names no `files`).
47
51
  - **Platforms:** macOS (Seatbelt, `sandbox-exec`) and Linux 6.7 or later
48
52
  (Landlock; the launcher needs Python 3).
49
53
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@redue/cli",
3
3
  "private": false,
4
- "version": "0.1.0-alpha.31",
4
+ "version": "0.1.0-alpha.33",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"