@redue/cli 0.1.0-alpha.27 → 0.1.0-alpha.29

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,96 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.29 — 2026-11-03
4
+
5
+ Editing documentation no longer makes sandboxed checks STALE, a fast check
6
+ is skipped or recorded as it should be, and macOS test runs can no longer
7
+ reach a service started outside them.
8
+
9
+ - **Documentation is hidden too (`sandbox-v3`).** A sandboxed run now also
10
+ hides the project's documentation: files named `*.md`, `*.mdx`, `*.rst`,
11
+ `LICENSE*` or `CHANGELOG*` anywhere, and text, image and PDF files in the
12
+ root `docs/` folder (code there stays visible). The receipt names the
13
+ files the run hid, and a later edit to one of them leaves the result
14
+ CURRENT: editing the README no longer makes the tests STALE. A new
15
+ documentation file still counts. `init` tries this first, then the
16
+ git-ignored files only (`sandbox-v2`), then the whole checkout. A check
17
+ that reads documentation (a doctest over the README) keeps `sandbox-v2`.
18
+ Run `redue init` again to move an existing check to `sandbox-v3`.
19
+ - **A fast check is no longer missed by the hook.** The hook waited half of
20
+ the check's last run for `redue status`, so a 0.2 s mypy got 0.1 s, the
21
+ status answer came too late, and the agent's run went unrecorded. The
22
+ wait is now at least 2 s. `REDUE_HOOK_LOG` (an absolute file path) makes
23
+ the hook write one line per Bash command with its decision and why.
24
+ - **macOS test runs get no network.** Seatbelt cannot refuse only the ports
25
+ that were already listening (a per-port rule under a loopback allow is not
26
+ applied; an allow list of every other port does not compile), so a macOS
27
+ test run now gets Unix sockets in its own temporary directory only, as a
28
+ typecheck or lint does. A suite that needs loopback fails `init`'s trial
29
+ and runs without REDUE. Test checks set up by alpha.28 on macOS are not
30
+ CURRENT after the upgrade.
31
+ - **`redue status` reconciles by default.** `redue status --cached` gives the
32
+ observer's last published answer; `--sync` is still accepted. Like
33
+ `--sync`, plain `status` exits 2 when it cannot reach the observer (after
34
+ `redue stop`, for example); `--cached` exits 0.
35
+ - **`redue setup` says more and prints less.** It lists the checks it left
36
+ out and why (a kind it found nothing for, a proposed `tsc --noEmit` no
37
+ recipe takes), says what recording only means, and prints one line per
38
+ recorded run instead of the check's whole output.
39
+ - `redue remove-state` waits for the observer to exit before removing its
40
+ state (a Windows ENOTEMPTY).
41
+ - **Measured.** In the product census, repositories whose qualifying
42
+ sandboxed check keeps the documentation hidden, out of those with one:
43
+ TypeScript on Linux, typecheck 17 of 19, lint 20 of 20, tests 7 of 8; on
44
+ macOS 12 of 13, 14 of 16, 6 of 9. Python on Linux 15 of 15, 22 of 22, 12
45
+ of 17; on macOS 11 of 13, 17 of 17, 8 of 11. No network for macOS test
46
+ runs cost 1 TypeScript suite (it starts a server on 127.0.0.1) and 2
47
+ Python suites: test suites qualifying on macOS went from 12 to 11
48
+ (TypeScript) and 14 to 11 (Python; one checkout no longer installs). The
49
+ strace cross-check found no read outside the recorded inputs. In a Claude
50
+ Code follow-up on starlette and nestjs-api-gateway, a README edit made no
51
+ check STALE; the hook skipped mypy, pytest and the lint, and each skipped
52
+ command passed when run plainly.
53
+
54
+ ## 0.1.0-alpha.28 — 2026-11-02
55
+
56
+ Test suites can run in the sandbox, so REDUE can reuse a passing test run
57
+ for more projects, Python included.
58
+
59
+ - **Sandboxed tests.** A root test script that the direct test recipe does
60
+ not take, and pytest in a Python project's `.venv`, can now use the
61
+ sandboxed recipe. `init` keeps it only when the run in the sandbox gives the
62
+ same exit status and test counts as a plain run. On top of what a
63
+ typecheck or lint gets, a test run gets:
64
+ - **Loopback only.** It may listen on any address and connect to the local
65
+ host and Unix sockets, never to a remote host. On macOS, Seatbelt
66
+ enforces that. On Linux, a seccomp filter refuses UDP and other non-TCP
67
+ sockets, TCP Fast Open and `io_uring`, and hands each `connect` to
68
+ REDUE's launcher. The launcher allows a local port nothing listens on or
69
+ one the run's own processes listen on, and refuses everything else,
70
+ including a service you started outside the run (a local database). If
71
+ the launcher cannot start or stops early, the result is UNVERIFIED.
72
+ - **A private `/tmp`.** Its `TMPDIR` is new for each run, and it may not
73
+ write elsewhere in `/tmp` or `/var/tmp`.
74
+ - **A fresh home** for each run, as on CI.
75
+ - **Ignored files hidden** when the trial agrees, else the whole checkout.
76
+ - **Measured.** In the product census, test suites with a qualifying check
77
+ (alpha.27 in brackets): TypeScript on Linux 11 (2), on macOS 12 (2);
78
+ Python on Linux 17 (0), on macOS 14 (0). The strace cross-check found no
79
+ read outside the recorded inputs. The Linux connect supervisor added at
80
+ most 1.5% over the sandbox without it on suites with worker processes
81
+ (pytest-xdist, Jest, Vitest): apprise 233 s to 237 s, run-to-run spread
82
+ 218-243 s. In a pilot with Claude Code on two pytest projects, the hook
83
+ skipped both "do the tests still pass?" reruns (19 s to 6 s and 28 s to
84
+ 7 s), and plain reruns agreed with both skips.
85
+ - **macOS hides contents only.** On macOS, a hidden file's contents and
86
+ execution are refused, but its metadata is not, as on Linux. A test that
87
+ writes into an ignored folder works.
88
+ - Known limits: a suite that reads back ignored files it writes in the same
89
+ run (a coverage database, a build it starts) gets the whole checkout, and
90
+ one that writes tracked files or coverage data during the run ends
91
+ UNVERIFIED. On Linux, a test that needs a service started outside the run
92
+ fails in the sandbox, so `init` leaves it to a plain run.
93
+
3
94
  ## 0.1.0-alpha.27 — 2026-11-01
