@redue/cli 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (60) hide show
  1. package/CHANGELOG.md +491 -0
  2. package/README.md +107 -2
  3. package/bin/redue.mjs +132 -50
  4. package/docs/README.md +2 -1
  5. package/docs/user/agents.md +299 -15
  6. package/docs/user/configuration.md +28 -2
  7. package/docs/user/direct-typescript.md +209 -0
  8. package/docs/user/getting-started.md +23 -2
  9. package/docs/user/json.md +2 -0
  10. package/docs/user/node-script.md +154 -0
  11. package/docs/user/npm-launcher-notice.md +42 -0
  12. package/docs/user/pilot.md +146 -0
  13. package/docs/user/receipt-selection-notice.md +60 -0
  14. package/docs/user/supported-projects.md +56 -23
  15. package/docs/user/tests-direct.md +132 -0
  16. package/package.json +4 -3
  17. package/src/agent-integration.mjs +388 -0
  18. package/src/agent-policy.md +28 -0
  19. package/src/cached-state.mjs +46 -8
  20. package/src/claude-hook.mjs +449 -0
  21. package/src/cli.mjs +263 -40
  22. package/src/daemon.mjs +433 -94
  23. package/src/decision-profile.mjs +15 -1
  24. package/src/decision-validation.mjs +10 -0
  25. package/src/declared-plan.mjs +29 -6
  26. package/src/direct-run-certificate.mjs +252 -0
  27. package/src/direct-typescript-probe.mjs +24 -0
  28. package/src/direct-typescript.mjs +185 -0
  29. package/src/glob.mjs +8 -0
  30. package/src/history-worker.mjs +21 -13
  31. package/src/index.mjs +72 -19
  32. package/src/installed-inputs.mjs +7 -1
  33. package/src/linux-inotify.py +39 -10
  34. package/src/node-onboarding.mjs +172 -53
  35. package/src/node-script.mjs +364 -0
  36. package/src/npm-integrity.mjs +124 -0
  37. package/src/npm-typecheck-environment.mjs +36 -0
  38. package/src/npmrc.mjs +16 -0
  39. package/src/observation.mjs +34 -12
  40. package/src/path-identity.mjs +8 -0
  41. package/src/plan-worker.mjs +17 -17
  42. package/src/pnpm-inputs.mjs +114 -54
  43. package/src/pnpm-shim-templates.json +180 -0
  44. package/src/pnpm-shims.mjs +54 -0
  45. package/src/pnpm-typecheck-probe.mjs +11 -6
  46. package/src/presentation.mjs +9 -1
  47. package/src/probe-deadline.mjs +37 -0
  48. package/src/probe-result.mjs +7 -3
  49. package/src/project-location.mjs +34 -0
  50. package/src/run-recorder.cjs +91 -0
  51. package/src/state-store.mjs +37 -1
  52. package/src/test-direct.mjs +560 -0
  53. package/src/test-recorder.cjs +406 -0
  54. package/src/typescript-contract-probe.mjs +4 -187
  55. package/src/typescript-input-facts.mjs +92 -0
  56. package/src/typescript-invocation.mjs +92 -0
  57. package/src/typescript-list.mjs +108 -13
  58. package/src/typescript-qualification.mjs +29 -49
  59. package/src/workspace-manifests.mjs +24 -0
  60. package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/bin/redue.mjs CHANGED
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decision-profile.mjs';
4
+ traceBase({...traceContext(),request:traceId(),work:'client'});
5
+ mark('client.loaded');
6
+ import {directCompiler,directQualification,directEnvironment,directInterpretation} from '../src/direct-typescript.mjs';
7
+ import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
8
+ import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
9
+ import {scriptEnvironment,scriptInterpretation,scriptKinds,scriptQualification} from '../src/node-script.mjs';
3
10
  import os from 'node:os';
4
11
  import path from 'node:path';
5
12
  import {createHash} from 'node:crypto';
6
13
  import {spawn,spawnSync} from 'node:child_process';
7
14
  import {createRequire} from 'node:module';
