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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +831 -0
  2. package/README.md +157 -99
  3. package/bin/redue.mjs +322 -66
  4. package/docs/README.md +3 -1
  5. package/docs/user/agents.md +333 -15
  6. package/docs/user/configuration.md +28 -2
  7. package/docs/user/direct-typescript.md +219 -0
  8. package/docs/user/getting-started.md +50 -7
  9. package/docs/user/json.md +4 -2
  10. package/docs/user/node-script.md +177 -0
  11. package/docs/user/npm-launcher-notice.md +42 -0
  12. package/docs/user/pilot.md +146 -0
  13. package/docs/user/sandbox.md +326 -0
  14. package/docs/user/shell.md +3 -3
  15. package/docs/user/supported-projects.md +69 -23
  16. package/docs/user/tests-direct.md +143 -0
  17. package/package.json +4 -3
  18. package/src/agent-integration.mjs +467 -0
  19. package/src/agent-policy.md +28 -0
  20. package/src/cached-state.mjs +24 -1
  21. package/src/claude-hook.mjs +616 -0
  22. package/src/cli.mjs +449 -54
  23. package/src/daemon.mjs +442 -93
  24. package/src/decision-profile.mjs +15 -1
  25. package/src/decision-validation.mjs +12 -1
  26. package/src/declared-plan.mjs +38 -6
  27. package/src/direct-run-certificate.mjs +254 -0
  28. package/src/direct-typescript-probe.mjs +27 -0
  29. package/src/direct-typescript.mjs +205 -0
  30. package/src/glob.mjs +8 -0
  31. package/src/history-worker.mjs +21 -13
  32. package/src/index.mjs +108 -21
  33. package/src/installed-inputs.mjs +7 -1
  34. package/src/installed-observation.mjs +23 -0
  35. package/src/landlock.py +527 -0
  36. package/src/linux-inotify.py +39 -10
  37. package/src/node-onboarding.mjs +203 -54
  38. package/src/node-script.mjs +406 -0
  39. package/src/npm-integrity.mjs +124 -0
  40. package/src/npm-typecheck-environment.mjs +36 -0
  41. package/src/npmrc.mjs +16 -0
  42. package/src/observation.mjs +34 -12
  43. package/src/path-identity.mjs +8 -0
  44. package/src/plan-worker.mjs +17 -17
  45. package/src/pnpm-inputs.mjs +122 -57
  46. package/src/pnpm-shim-templates.json +180 -0
  47. package/src/pnpm-shims.mjs +54 -0
  48. package/src/pnpm-typecheck-probe.mjs +11 -6
  49. package/src/presentation.mjs +13 -4
  50. package/src/probe-deadline.mjs +37 -0
  51. package/src/probe-result.mjs +7 -3
  52. package/src/project-location.mjs +34 -0
  53. package/src/python-onboarding.mjs +89 -0
  54. package/src/run-recorder.cjs +91 -0
  55. package/src/sandbox.mjs +659 -0
  56. package/src/setup.mjs +224 -0
  57. package/src/stats.mjs +72 -0
  58. package/src/test-direct.mjs +608 -0
  59. package/src/test-recorder.cjs +435 -0
  60. package/src/typescript-contract-probe.mjs +4 -187
  61. package/src/typescript-input-facts.mjs +92 -0
  62. package/src/typescript-invocation.mjs +92 -0
  63. package/src/typescript-list.mjs +118 -13
  64. package/src/typescript-qualification.mjs +29 -49
  65. package/src/workspace-manifests.mjs +24 -0
  66. package/src/yarn-workspace-inputs.mjs +3 -0
  67. package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/bin/redue.mjs CHANGED
@@ -1,15 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decision-profile.mjs';
4
+ traceBase({...traceContext(),request:traceId(),work:'client'});
5
+ mark('client.loaded');
6
+ import {directCompiler,directQualification,directEnvironment,directInstalledInputs,directInterpretation} from '../src/direct-typescript.mjs';
7
+ import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
8
+ import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
9
+ import {scriptEnvironment,scriptInterpretation,scriptKinds,scriptQualification} from '../src/node-script.mjs';
10
+ import {docsQualification,hiddenDocsIssue,hiddenListIssue,isSandboxed,pythonTool,sandboxEnvironment,sandboxInterpretationOf,sandboxKinds,sandboxQualification} from '../src/sandbox.mjs';
3
11
  import os from 'node:os';
4
12
  import path from 'node:path';
5
13
  import {createHash} from 'node:crypto';
6
14
  import {spawn,spawnSync} from 'node:child_process';
7
15
  import {createRequire} from 'node:module';
8
- import {fileURLToPath} from 'node:url';
9
- import {discoverNodeProject} from '../src/node-onboarding.mjs';
16
+ import {fileURLToPath,pathToFileURL} from 'node:url';
10
17
  import {processAlive} from '../src/process-liveness.mjs';
11
18
  import {samePath,withinPath,realObservedPath} from '../src/path-identity.mjs';
12
- import {stateIdentity,userStateBase} from '../src/platform-state.mjs';
19
+ import {discoverConfig,projectState,canonicalConfig} from '../src/project-location.mjs';
13
20
  import {findExecutable} from '../src/executable-lookup.mjs';
14
21
  import {readCachedStatus} from '../src/cached-state.mjs';
15
22
  import {assertOwnedStatePlacement} from '../src/owned-state.mjs';
