@redue/cli 0.0.0-stage → 0.1.0-alpha.11

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 (66) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +180 -0
  3. package/README.md +162 -2
  4. package/bin/redue.mjs +371 -0
  5. package/bin/vstate.mjs +3 -0
  6. package/docs/README.md +21 -0
  7. package/docs/user/agents.md +253 -0
  8. package/docs/user/configuration.md +60 -0
  9. package/docs/user/direct-typescript.md +114 -0
  10. package/docs/user/getting-started.md +148 -0
  11. package/docs/user/json.md +59 -0
  12. package/docs/user/npm-launcher-notice.md +42 -0
  13. package/docs/user/pilot.md +145 -0
  14. package/docs/user/receipt-selection-notice.md +60 -0
  15. package/docs/user/shell.md +25 -0
  16. package/docs/user/supported-projects.md +90 -0
  17. package/examples/redue.config.json +8 -0
  18. package/package.json +47 -4
  19. package/src/agent-integration.mjs +294 -0
  20. package/src/agent-policy.md +70 -0
  21. package/src/cached-state.mjs +42 -0
  22. package/src/claude-hook.mjs +115 -0
  23. package/src/cli.mjs +499 -0
  24. package/src/control-endpoint.mjs +11 -0
  25. package/src/daemon.mjs +1236 -0
  26. package/src/decision-profile.mjs +32 -0
  27. package/src/decision-validation.mjs +51 -0
  28. package/src/declared-plan.mjs +125 -0
  29. package/src/direct-run-certificate.mjs +185 -0
  30. package/src/direct-typescript-probe.mjs +22 -0
  31. package/src/direct-typescript.mjs +109 -0
  32. package/src/executable-lookup.mjs +18 -0
  33. package/src/glob.mjs +9 -0
  34. package/src/history-worker.mjs +26 -0
  35. package/src/index.mjs +306 -0
  36. package/src/installed-inputs.mjs +110 -0
  37. package/src/linux-inotify.py +228 -0
  38. package/src/linux-observer.mjs +79 -0
  39. package/src/node-onboarding.mjs +274 -0
  40. package/src/npm-typecheck-environment.mjs +36 -0
  41. package/src/npm-typecheck-probe.mjs +120 -0
  42. package/src/observation.mjs +59 -0
  43. package/src/owned-state.mjs +32 -0
  44. package/src/path-identity.mjs +43 -0
  45. package/src/plan-worker.mjs +140 -0
  46. package/src/plan.mjs +10 -0
  47. package/src/platform-observation.mjs +17 -0
  48. package/src/platform-state.mjs +14 -0
  49. package/src/pnpm-inputs.mjs +355 -0
  50. package/src/pnpm-typecheck-probe.mjs +47 -0
  51. package/src/presentation.mjs +53 -0
  52. package/src/probe-result.mjs +25 -0
  53. package/src/process-liveness.mjs +11 -0
  54. package/src/project-location.mjs +34 -0
  55. package/src/run-lock.mjs +77 -0
  56. package/src/run-recorder.cjs +78 -0
  57. package/src/state-store.mjs +73 -0
  58. package/src/toolchain-probe.mjs +21 -0
  59. package/src/typescript-contract-probe.mjs +5 -0
  60. package/src/typescript-input-facts.mjs +68 -0
  61. package/src/typescript-list.mjs +141 -0
  62. package/src/typescript-qualification.mjs +77 -0
  63. package/src/windows-command.mjs +16 -0
  64. package/src/windows-process.mjs +15 -0
  65. package/src/yarn-workspace-inputs.mjs +148 -0
  66. package/src/yarn-workspace-typecheck-probe.mjs +107 -0