8
- import {fileURLToPath} from 'node:url';
9
- import {discoverNodeProject} from '../src/node-onboarding.mjs';
15
+ import {fileURLToPath,pathToFileURL} from 'node:url';
10
16
  import {processAlive} from '../src/process-liveness.mjs';
11
17
  import {samePath,withinPath,realObservedPath} from '../src/path-identity.mjs';
12
- import {stateIdentity,userStateBase} from '../src/platform-state.mjs';
18
+ import {discoverConfig,projectState,canonicalConfig} from '../src/project-location.mjs';
13
19
  import {findExecutable} from '../src/executable-lookup.mjs';
14
20
  import {readCachedStatus} from '../src/cached-state.mjs';
15
21
  import {assertOwnedStatePlacement} from '../src/owned-state.mjs';
@@ -22,8 +28,12 @@ const help=`REDUE ${version}
22
28
  Usage: redue [--config FILE] [--state-dir DIR] COMMAND [OPTIONS]
23
29
 
24
30
  Commands:
25
- init [--dry-run] [--workspace NAME] [--check KIND]
31
+ init [--dry-run] [--workspace NAME] [--check KIND] [--recipe typescript-direct|tests-direct|node-script]
26
32
  Discover existing scripts; optionally select one workspace/check
33
+ agent setup codex|claude [--dry-run|--apply] [--json]
34
+ agent doctor [codex|claude] [--json]
35
+ agent remove codex|claude [--dry-run|--apply] [--json]
36
+ Connect project instructions; explicit setup only
27
37
  start Start the local observer
28
38
  stop Stop this observer
29
39
  status [--json|--short] Read conservative cached status
@@ -36,13 +46,20 @@ Commands:
36
46
  No checks run in the background. Direct commands outside redue create no receipts.`;
37
47
 
38
48
  function error(message){console.error(`redue: ${message}`);process.exit(2);}
49
+ // Exits once queued output is written. On macOS, output to a pipe is written
50
+ // asynchronously; exiting with it queued cuts it (at 8 KB through the pipe a
51
+ // Node program, such as an agent's shell tool, reads).
52
+ async function exit(code){await new Promise(resolve=>process.stdout.write('',resolve));process.exit(code);}
39
53
  const args=process.argv.slice(2);
54
+ if(args.length===3&&args[0]==='agent'&&args[1]==='hook'&&args[2]==='claude'){
55
+ // Runs before config discovery: the hook must exit 0 on every failure,
56
+ // because Claude Code treats exit 2 as a block.
57
+ const {runClaudeHook}=await import('../src/claude-hook.mjs');
58
+ await runClaudeHook(fileURLToPath(import.meta.url));process.exit(0);
59
+ }
40
60
  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;