@@ -22,27 +29,57 @@ const help=`REDUE ${version}
22
29
  Usage: redue [--config FILE] [--state-dir DIR] COMMAND [OPTIONS]
23
30
 
24
31
  Commands:
25
- init [--dry-run] [--workspace NAME] [--check KIND]
26
- Discover existing scripts; optionally select one workspace/check
32
+ setup claude [--yes] Set up REDUE and Claude Code in this project in one step:
33
+ init, agent setup claude, start, one recorded run per check
34
+ remove [--yes] Undo setup: integration, observer state, and the config setup created
35
+ init [--dry-run] [--workspace NAME] [--check KIND] [--recipe typescript-direct|tests-direct|node-script|sandbox]
36
+ [--no-time-limit]
37
+ Discover existing scripts; optionally select one workspace/check.
38
+ --no-time-limit: trial runs have no 5-minute limit; with a config, the checks
39
+ it does not run in the sandbox are tried, and those that qualify are updated
40
+ agent setup codex|claude [--dry-run|--apply] [--json]
41
+ agent doctor [codex|claude] [--json]
42
+ agent remove codex|claude [--dry-run|--apply] [--json]
43
+ Connect project instructions; explicit setup only
27
44
  start Start the local observer
28
45
  stop Stop this observer
29
- status [--json|--short] Read conservative cached status
30
- status --sync Reconcile inputs and caller context before answering
46
+ status [--json] Reconcile inputs and caller context, then answer (--sync is the same)
47
+ status --cached [--json] | --short The last answer the observer published, without reconciling
31
48
  explain [CHECK] [--json] Explain applicability (synchronized; may cost more than a rerun)
32
49
  detail [CHECK] [--json] Alias for explain
33
- run CHECK Execute one configured check and record its outcome
50
+ run CHECK [--if-due] Execute one configured check and record its outcome; with --if-due,
51
+ only when its last pass does not apply to the files as they are now
52
+ stats [--json] Per check: skips, time saved, recorded runs, why checks were UNVERIFIED
34
53
  remove-state Stop and remove only this configuration's owned local state
35
54
 
36
55
  No checks run in the background. Direct commands outside redue create no receipts.`;
37
56
 
38
57
  function error(message){console.error(`redue: ${message}`);process.exit(2);}
58
+ // Exits once queued output is written. On macOS, output to a pipe is written
59
+ // asynchronously; exiting with it queued cuts it (at 8 KB through the pipe a
60
+ // Node program, such as an agent's shell tool, reads).
61
+ async function exit(code){await new Promise(resolve=>process.stdout.write('',resolve));process.exit(code);}
39
62
  const args=process.argv.slice(2);
63
+ if(args.length===3&&args[0]==='agent'&&args[1]==='hook'&&args[2]==='claude'){
64
+ // Runs before config discovery: the hook must exit 0 on every failure,
65
+ // because Claude Code treats exit 2 as a block.
66
+ const {runClaudeHook}=await import('../src/claude-hook.mjs');
67
+ await runClaudeHook(fileURLToPath(import.meta.url));process.exit(0);
68
+ }
40
69
  if(args.length===1&&args[0]==='--version'){console.log(version);process.exit(0);}