4
95
 
5
96
  A build or test run no longer makes a sandboxed typecheck or lint STALE,
package/README.md CHANGED
@@ -26,13 +26,17 @@ redue setup claude
26
26
  stops before writing anything if the dependencies are not installed. It:
27
27
 
28
28
  - finds your typecheck, lint and test scripts and writes `redue.config.json`
29
- (it never changes your scripts, source or dependencies). Checks REDUE can
30
- only record, not reuse, are listed as recording-only;
29
+ (it never changes your scripts, source or dependencies). In a Python
30
+ project it finds these tools in `.venv/bin`, with the arguments your CI
31
+ workflows, Makefile, justfile, tox, nox or scripts give them: mypy,
32
+ pyright and ty (typecheck), ruff (lint) and pytest (test). It says which
33
+ checks it left out and why, and which it can only record, not reuse;
31
34
  - connects Claude Code: a short section in `CLAUDE.md`, a skill, and two hooks
32
35
  in `.claude/settings.local.json`;
33
36
  - starts REDUE's file observer, waits while it indexes the project (a minute
34
37
  or more in a large one), then runs each check that can qualify once, so
35
- there is a first result to reuse;
38
+ there is a first result to reuse, and prints one line per run (the run's
39
+ own output only when it fails);
36
40
  - prints every file it writes. Running it again changes nothing.