61
+ if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);await exit(0);}
62
+ let configFile=null,stateOverride=null;
46
63
  for(let i=0;i<args.length;){
47
64
  if(args[i]==='--config'||args[i]==='--state-dir'){
48
65
  const flag=args[i],value=args[i+1];if(!value)error(`${flag} requires a path`);
@@ -51,17 +68,28 @@ for(let i=0;i<args.length;){
51
68
  }else i++;
52
69
  }
53
70
  const command=args.shift();
71
+ try{configFile??=command==='init'||stateOverride&&['stop','remove-state'].includes(command)?
72
+ path.resolve(fs.existsSync('redue.config.json')||!fs.existsSync('vstate.config.json')?'redue.config.json':'vstate.config.json'):discoverConfig();}
73
+ catch(e){error(e.message);}
74
+ if(fs.existsSync(path.dirname(configFile)))configFile=canonicalConfig(configFile);
75
+ if(command==='agent'){
76
+ // Loaded on demand: onboarding and agent setup are not on the run/status path.
77
+ const {agentCommand}=await import('../src/agent-integration.mjs');
78
+ try{await agentCommand(args,{configFile,stateOverride,entry:fileURLToPath(import.meta.url)});}
79
+ catch(e){error(e.message);}
80
+ await exit(0);
81
+ }
54
82
  if(!['init','start','stop','status','detail','explain','run','remove-state'].includes(command))
55
83
  error(`unknown command ${command||'<none>'}; use --help`);
56
84
  const json=args.includes('--json');
57
85
  const sync=args.includes('--sync');
58
86
  const dryRun=args.includes('--dry-run');
59
87
  const short=args.includes('--short');
60
- let workspaceSelection=null,checkSelection=null;
88
+ let workspaceSelection=null,checkSelection=null,recipeSelection=null;
61
89
  if(command==='init')for(let i=0;i<args.length;){
62
- if(args[i]==='--workspace'||args[i]==='--check'){
90
+ if(args[i]==='--workspace'||args[i]==='--check'||args[i]==='--recipe'){
63
91
  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;
92
+ if(flag==='--workspace')workspaceSelection=value;else if(flag==='--recipe')recipeSelection=value;else checkSelection=value;
65
93
  args.splice(i,2);
66
94
  }else i++;
67
95
  }
@@ -73,7 +101,8 @@ if(short&&(command!=='status'||json||sync))error('--short is a cached status opt
73
101
  if(dryRun&&command!=='init')error('--dry-run is for init');
74
102
 
75
103
  if(command==='init'){
76
- let discovery;try{discovery=discoverNodeProject(process.cwd());}
104
+ const {discoverNodeProject}=await import('../src/node-onboarding.mjs');
105
+ let discovery;try{discovery=discoverNodeProject(process.cwd(),{recipe:recipeSelection});}
77
106
  catch(e){error(e.message);}
78
107
  if(workspaceSelection||checkSelection){
79
108
  const chosen=discovery.checks.filter(row=>
@@ -91,7 +120,7 @@ if(command==='init'){
91
120
  package_manager:discovery.manager.name,version:discovery.manager.version,
92
121
  installation_layout:discovery.manager.layout,
93
122
  issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
94
- ambiguous:discovery.ambiguous,
123
+ ambiguous:discovery.ambiguous,skipped:discovery.skipped||[],
95
124
  checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
96
125
  script:check.script||null,command:check.command||null,
97
126
  workspace:check.workspace||null,level,reason})),config:configFile,
@@ -104,11 +133,13 @@ if(command==='init'){
104
133
  console.log(` ${row.name} (${row.script||row.command?.join(' ')}) — `+
105
134
  `${{ready:'ready to qualify',recording:'recording-only',unsupported:'unsupported'}[row.level]||row.level}: ${row.reason}`);
106
135
  if(row.script&&row.command)console.log(` Recorded invocation: ${row.command.map(value=>
107
- value==='@node'?'node':value.startsWith('@typescript-bin:')?
136
+ value==='@node'?'node':value==='@typescript-compiler:.'?'[reviewed local TypeScript compiler implementation]':
137
+ value.startsWith('@test-runner:')?`[local ${value.slice('@test-runner:'.length)}]`:value.startsWith('@typescript-bin:')?
108
138
  `[installed TypeScript in ${value.slice('@typescript-bin:'.length)}]`:value).join(' ')}`);
109
139
  }
110
140
  for(const row of summary.ambiguous)
111
141
  console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
142
+ for(const row of summary.skipped)console.log(` Skipped: ${row.reason}`);
112
143
  if(!summary.checks.length)console.log('No useful existing verification scripts found.');
113
144
  console.log(configured?`Existing ${configFile} left unchanged.`:
114
145
  dryRun?`Preview only; ${configFile} was not written.`:
@@ -121,10 +152,11 @@ if(command==='init'){
121
152
  if(!json){console.log(`Created ${configFile}. No checks were executed.`);
122
153
  console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
123
154
  console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
155
+ console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
124
156
  console.log('Use redue status first. --sync can establish applicability, but may cost more than rerunning a cheap check.');}
125
157
  } else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
126
158
  if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
127
- process.exit(0);
159
+ await exit(0);
128
160
  }
129
161
  function printLifecycle(output,action,machine){
130
162
  if(machine){process.stdout.write(output);return;}
@@ -141,11 +173,12 @@ function printLifecycle(output,action,machine){
141
173
  if(stateOverride&&['stop','remove-state'].includes(command)){
142
174
  const runtime=path.join(stateOverride,'project-runtime-v1.json');
143
175
  if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
176
+ mark('client.engine_submit');
144
177
  const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
145
178
  {cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
146
179
  if(result.error)error(result.error.message);
147
180
  if(result.stdout)printLifecycle(result.stdout,command,json);
148
- process.exit(result.status??2);
181
+ await exit(result.status??2);
149
182
  }
150
183
  let publicConfig;
151
184
  try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
@@ -157,10 +190,7 @@ for(const key of Object.keys(publicConfig))
157
190
  if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
158
191
  if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
159
192
  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)}`);
193
+ const state=stateOverride||projectState(root,configFile);
164
194
  if(!/^(?:vstate|redue)-/.test(path.basename(state)))
165
195
  error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
166
196
  const runtimeFile=path.join(state,'project-runtime-v1.json');
@@ -169,9 +199,18 @@ function which(name){
169
199
  if(found)return found;
170
200
  error(`cannot resolve executable ${name} on PATH`);
171
201
  }
172
- function token(value){
202
+ function token(value,cwd='.'){
173
203
  if(value==='@node')return process.execPath;
174
204
  if(value==='@project')return root;
205
+ if(value==='@typescript-compiler:.'){
206
+ try{return directCompiler(root,{verify:false}).entry;}catch(e){error(`direct compiler unavailable: ${e.code||e.message}`);}
207
+ }
208
+ if(value.startsWith('@test-runner:')){
209
+ // Qualification reports a missing runner; status and stop still work.
210
+ try{return testRunnerAt(root,cwd,value.slice('@test-runner:'.length)).entry;}
211
+ catch{return path.join(root,cwd,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
212
+ }
213
+ if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
175
214
  if(value.startsWith('@typescript-bin:')){
176
215
  const dir=value.slice('@typescript-bin:'.length);
177
216
  if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
@@ -192,10 +231,10 @@ function token(value){
192
231
  if(value.startsWith('@which:'))return which(value.slice(7));
193
232
  return value;
194
233
  }
195
- function argv(values,label){
234
+ function argv(values,label,cwd){
196
235
  if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
197
236
  error(`${label} must be nonempty exact argv`);
198
- return values.map(token);
237
+ return values.map(value=>token(value,cwd));
199
238
  }
200
239
  const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
201
240
  'probeContinuity'];
@@ -208,8 +247,11 @@ const checks=publicConfig.checks.map(check=>{
208
247
  for(const category of categories)if(check.coverage?.[category]&&
209
248
  typeof check.coverageReview?.[category]!=='string')
210
249
  error(`${check.name}: reviewed coverage ${category} needs a rationale`);
250
+ const npmDirect=check.qualification===directQualification;
211
251
  const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
212
- if(check.script&&check.command&&!pnpmTypecheck)
252
+ const testsDirect=check.qualification===testsQualification;
253
+ const generalScript=check.qualification===scriptQualification;
254
+ if(check.script&&check.command&&!pnpmTypecheck&&!npmDirect&&!testsDirect)
213
255
  error(`${check.name}: choose script or command, not both`);
214
256
  const manager=publicConfig.packageManager;
215
257
  if(check.script&&!['npm','yarn','pnpm'].includes(manager))
@@ -218,8 +260,20 @@ const checks=publicConfig.checks.map(check=>{
218
260
  [`@which:${manager}`,'run',check.script]:null;
219
261
  const npmAutomatic=check.qualification==='typescript-noemit-v1';
220
262
  const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
221
- if(check.qualification&&!npmAutomatic&&!yarnWorkspace&&!pnpmTypecheck)
263
+ if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck&&!testsDirect&&!generalScript)
222
264
  error(`${check.name}: unknown qualification contract`);
265
+ // The general recipe runs the root package script through the package manager.
266
+ if(generalScript&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.command||check.cwd&&check.cwd!=='.'||
267
+ check.workspace||!scriptKinds.includes(check.kind)))
268
+ error(`${check.name}: the general recipe needs a root typecheck, test or lint script, without command or cwd`);
269
+ if(npmDirect&&(manager!=='npm'||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
270
+ check.command[0]!=='@node'||check.command[1]!=='@typescript-compiler:.'||
271
+ !tscRun({},{script:null,args:check.command.slice(2)}).run?.noEmit))
272
+ error(`${check.name}: direct recipe requires its explicit local compiler command`);
273
+ // The script's own arguments follow the runner; qualification compares them with the script.
274
+ if(testsDirect&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.cwd&&check.cwd!=='.'&&!check.workspace||!Array.isArray(check.command)||
275
+ check.command[0]!=='@node'||!/^@test-runner:(?:vitest|jest)$/.test(check.command[1]||'')))
276
+ error(`${check.name}: test recipe requires its explicit local test runner command`);
223
277
  if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
224
278
  error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
225
279
  if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
@@ -227,25 +281,32 @@ const checks=publicConfig.checks.map(check=>{
227
281
  check.command[1]!=='workspace'||check.command[2]!==check.workspace||
228
282
  check.command[3]!=='run'))
229
283
  error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
230
- if(pnpmTypecheck&&(manager!=='pnpm'||!check.script||!check.command||
284
+ if(pnpmTypecheck&&(manager!=='pnpm'||!Array.isArray(check.command)||
231
285
  check.command[0]!=='@node'||
232
286
  check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
233
287
  check.workspace&&check.cwd==='.'||
234
- !check.command.slice(2).every(value=>typeof value==='string'&&value)))
288
+ !tscRun({},{script:null,args:check.command.slice(2)}).run))
235
289
  error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
236
290
  const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
237
291
  check.kind==='lint'?'lint plugins, config and resolver inputs need review':
238
292
  check.kind==='build'?'build environment and generated inputs need review':
239
293
  'check inputs need review';
240
- const autoProbes=npmAutomatic?[['@node','@vstate/typescript-contract-probe',
294
+ // A chained script has one check per tsc run: each check's run counts them,
295
+ // so a run added to or removed from the script invalidates them all.
296
+ const runCount=(npmDirect||pnpmTypecheck)&&check.script?publicConfig.checks.filter(other=>
297
+ other.qualification===check.qualification&&other.script===check.script&&
298
+ (other.cwd||'.')===(check.cwd||'.')&&(other.workspace||null)===(check.workspace||null)).length:1;
299
+ if(runCount>16)error(`${check.name}: more than 16 checks for script ${check.script}`);
300
+ const run=npmDirect||pnpmTypecheck?runArgv({script:check.script??null,args:check.command.slice(2),count:runCount}):null;
301
+ const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',...run]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
241
302
  '@project',check.script,'@which:npm']]:yarnWorkspace?
242
303
  [['@node','@vstate/yarn-workspace-typecheck-probe','@project',
243
304
  check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
244
305
  [['@node','@vstate/pnpm-typecheck-probe','@project',
245
- check.workspace||'.',check.script]]:[];
306
+ check.workspace||'.',...run]]:[];
246
307
  const windowsContext=process.platform==='win32'?
247
308
  ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
248
- const autoEnvironment=npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
309
+ const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:generalScript?scriptEnvironment(manager):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
249
310
  'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
250
311
  pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
251
312
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
@@ -254,10 +315,11 @@ const checks=publicConfig.checks.map(check=>{
254
315
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
255
316
  prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
256
317
  pathExecutables:['node'],executableIdentity:true}:{};
257
- return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`),
318
+ return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`,
319
+ typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
258
320
  cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
259
321
  installedInputs:check.installedInputs||(
260
- npmAutomatic?['node_modules/**']:[]),
322
+ npmAutomatic||npmDirect||testsDirect?['node_modules/**']:[]),
261
323
  allowedExternalRoots:check.allowedExternalRoots||[],
262
324
  probes:[...(check.probes||[]),...autoProbes]
263
325
  .map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
@@ -266,6 +328,11 @@ const checks=publicConfig.checks.map(check=>{
266
328
  coverageReasons:check.coverageReasons||Object.fromEntries(
267
329
  categories.map(category=>[category,reason])),
268
330
  qualification:check.qualification||null,script:check.script||null,
331
+ ...(runCount>1?{runCount}:{}),
332
+ ...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
333
+ ...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
334
+ ...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
335
+ ...(generalScript?{qualificationInterpretation:scriptInterpretation}:{}),
269
336
  workspace:check.workspace||null,kind:check.kind||null};
270
337
  });
271
338
  const runtime={schema:1,provider:'declared-project@1',root,state,checks};
@@ -277,7 +344,13 @@ if(maintenance&&!fs.existsSync(runtimeFile)){
277
344
  process.exit(0);
278
345
  }
279
346
  if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
280
- if(!maintenance&&fs.existsSync(runtimeFile)){
347
+ // Two commands can meet on fresh state (SessionStart's start and the first
348
+ // status): whichever creates the runtime file first wins, and the other
349
+ // treats it as existing.
350
+ let created=false;
351
+ if(!maintenance&&!fs.existsSync(runtimeFile))
352
+ try{fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});created=true;}catch(e){if(e.code!=='EEXIST')throw e;}
353
+ if(!maintenance&&!created){
281
354
  const previous=fs.readFileSync(runtimeFile,'utf8');
282
355
  if(previous!==serialized){
283
356
  const pidFile=path.join(state,'observer.lock','pid');
@@ -287,18 +360,24 @@ if(!maintenance&&fs.existsSync(runtimeFile)){
287
360
  }
288
361
  fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
289
362
  }
290
- }else if(!maintenance)fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});
363
+ }
291
364
 
292
365
  if(command==='status'&&!sync){
293
366
  const value=readCachedStatus(state,publicConfig.checks);
294
367
  console.log(json?JSON.stringify(value):renderStatus(value,{short}));
295
- process.exit(0);
368
+ await exit(0);
296
369
  }
297
370
 
298
371
  const details=command==='detail'||command==='explain';
299
372
  const internal=command==='remove-state'?'uninstall':
300
373
  details?'sync':command==='status'&&sync?'sync':command;
301
- if(command==='run'){
374
+ // On macOS and Linux the engine runs in this process, which saves a Node startup
375
+ // on every run; its own handlers receive SIGINT and SIGTERM. Windows keeps a
376
+ // child engine because cancellation reaches it over IPC.
377
+ if(command==='run'&&process.platform!=='win32'){
378
+ process.argv=[process.argv[0],engine,runtimeFile,internal,check,...(!json?['--human']:[])];
379
+ await import(pathToFileURL(engine).href);
380
+ }else if(command==='run'){
302
381
  const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
303
382
  {stdio:['inherit','inherit','inherit','ipc'],env:process.env});
304
383
  const forward=signal=>{if(child.exitCode!==null)return;
@@ -313,17 +392,20 @@ if(command==='run'){
313
392
  process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
314
393
  if(outcome.error)error(outcome.error.message);
315
394
  process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
395
+ }else{
396
+ mark('client.engine_submit');
397
+ const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
398
+ {stdio:['inherit','pipe','inherit'],
399
+ encoding:'utf8',env:traceEnvironment()});
400
+ mark('client.engine_complete',{exit:result.status,signal:result.signal});
401
+ if(result.error)error(result.error.message);
402
+ if(result.stdout){
403
+ if(['status','detail','explain'].includes(command)){
404
+ let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
405
+ if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
406
+ if(json)console.log(JSON.stringify(value));
407
+ else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
408
+ }else printLifecycle(result.stdout,command,json);
409
+ }
410
+ await exit(result.status??1);
316
411
  }
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/docs/README.md CHANGED
@@ -3,9 +3,10 @@
3
3
  For users:
4
4
 
5
5
  - [Install, initialize, run, and remove](user/getting-started.md)
6
+ - [Fixed-build opt-in development pilot](user/pilot.md)
6
7
  - [Supported projects and conservative boundaries](user/supported-projects.md)
7
8
  - [Configuration](user/configuration.md)
8
- - [Coding-agent decision policy](user/agents.md)
9
+ - [Connect coding agents and apply the decision policy](user/agents.md)
9
10
  - [JSON contract](user/json.md)
10
11
  - [Shell usage](user/shell.md)
11
12