package/bin/redue.mjs ADDED
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
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,directInterpretation} from '../src/direct-typescript.mjs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import {createHash} from 'node:crypto';
10
+ import {spawn,spawnSync} from 'node:child_process';
11
+ import {createRequire} from 'node:module';
12
+ import {fileURLToPath,pathToFileURL} from 'node:url';
13
+ import {processAlive} from '../src/process-liveness.mjs';
14
+ import {samePath,withinPath,realObservedPath} from '../src/path-identity.mjs';
15
+ import {discoverConfig,projectState,canonicalConfig} from '../src/project-location.mjs';
16
+ import {findExecutable} from '../src/executable-lookup.mjs';
17
+ import {readCachedStatus} from '../src/cached-state.mjs';
18
+ import {assertOwnedStatePlacement} from '../src/owned-state.mjs';
19
+ import {renderStatus,renderExplain} from '../src/presentation.mjs';
20
+
21
+ const productRoot=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
22
+ const engine=path.join(productRoot,'src','cli.mjs');
23
+ const version=JSON.parse(fs.readFileSync(path.join(productRoot,'package.json'))).version;
24
+ const help=`REDUE ${version}
25
+ Usage: redue [--config FILE] [--state-dir DIR] COMMAND [OPTIONS]
26
+
27
+ Commands:
28
+ init [--dry-run] [--workspace NAME] [--check KIND] [--recipe typescript-direct]
29
+ Discover existing scripts; optionally select one workspace/check
30
+ agent setup codex|claude [--dry-run|--apply] [--json]
31
+ agent doctor [codex|claude] [--json]
32
+ agent remove codex|claude [--dry-run|--apply] [--json]
33
+ Connect project instructions; explicit setup only
34
+ start Start the local observer
35
+ stop Stop this observer
36
+ status [--json|--short] Read conservative cached status
37
+ status --sync Reconcile inputs and caller context before answering
38
+ explain [CHECK] [--json] Explain applicability (synchronized; may cost more than a rerun)
39
+ detail [CHECK] [--json] Alias for explain
40
+ run CHECK Execute one configured check and record its outcome
41
+ remove-state Stop and remove only this configuration's owned local state
42
+
43
+ No checks run in the background. Direct commands outside redue create no receipts.`;
44
+
45
+ function error(message){console.error(`redue: ${message}`);process.exit(2);}
46
+ const args=process.argv.slice(2);
47
+ if(args.length===3&&args[0]==='agent'&&args[1]==='hook'&&args[2]==='claude'){
48
+ // Runs before config discovery: the hook must exit 0 on every failure,
49
+ // because Claude Code treats exit 2 as a block.
50
+ const {runClaudeHook}=await import('../src/claude-hook.mjs');
51
+ await runClaudeHook(fileURLToPath(import.meta.url));process.exit(0);
52
+ }
53
+ if(args.length===1&&args[0]==='--version'){console.log(version);process.exit(0);}
54
+ if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);process.exit(0);}
55
+ let configFile=null,stateOverride=null;
56
+ for(let i=0;i<args.length;){
57
+ if(args[i]==='--config'||args[i]==='--state-dir'){
58
+ const flag=args[i],value=args[i+1];if(!value)error(`${flag} requires a path`);
59
+ if(flag==='--config')configFile=path.resolve(value);else stateOverride=path.resolve(value);
60
+ args.splice(i,2);
61
+ }else i++;
62
+ }
63
+ const command=args.shift();
64
+ try{configFile??=command==='init'||stateOverride&&['stop','remove-state'].includes(command)?
65
+ path.resolve(fs.existsSync('redue.config.json')||!fs.existsSync('vstate.config.json')?'redue.config.json':'vstate.config.json'):discoverConfig();}
66
+ catch(e){error(e.message);}
67
+ if(fs.existsSync(path.dirname(configFile)))configFile=canonicalConfig(configFile);
68
+ if(command==='agent'){
69
+ // Loaded on demand: onboarding and agent setup are not on the run/status path.
70
+ const {agentCommand}=await import('../src/agent-integration.mjs');
71
+ try{await agentCommand(args,{configFile,stateOverride,entry:fileURLToPath(import.meta.url)});}
72
+ catch(e){error(e.message);}
73
+ // Exiting with piped output still queued truncates it (8 KB on macOS).
74
+ await new Promise(resolve=>process.stdout.write('',resolve));
75
+ process.exit(0);
76
+ }
77
+ if(!['init','start','stop','status','detail','explain','run','remove-state'].includes(command))
78
+ error(`unknown command ${command||'<none>'}; use --help`);
79
+ const json=args.includes('--json');
80
+ const sync=args.includes('--sync');
81
+ const dryRun=args.includes('--dry-run');
82
+ const short=args.includes('--short');
83
+ let workspaceSelection=null,checkSelection=null,recipeSelection=null;
84
+ if(command==='init')for(let i=0;i<args.length;){
85
+ if(args[i]==='--workspace'||args[i]==='--check'||args[i]==='--recipe'){
86
+ const flag=args[i],value=args[i+1];if(!value||value.startsWith('--'))error(`${flag} requires a name`);
87
+ if(flag==='--workspace')workspaceSelection=value;else if(flag==='--recipe')recipeSelection=value;else checkSelection=value;
88
+ args.splice(i,2);
89
+ }else i++;
90
+ }
91
+ const check=['run','detail','explain'].includes(command)&&args[0]&&!args[0].startsWith('--')?args.shift():null;
92
+ if(command==='run'&&!check)error('run requires a check name');
93
+ if(args.some(value=>!['--json','--sync','--dry-run','--short'].includes(value)))error('unknown option; use --help');
94
+ if(sync&&!['status','detail','explain'].includes(command))error('--sync is for status or detail');
95
+ if(short&&(command!=='status'||json||sync))error('--short is a cached status option; do not combine it with --sync or --json');
96
+ if(dryRun&&command!=='init')error('--dry-run is for init');
97
+
98
+ if(command==='init'){
99
+ const {discoverNodeProject}=await import('../src/node-onboarding.mjs');
100
+ let discovery;try{discovery=discoverNodeProject(process.cwd(),{recipe:recipeSelection});}
101
+ catch(e){error(e.message);}
102
+ if(workspaceSelection||checkSelection){
103
+ const chosen=discovery.checks.filter(row=>
104
+ (!workspaceSelection||row.config.workspace===workspaceSelection||
105
+ row.config.command?.[2]===workspaceSelection)&&
106
+ (!checkSelection||row.config.kind===checkSelection));
107
+ if(!chosen.length)error('no discovered check matches the selected workspace and kind');
108
+ discovery.checks=chosen;discovery.config.checks=chosen.map(row=>row.config);
109
+ }
110
+ const configured=fs.existsSync(configFile),unsupported=Boolean(discovery.observationIssue||
111
+ discovery.manager.issue&&!['npm','yarn','pnpm'].includes(discovery.manager.name)||
112
+ discovery.manager.name==='yarn'&&!discovery.manager.version);
113
+ const willWrite=!dryRun&&!configured&&!unsupported&&discovery.checks.length>0;
114
+ const summary={schema:1,repository:discovery.root,node:discovery.node,
115
+ package_manager:discovery.manager.name,version:discovery.manager.version,
116
+ installation_layout:discovery.manager.layout,
117
+ issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
118
+ ambiguous:discovery.ambiguous,
119
+ checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
120
+ script:check.script||null,command:check.command||null,
121
+ workspace:check.workspace||null,level,reason})),config:configFile,
122
+ written:willWrite,existing:configured};
123
+ if(!json) {
124
+ console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
125
+ `${summary.version?' '+summary.version:''}; ${summary.installation_layout} installation.`);
126
+ if(summary.issue)console.log(`Boundary: ${summary.issue}`);
127
+ for(const row of summary.checks){
128
+ console.log(` ${row.name} (${row.script||row.command?.join(' ')}) — `+
129
+ `${{ready:'ready to qualify',recording:'recording-only',unsupported:'unsupported'}[row.level]||row.level}: ${row.reason}`);
130
+ if(row.script&&row.command)console.log(` Recorded invocation: ${row.command.map(value=>
131
+ value==='@node'?'node':value==='@typescript-compiler:.'?'[reviewed local TypeScript compiler implementation]':value.startsWith('@typescript-bin:')?
132
+ `[installed TypeScript in ${value.slice('@typescript-bin:'.length)}]`:value).join(' ')}`);
133
+ }
134
+ for(const row of summary.ambiguous)
135
+ console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
136
+ if(!summary.checks.length)console.log('No useful existing verification scripts found.');
137
+ console.log(configured?`Existing ${configFile} left unchanged.`:
138
+ dryRun?`Preview only; ${configFile} was not written.`:
139
+ willWrite?`Writing ${configFile}; inspect it before running checks.`:'No configuration will be written.');
140
+ }
141
+ if(willWrite){
142
+ if(!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
143
+ error('init writes only at the repository root; use --config there or --dry-run');
144
+ fs.writeFileSync(configFile,JSON.stringify(discovery.config,null,2)+'\n',{flag:'wx'});
145
+ if(!json){console.log(`Created ${configFile}. No checks were executed.`);
146
+ console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
147
+ console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
148
+ console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
149
+ console.log('Use redue status first. --sync can establish applicability, but may cost more than rerunning a cheap check.');}
150
+ } else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
151
+ if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
152
+ process.exit(0);
153
+ }
154
+ function printLifecycle(output,action,machine){
155
+ if(machine){process.stdout.write(output);return;}
156
+ let value;try{value=JSON.parse(output);}catch{process.stdout.write(output);return;}
157
+ if(action==='start'){
158
+ console.log(value.ready?'Observer ready.':'Observer started; checking inputs. Applicability stays UNVERIFIED until ready.');
159
+ console.log('Use redue status. Stop with redue stop.');
160
+ }else if(action==='remove-state')console.log(`Removed only REDUE-owned state: ${value.removed}`);
161
+ else console.log(value.ok?'Observer stopped. Historical evidence is retained.':
162
+ `Observer unavailable: ${value.reason||'not running'}. Previous evidence is not CURRENT.`);
163
+ }
164
+ // Owned-state maintenance must not need the checkout, its package manager,
165
+ // or installed compiler to remain present. No command is run from this path.
166
+ if(stateOverride&&['stop','remove-state'].includes(command)){
167
+ const runtime=path.join(stateOverride,'project-runtime-v1.json');
168
+ if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
169
+ mark('client.engine_submit');
170
+ const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
171
+ {cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
172
+ if(result.error)error(result.error.message);
173
+ if(result.stdout)printLifecycle(result.stdout,command,json);
174
+ process.exit(result.status??2);
175
+ }
176
+ let publicConfig;
177
+ try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
178
+ catch(e){error(e.code==='ENOENT'?`no configuration at ${configFile}; run redue init from the project root, or use --config FILE`:`cannot read ${configFile}: ${e.message}`);}
179
+ if(publicConfig.schema!==1)error('config schema must be 1');
180
+ if(!Array.isArray(publicConfig.checks)||!publicConfig.checks.length)
181
+ error('config needs at least one explicitly defined check');
182
+ for(const key of Object.keys(publicConfig))
183
+ if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
184
+ if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
185
+ const root=realObservedPath(path.resolve(path.dirname(configFile),publicConfig.root||'.'));
186
+ const state=stateOverride||projectState(root,configFile);
187
+ if(!/^(?:vstate|redue)-/.test(path.basename(state)))
188
+ error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
189
+ const runtimeFile=path.join(state,'project-runtime-v1.json');
190
+ function which(name){
191
+ const found=findExecutable(name);
192
+ if(found)return found;
193
+ error(`cannot resolve executable ${name} on PATH`);
194
+ }
195
+ function token(value){
196
+ if(value==='@node')return process.execPath;
197
+ if(value==='@project')return root;
198
+ if(value==='@typescript-compiler:.'){
199
+ try{return directCompiler(root,{verify:false}).entry;}catch(e){error(`direct compiler unavailable: ${e.code||e.message}`);}
200
+ }
201
+ if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
202
+ if(value.startsWith('@typescript-bin:')){
203
+ const dir=value.slice('@typescript-bin:'.length);
204
+ if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
205
+ error('TypeScript executable workspace path is unsafe');
206
+ const manifest=path.join(root,dir,'package.json');
207
+ let resolved;try{resolved=createRequire(manifest).resolve('typescript/package.json');}
208
+ catch{error('installed TypeScript compiler cannot be resolved');}
209
+ const compiler=path.join(path.dirname(realObservedPath(resolved)),'bin','tsc');
210
+ if(!withinPath(compiler,root)||!fs.existsSync(compiler))
211
+ error('TypeScript executable is outside the observed project');
212
+ return compiler;
213
+ }
214
+ if(value==='@vstate/npm-typecheck-probe')return path.join(productRoot,'src','npm-typecheck-probe.mjs');
215
+ if(value==='@vstate/typescript-contract-probe')return path.join(productRoot,'src','typescript-contract-probe.mjs');
216
+ if(value==='@vstate/yarn-workspace-typecheck-probe')return path.join(productRoot,'src','yarn-workspace-typecheck-probe.mjs');
217
+ if(value==='@vstate/pnpm-typecheck-probe')return path.join(productRoot,'src','pnpm-typecheck-probe.mjs');
218
+ if(value==='@vstate/toolchain-probe')return path.join(productRoot,'src','toolchain-probe.mjs');
219
+ if(value.startsWith('@which:'))return which(value.slice(7));
220
+ return value;
221
+ }
222
+ function argv(values,label){
223
+ if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
224
+ error(`${label} must be nonempty exact argv`);
225
+ return values.map(token);
226
+ }
227
+ const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
228
+ 'probeContinuity'];
229
+ const checks=publicConfig.checks.map(check=>{
230
+ const allowed=['name','command','cwd','inputs','generatedInputs','installedInputs',
231
+ 'allowedExternalRoots','probes','environment','coverage','coverageReview','coverageReasons',
232
+ 'script','kind','qualification','workspace'];
233
+ for(const key of Object.keys(check))if(!allowed.includes(key))
234
+ error(`unsupported check field ${check.name}.${key}`);
235
+ for(const category of categories)if(check.coverage?.[category]&&
236
+ typeof check.coverageReview?.[category]!=='string')
237
+ error(`${check.name}: reviewed coverage ${category} needs a rationale`);
238
+ const npmDirect=check.qualification===directQualification;
239
+ const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
240
+ if(check.script&&check.command&&!pnpmTypecheck&&!npmDirect)
241
+ error(`${check.name}: choose script or command, not both`);
242
+ const manager=publicConfig.packageManager;
243
+ if(check.script&&!['npm','yarn','pnpm'].includes(manager))
244
+ error(`${check.name}: script checks need supported npm, Yarn or pnpm packageManager`);
245
+ const scriptCommand=check.script&&!check.command?
246
+ [`@which:${manager}`,'run',check.script]:null;
247
+ const npmAutomatic=check.qualification==='typescript-noemit-v1';
248
+ const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
249
+ if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck)
250
+ error(`${check.name}: unknown qualification contract`);
251
+ if(npmDirect&&(manager!=='npm'||!check.script||check.cwd&&check.cwd!=='.'||
252
+ JSON.stringify(check.command)!==JSON.stringify(['@node','@typescript-compiler:.','--noEmit'])))
253
+ error(`${check.name}: direct recipe requires its explicit local compiler command`);
254
+ if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
255
+ error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
256
+ if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
257
+ check.command?.length!==5||check.command[0]!=='@which:yarn'||
258
+ check.command[1]!=='workspace'||check.command[2]!==check.workspace||
259
+ check.command[3]!=='run'))
260
+ error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
261
+ if(pnpmTypecheck&&(manager!=='pnpm'||!check.script||!check.command||
262
+ check.command[0]!=='@node'||
263
+ check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
264
+ check.workspace&&check.cwd==='.'||
265
+ !check.command.slice(2).every(value=>typeof value==='string'&&value)))
266
+ error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
267
+ const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
268
+ check.kind==='lint'?'lint plugins, config and resolver inputs need review':
269
+ check.kind==='build'?'build environment and generated inputs need review':
270
+ 'check inputs need review';
271
+ const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',check.script]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
272
+ '@project',check.script,'@which:npm']]:yarnWorkspace?
273
+ [['@node','@vstate/yarn-workspace-typecheck-probe','@project',
274
+ check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
275
+ [['@node','@vstate/pnpm-typecheck-probe','@project',
276
+ check.workspace||'.',check.script]]:[];
277
+ const windowsContext=process.platform==='win32'?
278
+ ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
279
+ const autoEnvironment=npmDirect?directEnvironment:npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
280
+ 'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
281
+ pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
282
+ {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
283
+ prefixes:['YARN_','COREPACK_','npm_config_','DYLD_','TSGO_'],
284
+ pathExecutables:['node','yarn'],executableIdentity:true}:pnpmTypecheck?
285
+ {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
286
+ prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
287
+ pathExecutables:['node'],executableIdentity:true}:{};
288
+ return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`),
289
+ cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
290
+ installedInputs:check.installedInputs||(
291
+ npmAutomatic||npmDirect?['node_modules/**']:[]),
292
+ allowedExternalRoots:check.allowedExternalRoots||[],
293
+ probes:[...(check.probes||[]),...autoProbes]
294
+ .map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
295
+ environment:check.environment||autoEnvironment,coverage:check.coverage||{},
296
+ coverageReview:check.coverageReview||{},
297
+ coverageReasons:check.coverageReasons||Object.fromEntries(
298
+ categories.map(category=>[category,reason])),
299
+ qualification:check.qualification||null,script:check.script||null,
300
+ ...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
301
+ ...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
302
+ workspace:check.workspace||null,kind:check.kind||null};
303
+ });
304
+ const runtime={schema:1,provider:'declared-project@1',root,state,checks};
305
+ try{assertOwnedStatePlacement(runtime);}catch(e){error(e.message);}
306
+ const serialized=JSON.stringify(runtime,null,2)+'\n';
307
+ const maintenance=command==='stop'||command==='remove-state';
308
+ if(maintenance&&!fs.existsSync(runtimeFile)){
309
+ console.log(`No REDUE state exists for ${configFile}.`);
310
+ process.exit(0);
311
+ }
312
+ if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
313
+ if(!maintenance&&fs.existsSync(runtimeFile)){
314
+ const previous=fs.readFileSync(runtimeFile,'utf8');
315
+ if(previous!==serialized){
316
+ const pidFile=path.join(state,'observer.lock','pid');
317
+ if(fs.existsSync(pidFile)){
318
+ const pid=Number(fs.readFileSync(pidFile));
319
+ if(processAlive(pid))error('config changed while observer is running; stop it before restarting with the new plan');
320
+ }
321
+ fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
322
+ }
323
+ }else if(!maintenance)fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});
324
+
325
+ if(command==='status'&&!sync){
326
+ const value=readCachedStatus(state,publicConfig.checks);
327
+ console.log(json?JSON.stringify(value):renderStatus(value,{short}));
328
+ process.exit(0);
329
+ }
330
+
331
+ const details=command==='detail'||command==='explain';
332
+ const internal=command==='remove-state'?'uninstall':
333
+ details?'sync':command==='status'&&sync?'sync':command;
334
+ // On macOS and Linux the engine runs in this process, which saves a Node startup
335
+ // on every run; its own handlers receive SIGINT and SIGTERM. Windows keeps a
336
+ // child engine because cancellation reaches it over IPC.
337
+ if(command==='run'&&process.platform!=='win32'){
338
+ process.argv=[process.argv[0],engine,runtimeFile,internal,check,...(!json?['--human']:[])];
339
+ await import(pathToFileURL(engine).href);
340
+ }else if(command==='run'){
341
+ const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
342
+ {stdio:['inherit','inherit','inherit','ipc'],env:process.env});
343
+ const forward=signal=>{if(child.exitCode!==null)return;
344
+ if(process.platform==='win32'&&child.connected)child.send({action:'cancel',signal});
345
+ else child.kill(signal);};
346
+ const interrupt=()=>forward('SIGINT'),terminate=()=>forward('SIGTERM');
347
+ process.on('SIGINT',interrupt);process.on('SIGTERM',terminate);
348
+ const outcome=await new Promise(resolve=>{
349
+ child.once('error',error=>resolve({error}));
350
+ child.once('close',(status,signal)=>resolve({status,signal}));
351
+ });
352
+ process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
353
+ if(outcome.error)error(outcome.error.message);
354
+ process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
355
+ }else{
356
+ mark('client.engine_submit');
357
+ const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
358
+ {stdio:['inherit','pipe','inherit'],
359
+ encoding:'utf8',env:traceEnvironment()});
360
+ mark('client.engine_complete',{exit:result.status,signal:result.signal});
361
+ if(result.error)error(result.error.message);
362
+ if(result.stdout){
363
+ if(['status','detail','explain'].includes(command)){
364
+ let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
365
+ if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
366
+ if(json)console.log(JSON.stringify(value));
367
+ else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
368
+ }else printLifecycle(result.stdout,command,json);
369
+ }
370
+ process.exit(result.status??1);
371
+ }
package/bin/vstate.mjs ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Compatibility entry point for local pre-release workflows.
3
+ import './redue.mjs';
package/docs/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # Documentation
2
+
3
+ For users:
4
+
5
+ - [Install, initialize, run, and remove](user/getting-started.md)
6
+ - [Fixed-build opt-in development pilot](user/pilot.md)
7
+ - [Supported projects and conservative boundaries](user/supported-projects.md)
8
+ - [Configuration](user/configuration.md)
9
+ - [Connect coding agents and apply the decision policy](user/agents.md)
10
+ - [JSON contract](user/json.md)
11
+ - [Shell usage](user/shell.md)
12
+
13
+ For contributors:
14
+
15
+ - [Architecture](contributor/architecture.md)
16
+ - [Observation and recovery](contributor/observation-recovery.md)
17
+ - [Reproducible terminal demo](contributor/demo.md)
18
+ - [Release procedure](contributor/releasing.md)
19
+
20
+ [Historical acceptance evidence](acceptance/README.md) is retained separately.
21
+ It records measured environments and limitations; it is not the getting-started guide.
@@ -0,0 +1,253 @@
1
+ # Agent decision policy
2
+
3
+ Use the CLI with explicit project-local activation below, or add this policy to a
4
+ generic agent's instructions with the project owner's permission.
5
+
6
+ 1. If fresh execution is already required, execute it; do not synchronize merely
7
+ to confirm an irrelevant reuse opportunity. Use `redue run CHECK` when recording
8
+ the configured check. An explicit npm-script request still requires npm.
9
+ 2. For reuse of the explicitly configured direct compiler recipe
10
+ (`qualification: "npm-typescript-direct-v1"`, direct compiler command; receipt
11
+ `verification_recipe: "npm-direct-typescript@1"`), query once:
12
+ `redue --config CONFIG status --sync --json`. Do not infer a recipe from its name.
13
+ Cached status cannot establish its caller context. In mixed configurations,
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.
17
+ 4. Reuse only schema 1, healthy observation and the selected check's CURRENT/PASS
18
+ with `reuse_eligible: true`. Missing eligibility means false. A historical FAIL
19
+ is never green evidence. Do not rerun eligible evidence merely for freshness.
20
+ 5. STALE requires execution when verification is needed. UNVERIFIED requires an
21
+ honest choice of synchronization, permitted execution or reporting the limitation.
22
+ Recording-only success can remain UNVERIFIED. A deliberate cheap-check rerun is
23
+ permitted; there are no universal platform/package-manager timing thresholds.
24
+ 6. Use a successful synchronized response for the current decision without repeated
25
+ sync/explain calls unless edits, context changes or missing information require it.
26
+ Do not loop until green or use an earlier response after a later query fails.
27
+
28
+ `redue explain CHECK --json` gives synchronized detail, including a known changed
29
+ input when available. It has the same cost caveat as synchronized status.
30
+
31
+ No previous conversation history is needed to read a persisted receipt. Still
32
+ consider independent task-specific reasons to run verification: the configured
33
+ check is not universal product correctness. Report additional uncaptured checks
34
+ separately. Do not infer that an outer invocation's success proves a specific
35
+ check ran or that an upstream cache was hit; inspect `target_provenance`.
36
+
37
+ Reassess at the decision point after edits or context changes; previous CURRENT is
38
+ not continuing authorization. A status exit code of zero is not proof of CURRENT.
39
+ An empty changed-input list does not prove nothing changed. `run CHECK --json`
40
+ streams check output; use a subsequent status/explain query for structured evidence.
41
+ Explicit fresh-execution requests still require execution.
42
+
43
+ On a CLI error, unhealthy observation, or unavailable context, do not infer
44
+ CURRENT. Do not delete state or reset receipts to make the output green.
45
+
46
+ REDUE tells you what evidence exists and whether it still applies. It does not
47
+ promise that establishing applicability is cheaper than every possible check.
48
+
49
+ ## Connect once (experimental)
50
+
51
+ Alpha.5 and later support opt-in project-local activation and direct
52
+ TypeScript evidence, with fresh-session reuse demonstrated for Codex CLI 0.160.1
53
+ and subsequent 0.162.0 trials on macOS under the documented owner-approved checkout/state/socket route. CLI
54
+ mechanics are tested on macOS/Linux/Windows; model behavior is not claimed for
55
+ every host/platform or sandbox policy. Claude setup/removal mechanics are tested,
56
+ but Claude behavior and cross-host reuse remain untested. Reuse is not guaranteed,
57
+ and the existing subsecond example establishes no meaningful net time saving.
58
+
59
+ After installing REDUE and running `redue init` in a trusted project:
60
+
61
+ ```sh
62
+ redue agent setup codex --dry-run
63
+ redue agent setup codex --apply
64
+ # Or: redue agent setup claude --apply
65
+ redue agent doctor
66
+ ```
67
+
68
+ Start a fresh agent session in this checkout, then give it your ordinary task.
69
+ Interactive setup shows the changes and asks before applying; noninteractive
70
+ setup requires `--apply`. `--dry-run` never writes. Installing REDUE or running
71
+ `init` does not connect an agent automatically. Setup does not start an observer
72
+ or execute verification. Use `redue start` when you want observation.
73
+
74
+ ## Claude Code typecheck hook
75
+
76
+ `redue agent setup claude --apply` also adds a PreToolUse hook to
77
+ `.claude/settings.local.json`. The hook needs a local REDUE install, so it is
78
+ per-developer; keep that file out of commits. Other settings in the file are kept,
79
+ but the file is rewritten as 2-space JSON. `.claude/settings.json` is not touched.
80
+ `redue agent remove claude --apply` removes only the entry REDUE added.
81
+
82
+ The hook skips an exact rerun of a configured typecheck: a check with
83
+ `kind: "typecheck"` or named `typecheck`. It matches these commands, run from the
84
+ project root:
85
+
86
+ - `redue run CHECK`
87
+ - `npm run SCRIPT`, when the check has an npm script
88
+ - `npx tsc --noEmit`, for the direct-TypeScript recipe
89
+ - the check's `command`, when it has no `@` tokens
90
+
91
+ Each form may end with `2>&1` and then one `| tail -n N`, `| head -n N`,
92
+ `| tail -N` or `| head -N`. Nothing else matches. Each form has its own `if`
93
+ rule, so other Bash commands never start the hook.
94
+
95
+ On a match, the hook runs `redue status --sync --json` with a 2-second timeout.
96
+ It blocks the command only when the check is CURRENT, PASS and
97
+ `reuse_eligible: true`. The message names the check and the receipt's run ID. Every
98
+ other result allows the command: STALE, UNVERIFIED, FAILED, a timeout, an error, or
99
+ a missing `redue`. A command that starts with `REDUE_FORCE=1` always runs.
100
+
101
+ When the check is not CURRENT but the observer answered for it, the hook rewrites
102
+ the command to `redue run CHECK` and allows it, keeping any `2>&1` and
103
+ `| tail`/`| head` suffix. It adds `--config FILE` when the config is not
104
+ `redue.config.json`. The run is recorded, so the next identical command can be
105
+ skipped. Without a healthy answer (timeout, error, no observer) the command runs
106
+ as typed. Claude Code still applies deny and ask rules to the rewritten command.
107
+
108
+ Limits:
109
+
110
+ - The hook judges its own environment, which Claude Code passes to it. The Bash
111
+ tool can add variables from your shell profile. The hook cannot see those.
112
+ - Commands run from a subdirectory, or combined with `&&`, `;` or other pipes,
113
+ are not matched.
114
+ - Only the Bash tool is covered, not PowerShell.
115
+ - Run setup again after you rename a check or script.
116
+ - The rewrite is allowed without a permission prompt, even where the original
117
+ command would have prompted.
118
+
119
+ ## Update an existing connection
120
+
121
+ Installing a newer package does **not** rewrite project instructions. From the
122
+ same project and installation, stop every observer using that installation before
123
+ replacing it. Keep the original config and state selection; do not create a second
124
+ config or delete receipts. Example for a default npm installation/config:
125
+
126
+ ```sh
127
+ redue --config redue.config.json stop
128
+ npm install --global @redue/cli@alpha
129
+ redue --config redue.config.json agent setup codex --dry-run
130
+ redue --config redue.config.json agent setup codex --apply
131
+ redue --config redue.config.json start
132
+ redue --config redue.config.json status --sync --json
133
+ ```
134
+
135
+ Use the **same `--prefix`** as the existing npm installation when it has a custom
136
+ prefix, and invoke its corresponding `redue` binary. Keep your actual config path
137
+ on every command. For Claude, select `claude` in both setup commands. Preserve any
138
+ existing `--state-dir` on lifecycle/status commands; setup does not embed custom
139
+ state locations, so retain the documented generic instruction path if your agent
140
+ workflow needs an explicit custom state. PowerShell may use the existing
141
+ `npm.cmd`/`redue.cmd` shims without changing script policy.
142
+
143
+ Review the preview before applying. Intact owned content updates; unrelated text
144
+ is preserved. Edited managed content is refused, not overwritten. If refused,
145
+ review/reconcile those edits deliberately; do not remove ownership records to force
146
+ an update. Restart the observer with its original selection even if you postpone
147
+ updating instructions, then start a **fresh agent session** after a successful
148
+ update. Setup creates no verification receipt.
149
+
150
+ Managed project metadata can itself be an observed input. Prior receipts remain
151
+ historical evidence, but setup may make them STALE or UNVERIFIED. Reassess normally
152
+ and run required checks; an upgrade is not a promise that existing receipts remain
153
+ CURRENT. Do not suppress that invalidation or reset state.
154
+
155
+ Codex gets a short managed section in project `AGENTS.md` and
156
+ `.agents/skills/redue-verification/SKILL.md`. Claude Code gets a managed section
157
+ in `CLAUDE.md` and `.claude/skills/redue-verification/SKILL.md`. Each uses the same
158
+ policy source; detailed guidance is loaded when verification is relevant. Small
159
+ ownership records in `.redue/agents/` support safe updates/removal. These are
160
+ portable project files, not receipts; review and commit them if the team wants
161
+ this integration. Do not ignore just the ownership records while committing the
162
+ managed files.
163
+
164
+ Existing instruction text is retained. Review it in the preview: setup cannot
165
+ prove arbitrary natural-language policies are consistent. Conflicts such as a
166
+ root `AGENTS.override.md` or an alternative `.claude/CLAUDE.md` are reported
167
+ instead of silently outranked. Global/managed policies, nested instructions,
168
+ Codex instruction-size limits, skill disabling, and host-specific exclusions
169
+ still apply. Complete the host's ordinary trust/permission prompts yourself;
170
+ REDUE never grants tool permission. Sandboxed hosts may need owner-approved access
171
+ to the checkout, owned state directory and local observer socket. A healthy daemon
172
+ does not establish that the agent can reach it; doctor reports control access
173
+ separately. Do not disable sandbox protections to make this work. Do not use a mode that disables project
174
+ instructions and expect automatic activation.
175
+
176
+ Control access is not simply file-read permission. The observer can reconcile and
177
+ write its owned index, run the trusted configuration's probes in its own process
178
+ context, reload receipts, and stop. Approve only the intended checkout/state and
179
+ endpoint, accounting for that authority. Doctor sends only a bounded metrics
180
+ request; it does not synchronize or run these probes.
181
+
182
+ `agent doctor [codex|claude] --json` is read-only. It distinguishes command lookup,
183
+ config resolution, cached observer health, a bounded read-only control-access probe,
184
+ managed-file integrity, detected host
185
+ version, and loading guidance. `behavior_verified: false` is deliberate: intact
186
+ files do not prove the agent followed them. For Claude, doctor reports whether the
187
+ typecheck hook is installed. Doctor neither
188
+ executes checks nor starts observers, reconciles inputs, or installs a host.
189
+
190
+ Doctor separates missing/unreadable state, a missing or refused endpoint, an
191
+ explicit EPERM/EACCES access denial, and timeout/invalid response. A denial does
192
+ not identify whether OS permissions or host policy caused it. A live PID and
193
+ healthy cached observation do not prove control access. An older cached response
194
+ without receipt-selection validation gets stop/start guidance. Windows command
195
+ shims whose target has not been established are reported as unknown, not a
196
+ proven installation mismatch.
197
+
198
+ Codex CLI 0.160.1 on macOS can use the approved exact observer-socket route while
199
+ other sockets and direct network access remain denied. Its independent sessions
200
+ receive different proxy values. Ordinary npm invocation is **not** equivalent
201
+ across arbitrary notifier state: REDUE withholds automatic npm-launcher
202
+ reuse. Do not change proxy settings or permissions to get green evidence.
203
+
204
+ The separate opt-in [direct local compiler recipe](direct-typescript.md) does not
205
+ run npm. Its reviewed compiler implementation does not consume npm/proxy settings;
206
+ a digest preserves their observed provenance without treating them as compiler
207
+ inputs. Other execution context remains strict, and cached status still cannot
208
+ assert caller eligibility. An explicit user request for `npm run typecheck` still
209
+ requires npm execution; direct compiler evidence is not a substitute.
210
+
211
+ Before model trials, run `redue agent doctor --json` and
212
+ `redue status --sync --json` inside the agent's actual command boundary. The
213
+ contributor helper `test/acceptance/codex-preflight.mjs --config FILE --check CHECK`
214
+ performs the same bounded, read-only decision check and exits 2 when reuse is not
215
+ eligible. It neither executes verification nor grants access; synchronization
216
+ may run configured probes. A successful outside-shell result is insufficient.
217
+
218
+ Use the same setup command after an upgrade; it updates intact managed
219
+ content idempotently. Changed skill text or a changed managed block causes a
220
+ conflict instead of an overwrite. Review your edits before retrying.
221
+
222
+ ```sh
223
+ redue agent remove codex --dry-run
224
+ redue agent remove codex --apply
225
+ redue agent remove claude --apply
226
+ ```
227
+
228
+ Removal deletes only intact REDUE-owned content for that host. Unrelated text,
229
+ other integrations, project config, receipts and installed dependencies remain.
230
+ User edits outside the managed section survive. Modified managed content is left
231
+ intact with a diagnostic; compare it with a preview and resolve manually. Empty
232
+ parent directories may remain. If the selected config has been deleted, restore
233
+ it or manually remove only the marked REDUE section, that host's skill, and its
234
+ `.redue/agents/HOST.json` ownership record after review. Do not delete an entire
235
+ instruction file containing user content.
236
+
237
+ ## Project identity and generic agents
238
+
239
+ Commands from nested directories search toward the nearest Git root. If more than
240
+ one configuration applies, select one with `--config FILE`; REDUE refuses to guess.
241
+ The generated instruction names its project-relative configuration and applies it
242
+ to status/run/explain. External configs or custom machine-local `--state-dir`
243
+ setups should use a reviewed generic instruction instead of committing absolute
244
+ paths. Other worktrees and remote containers have separate local evidence.
245
+
246
+ For another agent, put a short instruction in its documented project mechanism:
247
+ “Before deciding whether configured verification needs repeating, read REDUE's
248
+ agent policy, identify the selected recipe and use its decision query for the intended config.
249
+ Apply the schema/health/reuse rules.” Supply this document and the
250
+ [JSON contract](json.md) as the detailed policy. Review the agent's actual tool
251
+ trace before calling activation verified. A local npm installation does not install
252
+ REDUE in a remote container; that environment needs its own CLI, checkout,
253
+ dependencies and state. There is no cloud/team receipt synchronization.