37
41
 
38
42
  Then start a new Claude Code session in the project and work as usual. When
@@ -43,7 +47,7 @@ through REDUE so the new result is recorded.
43
47
  To look yourself:
44
48
 
45
49
  ```text
46
- $ redue status --sync
50
+ $ redue status
47
51
  ✓ typecheck CURRENT / PASS
48
52
  ✓ lint CURRENT / PASS
49
53
 
@@ -54,10 +58,10 @@ src/main.ts changed
54
58
  Due again when verification is needed: redue run "typecheck"
55
59
  ```
56
60
 
57
- `--sync` checks the files and your environment before it answers; this is
58
- what the agent asks. Plain `redue status` is a fast cached answer and can say
59
- UNVERIFIED ("cannot confirm your execution context") where `--sync` would say
60
- CURRENT.
61
+ `redue status` checks the files and your environment before it answers, as
62
+ the agent's hook does. `redue status --cached` gives the last answer the
63
+ observer published without checking; it can say UNVERIFIED ("cannot confirm
64
+ your execution context") where `redue status` would say CURRENT.
61
65
 
62
66
  `redue stats` shows, per check, the reruns the hook skipped, the time saved,
63
67
  the recorded runs and the top reasons a check was UNVERIFIED, from a log kept
@@ -65,28 +69,52 @@ on your machine.
65
69
 
66
70
  To undo everything setup did: `redue remove`.
67
71
 
72
+ ## Still CURRENT after your build and tests
73
+
74
+ Running the project's build or tests writes files (`dist/`, `.next/`,
75
+ coverage). REDUE leaves those out when the files are git-ignored, so a
76
+ typecheck or lint that passed stays CURRENT. In the census, after each
77
+ project's own build, tests, typecheck and lint ran, the share of sandboxed
78
+ checks still CURRENT:
79
+
80
+ | | alpha.26 | alpha.27 |
81
+ |---|---|---|
82
+ | TypeScript, Linux | 5 of 43 | 31 of 37 |
83
+ | TypeScript, macOS | 5 of 38 | 24 of 34 |
84
+ | Python, Linux | 24 of 37 | 31 of 38 |
85
+ | Python, macOS | 20 of 33 | 28 of 34 |
86
+
87
+ The rest changed for real: the build rewrote a tracked file (a sitemap, a
88
+ feed, a generated source), added a file git doesn't ignore, or installed
89
+ packages.
90
+
68
91
  ## What it covers today
69
92
 
70
93
  In a census of 120 active public TypeScript repositories (November 2026, REDUE
71
- at commit e264a67, before 0.1.0-alpha.27), each installed as its users would,
72
- with dependencies' install scripts on, 90 installed cleanly on a macOS runner.
94
+ at commit 8cb32d6, before 0.1.0-alpha.29), each installed as its users would,
95
+ with dependencies' install scripts on, 89 installed cleanly on a macOS runner.
73
96
  A check counts when two recorded runs on the same code both came out CURRENT
74
- on the same inputs. 35 repositories had at least one such check:
97
+ on the same inputs. 34 repositories had at least one such check:
75
98
 
76
99
  | Check | Repositories | How |
77
100
  |---|---|---|
