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

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 +467 -0
  2. package/README.md +107 -2
  3. package/bin/redue.mjs +130 -49
  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 +153 -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 +49 -21
  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 +157 -48
  35. package/src/node-script.mjs +362 -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 +22 -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=>
@@ -104,7 +133,8 @@ 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)
@@ -121,10 +151,11 @@ if(command==='init'){
121
151
  if(!json){console.log(`Created ${configFile}. No checks were executed.`);
122
152
  console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
123
153
  console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
154
+ console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
124
155
  console.log('Use redue status first. --sync can establish applicability, but may cost more than rerunning a cheap check.');}
125
156
  } else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
126
157
  if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
127
- process.exit(0);
158
+ await exit(0);
128
159
  }
129
160
  function printLifecycle(output,action,machine){
130
161
  if(machine){process.stdout.write(output);return;}
@@ -141,11 +172,12 @@ function printLifecycle(output,action,machine){
141
172
  if(stateOverride&&['stop','remove-state'].includes(command)){
142
173
  const runtime=path.join(stateOverride,'project-runtime-v1.json');
143
174
  if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
175
+ mark('client.engine_submit');
144
176
  const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
145
177
  {cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
146
178
  if(result.error)error(result.error.message);
147
179
  if(result.stdout)printLifecycle(result.stdout,command,json);
148
- process.exit(result.status??2);
180
+ await exit(result.status??2);
149
181
  }
150
182
  let publicConfig;
151
183
  try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
@@ -157,10 +189,7 @@ for(const key of Object.keys(publicConfig))
157
189
  if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
158
190
  if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
159
191
  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)}`);
192
+ const state=stateOverride||projectState(root,configFile);
164
193
  if(!/^(?:vstate|redue)-/.test(path.basename(state)))
165
194
  error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
166
195
  const runtimeFile=path.join(state,'project-runtime-v1.json');
@@ -169,9 +198,18 @@ function which(name){
169
198
  if(found)return found;
170
199
  error(`cannot resolve executable ${name} on PATH`);
171
200
  }
172
- function token(value){
201
+ function token(value,cwd='.'){
173
202
  if(value==='@node')return process.execPath;
174
203
  if(value==='@project')return root;
204
+ if(value==='@typescript-compiler:.'){
205
+ try{return directCompiler(root,{verify:false}).entry;}catch(e){error(`direct compiler unavailable: ${e.code||e.message}`);}
206
+ }
207
+ if(value.startsWith('@test-runner:')){
208
+ // Qualification reports a missing runner; status and stop still work.
209
+ try{return testRunnerAt(root,cwd,value.slice('@test-runner:'.length)).entry;}
210
+ catch{return path.join(root,cwd,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
211
+ }
212
+ if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
175
213
  if(value.startsWith('@typescript-bin:')){
176
214
  const dir=value.slice('@typescript-bin:'.length);
177
215
  if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
@@ -192,10 +230,10 @@ function token(value){
192
230
  if(value.startsWith('@which:'))return which(value.slice(7));
193
231
  return value;
194
232
  }
195
- function argv(values,label){
233
+ function argv(values,label,cwd){
196
234
  if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
197
235
  error(`${label} must be nonempty exact argv`);
198
- return values.map(token);
236
+ return values.map(value=>token(value,cwd));
199
237
  }
200
238
  const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
201
239
  'probeContinuity'];
@@ -208,8 +246,11 @@ const checks=publicConfig.checks.map(check=>{
208
246
  for(const category of categories)if(check.coverage?.[category]&&
209
247
  typeof check.coverageReview?.[category]!=='string')
210
248
  error(`${check.name}: reviewed coverage ${category} needs a rationale`);
249
+ const npmDirect=check.qualification===directQualification;
211
250
  const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
212
- if(check.script&&check.command&&!pnpmTypecheck)
251
+ const testsDirect=check.qualification===testsQualification;
252
+ const generalScript=check.qualification===scriptQualification;
253
+ if(check.script&&check.command&&!pnpmTypecheck&&!npmDirect&&!testsDirect)
213
254
  error(`${check.name}: choose script or command, not both`);
214
255
  const manager=publicConfig.packageManager;
215
256
  if(check.script&&!['npm','yarn','pnpm'].includes(manager))
@@ -218,8 +259,20 @@ const checks=publicConfig.checks.map(check=>{
218
259
  [`@which:${manager}`,'run',check.script]:null;
219
260
  const npmAutomatic=check.qualification==='typescript-noemit-v1';
220
261
  const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
221
- if(check.qualification&&!npmAutomatic&&!yarnWorkspace&&!pnpmTypecheck)
262
+ if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck&&!testsDirect&&!generalScript)
222
263
  error(`${check.name}: unknown qualification contract`);
264
+ // The general recipe runs the root package script through the package manager.
265
+ if(generalScript&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.command||check.cwd&&check.cwd!=='.'||
266
+ check.workspace||!scriptKinds.includes(check.kind)))
267
+ error(`${check.name}: the general recipe needs a root typecheck, test or lint script, without command or cwd`);
268
+ if(npmDirect&&(manager!=='npm'||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
269
+ check.command[0]!=='@node'||check.command[1]!=='@typescript-compiler:.'||
270
+ !tscRun({},{script:null,args:check.command.slice(2)}).run?.noEmit))
271
+ error(`${check.name}: direct recipe requires its explicit local compiler command`);
272
+ // The script's own arguments follow the runner; qualification compares them with the script.
273
+ if(testsDirect&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.cwd&&check.cwd!=='.'&&!check.workspace||!Array.isArray(check.command)||
274
+ check.command[0]!=='@node'||!/^@test-runner:(?:vitest|jest)$/.test(check.command[1]||'')))
275
+ error(`${check.name}: test recipe requires its explicit local test runner command`);
223
276
  if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
224
277
  error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
225
278
  if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
@@ -227,25 +280,32 @@ const checks=publicConfig.checks.map(check=>{
227
280
  check.command[1]!=='workspace'||check.command[2]!==check.workspace||
228
281
  check.command[3]!=='run'))
229
282
  error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
230
- if(pnpmTypecheck&&(manager!=='pnpm'||!check.script||!check.command||
283
+ if(pnpmTypecheck&&(manager!=='pnpm'||!Array.isArray(check.command)||
231
284
  check.command[0]!=='@node'||
232
285
  check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
233
286
  check.workspace&&check.cwd==='.'||
234
- !check.command.slice(2).every(value=>typeof value==='string'&&value)))
287
+ !tscRun({},{script:null,args:check.command.slice(2)}).run))
235
288
  error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
236
289
  const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
237
290
  check.kind==='lint'?'lint plugins, config and resolver inputs need review':
238
291
  check.kind==='build'?'build environment and generated inputs need review':
239
292
  'check inputs need review';
240
- const autoProbes=npmAutomatic?[['@node','@vstate/typescript-contract-probe',
293
+ // A chained script has one check per tsc run: each check's run counts them,
294
+ // so a run added to or removed from the script invalidates them all.
295
+ const runCount=(npmDirect||pnpmTypecheck)&&check.script?publicConfig.checks.filter(other=>
296
+ other.qualification===check.qualification&&other.script===check.script&&
297
+ (other.cwd||'.')===(check.cwd||'.')&&(other.workspace||null)===(check.workspace||null)).length:1;
298
+ if(runCount>16)error(`${check.name}: more than 16 checks for script ${check.script}`);
299
+ const run=npmDirect||pnpmTypecheck?runArgv({script:check.script??null,args:check.command.slice(2),count:runCount}):null;
300
+ const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',...run]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
241
301
  '@project',check.script,'@which:npm']]:yarnWorkspace?
242
302
  [['@node','@vstate/yarn-workspace-typecheck-probe','@project',
243
303
  check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
244
304
  [['@node','@vstate/pnpm-typecheck-probe','@project',
245
- check.workspace||'.',check.script]]:[];
305
+ check.workspace||'.',...run]]:[];
246
306
  const windowsContext=process.platform==='win32'?
247
307
  ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
248
- const autoEnvironment=npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
308
+ const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:generalScript?scriptEnvironment(manager):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
249
309
  'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
250
310
  pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
251
311
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
@@ -254,10 +314,11 @@ const checks=publicConfig.checks.map(check=>{
254
314
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
255
315
  prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
256
316
  pathExecutables:['node'],executableIdentity:true}:{};
257
- return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`),
317
+ return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`,
318
+ typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
258
319
  cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
259
320
  installedInputs:check.installedInputs||(
260
- npmAutomatic?['node_modules/**']:[]),
321
+ npmAutomatic||npmDirect||testsDirect?['node_modules/**']:[]),
261
322
  allowedExternalRoots:check.allowedExternalRoots||[],
262
323
  probes:[...(check.probes||[]),...autoProbes]
263
324
  .map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
@@ -266,6 +327,11 @@ const checks=publicConfig.checks.map(check=>{
266
327
  coverageReasons:check.coverageReasons||Object.fromEntries(
267
328
  categories.map(category=>[category,reason])),
268
329
  qualification:check.qualification||null,script:check.script||null,
330
+ ...(runCount>1?{runCount}:{}),
331
+ ...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
332
+ ...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
333
+ ...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
334
+ ...(generalScript?{qualificationInterpretation:scriptInterpretation}:{}),
269
335
  workspace:check.workspace||null,kind:check.kind||null};
270
336
  });
271
337
  const runtime={schema:1,provider:'declared-project@1',root,state,checks};
@@ -277,7 +343,13 @@ if(maintenance&&!fs.existsSync(runtimeFile)){
277
343
  process.exit(0);
278
344
  }
279
345
  if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
280
- if(!maintenance&&fs.existsSync(runtimeFile)){
346
+ // Two commands can meet on fresh state (SessionStart's start and the first
347
+ // status): whichever creates the runtime file first wins, and the other
348
+ // treats it as existing.
349
+ let created=false;
350
+ if(!maintenance&&!fs.existsSync(runtimeFile))
351
+ try{fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});created=true;}catch(e){if(e.code!=='EEXIST')throw e;}
352
+ if(!maintenance&&!created){
281
353
  const previous=fs.readFileSync(runtimeFile,'utf8');
282
354
  if(previous!==serialized){
283
355
  const pidFile=path.join(state,'observer.lock','pid');
@@ -287,18 +359,24 @@ if(!maintenance&&fs.existsSync(runtimeFile)){
287
359
  }
288
360
  fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
289
361
  }
290
- }else if(!maintenance)fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});
362
+ }
291
363
 
292
364
  if(command==='status'&&!sync){
293
365
  const value=readCachedStatus(state,publicConfig.checks);
294
366
  console.log(json?JSON.stringify(value):renderStatus(value,{short}));
295
- process.exit(0);
367
+ await exit(0);
296
368
  }
297
369
 
298
370
  const details=command==='detail'||command==='explain';
299
371
  const internal=command==='remove-state'?'uninstall':
300
372
  details?'sync':command==='status'&&sync?'sync':command;
301
- if(command==='run'){
373
+ // On macOS and Linux the engine runs in this process, which saves a Node startup
374
+ // on every run; its own handlers receive SIGINT and SIGTERM. Windows keeps a
375
+ // child engine because cancellation reaches it over IPC.
376
+ if(command==='run'&&process.platform!=='win32'){
377
+ process.argv=[process.argv[0],engine,runtimeFile,internal,check,...(!json?['--human']:[])];
378
+ await import(pathToFileURL(engine).href);
379
+ }else if(command==='run'){
302
380
  const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
303
381
  {stdio:['inherit','inherit','inherit','ipc'],env:process.env});
304
382
  const forward=signal=>{if(child.exitCode!==null)return;
@@ -313,17 +391,20 @@ if(command==='run'){
313
391
  process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
314
392
  if(outcome.error)error(outcome.error.message);
315
393
  process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
394
+ }else{
395
+ mark('client.engine_submit');
396
+ const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
397
+ {stdio:['inherit','pipe','inherit'],
398
+ encoding:'utf8',env:traceEnvironment()});
399
+ mark('client.engine_complete',{exit:result.status,signal:result.signal});
400
+ if(result.error)error(result.error.message);
401
+ if(result.stdout){
402
+ if(['status','detail','explain'].includes(command)){
403
+ let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
404
+ if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
405
+ if(json)console.log(JSON.stringify(value));
406
+ else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
407
+ }else printLifecycle(result.stdout,command,json);
408
+ }
409
+ await exit(result.status??1);
316
410
  }
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