@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 +81 -0
- package/README.md +117 -43
- package/bin/redue.mjs +82 -3
- package/docs/user/agents.md +38 -1
- package/docs/user/configuration.md +8 -0
- package/docs/user/direct-typescript.md +3 -2
- package/docs/user/json.md +18 -0
- package/docs/user/sandbox.md +7 -3
- package/package.json +1 -1
- package/src/agent-integration.mjs +61 -17
- package/src/cached-state.mjs +9 -6
- package/src/claude-hook.mjs +185 -5
- package/src/cli.mjs +45 -17
- package/src/daemon.mjs +62 -8
- package/src/landlock.py +3 -0
- package/src/presentation.mjs +4 -3
- package/src/python-onboarding.mjs +54 -12
- package/src/sandbox.mjs +33 -9
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
|
|
3
|
+
**Know what is verified on the code you have now.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
records each check's result with the exact inputs it ran on, and
|
|
8
|
-
your agent
|
|
9
|
-
|
|
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
|
-
##
|
|
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,
|
|
35
|
-
|
|
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
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
typecheck
|
|
57
|
-
src/main.ts changed
|
|
58
|
-
|
|
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:**
|
|
155
|
-
|
|
156
|
-
|
|
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}));
|
package/docs/user/agents.md
CHANGED
|
@@ -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
|
|
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
|
|
68
|
-
|
|
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.
|
package/docs/user/sandbox.md
CHANGED
|
@@ -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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|