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

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