78
- | Typecheck | 22 | 16 by running the project's script in an OS sandbox ([sandboxed recipe](docs/user/sandbox.md)), 15 of them with the files git ignores hidden; 5 by running the local `tsc` directly ([direct recipe](docs/user/direct-typescript.md)); 1 by running the script with every Node process recorded ([general recipe](docs/user/node-script.md)) |
79
- | Lint | 24 | 21 sandboxed recipe (19 with ignored files hidden), 3 general recipe |
80
- | Tests | 2 | Vitest run directly ([direct test recipe](docs/user/tests-direct.md)) |
101
+ | Typecheck | 20 | 13 by running the project's script in an OS sandbox ([sandboxed recipe](docs/user/sandbox.md)), 12 of them with the files git ignores and the documentation hidden; 5 by running the local `tsc` directly ([direct recipe](docs/user/direct-typescript.md)); 2 by running the script with every Node process recorded ([general recipe](docs/user/node-script.md)) |
102
+ | Lint | 20 | 16 sandboxed recipe (14 with ignored files and documentation hidden, 1 with ignored files hidden), 4 general recipe |
103
+ | Tests | 11 | 9 sandboxed recipe with no network (6 with ignored files and documentation hidden, 3 with ignored files hidden); 2 Vitest run directly ([direct test recipe](docs/user/tests-direct.md)) |
104
+
105
+ alpha.28 found 38 repositories on macOS. Test runs there no longer get
106
+ loopback, which cost 1 test suite (it starts a server on 127.0.0.1). Of the
107
+ other 3, one lint check was lost to a documentation-hiding bug fixed before
108
+ this release, one checkout now installs with pnpm in a way REDUE does not
109
+ take, and one `init` did not answer within the census's 10 minutes. On
110
+ Linux, where tests keep loopback, 10 test suites qualified.
81
111
 
82
112
  In a census of 120 Python repositories, 74 installed on a Linux runner
83
- and 23 had a qualifying check: typecheck (mypy, pyright, ty) 15, lint (ruff)
84
- 22, all through the sandboxed recipe with ignored files hidden. A check whose
85
- git-ignored files are hidden stays CURRENT when a build or test run rewrites
86
- them: after each project's build and tests ran, 31 of 37 such TypeScript
87
- checks and 31 of 38 Python checks were still CURRENT on Linux. On Linux, every sandboxed run was traced
88
- with strace: each file it read in the checkout was a recorded input, a cache
89
- or its own output.
113
+ and 31 had a qualifying check: typecheck (mypy, pyright, ty) 15, lint (ruff)
114
+ 22, tests (pytest) 17, all through the sandboxed recipe (15, 22 and 12 of
115
+ them with ignored files and documentation hidden). On Linux, every sandboxed
116
+ run was traced with strace: each file it read in the checkout was a recorded
117
+ input, a cache or its own output.
90
118
 
91
119
  Everything else stays UNVERIFIED, with a reason you can read in
92
120
  `redue explain`. The usual ones: a script starts a program REDUE cannot see
@@ -142,7 +170,7 @@ harness, tasks and raw results are in [`eval/`](eval/README.md).
142
170
  redue init # write redue.config.json from your scripts
143
171
  redue start # start the file observer
144
172
  redue run typecheck # run a check through REDUE and record the result