41
- if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);process.exit(0);}
42
- const preferredConfig=path.resolve('redue.config.json');
43
- const legacyConfig=path.resolve('vstate.config.json');
44
- let configFile=fs.existsSync(preferredConfig)||!fs.existsSync(legacyConfig)?
45
- preferredConfig:legacyConfig,stateOverride=null;
70
+ // Internal: the entry this command runs, so setup and the hook can tell
71
+ // whether the `redue` on PATH (a link, a package manager's shim, a wrapper)
72
+ // is this install.
73
+ if(args.length===1&&args[0]==='--entry'){console.log(fs.realpathSync(fileURLToPath(import.meta.url)));process.exit(0);}
74
+ if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);await exit(0);}
75
+ if(args[0]==='setup'||args[0]==='remove'){
76
+ // Loaded on demand; setup works before a config exists.
77
+ const {setupCommand}=await import('../src/setup.mjs');
78
+ try{await setupCommand(args,{entry:fileURLToPath(import.meta.url)});}
79
+ catch(e){error(e.message);}
80
+ await exit(0);
81
+ }
82
+ let configFile=null,stateOverride=null;
46
83
  for(let i=0;i<args.length;){
47
84
  if(args[i]==='--config'||args[i]==='--state-dir'){
48
85
  const flag=args[i],value=args[i+1];if(!value)error(`${flag} requires a path`);
@@ -51,30 +88,70 @@ for(let i=0;i<args.length;){
51
88
  }else i++;
52
89
  }
53
90
  const command=args.shift();
54
- if(!['init','start','stop','status','detail','explain','run','remove-state'].includes(command))
91
+ try{configFile??=command==='init'||stateOverride&&['stop','remove-state'].includes(command)?
92
+ path.resolve(fs.existsSync('redue.config.json')||!fs.existsSync('vstate.config.json')?'redue.config.json':'vstate.config.json'):discoverConfig();}
93
+ catch(e){error(e.message);}
94
+ if(fs.existsSync(path.dirname(configFile)))configFile=canonicalConfig(configFile);
95
+ if(command==='agent'){
96
+ // Loaded on demand: onboarding and agent setup are not on the run/status path.
97
+ const {agentCommand}=await import('../src/agent-integration.mjs');
98
+ try{await agentCommand(args,{configFile,stateOverride,entry:fileURLToPath(import.meta.url)});}
99
+ catch(e){error(e.message);}
100
+ await exit(0);
101
+ }
102
+ if(!['init','start','stop','status','detail','explain','run','stats','remove-state'].includes(command))
55
103
  error(`unknown command ${command||'<none>'}; use --help`);
56
104
  const json=args.includes('--json');
57
- const sync=args.includes('--sync');
105
+ // run --if-due: run the check only when its last pass does not apply now.
106
+ const ifDue=command==='run'&&args.includes('--if-due');
107
+ if(ifDue)args.splice(args.indexOf('--if-due'),1);
108
+ // status syncs by default (warm, it costs well under 100 ms more than the
109
+ // cached answer); --cached and --short read the cached answer.
110
+ const cachedOnly=args.includes('--cached');
58
111
  const dryRun=args.includes('--dry-run');
59
112
  const short=args.includes('--short');
60
- let workspaceSelection=null,checkSelection=null;
113
+ const sync=args.includes('--sync')||command==='status'&&!cachedOnly&&!short;
114
+ let workspaceSelection=null,checkSelection=null,recipeSelection=null;
61
115
  if(command==='init')for(let i=0;i<args.length;){
62
- if(args[i]==='--workspace'||args[i]==='--check'){
116
+ if(args[i]==='--workspace'||args[i]==='--check'||args[i]==='--recipe'){
63
117
  const flag=args[i],value=args[i+1];if(!value||value.startsWith('--'))error(`${flag} requires a name`);
64
- if(flag==='--workspace')workspaceSelection=value;else checkSelection=value;
118
+ if(flag==='--workspace')workspaceSelection=value;else if(flag==='--recipe')recipeSelection=value;else checkSelection=value;
65
119
  args.splice(i,2);
66
120
  }else i++;
67
121
  }
68
122
  const check=['run','detail','explain'].includes(command)&&args[0]&&!args[0].startsWith('--')?args.shift():null;
69
123
  if(command==='run'&&!check)error('run requires a check name');
70
- if(args.some(value=>!['--json','--sync','--dry-run','--short'].includes(value)))error('unknown option; use --help');
124
+ if(args.some(value=>!['--json','--sync','--cached','--dry-run','--short','--no-time-limit'].includes(value)))error('unknown option; use --help');
71
125
  if(sync&&!['status','detail','explain'].includes(command))error('--sync is for status or detail');
72
- if(short&&(command!=='status'||json||sync))error('--short is a cached status option; do not combine it with --sync or --json');
126
+ if(cachedOnly&&(command!=='status'||args.includes('--sync')))error('--cached is a status option; do not combine it with --sync');
127
+ if(short&&(command!=='status'||json||args.includes('--sync')))error('--short is a cached status option; do not combine it with --sync or --json');
73
128
  if(dryRun&&command!=='init')error('--dry-run is for init');
129
+ const noTimeLimit=args.includes('--no-time-limit');
130
+ if(noTimeLimit&&command!=='init')error('--no-time-limit is for init');
74
131
 
75
132
  if(command==='init'){
76
- let discovery;try{discovery=discoverNodeProject(process.cwd());}
77
- catch(e){error(e.message);}
133
+ const {discoverNodeProject,sourceLinkIssue}=await import('../src/node-onboarding.mjs');
134
+ const {discoverPythonProject}=await import('../src/python-onboarding.mjs');
135
+ const {sandboxTrial}=await import('../src/sandbox.mjs');
136
+ // A Node project, a Python project, or both in one checkout.
137
+ const python=!recipeSelection||recipeSelection==='sandbox'?discoverPythonProject(process.cwd()):null;
138
+ let discovery;
139
+ if(fs.existsSync('package.json')||!python)try{discovery=discoverNodeProject(process.cwd(),{recipe:recipeSelection});}
140
+ catch(e){error(e.message);}
141
+ else{const root=realObservedPath(process.cwd());
142
+ const git=spawnSync('git',['rev-parse','--show-toplevel'],{cwd:root,encoding:'utf8',timeout:3000,stdio:['ignore','pipe','ignore']});
143
+ discovery={schema:1,root,manager:{name:null,version:null,layout:null},observationIssue:git.status===0?sourceLinkIssue(root):
144
+ 'Git checkout is unavailable; filesystem observation needs a checkout',workspaces:false,node:process.version,
145
+ checks:[],ambiguous:[],skipped:[],config:{schema:1,checks:[]}};}
146
+ if(python){
147
+ // A Python check takes its tool's name where the name is taken.
148
+ const taken=new Set(discovery.checks.map(row=>row.config.name));
149
+ for(const row of python.checks){if(taken.has(row.config.name))row.config.name=`${row.config.kind}:${row.config.command[0].split('/').at(-1)}`;
150
+ taken.add(row.config.name);}
151
+ if(!discovery.observationIssue)discovery.checks.push(...python.checks);
152
+ discovery.skipped.push(...python.skipped);
153
+ discovery.python=true;
154
+ }
78
155
  if(workspaceSelection||checkSelection){
79
156
  const chosen=discovery.checks.filter(row=>
80
157
  (!workspaceSelection||row.config.workspace===workspaceSelection||
@@ -83,7 +160,72 @@ if(command==='init'){
83
160
  if(!chosen.length)error('no discovered check matches the selected workspace and kind');
84
161
  discovery.checks=chosen;discovery.config.checks=chosen.map(row=>row.config);
85
162
  }
86
- const configured=fs.existsSync(configFile),unsupported=Boolean(discovery.observationIssue||
163
+ // A sandboxed check is proposed only when it gives the same exit status and
164
+ // output inside the sandbox as outside it, with the checkout's git-ignored
165
+ // files and documentation hidden (sandbox-v3). When the plain run passed
166
+ // and the sandboxed one failed only because it was refused a file it hid
167
+ // (sandboxTrial's refusedHidden), the check gets the whole checkout
168
+ // (sandbox-v1) without another run: without the hiding it can only fail
169
+ // where the plain run passes. Any other difference is tried once with the
170
+ // whole checkout, reusing the plain run. Otherwise init writes what it
171
+ // would have without the sandboxed recipe, with the reason. Each check
172
+ // costs one plain run and one or two sandboxed runs. A run past the trial
173
+ // limit ends the check's trials (time_limit); --no-time-limit sets none.
174
+ const trialIssue=trial=>trial.issue?`the sandbox cannot run here (${trial.issue})`:
175
+ trial.timedOut?`it ran past init's 5-minute trial limit${trial.timedOut==='sandboxed'?' inside the sandbox':''}`+
176
+ ` (redue init --check ${trial.kind} --no-time-limit tries it without the limit)`:
177
+ // A failure both ways that shows a refusal cannot be told from a real one.
178
+ trial.same?null:trial.plain!==trial.sandboxed?`exit ${trial.sandboxed} inside the sandbox, ${trial.plain} outside it${trial.line?` (${trial.line})`:''}`:
179
+ trial.line?`it fails both ways (exit ${trial.plain}), with a sign of a refusal inside the sandbox (${trial.line})`:
180
+ `the output differs inside the sandbox: ${JSON.stringify(trial.output.sandboxed)} instead of ${JSON.stringify(trial.output.plain)}`;
181
+ // With --no-time-limit and a config, the checks the config has and does
182
+ // not run in the sandbox are tried, and one it lacks when --check names
183
+ // it. Those that qualify replace their rows (a row that runs the same
184
+ // script) or are added.
185
+ const configured=fs.existsSync(configFile);
186
+ let existing=null,existingText=null;
187
+ if(configured&&noTimeLimit){
188
+ try{existingText=fs.readFileSync(configFile,'utf8');existing=JSON.parse(existingText);}catch(e){error(`cannot read ${configFile}: ${e.message}`);}
189
+ if(!Array.isArray(existing?.checks))error(`cannot read ${configFile}: it has no checks`);
190
+ if((existing.root??'.')!=='.'||!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
191
+ error('init updates only a config at the repository root, without root; run it there');
192
+ }
193
+ const timeout=noTimeLimit?0:undefined,improved=new Map();
194
+ let trials=0;
195
+ for(const [at,row] of [...discovery.checks.entries()]){
196
+ if(row.qualification!==sandboxQualification||row.level!=='ready')continue;
197
+ const old=existing?.checks.find(check=>check.name===row.config.name);
198
+ const sameRun=old&&old.kind===row.config.kind&&(row.config.script?old.script===row.config.script:
199
+ JSON.stringify(old.command)===JSON.stringify(row.config.command));
200
+ const notTried=!existing?null:!old?(checkSelection?null:'the config does not have it; name its kind with --check to add it'):
201
+ isSandboxed(old.qualification)?'the config already runs it in the sandbox':sameRun?null:'the config runs something else under that name';
202
+ if(notTried){discovery.checks[at]=null;discovery.skipped.push({kind:row.config.name,reason:`${row.config.name}: not tried; ${notTried}`});continue;}
203
+ if(!json)console.error(`Running ${row.config.name} inside and outside the sandbox to confirm it...`);
204
+ const docs={...await sandboxTrial(discovery.root,row.config,{hide:'docs',timeout}),kind:row.config.kind};
205
+ if(!docs.issue)trials++;
206
+ if(!docs.issue&&docs.same){
207
+ row.config={...row.config,qualification:docsQualification};row.qualification=docsQualification;
208
+ 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}`;
209
+ improved.set(row.config.name,row.config);continue;}
210
+ if(!docs.issue&&!docs.timedOut&&docs.refusedHidden){
211
+ row.reason+=`; at init it passed, then failed reading a file that was hidden (${docs.refusedHidden}), so the whole checkout is its input, not run that way at init`;
212
+ improved.set(row.config.name,row.config);continue;}
213
+ // Without a list of the ignored files or the documentation, the whole checkout is tried on its own.
214
+ const trial=docs.timedOut?docs:docs.plainRun?{...await sandboxTrial(discovery.root,row.config,{plain:docs.plainRun,timeout}),kind:row.config.kind}:
215
+ [hiddenListIssue,hiddenDocsIssue].includes(docs.issue)?{...await sandboxTrial(discovery.root,row.config,{timeout}),kind:row.config.kind}:docs;
216
+ if(trial!==docs&&!trial.issue)trials++;
217
+ const issue=trialIssue(trial);
218
+ if(!issue){row.reason+=`; ran both ways at init, exit ${trial.plain}; the whole checkout is its input: `+
219
+ (docs.issue?docs.issue:`with the files git ignores and the documentation hidden, ${trialIssue(docs)}`);
220
+ improved.set(row.config.name,row.config);continue;}
221
+ // time_limit: the kind that --check takes to try it without the limit.
222
+ const limited=trial.timedOut?{timeLimit:row.config.kind}:{};
223
+ if(row.fallback)discovery.checks[at]={...row.fallback,...limited,reason:`Sandboxed recipe unavailable: ${issue}; ${row.fallback.reason}`};
224
+ else{discovery.checks[at]=null;discovery.skipped.push({kind:row.config.name,...trial.timedOut?{time_limit:row.config.kind}:{},reason:`${row.config.name}: sandboxed recipe unavailable: ${issue}`});}
225
+ }
226
+ discovery.checks=discovery.checks.filter(Boolean).map(({fallback,...row})=>row);
227
+ discovery.config.checks=discovery.checks.map(row=>row.config);
228
+ const unsupported=Boolean(discovery.observationIssue||
87
229
  discovery.manager.issue&&!['npm','yarn','pnpm'].includes(discovery.manager.name)||
88
230
  discovery.manager.name==='yarn'&&!discovery.manager.version);
89
231
  const willWrite=!dryRun&&!configured&&!unsupported&&discovery.checks.length>0;
@@ -91,26 +233,30 @@ if(command==='init'){
91
233
  package_manager:discovery.manager.name,version:discovery.manager.version,
92
234
  installation_layout:discovery.manager.layout,
93
235
  issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
94
- ambiguous:discovery.ambiguous,
95
- checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
236
+ ambiguous:discovery.ambiguous,skipped:discovery.skipped||[],
237
+ checks:discovery.checks.map(({config:check,level,reason,timeLimit})=>({name:check.name,
96
238
  script:check.script||null,command:check.command||null,
97
- workspace:check.workspace||null,level,reason})),config:configFile,
98
- written:willWrite,existing:configured};
239
+ workspace:check.workspace||null,level,reason,...timeLimit?{time_limit:timeLimit}:{}})),config:configFile,
240
+ written:willWrite,existing:configured,writable:!configured&&!unsupported&&discovery.checks.length>0};
99
241
  if(!json) {
100
- console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
242
+ if(summary.package_manager||!discovery.python)console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
101
243
  `${summary.version?' '+summary.version:''}; ${summary.installation_layout} installation.`);
244
+ if(discovery.python)console.log('Python project; tools run from .venv.');
102
245
  if(summary.issue)console.log(`Boundary: ${summary.issue}`);
103
246
  for(const row of summary.checks){
104
247
  console.log(` ${row.name} (${row.script||row.command?.join(' ')}) — `+
105
248
  `${{ready:'ready to qualify',recording:'recording-only',unsupported:'unsupported'}[row.level]||row.level}: ${row.reason}`);
106
249
  if(row.script&&row.command)console.log(` Recorded invocation: ${row.command.map(value=>
107
- value==='@node'?'node':value.startsWith('@typescript-bin:')?
250
+ value==='@node'?'node':value==='@typescript-compiler:.'?'[reviewed local TypeScript compiler implementation]':
251
+ value.startsWith('@test-runner:')?`[local ${value.slice('@test-runner:'.length)}]`:value.startsWith('@typescript-bin:')?
108
252
  `[installed TypeScript in ${value.slice('@typescript-bin:'.length)}]`:value).join(' ')}`);
109
253
  }
110
254
  for(const row of summary.ambiguous)
111
255
  console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
256
+ for(const row of summary.skipped)console.log(` Skipped: ${row.reason}`);
112
257
  if(!summary.checks.length)console.log('No useful existing verification scripts found.');
113
- console.log(configured?`Existing ${configFile} left unchanged.`:
258
+ console.log(existing&&improved.size?(dryRun?`Preview only; ${configFile} was not updated.`:`Updating ${[...improved.keys()].join(', ')} in ${configFile}.`):
259
+ configured?`Existing ${configFile} left unchanged.`:
114
260
  dryRun?`Preview only; ${configFile} was not written.`:
115
261
  willWrite?`Writing ${configFile}; inspect it before running checks.`:'No configuration will be written.');
116
262
  }
@@ -118,13 +264,31 @@ if(command==='init'){
118
264
  if(!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
119
265
  error('init writes only at the repository root; use --config there or --dry-run');
120
266
  fs.writeFileSync(configFile,JSON.stringify(discovery.config,null,2)+'\n',{flag:'wx'});
121
- if(!json){console.log(`Created ${configFile}. No checks were executed.`);
267
+ if(!json){console.log(`Created ${configFile}. ${trials?'Checks ran inside and outside the sandbox to choose their recipe; no results were recorded.':'No checks were executed.'}`);
122
268
  console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
123
269
  console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
124
- console.log('Use redue status first. --sync can establish applicability, but may cost more than rerunning a cheap check.');}
270
+ console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
271
+ console.log('Then redue status says whether each result applies to the files as they are now.');}
272
+ } else if(existing&&improved.size&&!dryRun){
273
+ if(fs.readFileSync(configFile,'utf8')!==existingText)error(`${configFile} changed while the checks ran; nothing was written. Run init again`);
274
+ // The observer keeps the config it started with: it is restarted around the update.
275
+ const state=stateOverride||projectState(discovery.root,configFile);
276
+ let running=false;try{running=processAlive(Number(fs.readFileSync(path.join(state,'observer.lock','pid'),'utf8')));}catch{}
277
+ const self=action=>spawnSync(process.execPath,[process.argv[1],'--config',configFile,...stateOverride?['--state-dir',stateOverride]:[],action],
278
+ {stdio:['ignore','pipe','pipe'],encoding:'utf8'});
279
+ if(running&&self('stop').status!==0)error('the observer did not stop; nothing was written');
280
+ const names=new Set(existing.checks.map(check=>check.name));
281
+ existing.checks=[...existing.checks.map(check=>improved.get(check.name)??check),...[...improved.values()].filter(check=>!names.has(check.name))];
282
+ const text=JSON.stringify(existing,null,2)+'\n';
283
+ fs.writeFileSync(configFile,text);
284
+ // A config setup created is still one redue remove deletes.
285
+ (await import('../src/setup.mjs')).configRewritten(discovery.root,existingText,text);
286
+ Object.assign(summary,{written:true,updated:[...improved.keys()],observer:!running?'not running':self('start').status===0?'restarted':'not started'});
287
+ if(!json)console.log(`Updated ${[...improved.keys()].join(', ')} in ${configFile}.`+
288
+ {restarted:' Restarted the observer with it.','not started':' The observer did not start again: run redue start.','not running':''}[summary.observer]);
125
289
  } else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
126
290
  if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
127
- process.exit(0);
291
+ await exit(0);
128
292
  }
129
293
  function printLifecycle(output,action,machine){
130
294
  if(machine){process.stdout.write(output);return;}
@@ -141,11 +305,12 @@ function printLifecycle(output,action,machine){
141
305
  if(stateOverride&&['stop','remove-state'].includes(command)){
142
306
  const runtime=path.join(stateOverride,'project-runtime-v1.json');
143
307
  if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
308
+ mark('client.engine_submit');
144
309
  const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
145
310
  {cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
146
311
  if(result.error)error(result.error.message);
147
312
  if(result.stdout)printLifecycle(result.stdout,command,json);
148
- process.exit(result.status??2);
313
+ await exit(result.status??2);
149
314
  }
150
315
  let publicConfig;
151
316
  try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
@@ -157,10 +322,7 @@ for(const key of Object.keys(publicConfig))
157
322
  if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
158
323
  if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
159
324
  const root=realObservedPath(path.resolve(path.dirname(configFile),publicConfig.root||'.'));
160
- const stateBase=userStateBase(process.platform,process.env,os.homedir());
161
- const state=stateOverride||path.join(stateBase,'vstate',
162
- `vstate-${createHash('sha256').update(stateIdentity(root)+'\0'+
163
- stateIdentity(configFile)).digest('hex').slice(0,16)}`);
325
+ const state=stateOverride||projectState(root,configFile);
164
326
  if(!/^(?:vstate|redue)-/.test(path.basename(state)))
165
327
  error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
166
328
  const runtimeFile=path.join(state,'project-runtime-v1.json');
@@ -169,9 +331,18 @@ function which(name){
169
331
  if(found)return found;
170
332
  error(`cannot resolve executable ${name} on PATH`);
171
333
  }
172
- function token(value){
334
+ function token(value,cwd='.'){
173
335
  if(value==='@node')return process.execPath;
174
336
  if(value==='@project')return root;
337
+ if(value==='@typescript-compiler:.'){
338
+ try{return directCompiler(root,{verify:false}).entry;}catch(e){error(`direct compiler unavailable: ${e.code||e.message}`);}
339
+ }
340
+ if(value.startsWith('@test-runner:')){
341
+ // Qualification reports a missing runner; status and stop still work.
342
+ try{return testRunnerAt(root,cwd,value.slice('@test-runner:'.length)).entry;}
343
+ catch{return path.join(root,cwd,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
344
+ }
345
+ if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
175
346
  if(value.startsWith('@typescript-bin:')){
176
347
  const dir=value.slice('@typescript-bin:'.length);
177
348
  if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
@@ -192,10 +363,10 @@ function token(value){
192
363
  if(value.startsWith('@which:'))return which(value.slice(7));
193
364
  return value;
194
365
  }
195
- function argv(values,label){
366
+ function argv(values,label,cwd){
196
367
  if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
197
368
  error(`${label} must be nonempty exact argv`);
198
- return values.map(token);
369
+ return values.map(value=>token(value,cwd));
199
370
  }
200
371
  const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
201
372
  'probeContinuity'];
@@ -208,8 +379,12 @@ const checks=publicConfig.checks.map(check=>{
208
379
  for(const category of categories)if(check.coverage?.[category]&&
209
380
  typeof check.coverageReview?.[category]!=='string')
210
381
  error(`${check.name}: reviewed coverage ${category} needs a rationale`);
382
+ const npmDirect=check.qualification===directQualification;
211
383
  const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
212
- if(check.script&&check.command&&!pnpmTypecheck)
384
+ const testsDirect=check.qualification===testsQualification;
385
+ const generalScript=check.qualification===scriptQualification;
386
+ const sandboxed=isSandboxed(check.qualification);
387
+ if(check.script&&check.command&&!pnpmTypecheck&&!npmDirect&&!testsDirect)
213
388
  error(`${check.name}: choose script or command, not both`);
214
389
  const manager=publicConfig.packageManager;
215
390
  if(check.script&&!['npm','yarn','pnpm'].includes(manager))
@@ -218,8 +393,24 @@ const checks=publicConfig.checks.map(check=>{
218
393
  [`@which:${manager}`,'run',check.script]:null;
219
394
  const npmAutomatic=check.qualification==='typescript-noemit-v1';
220
395
  const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
221
- if(check.qualification&&!npmAutomatic&&!yarnWorkspace&&!pnpmTypecheck)
396
+ if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck&&!testsDirect&&!generalScript&&!sandboxed)
222
397
  error(`${check.name}: unknown qualification contract`);
398
+ // The general recipe runs the root package script through the package manager.
399
+ if(generalScript&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.command||check.cwd&&check.cwd!=='.'||
400
+ check.workspace||!scriptKinds.includes(check.kind)))
401
+ error(`${check.name}: the general recipe needs a root typecheck, test or lint script, without command or cwd`);
402
+ // The sandboxed recipe runs a root package script, or a tool from the project's .venv.
403
+ if(sandboxed&&(check.cwd&&check.cwd!=='.'||check.workspace||!sandboxKinds.includes(check.kind)||
404
+ (check.script?!['npm','pnpm','yarn'].includes(manager):!pythonTool(check))))
405
+ error(`${check.name}: the sandboxed recipe needs a root ${sandboxKinds.join(' or ')} script, or a command that runs a tool from .venv/bin, without cwd`);
406
+ if(npmDirect&&(manager!=='npm'||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
407
+ check.command[0]!=='@node'||check.command[1]!=='@typescript-compiler:.'||
408
+ !tscRun({},{script:null,args:check.command.slice(2)}).run?.noEmit))
409
+ error(`${check.name}: direct recipe requires its explicit local compiler command`);
410
+ // The script's own arguments follow the runner; qualification compares them with the script.
411
+ if(testsDirect&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.cwd&&check.cwd!=='.'&&!check.workspace||!Array.isArray(check.command)||
412
+ check.command[0]!=='@node'||!/^@test-runner:(?:vitest|jest)$/.test(check.command[1]||'')))
413
+ error(`${check.name}: test recipe requires its explicit local test runner command`);
223
414
  if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
224
415
  error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
225
416
  if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
@@ -227,25 +418,33 @@ const checks=publicConfig.checks.map(check=>{
227
418
  check.command[1]!=='workspace'||check.command[2]!==check.workspace||
228
419
  check.command[3]!=='run'))
229
420
  error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
230
- if(pnpmTypecheck&&(manager!=='pnpm'||!check.script||!check.command||
421
+ if(pnpmTypecheck&&(manager!=='pnpm'||!Array.isArray(check.command)||
231
422
  check.command[0]!=='@node'||
232
423
  check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
233
424
  check.workspace&&check.cwd==='.'||
234
- !check.command.slice(2).every(value=>typeof value==='string'&&value)))
425
+ !tscRun({},{script:null,args:check.command.slice(2)}).run))
235
426
  error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
236
427
  const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
237
428
  check.kind==='lint'?'lint plugins, config and resolver inputs need review':
238
429
  check.kind==='build'?'build environment and generated inputs need review':
239
430
  'check inputs need review';
240
- const autoProbes=npmAutomatic?[['@node','@vstate/typescript-contract-probe',
431
+ // A chained script has one check per tsc run: each check's run counts them,
432
+ // so a run added to or removed from the script invalidates them all.
433
+ const runCount=(npmDirect||pnpmTypecheck)&&check.script?publicConfig.checks.filter(other=>
434
+ other.qualification===check.qualification&&other.script===check.script&&
435
+ (other.cwd||'.')===(check.cwd||'.')&&(other.workspace||null)===(check.workspace||null)).length:1;
436
+ if(runCount>16)error(`${check.name}: more than 16 checks for script ${check.script}`);
437
+ const run=npmDirect||pnpmTypecheck?runArgv({script:check.script??null,args:check.command.slice(2),count:runCount}):null;
438
+ const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',...run]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
241
439
  '@project',check.script,'@which:npm']]:yarnWorkspace?
242
440
  [['@node','@vstate/yarn-workspace-typecheck-probe','@project',
243
441
  check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
244
442
  [['@node','@vstate/pnpm-typecheck-probe','@project',
245
- check.workspace||'.',check.script]]:[];
443
+ check.workspace||'.',...run]]:[];
246
444
  const windowsContext=process.platform==='win32'?
247
445
  ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
248
- const autoEnvironment=npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
446
+ const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:generalScript?scriptEnvironment(manager):
447
+ sandboxed?sandboxEnvironment(Boolean(pythonTool(check)),check.script?manager:null,check.kind):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
249
448
  'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
250
449
  pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
251
450
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
@@ -254,10 +453,11 @@ const checks=publicConfig.checks.map(check=>{
254
453
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
255
454
  prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
256
455
  pathExecutables:['node'],executableIdentity:true}:{};
257
- return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`),
456
+ return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`,
457
+ typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
258
458
  cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
259
459
  installedInputs:check.installedInputs||(
260
- npmAutomatic?['node_modules/**']:[]),
460
+ npmDirect?[...directInstalledInputs]:npmAutomatic||testsDirect?['node_modules/**']:[]),
261
461
  allowedExternalRoots:check.allowedExternalRoots||[],
262
462
  probes:[...(check.probes||[]),...autoProbes]
263
463
  .map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
@@ -266,18 +466,36 @@ const checks=publicConfig.checks.map(check=>{
266
466
  coverageReasons:check.coverageReasons||Object.fromEntries(
267
467
  categories.map(category=>[category,reason])),
268
468
  qualification:check.qualification||null,script:check.script||null,
469
+ ...(runCount>1?{runCount}:{}),
470
+ ...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
471
+ ...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
472
+ ...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
473
+ ...(generalScript?{qualificationInterpretation:scriptInterpretation}:{}),
474
+ ...(sandboxed?{qualificationInterpretation:sandboxInterpretationOf(check.qualification,check.kind)}:{}),
269
475
  workspace:check.workspace||null,kind:check.kind||null};
270
476
  });
271
477
  const runtime={schema:1,provider:'declared-project@1',root,state,checks};
272
478
  try{assertOwnedStatePlacement(runtime);}catch(e){error(e.message);}
273
479
  const serialized=JSON.stringify(runtime,null,2)+'\n';
480
+ if(command==='stats'){
481
+ const {readStats,renderStats}=await import('../src/stats.mjs');
482
+ const value=readStats(state,publicConfig.checks.map(item=>item.name));
483
+ console.log(json?JSON.stringify(value):renderStats(value));
484
+ await exit(0);
485
+ }
274
486
  const maintenance=command==='stop'||command==='remove-state';
275
487
  if(maintenance&&!fs.existsSync(runtimeFile)){
276
488
  console.log(`No REDUE state exists for ${configFile}.`);
277
489
  process.exit(0);
278
490
  }
279
491
  if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
280
- if(!maintenance&&fs.existsSync(runtimeFile)){
492
+ // Two commands can meet on fresh state (SessionStart's start and the first
493
+ // status): whichever creates the runtime file first wins, and the other
494
+ // treats it as existing.
495
+ let created=false;
496
+ if(!maintenance&&!fs.existsSync(runtimeFile))
497
+ try{fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});created=true;}catch(e){if(e.code!=='EEXIST')throw e;}
498
+ if(!maintenance&&!created){
281
499
  const previous=fs.readFileSync(runtimeFile,'utf8');
282
500
  if(previous!==serialized){
283
501
  const pidFile=path.join(state,'observer.lock','pid');
@@ -287,18 +505,41 @@ if(!maintenance&&fs.existsSync(runtimeFile)){
287
505
  }
288
506
  fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
289
507
  }
290
- }else if(!maintenance)fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});
508
+ }
291
509
 
292
510
  if(command==='status'&&!sync){
293
511
  const value=readCachedStatus(state,publicConfig.checks);
294
512
  console.log(json?JSON.stringify(value):renderStatus(value,{short}));
295
- process.exit(0);
513
+ await exit(0);
296
514
  }
297
515
 
298
516
  const details=command==='detail'||command==='explain';
299
517
  const internal=command==='remove-state'?'uninstall':
300
518
  details?'sync':command==='status'&&sync?'sync':command;
301
- if(command==='run'){
519
+ // On macOS and Linux the engine runs in this process, which saves a Node startup
520
+ // on every run; its own handlers receive SIGINT and SIGTERM. Windows keeps a
521
+ // child engine because cancellation reaches it over IPC.
522
+ // Asked when this part of a command runs, after earlier parts may have
523
+ // changed files. REDUE_FORCE=1 runs it anyway.
524
+ if(ifDue&&process.env.REDUE_FORCE!=='1'){
525
+ const [{verdict},{note,skipNote,dueNote}]=await Promise.all([import('../src/claude-hook.mjs'),import('../src/stats.mjs')]);
526
+ const asked=spawnSync(process.execPath,[fileURLToPath(import.meta.url),'--config',configFile,...(stateOverride?['--state-dir',stateOverride]:[]),
527
+ 'status','--sync','--json'],{encoding:'utf8',stdio:['ignore','pipe','ignore'],env:process.env,timeout:600_000});
528
+ let status=null;try{status=JSON.parse(asked.stdout);}catch{}
529
+ const due=dueNote(status,check);
530
+ if(due)note(state,due);
531
+ if(verdict(status,check)){
532
+ note(state,skipNote(status,check,'if-due'));
533
+ const row=status.checks.find(item=>item.name===check);
534
+ if(json)console.log(JSON.stringify({check,skipped:true,result:'PASS',run_id:row.invocation.runId}));
535
+ else console.error(`REDUE: ${check} passed on these exact inputs (run ${row.invocation.runId}); not run again. REDUE_FORCE=1 runs it anyway.`);
536
+ await exit(0);
537
+ }
538
+ }
539
+ if(command==='run'&&process.platform!=='win32'){
540
+ process.argv=[process.argv[0],engine,runtimeFile,internal,check,...(!json?['--human']:[])];
541
+ await import(pathToFileURL(engine).href);
542
+ }else if(command==='run'){
302
543
  const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
303
544
  {stdio:['inherit','inherit','inherit','ipc'],env:process.env});
304
545
  const forward=signal=>{if(child.exitCode!==null)return;
@@ -313,17 +554,32 @@ if(command==='run'){
313
554
  process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
314
555
  if(outcome.error)error(outcome.error.message);
315
556
  process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
557
+ }else if(internal==='sync'&&process.platform!=='win32'){
558
+ // As for run, the synchronized status engine runs in this process.
559
+ process.argv=[process.argv[0],engine,runtimeFile,internal,'','--value'];
560
+ mark('client.engine_submit',{inProcess:true});
561
+ const synchronized=await import(pathToFileURL(engine).href);
562
+ await synchronized.finished;
563
+ mark('client.engine_complete',{exit:process.exitCode??0});
564
+ if(synchronized.syncValue)showSynchronized(synchronized.syncValue);
565
+ await exit(process.exitCode??0);
566
+ }else{
567
+ mark('client.engine_submit');
568
+ const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
569
+ {stdio:['inherit','pipe','inherit'],
570
+ encoding:'utf8',env:traceEnvironment()});
571
+ mark('client.engine_complete',{exit:result.status,signal:result.signal});
572
+ if(result.error)error(result.error.message);
573
+ if(result.stdout){
574
+ if(['status','detail','explain'].includes(command)){
575
+ let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
576
+ showSynchronized(value);
577
+ }else printLifecycle(result.stdout,command,json);
578
+ }
579
+ await exit(result.status??1);
316
580
  }
317
- const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
318
- {stdio:['inherit','pipe','inherit'],
319
- encoding:'utf8',env:process.env});
320
- if(result.error)error(result.error.message);
321
- if(result.stdout){
322
- if(['status','detail','explain'].includes(command)){
323
- let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
324
- if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
325
- if(json)console.log(JSON.stringify(value));
326
- else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
327
- }else printLifecycle(result.stdout,command,json);
581
+ function showSynchronized(value){
582
+ if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
583
+ if(json)console.log(JSON.stringify(value));
584
+ else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
328
585
  }
329
- process.exit(result.status??1);
package/docs/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  For users:
4
4
 
5
5
  - [Install, initialize, run, and remove](user/getting-started.md)
6
+ - [Fixed-build opt-in development pilot](user/pilot.md)
6
7
  - [Supported projects and conservative boundaries](user/supported-projects.md)
8
+ - [Sandboxed recipe: typecheck and lint in any language, Python included](user/sandbox.md)
7
9
  - [Configuration](user/configuration.md)
8
- - [Coding-agent decision policy](user/agents.md)
10
+ - [Connect coding agents and apply the decision policy](user/agents.md)
9
11
  - [JSON contract](user/json.md)
10
12
  - [Shell usage](user/shell.md)
11
13