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

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 (53) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +180 -0
  3. package/README.md +126 -2
  4. package/bin/redue.mjs +329 -0
  5. package/bin/vstate.mjs +3 -0
  6. package/docs/README.md +20 -0
  7. package/docs/user/agents.md +32 -0
  8. package/docs/user/configuration.md +57 -0
  9. package/docs/user/getting-started.md +127 -0
  10. package/docs/user/json.md +59 -0
  11. package/docs/user/receipt-selection-notice.md +60 -0
  12. package/docs/user/shell.md +25 -0
  13. package/docs/user/supported-projects.md +89 -0
  14. package/examples/redue.config.json +8 -0
  15. package/package.json +46 -4
  16. package/src/cached-state.mjs +42 -0
  17. package/src/cli.mjs +401 -0
  18. package/src/control-endpoint.mjs +11 -0
  19. package/src/daemon.mjs +1004 -0
  20. package/src/decision-profile.mjs +18 -0
  21. package/src/decision-validation.mjs +41 -0
  22. package/src/declared-plan.mjs +125 -0
  23. package/src/executable-lookup.mjs +18 -0
  24. package/src/glob.mjs +9 -0
  25. package/src/history-worker.mjs +18 -0
  26. package/src/index.mjs +306 -0
  27. package/src/installed-inputs.mjs +110 -0
  28. package/src/linux-inotify.py +228 -0
  29. package/src/linux-observer.mjs +79 -0
  30. package/src/node-onboarding.mjs +263 -0
  31. package/src/npm-typecheck-probe.mjs +120 -0
  32. package/src/observation.mjs +37 -0
  33. package/src/owned-state.mjs +32 -0
  34. package/src/path-identity.mjs +43 -0
  35. package/src/plan-worker.mjs +131 -0
  36. package/src/plan.mjs +10 -0
  37. package/src/platform-observation.mjs +17 -0
  38. package/src/platform-state.mjs +14 -0
  39. package/src/pnpm-inputs.mjs +355 -0
  40. package/src/pnpm-typecheck-probe.mjs +47 -0
  41. package/src/presentation.mjs +52 -0
  42. package/src/probe-result.mjs +21 -0
  43. package/src/process-liveness.mjs +11 -0
  44. package/src/run-lock.mjs +77 -0
  45. package/src/state-store.mjs +73 -0
  46. package/src/toolchain-probe.mjs +21 -0
  47. package/src/typescript-contract-probe.mjs +188 -0
  48. package/src/typescript-list.mjs +68 -0
  49. package/src/typescript-qualification.mjs +96 -0
  50. package/src/windows-command.mjs +16 -0
  51. package/src/windows-process.mjs +15 -0
  52. package/src/yarn-workspace-inputs.mjs +148 -0
  53. package/src/yarn-workspace-typecheck-probe.mjs +107 -0