145
- redue status --sync # what still holds
173
+ redue status # what still holds
146
174
  ```
147
175
 
148
176
  Codex is supported too (`redue agent setup codex --apply`); see
package/bin/redue.mjs CHANGED
@@ -7,7 +7,7 @@ import {directCompiler,directQualification,directEnvironment,directInstalledInpu
7
7
  import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
8
8
  import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
9
9
  import {scriptEnvironment,scriptInterpretation,scriptKinds,scriptQualification} from '../src/node-script.mjs';
10
- import {hiddenListIssue,hiddenQualification,isSandboxed,pythonTool,sandboxEnvironment,sandboxInterpretationOf,sandboxKinds,sandboxQualification} from '../src/sandbox.mjs';
10
+ import {docsQualification,hiddenDocsIssue,hiddenListIssue,hiddenQualification,isSandboxed,pythonTool,sandboxEnvironment,sandboxInterpretationOf,sandboxKinds,sandboxQualification} from '../src/sandbox.mjs';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import {createHash} from 'node:crypto';
@@ -40,8 +40,8 @@ Commands:
40
40
  Connect project instructions; explicit setup only
41
41
  start Start the local observer
42
42
  stop Stop this observer
43
- status [--json|--short] Read conservative cached status
44
- status --sync Reconcile inputs and caller context before answering
43
+ status [--json] Reconcile inputs and caller context, then answer (--sync is the same)
44
+ status --cached [--json] | --short The last answer the observer published, without reconciling
45
45
  explain [CHECK] [--json] Explain applicability (synchronized; may cost more than a rerun)
46
46
  detail [CHECK] [--json] Alias for explain
47
47
  run CHECK [--if-due] Execute one configured check and record its outcome; with --if-due,
@@ -102,9 +102,12 @@ const json=args.includes('--json');
102
102
  // run --if-due: run the check only when its last pass does not apply now.
103
103
  const ifDue=command==='run'&&args.includes('--if-due');
104
104
  if(ifDue)args.splice(args.indexOf('--if-due'),1);
105
- const sync=args.includes('--sync');
105
+ // status syncs by default (warm, it costs well under 100 ms more than the
106
+ // cached answer); --cached and --short read the cached answer.
107
+ const cachedOnly=args.includes('--cached');
106
108
  const dryRun=args.includes('--dry-run');
107
109
  const short=args.includes('--short');
110
+ const sync=args.includes('--sync')||command==='status'&&!cachedOnly&&!short;
108
111
  let workspaceSelection=null,checkSelection=null,recipeSelection=null;
109
112
  if(command==='init')for(let i=0;i<args.length;){
110
113
  if(args[i]==='--workspace'||args[i]==='--check'||args[i]==='--recipe'){
@@ -115,9 +118,10 @@ if(command==='init')for(let i=0;i<args.length;){
115
118
  }
116
119
  const check=['run','detail','explain'].includes(command)&&args[0]&&!args[0].startsWith('--')?args.shift():null;
117
120
  if(command==='run'&&!check)error('run requires a check name');
118
- if(args.some(value=>!['--json','--sync','--dry-run','--short'].includes(value)))error('unknown option; use --help');
121
+ if(args.some(value=>!['--json','--sync','--cached','--dry-run','--short'].includes(value)))error('unknown option; use --help');
119
122
  if(sync&&!['status','detail','explain'].includes(command))error('--sync is for status or detail');
120
- if(short&&(command!=='status'||json||sync))error('--short is a cached status option; do not combine it with --sync or --json');
123
+ if(cachedOnly&&(command!=='status'||args.includes('--sync')))error('--cached is a status option; do not combine it with --sync');
124
+ if(short&&(command!=='status'||json||args.includes('--sync')))error('--short is a cached status option; do not combine it with --sync or --json');
121
125
  if(dryRun&&command!=='init')error('--dry-run is for init');
122
126
 
123
127
  if(command==='init'){
@@ -153,6 +157,7 @@ if(command==='init'){
153
157
  }
154
158
  // A sandboxed check is proposed only when it gives the same exit status and
155
159
  // output inside the sandbox as outside it: first with the checkout's
160
+ // git-ignored files and documentation hidden (sandbox-v3), then with the
156
161
  // git-ignored files hidden (sandbox-v2), else with the whole checkout
157
162
  // (sandbox-v1), reusing the plain run. Otherwise init writes what it would
158
163
  // have without the sandboxed recipe, with the reason.
@@ -165,11 +170,18 @@ if(command==='init'){
165
170
  for(const [at,row] of [...discovery.checks.entries()]){
166
171
  if(row.qualification!==sandboxQualification||row.level!=='ready')continue;
167
172
  if(!json)console.error(`Running ${row.config.name} inside and outside the sandbox to confirm it...`);
168
- const hidden=await sandboxTrial(discovery.root,row.config,{hide:true});
169
- if(!hidden.issue)trials++;
173
+ const docs=await sandboxTrial(discovery.root,row.config,{hide:'docs'});
174
+ if(!docs.issue)trials++;
175
+ if(!docs.issue&&docs.same){
176
+ row.config={...row.config,qualification:docsQualification};row.qualification=docsQualification;
177
+ row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore, except its documentation, are its input")+`; ran both ways at init, exit ${docs.plain}`;continue;}
178
+ // Without a list of the documentation, the git-ignored files are still tried.
179
+ const hidden=docs.plainRun?await sandboxTrial(discovery.root,row.config,{hide:true,plain:docs.plainRun}):
180
+ docs.issue===hiddenDocsIssue?await sandboxTrial(discovery.root,row.config,{hide:true}):docs;
170
181
  if(!hidden.issue&&hidden.same){
171
182
  row.config={...row.config,qualification:hiddenQualification};row.qualification=hiddenQualification;
172
- row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore are its input")+`; ran both ways at init, exit ${hidden.plain}`;continue;}
183
+ row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore are its input")+
184
+ `; ran both ways at init, exit ${hidden.plain}; it needs the documentation (${trialIssue(docs)})`;continue;}
173
185
  // Without git, nothing is hidden: the whole checkout is tried on its own.
174
186
  const trial=hidden.plainRun?await sandboxTrial(discovery.root,row.config,{plain:hidden.plainRun}):
175
187
  hidden.issue===hiddenListIssue?await sandboxTrial(discovery.root,row.config):hidden;
@@ -222,7 +234,7 @@ if(command==='init'){
222
234
  console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
223
235
  console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
224
236
  console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
225
- console.log('Use redue status first. --sync can establish applicability, but may cost more than rerunning a cheap check.');}
237
+ console.log('Then redue status says whether each result applies to the files as they are now.');}
226
238
  } else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
227
239
  if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
228
240
  await exit(0);
@@ -381,7 +393,7 @@ const checks=publicConfig.checks.map(check=>{
381
393
  const windowsContext=process.platform==='win32'?
382
394
  ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
383
395
  const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:generalScript?scriptEnvironment(manager):
384
- sandboxed?sandboxEnvironment(Boolean(pythonTool(check)),check.script?manager:null):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
396
+ sandboxed?sandboxEnvironment(Boolean(pythonTool(check)),check.script?manager:null,check.kind):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
385
397
  'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
386
398
  pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
387
399
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
@@ -408,7 +420,7 @@ const checks=publicConfig.checks.map(check=>{
408
420
  ...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
409
421
  ...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
410
422
  ...(generalScript?{qualificationInterpretation:scriptInterpretation}:{}),
411
- ...(sandboxed?{qualificationInterpretation:sandboxInterpretationOf(check.qualification)}:{}),
423
+ ...(sandboxed?{qualificationInterpretation:sandboxInterpretationOf(check.qualification,check.kind)}:{}),
412
424
  workspace:check.workspace||null,kind:check.kind||null};
413
425
  });
414
426
  const runtime={schema:1,provider:'declared-project@1',root,state,checks};
@@ -12,8 +12,8 @@ generic agent's instructions with the project owner's permission.
12
12
  `redue --config CONFIG status --sync --json`. Do not infer a recipe from its name.
13
13
  Cached status cannot establish its caller context. In mixed configurations,
14
14
  assess each check's contract separately; this is not a global sync-first policy.
15
- 3. For other/unknown recipes, query ordinary `status --json` first and decide whether
16
- synchronization is worthwhile. Ordinary status and `--short` remain cheap inspection.
15
+ 3. For other/unknown recipes, `status --json` synchronizes too (`--sync` is the
16
+ same); `status --cached --json` and `--short` remain cheap inspection.
17
17
  4. Reuse only schema 1, healthy observation and the selected check's CURRENT/PASS
18
18
  with `reuse_eligible: true`. Missing eligibility means false. A historical FAIL
19
19
  is never green evidence. Do not rerun eligible evidence merely for freshness.
@@ -167,11 +167,18 @@ commands never start the hook. The installed skill lists these exact commands,
167
167
  so the agent uses forms the hook recognizes.
168
168
 
169
169
  On a match, the hook runs `redue status --sync --json`. It waits up to half the
170
- duration of the check's last completed run, at most 10 seconds, so waiting never
171
- costs more than half a run. With no completed run it waits 2 seconds. A check
172
- that takes a few hundred milliseconds is therefore rarely skipped; skipping it
173
- would save almost nothing, and the hook does not rewrite it to `redue run`
174
- either. Claude Code's own limit for the hook is 15 seconds.
170
+ duration of the check's last completed run, at least 2 seconds and at most 10
171
+ seconds. A status call usually takes 70-250 ms, so a fast check (mypy in 0.2 s)
172
+ is still skipped when CURRENT, or run through `redue run` and recorded when not.
173
+ When the observer is slow or stuck, a matching command can wait the full 2
174
+ seconds before it runs as written. Claude Code's own limit for the hook is 15
175
+ seconds.
176
+
177
+ To see why the hook decided what it did, set `REDUE_HOOK_LOG` to an absolute
178
+ file path in the environment Claude Code starts with. The hook appends one JSON
179
+ line per Bash command: the command, the decision (`deny`, `rewrite` or
180
+ `allow`) and why, such as `no check matches the command` or `status did not
181
+ answer within 2000 ms`.
175
182
  It blocks the command only when the check is CURRENT, PASS and
176
183
  `reuse_eligible: true`. The message names the check and the receipt's run ID. Every
177
184
  other result allows the command: STALE, UNVERIFIED, FAILED, a timeout, an error, or
@@ -117,9 +117,10 @@ compiler invocation it selects, and for npm the
117
117
  run through the package manager. No existing configuration is silently migrated.
118
118
  Only use configs and probes from trusted repositories: these execute local code.
119
119
 
120
- `status` is cached and conservative. `status --sync` and `explain` revalidate caller
121
- context and may execute read-only probes. Synchronization can be slower than a cheap
122
- check, especially on Windows. Choose deliberately. A timeout is UNVERIFIED, not green.
120
+ `status` and `explain` revalidate caller context and may execute read-only probes
121
+ (`status --sync` is the same as `status`). `status --cached` is the cached,
122
+ conservative answer, without revalidating. Synchronization can be slower than a
123
+ cheap check, especially on Windows. A timeout is UNVERIFIED, not green.
123
124
 
124
125
  ## Lifecycle and removal
125
126
 
package/docs/user/json.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Alpha JSON interface (schema 1)
2
2
 
3
- Use `status --json` for cached state, `status --sync --json` for caller-aware
4
- applicability, and `explain CHECK --json` for selected synchronized detail.
3
+ Use `status --json` (or `status --sync --json`) for caller-aware applicability,
4
+ `status --cached --json` for cached state, and `explain CHECK --json` for selected synchronized detail.
5
5
  `detail` remains an alias. Successful reads emit one JSON object, no human prose.
6
6
 
7
7
  ```json
@@ -8,8 +8,8 @@ pnpm and Yarn TypeScript recipes, the [direct test recipe](tests-direct.md)
8
8
  and, on macOS and Linux, the [sandboxed recipe](sandbox.md) first. It uses
9
9
  the general recipe for a root typecheck or lint script none of them takes,
10
10
  and where the sandbox cannot run (at init, or at run time inside Claude
11
- Code's macOS sandbox). Test scripts use the direct test recipe; `init` does
12
- not propose the general recipe for them.
11
+ Code's macOS sandbox). Test scripts use the direct test recipe or the
12
+ sandboxed recipe; `init` does not propose the general recipe for them.
13
13
 
14
14
  It supports a project installed by npm, pnpm 9-12 (isolated or hoisted
15
15
  `node_modules`) or Yarn with the `node-modules` linker, with one lockfile kind