package/bin/redue.mjs ADDED
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {createHash} from 'node:crypto';
6
+ import {spawn,spawnSync} from 'node:child_process';
7
+ import {createRequire} from 'node:module';
8
+ import {fileURLToPath} from 'node:url';
9
+ import {discoverNodeProject} from '../src/node-onboarding.mjs';
10
+ import {processAlive} from '../src/process-liveness.mjs';
11
+ import {samePath,withinPath,realObservedPath} from '../src/path-identity.mjs';
12
+ import {stateIdentity,userStateBase} from '../src/platform-state.mjs';
13
+ import {findExecutable} from '../src/executable-lookup.mjs';
14
+ import {readCachedStatus} from '../src/cached-state.mjs';
15
+ import {assertOwnedStatePlacement} from '../src/owned-state.mjs';
16
+ import {renderStatus,renderExplain} from '../src/presentation.mjs';
17
+
18
+ const productRoot=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
19
+ const engine=path.join(productRoot,'src','cli.mjs');
20
+ const version=JSON.parse(fs.readFileSync(path.join(productRoot,'package.json'))).version;
21
+ const help=`REDUE ${version}
22
+ Usage: redue [--config FILE] [--state-dir DIR] COMMAND [OPTIONS]
23
+
24
+ Commands:
25
+ init [--dry-run] [--workspace NAME] [--check KIND]
26
+ Discover existing scripts; optionally select one workspace/check
27
+ start Start the local observer
28
+ stop Stop this observer
29
+ status [--json|--short] Read conservative cached status
30
+ status --sync Reconcile inputs and caller context before answering
31
+ explain [CHECK] [--json] Explain applicability (synchronized; may cost more than a rerun)
32
+ detail [CHECK] [--json] Alias for explain
33
+ run CHECK Execute one configured check and record its outcome
34
+ remove-state Stop and remove only this configuration's owned local state
35
+
36
+ No checks run in the background. Direct commands outside redue create no receipts.`;
37
+
38
+ function error(message){console.error(`redue: ${message}`);process.exit(2);}
39
+ const args=process.argv.slice(2);
40
+ 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;
46
+ for(let i=0;i<args.length;){
47
+ if(args[i]==='--config'||args[i]==='--state-dir'){
48
+ const flag=args[i],value=args[i+1];if(!value)error(`${flag} requires a path`);
49
+ if(flag==='--config')configFile=path.resolve(value);else stateOverride=path.resolve(value);
50
+ args.splice(i,2);
51
+ }else i++;
52
+ }
53
+ const command=args.shift();
54
+ if(!['init','start','stop','status','detail','explain','run','remove-state'].includes(command))
55
+ error(`unknown command ${command||'<none>'}; use --help`);
56
+ const json=args.includes('--json');
57
+ const sync=args.includes('--sync');
58
+ const dryRun=args.includes('--dry-run');
59
+ const short=args.includes('--short');
60
+ let workspaceSelection=null,checkSelection=null;
61
+ if(command==='init')for(let i=0;i<args.length;){
62
+ if(args[i]==='--workspace'||args[i]==='--check'){
63
+ 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;
65
+ args.splice(i,2);
66
+ }else i++;
67
+ }
68
+ const check=['run','detail','explain'].includes(command)&&args[0]&&!args[0].startsWith('--')?args.shift():null;
69
+ 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');
71
+ 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');
73
+ if(dryRun&&command!=='init')error('--dry-run is for init');
74
+
75
+ if(command==='init'){
76
+ let discovery;try{discovery=discoverNodeProject(process.cwd());}
77
+ catch(e){error(e.message);}
78
+ if(workspaceSelection||checkSelection){
79
+ const chosen=discovery.checks.filter(row=>
80
+ (!workspaceSelection||row.config.workspace===workspaceSelection||
81
+ row.config.command?.[2]===workspaceSelection)&&
82
+ (!checkSelection||row.config.kind===checkSelection));
83
+ if(!chosen.length)error('no discovered check matches the selected workspace and kind');
84
+ discovery.checks=chosen;discovery.config.checks=chosen.map(row=>row.config);
85
+ }
86
+ const configured=fs.existsSync(configFile),unsupported=Boolean(discovery.observationIssue||
87
+ discovery.manager.issue&&!['npm','yarn','pnpm'].includes(discovery.manager.name)||
88
+ discovery.manager.name==='yarn'&&!discovery.manager.version);
89
+ const willWrite=!dryRun&&!configured&&!unsupported&&discovery.checks.length>0;
90
+ const summary={schema:1,repository:discovery.root,node:discovery.node,
91
+ package_manager:discovery.manager.name,version:discovery.manager.version,
92
+ installation_layout:discovery.manager.layout,
93
+ 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,
96
+ script:check.script||null,command:check.command||null,
97
+ workspace:check.workspace||null,level,reason})),config:configFile,
98
+ written:willWrite,existing:configured};
99
+ if(!json) {
100
+ console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
101
+ `${summary.version?' '+summary.version:''}; ${summary.installation_layout} installation.`);
102
+ if(summary.issue)console.log(`Boundary: ${summary.issue}`);
103
+ for(const row of summary.checks){
104
+ console.log(` ${row.name} (${row.script||row.command?.join(' ')}) — `+
105
+ `${{ready:'ready to qualify',recording:'recording-only',unsupported:'unsupported'}[row.level]||row.level}: ${row.reason}`);
106
+ if(row.script&&row.command)console.log(` Recorded invocation: ${row.command.map(value=>
107
+ value==='@node'?'node':value.startsWith('@typescript-bin:')?
108
+ `[installed TypeScript in ${value.slice('@typescript-bin:'.length)}]`:value).join(' ')}`);
109
+ }
110
+ for(const row of summary.ambiguous)
111
+ console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
112
+ if(!summary.checks.length)console.log('No useful existing verification scripts found.');
113
+ console.log(configured?`Existing ${configFile} left unchanged.`:
114
+ dryRun?`Preview only; ${configFile} was not written.`:
115
+ willWrite?`Writing ${configFile}; inspect it before running checks.`:'No configuration will be written.');
116
+ }
117
+ if(willWrite){
118
+ if(!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
119
+ error('init writes only at the repository root; use --config there or --dry-run');
120
+ fs.writeFileSync(configFile,JSON.stringify(discovery.config,null,2)+'\n',{flag:'wx'});
121
+ if(!json){console.log(`Created ${configFile}. No checks were executed.`);
122
+ console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
123
+ 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.');}
125
+ } else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
126
+ if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
127
+ process.exit(0);
128
+ }
129
+ function printLifecycle(output,action,machine){
130
+ if(machine){process.stdout.write(output);return;}
131
+ let value;try{value=JSON.parse(output);}catch{process.stdout.write(output);return;}
132
+ if(action==='start'){
133
+ console.log(value.ready?'Observer ready.':'Observer started; checking inputs. Applicability stays UNVERIFIED until ready.');
134
+ console.log('Use redue status. Stop with redue stop.');
135
+ }else if(action==='remove-state')console.log(`Removed only REDUE-owned state: ${value.removed}`);
136
+ else console.log(value.ok?'Observer stopped. Historical evidence is retained.':
137
+ `Observer unavailable: ${value.reason||'not running'}. Previous evidence is not CURRENT.`);
138
+ }
139
+ // Owned-state maintenance must not need the checkout, its package manager,
140
+ // or installed compiler to remain present. No command is run from this path.
141
+ if(stateOverride&&['stop','remove-state'].includes(command)){
142
+ const runtime=path.join(stateOverride,'project-runtime-v1.json');
143
+ if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
144
+ const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
145
+ {cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
146
+ if(result.error)error(result.error.message);
147
+ if(result.stdout)printLifecycle(result.stdout,command,json);
148
+ process.exit(result.status??2);
149
+ }
150
+ let publicConfig;
151
+ try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
152
+ 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}`);}
153
+ if(publicConfig.schema!==1)error('config schema must be 1');
154
+ if(!Array.isArray(publicConfig.checks)||!publicConfig.checks.length)
155
+ error('config needs at least one explicitly defined check');
156
+ for(const key of Object.keys(publicConfig))
157
+ if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
158
+ if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
159
+ 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)}`);
164
+ if(!/^(?:vstate|redue)-/.test(path.basename(state)))
165
+ error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
166
+ const runtimeFile=path.join(state,'project-runtime-v1.json');
167
+ function which(name){
168
+ const found=findExecutable(name);
169
+ if(found)return found;
170
+ error(`cannot resolve executable ${name} on PATH`);
171
+ }
172
+ function token(value){
173
+ if(value==='@node')return process.execPath;
174
+ if(value==='@project')return root;
175
+ if(value.startsWith('@typescript-bin:')){
176
+ const dir=value.slice('@typescript-bin:'.length);
177
+ if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
178
+ error('TypeScript executable workspace path is unsafe');
179
+ const manifest=path.join(root,dir,'package.json');
180
+ let resolved;try{resolved=createRequire(manifest).resolve('typescript/package.json');}
181
+ catch{error('installed TypeScript compiler cannot be resolved');}
182
+ const compiler=path.join(path.dirname(realObservedPath(resolved)),'bin','tsc');
183
+ if(!withinPath(compiler,root)||!fs.existsSync(compiler))
184
+ error('TypeScript executable is outside the observed project');
185
+ return compiler;
186
+ }
187
+ if(value==='@vstate/npm-typecheck-probe')return path.join(productRoot,'src','npm-typecheck-probe.mjs');
188
+ if(value==='@vstate/typescript-contract-probe')return path.join(productRoot,'src','typescript-contract-probe.mjs');
189
+ if(value==='@vstate/yarn-workspace-typecheck-probe')return path.join(productRoot,'src','yarn-workspace-typecheck-probe.mjs');
190
+ if(value==='@vstate/pnpm-typecheck-probe')return path.join(productRoot,'src','pnpm-typecheck-probe.mjs');
191
+ if(value==='@vstate/toolchain-probe')return path.join(productRoot,'src','toolchain-probe.mjs');
192
+ if(value.startsWith('@which:'))return which(value.slice(7));
193
+ return value;
194
+ }
195
+ function argv(values,label){
196
+ if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
197
+ error(`${label} must be nonempty exact argv`);
198
+ return values.map(token);
199
+ }
200
+ const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
201
+ 'probeContinuity'];
202
+ const checks=publicConfig.checks.map(check=>{
203
+ const allowed=['name','command','cwd','inputs','generatedInputs','installedInputs',
204
+ 'allowedExternalRoots','probes','environment','coverage','coverageReview','coverageReasons',
205
+ 'script','kind','qualification','workspace'];
206
+ for(const key of Object.keys(check))if(!allowed.includes(key))
207
+ error(`unsupported check field ${check.name}.${key}`);
208
+ for(const category of categories)if(check.coverage?.[category]&&
209
+ typeof check.coverageReview?.[category]!=='string')
210
+ error(`${check.name}: reviewed coverage ${category} needs a rationale`);
211
+ const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
212
+ if(check.script&&check.command&&!pnpmTypecheck)
213
+ error(`${check.name}: choose script or command, not both`);
214
+ const manager=publicConfig.packageManager;
215
+ if(check.script&&!['npm','yarn','pnpm'].includes(manager))
216
+ error(`${check.name}: script checks need supported npm, Yarn or pnpm packageManager`);
217
+ const scriptCommand=check.script&&!check.command?
218
+ [`@which:${manager}`,'run',check.script]:null;
219
+ const npmAutomatic=check.qualification==='typescript-noemit-v1';
220
+ const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
221
+ if(check.qualification&&!npmAutomatic&&!yarnWorkspace&&!pnpmTypecheck)
222
+ error(`${check.name}: unknown qualification contract`);
223
+ if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
224
+ error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
225
+ if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
226
+ check.command?.length!==5||check.command[0]!=='@which:yarn'||
227
+ check.command[1]!=='workspace'||check.command[2]!==check.workspace||
228
+ check.command[3]!=='run'))
229
+ error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
230
+ if(pnpmTypecheck&&(manager!=='pnpm'||!check.script||!check.command||
231
+ check.command[0]!=='@node'||
232
+ check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
233
+ check.workspace&&check.cwd==='.'||
234
+ !check.command.slice(2).every(value=>typeof value==='string'&&value)))
235
+ error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
236
+ const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
237
+ check.kind==='lint'?'lint plugins, config and resolver inputs need review':
238
+ check.kind==='build'?'build environment and generated inputs need review':
239
+ 'check inputs need review';
240
+ const autoProbes=npmAutomatic?[['@node','@vstate/typescript-contract-probe',
241
+ '@project',check.script,'@which:npm']]:yarnWorkspace?
242
+ [['@node','@vstate/yarn-workspace-typecheck-probe','@project',
243
+ check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
244
+ [['@node','@vstate/pnpm-typecheck-probe','@project',
245
+ check.workspace||'.',check.script]]:[];
246
+ const windowsContext=process.platform==='win32'?
247
+ ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
248
+ const autoEnvironment=npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
249
+ 'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
250
+ pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
251
+ {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
252
+ prefixes:['YARN_','COREPACK_','npm_config_','DYLD_','TSGO_'],
253
+ pathExecutables:['node','yarn'],executableIdentity:true}:pnpmTypecheck?
254
+ {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
255
+ prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
256
+ pathExecutables:['node'],executableIdentity:true}:{};
257
+ return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`),
258
+ cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
259
+ installedInputs:check.installedInputs||(
260
+ npmAutomatic?['node_modules/**']:[]),
261
+ allowedExternalRoots:check.allowedExternalRoots||[],
262
+ probes:[...(check.probes||[]),...autoProbes]
263
+ .map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
264
+ environment:check.environment||autoEnvironment,coverage:check.coverage||{},
265
+ coverageReview:check.coverageReview||{},
266
+ coverageReasons:check.coverageReasons||Object.fromEntries(
267
+ categories.map(category=>[category,reason])),
268
+ qualification:check.qualification||null,script:check.script||null,
269
+ workspace:check.workspace||null,kind:check.kind||null};
270
+ });
271
+ const runtime={schema:1,provider:'declared-project@1',root,state,checks};
272
+ try{assertOwnedStatePlacement(runtime);}catch(e){error(e.message);}
273
+ const serialized=JSON.stringify(runtime,null,2)+'\n';
274
+ const maintenance=command==='stop'||command==='remove-state';
275
+ if(maintenance&&!fs.existsSync(runtimeFile)){
276
+ console.log(`No REDUE state exists for ${configFile}.`);
277
+ process.exit(0);
278
+ }
279
+ if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
280
+ if(!maintenance&&fs.existsSync(runtimeFile)){
281
+ const previous=fs.readFileSync(runtimeFile,'utf8');
282
+ if(previous!==serialized){
283
+ const pidFile=path.join(state,'observer.lock','pid');
284
+ if(fs.existsSync(pidFile)){
285
+ const pid=Number(fs.readFileSync(pidFile));
286
+ if(processAlive(pid))error('config changed while observer is running; stop it before restarting with the new plan');
287
+ }
288
+ fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
289
+ }
290
+ }else if(!maintenance)fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});
291
+
292
+ if(command==='status'&&!sync){
293
+ const value=readCachedStatus(state,publicConfig.checks);
294
+ console.log(json?JSON.stringify(value):renderStatus(value,{short}));
295
+ process.exit(0);
296
+ }
297
+
298
+ const details=command==='detail'||command==='explain';
299
+ const internal=command==='remove-state'?'uninstall':
300
+ details?'sync':command==='status'&&sync?'sync':command;
301
+ if(command==='run'){
302
+ const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
303
+ {stdio:['inherit','inherit','inherit','ipc'],env:process.env});
304
+ const forward=signal=>{if(child.exitCode!==null)return;
305
+ if(process.platform==='win32'&&child.connected)child.send({action:'cancel',signal});
306
+ else child.kill(signal);};
307
+ const interrupt=()=>forward('SIGINT'),terminate=()=>forward('SIGTERM');
308
+ process.on('SIGINT',interrupt);process.on('SIGTERM',terminate);
309
+ const outcome=await new Promise(resolve=>{
310
+ child.once('error',error=>resolve({error}));
311
+ child.once('close',(status,signal)=>resolve({status,signal}));
312
+ });
313
+ process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
314
+ if(outcome.error)error(outcome.error.message);
315
+ process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
316
+ }
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);
328
+ }
329
+ process.exit(result.status??1);
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,20 @@
1
+ # Documentation
2
+
3
+ For users:
4
+
5
+ - [Install, initialize, run, and remove](user/getting-started.md)
6
+ - [Supported projects and conservative boundaries](user/supported-projects.md)
7
+ - [Configuration](user/configuration.md)
8
+ - [Coding-agent decision policy](user/agents.md)
9
+ - [JSON contract](user/json.md)
10
+ - [Shell usage](user/shell.md)
11
+
12
+ For contributors:
13
+
14
+ - [Architecture](contributor/architecture.md)
15
+ - [Observation and recovery](contributor/observation-recovery.md)
16
+ - [Reproducible terminal demo](contributor/demo.md)
17
+ - [Release procedure](contributor/releasing.md)
18
+
19
+ [Historical acceptance evidence](acceptance/README.md) is retained separately.
20
+ It records measured environments and limitations; it is not the getting-started guide.
@@ -0,0 +1,32 @@
1
+ # Agent decision policy
2
+
3
+ Use the CLI; no SDK or bespoke agent integration is required. Add these
4
+ instructions to a session or project only with its owner's permission.
5
+
6
+ 1. Query ordinary `redue status --json` first.
7
+ 2. If a check has `freshness: "CURRENT"`, `result: "PASS"`, and
8
+ `reuse_eligible: true`, reuse it. Do not rerun merely for freshness.
9
+ 3. If STALE, run `redue run CHECK` when that verification is needed.
10
+ 4. If the historical `result` is FAIL, report it honestly. The human CLI calls
11
+ this FAILED; applicability is a separate field, never an excuse to call it green.
12
+ 5. If UNVERIFIED, inspect its reason. Use `status --sync --json` when establishing
13
+ applicability is worthwhile, or rerun the underlying check when cheaper/simpler.
14
+ Use `redue run CHECK` if a new receipt is wanted. Incomplete coverage can remain
15
+ UNVERIFIED even after success.
16
+ 6. Do not assume synchronization is cheaper than execution. There are no universal
17
+ package-manager or OS thresholds. Inexpensive checks may be cheaper to rerun.
18
+
19
+ `redue explain CHECK --json` gives synchronized detail, including a known changed
20
+ input when available. It has the same cost caveat as synchronized status.
21
+
22
+ No previous conversation history is needed to read a persisted receipt. Still
23
+ consider independent task-specific reasons to run verification: the configured
24
+ check is not universal product correctness. Report additional uncaptured checks
25
+ separately. Do not infer that an outer invocation's success proves a specific
26
+ check ran or that an upstream cache was hit; inspect `target_provenance`.
27
+
28
+ On a CLI error, unhealthy observation, or unavailable context, do not infer
29
+ CURRENT. Do not delete state or reset receipts to make the output green.
30
+
31
+ REDUE tells you what evidence exists and whether it still applies. It does not
32
+ promise that establishing applicability is cheaper than every possible check.
@@ -0,0 +1,57 @@
1
+ # Configuration
2
+
3
+ Prefer `redue init` over hand-authoring a qualified contract. A typical generated
4
+ npm configuration is concise and portable:
5
+
6
+ ```json
7
+ {
8
+ "schema": 1,
9
+ "root": ".",
10
+ "packageManager": "npm",
11
+ "checks": [{
12
+ "name": "typecheck",
13
+ "script": "typecheck",
14
+ "kind": "typecheck",
15
+ "qualification": "typescript-noemit-v1",
16
+ "inputs": ["package.json", "package-lock.json", "tsconfig*.json", "src/**"]
17
+ }]
18
+ }
19
+ ```
20
+
21
+ The exact inputs are derived from the detected project; use the emitted config,
22
+ not this example to assert your project's coverage. Generated Yarn/pnpm workspace
23
+ checks also identify the owning workspace and actual command. Large dependency
24
+ relationships stay in local runtime state. `qualification` selects a supported
25
+ validation recipe; it is not a blanket coverage assertion.
26
+
27
+ Use `--config FILE` to select an external configuration. `root` is relative to
28
+ that file. Runtime paths, resolved executables, local evidence, and sockets stay
29
+ out of version-controlled configuration.
30
+
31
+ ## Arbitrary commands
32
+
33
+ An unrecognized ecosystem can still record ordinary execution:
34
+
35
+ ```json
36
+ {
37
+ "schema": 1,
38
+ "checks": [{
39
+ "name": "migration",
40
+ "command": ["@node", "tools/check-migrations.mjs"],
41
+ "inputs": ["migrations/**", "tools/check-migrations.mjs"]
42
+ }]
43
+ }
44
+ ```
45
+
46
+ Commands are exact argv, not shell text. Use `cwd` for a project-relative working
47
+ directory. `@node`, `@project`, and `@which:NAME` resolve explicit invocation facts.
48
+ Manual checks start recording-only: a plausible list of files is not proof of a
49
+ complete input boundary. Do not copy coverage assertions from unrelated projects.
50
+
51
+ Advanced explicit contracts may declare `generatedInputs`, `installedInputs`, narrow
52
+ `allowedExternalRoots`, read-only `probes`, and relevant `environment`. These are
53
+ reviewed escape hatches, not onboarding requirements. A coverage rationale cannot
54
+ override unsupported storage, unresolved links, missing inputs, or observation failure.
55
+
56
+ Changing commands, relevant configuration, installed material, or consumed inputs
57
+ invalidates prior applicability. It does not erase historical execution outcomes.
@@ -0,0 +1,127 @@
1
+ # Install and use REDUE
2
+
3
+ Requirements: Git; Node 22 or newer with npm; an existing local JS/TS checkout
4
+ with its dependencies installed. Linux additionally needs `python3`. Supported
5
+ storage is local APFS on macOS, NTFS on Windows, and the documented native Linux
6
+ filesystems. Network/FUSE/overlay mounts are not qualified Linux storage.
7
+ Node 22 and 24 are the release-test matrix; newer majors are not yet acceptance claims.
8
+
9
+ Install the public alpha:
10
+
11
+ ```sh
12
+ npm install --global @redue/cli@alpha
13
+ redue --version
14
+ ```
15
+
16
+ For an unpublished source candidate, run `npm ci` and `npm pack --json`, then
17
+ install the exact filename reported by npm with `npm install --global ./<filename>`.
18
+
19
+ These commands also work in PowerShell (use `npm.cmd`/`redue.cmd` when a local
20
+ PowerShell script policy blocks generated `.ps1` shims; do not change policy).
21
+ No build step, custom installer, account, or login service is required. Installing
22
+ npm dependencies uses the registry; normal local observation does not require network.
23
+
24
+ For a reversible user-owned prefix, POSIX shells:
25
+
26
+ ```sh
27
+ npm install --global --prefix "$HOME/.local/redue-alpha" @redue/cli@alpha
28
+ export PATH="$HOME/.local/redue-alpha/bin:$PATH"
29
+ ```
30
+
31
+ PowerShell:
32
+
33
+ ```powershell
34
+ $reduePrefix = Join-Path $env:LOCALAPPDATA 'redue-alpha-install'
35
+ npm.cmd install --global --prefix $reduePrefix @redue/cli@alpha
36
+ $env:PATH = "$reduePrefix;$env:PATH"
37
+ redue.cmd --help
38
+ ```
39
+
40
+ These PATH changes affect only the current shell. Alternatively invoke the
41
+ installed entry point directly: POSIX `node "$HOME/.local/redue-alpha/lib/node_modules/@redue/cli/bin/redue.mjs"`;
42
+ Windows `node "$reduePrefix/node_modules/@redue/cli/bin/redue.mjs"`.
43
+
44
+ ## Moving from alpha.1
45
+
46
+ If the personal-scope alpha.1 package is installed, stop the project's observer
47
+ and replace that package before installing the canonical command:
48
+
49
+ ```sh
50
+ redue stop
51
+ npm uninstall --global @msiric/redue
52
+ npm install --global @redue/cli@alpha
53
+ ```
54
+
55
+ Use the same `--prefix` on both npm commands if your installation has a custom
56
+ prefix. Uninstalling the package does not remove local verification evidence;
57
+ `redue remove-state` is a separate, explicit action. Alpha.1 remains available as
58
+ historical software and is superseded for package identity, not a runtime defect.
59
+
60
+ ## First project
61
+
62
+ From the project root:
63
+
64
+ ```sh
65
+ redue init --dry-run
66
+ redue init
67
+ redue start
68
+ redue run typecheck
69
+ redue status
70
+ redue explain typecheck
71
+ ```
72
+
73
+ Use a check actually printed by `init`. For a workspace:
74
+ `redue init --workspace @example/package --check typecheck`.
75
+ Initialization previews detected commands and qualification confidence before writing
76
+ `redue.config.json`. An existing config is never overwritten. No checks are run and
77
+ no dependencies installed. A missing/unsupported boundary is explained.
78
+
79
+ “Ready to qualify” means REDUE has a supported recipe whose prerequisites will be
80
+ validated. It is not a receipt. “Recording-only” can record PASS/FAIL without claiming
81
+ reusability. Qualification can change when configuration or installation changes.
82
+
83
+ `run` streams normal output and preserves execution environment and cancellation.
84
+ For the narrow pnpm TypeScript contract, `init` explicitly shows the direct installed
85
+ compiler invocation it selects; other scripts run through the package manager.
86
+ Only use configs and probes from trusted repositories: these execute local code.
87
+
88
+ `status` is cached and conservative. `status --sync` and `explain` revalidate caller
89
+ context and may execute read-only probes. Synchronization can be slower than a cheap
90
+ check, especially on Windows. Choose deliberately. A timeout is UNVERIFIED, not green.
91
+
92
+ ## Lifecycle and removal
93
+
94
+ `start` starts this config's owned observer, or reports that it is already running.
95
+ If already running, use `status`; do not create a second state directory just to
96
+ start another observer. Startup may report initialization before inputs are ready.
97
+ `stop` stops that observer and retains historical evidence. After restart, observation
98
+ is reconciled before reuse.
99
+
100
+ ```sh
101
+ redue stop
102
+ redue start
103
+ redue remove-state
104
+ npm uninstall --global @redue/cli
105
+ ```
106
+
107
+ For an isolated installation, use the same `--prefix` on uninstall. Remove state
108
+ **before** uninstalling the command. No broad recursive deletion command is needed.
109
+ `remove-state` checks its ownership marker and stops only that observer. It does not
110
+ remove project files, dependencies, another tool's watches, or other REDUE configurations.
111
+ To remove project configuration, inspect and delete `redue.config.json` yourself.
112
+
113
+ Runtime state is separate from durable project config. Legacy directory spelling is
114
+ retained for compatibility, not copied into your config:
115
+
116
+ | OS | Default state base |
117
+ | --- | --- |
118
+ | macOS | `~/Library/Application Support/vstate/` |
119
+ | Linux | `$XDG_STATE_HOME/vstate/`, otherwise `~/.local/state/vstate/` |
120
+ | Windows | `%LOCALAPPDATA%/vstate/` |
121
+
122
+ A checkout/config identity selects a dedicated child directory. Use
123
+ `--state-dir /your/owned/redue-project-state` to choose a dedicated directory outside
124
+ all observed roots; its basename must start with `redue-` or legacy `vstate-`.
125
+ For a removed checkout, `redue --state-dir /your/owned/redue-project-state remove-state`
126
+ still works without rebuilding its compiler/configuration. Ownership/active-run conflicts
127
+ are actionable errors, never resolved merely by deleting an old lock.
@@ -0,0 +1,59 @@
1
+ # Alpha JSON interface (schema 1)
2
+
3
+ Use `status --json` for cached state, `status --sync --json` for caller-aware
4
+ applicability, and `explain CHECK --json` for selected synchronized detail.
5
+ `detail` remains an alias. Successful reads emit one JSON object, no human prose.
6
+
7
+ ```json
8
+ {
9
+ "schema": 1,
10
+ "state": "stale",
11
+ "current": 0, "stale": 1, "failed": 0, "observed_failed": 0, "unverified": 0,
12
+ "checks": [{
13
+ "name": "typecheck",
14
+ "result": "PASS",
15
+ "freshness": "STALE",
16
+ "reason": "src/main.ts changed",
17
+ "changed_inputs": ["src/main.ts"],
18
+ "reuse_eligible": false,
19
+ "invocation": {"runId": "opaque-run-id", "status": "exited", "exitCode": 0, "durationMs": 940}
20
+ }],
21
+ "observation": {"healthy": true}
22
+ }
23
+ ```
24
+
25
+ Stable-enough-for-alpha fields:
26
+
27
+ | Field | Contract |
28
+ | --- | --- |
29
+ | `checks[].name` | Configured check identity. |
30
+ | `result` | Historical `PASS`, `FAIL`, or null. A failed invocation is not green even with CURRENT applicability. |
31
+ | `freshness` | `CURRENT`, `STALE`, or `UNVERIFIED`, independent of result. |
32
+ | `reuse_eligible` | True only for qualified, applicable passing evidence. Never derive permission to reuse from outcome alone. Missing means false. |
33
+ | `reason` | Human explanation; not a stable machine enum. Do not parse it to authorize reuse. |
34
+ | `changed_inputs` | Known changed relevant paths when available, possibly only the first difference; empty/missing is not proof of no changes. |
35
+ | `invocation` | Null without a receipt; otherwise run ID, completion `status`, duration, exit code/signal when known. Status distinguishes exited, interrupted, and start/capture failure. |
36
+ | `target_provenance` | Available target evidence. Unknown is not executed or cache-hit. |
37
+ | `observation.healthy` | False means no check is reusable. Additional reason/phase diagnostics may appear. |
38
+ | `state` | Aggregate lowercase current/stale/failed/unverified (and runtime pending states where applicable). Use per-check fields for decisions. |
39
+
40
+ Counts cover the whole configuration even when `explain CHECK` filters `checks`.
41
+ `failed` counts applicable failures; `observed_failed` preserves historical failures
42
+ regardless of applicability. Fields not listed here are diagnostic and may evolve.
43
+ Ignore unknown fields; reject unknown schema versions. These responses contain local
44
+ paths/check names and must not be uploaded automatically.
45
+
46
+ `init --json` returns discovery levels (`ready`, `recording`, `unsupported`),
47
+ reasons, workspace ownership, `written`, and `proposed_config`. Ready is not PASS.
48
+ `init --dry-run --json` writes no config and runs no check.
49
+
50
+ Read commands normally exit 0 even when the evidence is not current. Errors exit
51
+ nonzero; synchronized transport failures may still return conservative JSON.
52
+ Usage/configuration errors go to stderr and are not guaranteed JSON. Never treat
53
+ a failed read as authority from a previously cached answer.
54
+
55
+ `run CHECK --json` preserves ordinary command stdin/stdout/stderr and appends a
56
+ JSON summary. **Its stdout is not a JSON-only stream.** Query status/details after
57
+ completion for machine-readable evidence. Normal check exit codes are preserved;
58
+ interruption/process-start failures are nonzero. REDUE retains metadata and receipts,
59
+ not arbitrary terminal output. The CLI does not infer tests executed from prose.