@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.30
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.
- package/CHANGELOG.md +806 -0
- package/README.md +157 -99
- package/bin/redue.mjs +267 -61
- package/docs/README.md +3 -1
- package/docs/user/agents.md +333 -15
- package/docs/user/configuration.md +28 -2
- package/docs/user/direct-typescript.md +219 -0
- package/docs/user/getting-started.md +49 -7
- package/docs/user/json.md +4 -2
- package/docs/user/node-script.md +177 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +146 -0
- package/docs/user/sandbox.md +312 -0
- package/docs/user/shell.md +3 -3
- package/docs/user/supported-projects.md +69 -23
- package/docs/user/tests-direct.md +143 -0
- package/package.json +4 -3
- package/src/agent-integration.mjs +467 -0
- package/src/agent-policy.md +28 -0
- package/src/cached-state.mjs +24 -1
- package/src/claude-hook.mjs +616 -0
- package/src/cli.mjs +449 -54
- package/src/daemon.mjs +442 -93
- package/src/decision-profile.mjs +15 -1
- package/src/decision-validation.mjs +12 -1
- package/src/declared-plan.mjs +38 -6
- package/src/direct-run-certificate.mjs +254 -0
- package/src/direct-typescript-probe.mjs +27 -0
- package/src/direct-typescript.mjs +205 -0
- package/src/glob.mjs +8 -0
- package/src/history-worker.mjs +21 -13
- package/src/index.mjs +108 -21
- package/src/installed-inputs.mjs +7 -1
- package/src/installed-observation.mjs +23 -0
- package/src/landlock.py +527 -0
- package/src/linux-inotify.py +39 -10
- package/src/node-onboarding.mjs +203 -54
- package/src/node-script.mjs +406 -0
- package/src/npm-integrity.mjs +124 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npmrc.mjs +16 -0
- package/src/observation.mjs +34 -12
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +17 -17
- package/src/pnpm-inputs.mjs +122 -57
- package/src/pnpm-shim-templates.json +180 -0
- package/src/pnpm-shims.mjs +54 -0
- package/src/pnpm-typecheck-probe.mjs +11 -6
- package/src/presentation.mjs +13 -4
- package/src/probe-deadline.mjs +37 -0
- package/src/probe-result.mjs +7 -3
- package/src/project-location.mjs +34 -0
- package/src/python-onboarding.mjs +89 -0
- package/src/run-recorder.cjs +91 -0
- package/src/sandbox.mjs +653 -0
- package/src/setup.mjs +205 -0
- package/src/stats.mjs +72 -0
- package/src/test-direct.mjs +608 -0
- package/src/test-recorder.cjs +435 -0
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-input-facts.mjs +92 -0
- package/src/typescript-invocation.mjs +92 -0
- package/src/typescript-list.mjs +118 -13
- package/src/typescript-qualification.mjs +29 -49
- package/src/workspace-manifests.mjs +24 -0
- package/src/yarn-workspace-inputs.mjs +3 -0
- package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/bin/redue.mjs
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
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,directInstalledInputs,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';
|
|
10
|
+
import {docsQualification,hiddenDocsIssue,hiddenListIssue,isSandboxed,pythonTool,sandboxEnvironment,sandboxInterpretationOf,sandboxKinds,sandboxQualification} from '../src/sandbox.mjs';
|
|
3
11
|
import os from 'node:os';
|
|
4
12
|
import path from 'node:path';
|
|
5
13
|
import {createHash} from 'node:crypto';
|
|
6
14
|
import {spawn,spawnSync} from 'node:child_process';
|
|
7
15
|
import {createRequire} from 'node:module';
|
|
8
|
-
import {fileURLToPath} from 'node:url';
|
|
9
|
-
import {discoverNodeProject} from '../src/node-onboarding.mjs';
|
|
16
|
+
import {fileURLToPath,pathToFileURL} from 'node:url';
|
|
10
17
|
import {processAlive} from '../src/process-liveness.mjs';
|
|
11
18
|
import {samePath,withinPath,realObservedPath} from '../src/path-identity.mjs';
|
|
12
|
-
import {
|
|
19
|
+
import {discoverConfig,projectState,canonicalConfig} from '../src/project-location.mjs';
|
|
13
20
|
import {findExecutable} from '../src/executable-lookup.mjs';
|
|
14
21
|
import {readCachedStatus} from '../src/cached-state.mjs';
|
|
15
22
|
import {assertOwnedStatePlacement} from '../src/owned-state.mjs';
|
|
@@ -22,27 +29,54 @@ const help=`REDUE ${version}
|
|
|
22
29
|
Usage: redue [--config FILE] [--state-dir DIR] COMMAND [OPTIONS]
|
|
23
30
|
|
|
24
31
|
Commands:
|
|
25
|
-
|
|
32
|
+
setup claude [--yes] Set up REDUE and Claude Code in this project in one step:
|
|
33
|
+
init, agent setup claude, start, one recorded run per check
|
|
34
|
+
remove [--yes] Undo setup: integration, observer state, and the config setup created
|
|
35
|
+
init [--dry-run] [--workspace NAME] [--check KIND] [--recipe typescript-direct|tests-direct|node-script|sandbox]
|
|
26
36
|
Discover existing scripts; optionally select one workspace/check
|
|
37
|
+
agent setup codex|claude [--dry-run|--apply] [--json]
|
|
38
|
+
agent doctor [codex|claude] [--json]
|
|
39
|
+
agent remove codex|claude [--dry-run|--apply] [--json]
|
|
40
|
+
Connect project instructions; explicit setup only
|
|
27
41
|
start Start the local observer
|
|
28
42
|
stop Stop this observer
|
|
29
|
-
status [--json
|
|
30
|
-
status --
|
|
43
|
+
status [--json] Reconcile inputs and caller context, then answer (--sync is the same)
|
|
44
|
+
status --cached [--json] | --short The last answer the observer published, without reconciling
|
|
31
45
|
explain [CHECK] [--json] Explain applicability (synchronized; may cost more than a rerun)
|
|
32
46
|
detail [CHECK] [--json] Alias for explain
|
|
33
|
-
run CHECK
|
|
47
|
+
run CHECK [--if-due] Execute one configured check and record its outcome; with --if-due,
|
|
48
|
+
only when its last pass does not apply to the files as they are now
|
|
49
|
+
stats [--json] Per check: skips, time saved, recorded runs, why checks were UNVERIFIED
|
|
34
50
|
remove-state Stop and remove only this configuration's owned local state
|
|
35
51
|
|
|
36
52
|
No checks run in the background. Direct commands outside redue create no receipts.`;
|
|
37
53
|
|
|
38
54
|
function error(message){console.error(`redue: ${message}`);process.exit(2);}
|
|
55
|
+
// Exits once queued output is written. On macOS, output to a pipe is written
|
|
56
|
+
// asynchronously; exiting with it queued cuts it (at 8 KB through the pipe a
|
|
57
|
+
// Node program, such as an agent's shell tool, reads).
|
|
58
|
+
async function exit(code){await new Promise(resolve=>process.stdout.write('',resolve));process.exit(code);}
|
|
39
59
|
const args=process.argv.slice(2);
|
|
60
|
+
if(args.length===3&&args[0]==='agent'&&args[1]==='hook'&&args[2]==='claude'){
|
|
61
|
+
// Runs before config discovery: the hook must exit 0 on every failure,
|
|
62
|
+
// because Claude Code treats exit 2 as a block.
|
|
63
|
+
const {runClaudeHook}=await import('../src/claude-hook.mjs');
|
|
64
|
+
await runClaudeHook(fileURLToPath(import.meta.url));process.exit(0);
|
|
65
|
+
}
|
|
40
66
|
if(args.length===1&&args[0]==='--version'){console.log(version);process.exit(0);}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
// Internal: the entry this command runs, so setup and the hook can tell
|
|
68
|
+
// whether the `redue` on PATH (a link, a package manager's shim, a wrapper)
|
|
69
|
+
// is this install.
|
|
70
|
+
if(args.length===1&&args[0]==='--entry'){console.log(fs.realpathSync(fileURLToPath(import.meta.url)));process.exit(0);}
|
|
71
|
+
if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);await exit(0);}
|
|
72
|
+
if(args[0]==='setup'||args[0]==='remove'){
|
|
73
|
+
// Loaded on demand; setup works before a config exists.
|
|
74
|
+
const {setupCommand}=await import('../src/setup.mjs');
|
|
75
|
+
try{await setupCommand(args,{entry:fileURLToPath(import.meta.url)});}
|
|
76
|
+
catch(e){error(e.message);}
|
|
77
|
+
await exit(0);
|
|
78
|
+
}
|
|
79
|
+
let configFile=null,stateOverride=null;
|
|
46
80
|
for(let i=0;i<args.length;){
|
|
47
81
|
if(args[i]==='--config'||args[i]==='--state-dir'){
|
|
48
82
|
const flag=args[i],value=args[i+1];if(!value)error(`${flag} requires a path`);
|
|
@@ -51,30 +85,68 @@ for(let i=0;i<args.length;){
|
|
|
51
85
|
}else i++;
|
|
52
86
|
}
|
|
53
87
|
const command=args.shift();
|
|
54
|
-
|
|
88
|
+
try{configFile??=command==='init'||stateOverride&&['stop','remove-state'].includes(command)?
|
|
89
|
+
path.resolve(fs.existsSync('redue.config.json')||!fs.existsSync('vstate.config.json')?'redue.config.json':'vstate.config.json'):discoverConfig();}
|
|
90
|
+
catch(e){error(e.message);}
|
|
91
|
+
if(fs.existsSync(path.dirname(configFile)))configFile=canonicalConfig(configFile);
|
|
92
|
+
if(command==='agent'){
|
|
93
|
+
// Loaded on demand: onboarding and agent setup are not on the run/status path.
|
|
94
|
+
const {agentCommand}=await import('../src/agent-integration.mjs');
|
|
95
|
+
try{await agentCommand(args,{configFile,stateOverride,entry:fileURLToPath(import.meta.url)});}
|
|
96
|
+
catch(e){error(e.message);}
|
|
97
|
+
await exit(0);
|
|
98
|
+
}
|
|
99
|
+
if(!['init','start','stop','status','detail','explain','run','stats','remove-state'].includes(command))
|
|
55
100
|
error(`unknown command ${command||'<none>'}; use --help`);
|
|
56
101
|
const json=args.includes('--json');
|
|
57
|
-
|
|
102
|
+
// run --if-due: run the check only when its last pass does not apply now.
|
|
103
|
+
const ifDue=command==='run'&&args.includes('--if-due');
|
|
104
|
+
if(ifDue)args.splice(args.indexOf('--if-due'),1);
|
|
105
|
+
// status syncs by default (warm, it costs well under 100 ms more than the
|
|
106
|
+
// cached answer); --cached and --short read the cached answer.
|
|
107
|
+
const cachedOnly=args.includes('--cached');
|
|
58
108
|
const dryRun=args.includes('--dry-run');
|
|
59
109
|
const short=args.includes('--short');
|
|
60
|
-
|
|
110
|
+
const sync=args.includes('--sync')||command==='status'&&!cachedOnly&&!short;
|
|
111
|
+
let workspaceSelection=null,checkSelection=null,recipeSelection=null;
|
|
61
112
|
if(command==='init')for(let i=0;i<args.length;){
|
|
62
|
-
if(args[i]==='--workspace'||args[i]==='--check'){
|
|
113
|
+
if(args[i]==='--workspace'||args[i]==='--check'||args[i]==='--recipe'){
|
|
63
114
|
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;
|
|
115
|
+
if(flag==='--workspace')workspaceSelection=value;else if(flag==='--recipe')recipeSelection=value;else checkSelection=value;
|
|
65
116
|
args.splice(i,2);
|
|
66
117
|
}else i++;
|
|
67
118
|
}
|
|
68
119
|
const check=['run','detail','explain'].includes(command)&&args[0]&&!args[0].startsWith('--')?args.shift():null;
|
|
69
120
|
if(command==='run'&&!check)error('run requires a check name');
|
|
70
|
-
if(args.some(value=>!['--json','--sync','--dry-run','--short'].includes(value)))error('unknown option; use --help');
|
|
121
|
+
if(args.some(value=>!['--json','--sync','--cached','--dry-run','--short'].includes(value)))error('unknown option; use --help');
|
|
71
122
|
if(sync&&!['status','detail','explain'].includes(command))error('--sync is for status or detail');
|
|
72
|
-
if(
|
|
123
|
+
if(cachedOnly&&(command!=='status'||args.includes('--sync')))error('--cached is a status option; do not combine it with --sync');
|
|
124
|
+
if(short&&(command!=='status'||json||args.includes('--sync')))error('--short is a cached status option; do not combine it with --sync or --json');
|
|
73
125
|
if(dryRun&&command!=='init')error('--dry-run is for init');
|
|
74
126
|
|
|
75
127
|
if(command==='init'){
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
const {discoverNodeProject,sourceLinkIssue}=await import('../src/node-onboarding.mjs');
|
|
129
|
+
const {discoverPythonProject}=await import('../src/python-onboarding.mjs');
|
|
130
|
+
const {sandboxTrial}=await import('../src/sandbox.mjs');
|
|
131
|
+
// A Node project, a Python project, or both in one checkout.
|
|
132
|
+
const python=!recipeSelection||recipeSelection==='sandbox'?discoverPythonProject(process.cwd()):null;
|
|
133
|
+
let discovery;
|
|
134
|
+
if(fs.existsSync('package.json')||!python)try{discovery=discoverNodeProject(process.cwd(),{recipe:recipeSelection});}
|
|
135
|
+
catch(e){error(e.message);}
|
|
136
|
+
else{const root=realObservedPath(process.cwd());
|
|
137
|
+
const git=spawnSync('git',['rev-parse','--show-toplevel'],{cwd:root,encoding:'utf8',timeout:3000,stdio:['ignore','pipe','ignore']});
|
|
138
|
+
discovery={schema:1,root,manager:{name:null,version:null,layout:null},observationIssue:git.status===0?sourceLinkIssue(root):
|
|
139
|
+
'Git checkout is unavailable; filesystem observation needs a checkout',workspaces:false,node:process.version,
|
|
140
|
+
checks:[],ambiguous:[],skipped:[],config:{schema:1,checks:[]}};}
|
|
141
|
+
if(python){
|
|
142
|
+
// A Python check takes its tool's name where the name is taken.
|
|
143
|
+
const taken=new Set(discovery.checks.map(row=>row.config.name));
|
|
144
|
+
for(const row of python.checks){if(taken.has(row.config.name))row.config.name=`${row.config.kind}:${row.config.command[0].split('/').at(-1)}`;
|
|
145
|
+
taken.add(row.config.name);}
|
|
146
|
+
if(!discovery.observationIssue)discovery.checks.push(...python.checks);
|
|
147
|
+
discovery.skipped.push(...python.skipped);
|
|
148
|
+
discovery.python=true;
|
|
149
|
+
}
|
|
78
150
|
if(workspaceSelection||checkSelection){
|
|
79
151
|
const chosen=discovery.checks.filter(row=>
|
|
80
152
|
(!workspaceSelection||row.config.workspace===workspaceSelection||
|
|
@@ -83,6 +155,44 @@ if(command==='init'){
|
|
|
83
155
|
if(!chosen.length)error('no discovered check matches the selected workspace and kind');
|
|
84
156
|
discovery.checks=chosen;discovery.config.checks=chosen.map(row=>row.config);
|
|
85
157
|
}
|
|
158
|
+
// A sandboxed check is proposed only when it gives the same exit status and
|
|
159
|
+
// output inside the sandbox as outside it, with the checkout's git-ignored
|
|
160
|
+
// files and documentation hidden (sandbox-v3). When the plain run passed
|
|
161
|
+
// and the sandboxed one failed only because it was refused a file it hid
|
|
162
|
+
// (sandboxTrial's refusedHidden), the check gets the whole checkout
|
|
163
|
+
// (sandbox-v1) without another run: without the hiding it can only fail
|
|
164
|
+
// where the plain run passes. Any other difference is tried once with the
|
|
165
|
+
// whole checkout, reusing the plain run. Otherwise init writes what it
|
|
166
|
+
// would have without the sandboxed recipe, with the reason. Each check
|
|
167
|
+
// costs one plain run and one or two sandboxed runs.
|
|
168
|
+
const trialIssue=trial=>trial.issue?`the sandbox cannot run here (${trial.issue})`:
|
|
169
|
+
// A failure both ways that shows a refusal cannot be told from a real one.
|
|
170
|
+
trial.same?null:trial.plain!==trial.sandboxed?`exit ${trial.sandboxed} inside the sandbox, ${trial.plain} outside it${trial.line?` (${trial.line})`:''}`:
|
|
171
|
+
trial.line?`it fails both ways (exit ${trial.plain}), with a sign of a refusal inside the sandbox (${trial.line})`:
|
|
172
|
+
`the output differs inside the sandbox: ${JSON.stringify(trial.output.sandboxed)} instead of ${JSON.stringify(trial.output.plain)}`;
|
|
173
|
+
let trials=0;
|
|
174
|
+
for(const [at,row] of [...discovery.checks.entries()]){
|
|
175
|
+
if(row.qualification!==sandboxQualification||row.level!=='ready')continue;
|
|
176
|
+
if(!json)console.error(`Running ${row.config.name} inside and outside the sandbox to confirm it...`);
|
|
177
|
+
const docs=await sandboxTrial(discovery.root,row.config,{hide:'docs'});
|
|
178
|
+
if(!docs.issue)trials++;
|
|
179
|
+
if(!docs.issue&&docs.same){
|
|
180
|
+
row.config={...row.config,qualification:docsQualification};row.qualification=docsQualification;
|
|
181
|
+
row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore, except its documentation, are its input")+`; ran both ways at init, exit ${docs.plain}`;continue;}
|
|
182
|
+
if(!docs.issue&&docs.refusedHidden){
|
|
183
|
+
row.reason+=`; at init it passed, then failed reading a file that was hidden (${docs.refusedHidden}), so the whole checkout is its input, not run that way at init`;continue;}
|
|
184
|
+
// Without a list of the ignored files or the documentation, the whole checkout is tried on its own.
|
|
185
|
+
const trial=docs.plainRun?await sandboxTrial(discovery.root,row.config,{plain:docs.plainRun}):
|
|
186
|
+
[hiddenListIssue,hiddenDocsIssue].includes(docs.issue)?await sandboxTrial(discovery.root,row.config):docs;
|
|
187
|
+
if(trial!==docs&&!trial.issue)trials++;
|
|
188
|
+
const issue=trialIssue(trial);
|
|
189
|
+
if(!issue){row.reason+=`; ran both ways at init, exit ${trial.plain}; the whole checkout is its input: `+
|
|
190
|
+
(docs.issue?docs.issue:`with the files git ignores and the documentation hidden, ${trialIssue(docs)}`);continue;}
|
|
191
|
+
if(row.fallback)discovery.checks[at]={...row.fallback,reason:`Sandboxed recipe unavailable: ${issue}; ${row.fallback.reason}`};
|
|
192
|
+
else{discovery.checks[at]=null;discovery.skipped.push({kind:row.config.name,reason:`${row.config.name}: sandboxed recipe unavailable: ${issue}`});}
|
|
193
|
+
}
|
|
194
|
+
discovery.checks=discovery.checks.filter(Boolean).map(({fallback,...row})=>row);
|
|
195
|
+
discovery.config.checks=discovery.checks.map(row=>row.config);
|
|
86
196
|
const configured=fs.existsSync(configFile),unsupported=Boolean(discovery.observationIssue||
|
|
87
197
|
discovery.manager.issue&&!['npm','yarn','pnpm'].includes(discovery.manager.name)||
|
|
88
198
|
discovery.manager.name==='yarn'&&!discovery.manager.version);
|
|
@@ -91,24 +201,27 @@ if(command==='init'){
|
|
|
91
201
|
package_manager:discovery.manager.name,version:discovery.manager.version,
|
|
92
202
|
installation_layout:discovery.manager.layout,
|
|
93
203
|
issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
|
|
94
|
-
ambiguous:discovery.ambiguous,
|
|
204
|
+
ambiguous:discovery.ambiguous,skipped:discovery.skipped||[],
|
|
95
205
|
checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
|
|
96
206
|
script:check.script||null,command:check.command||null,
|
|
97
207
|
workspace:check.workspace||null,level,reason})),config:configFile,
|
|
98
|
-
written:willWrite,existing:configured};
|
|
208
|
+
written:willWrite,existing:configured,writable:!configured&&!unsupported&&discovery.checks.length>0};
|
|
99
209
|
if(!json) {
|
|
100
|
-
console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
|
|
210
|
+
if(summary.package_manager||!discovery.python)console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
|
|
101
211
|
`${summary.version?' '+summary.version:''}; ${summary.installation_layout} installation.`);
|
|
212
|
+
if(discovery.python)console.log('Python project; tools run from .venv.');
|
|
102
213
|
if(summary.issue)console.log(`Boundary: ${summary.issue}`);
|
|
103
214
|
for(const row of summary.checks){
|
|
104
215
|
console.log(` ${row.name} (${row.script||row.command?.join(' ')}) — `+
|
|
105
216
|
`${{ready:'ready to qualify',recording:'recording-only',unsupported:'unsupported'}[row.level]||row.level}: ${row.reason}`);
|
|
106
217
|
if(row.script&&row.command)console.log(` Recorded invocation: ${row.command.map(value=>
|
|
107
|
-
value==='@node'?'node':value
|
|
218
|
+
value==='@node'?'node':value==='@typescript-compiler:.'?'[reviewed local TypeScript compiler implementation]':
|
|
219
|
+
value.startsWith('@test-runner:')?`[local ${value.slice('@test-runner:'.length)}]`:value.startsWith('@typescript-bin:')?
|
|
108
220
|
`[installed TypeScript in ${value.slice('@typescript-bin:'.length)}]`:value).join(' ')}`);
|
|
109
221
|
}
|
|
110
222
|
for(const row of summary.ambiguous)
|
|
111
223
|
console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
|
|
224
|
+
for(const row of summary.skipped)console.log(` Skipped: ${row.reason}`);
|
|
112
225
|
if(!summary.checks.length)console.log('No useful existing verification scripts found.');
|
|
113
226
|
console.log(configured?`Existing ${configFile} left unchanged.`:
|
|
114
227
|
dryRun?`Preview only; ${configFile} was not written.`:
|
|
@@ -118,13 +231,14 @@ if(command==='init'){
|
|
|
118
231
|
if(!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
|
|
119
232
|
error('init writes only at the repository root; use --config there or --dry-run');
|
|
120
233
|
fs.writeFileSync(configFile,JSON.stringify(discovery.config,null,2)+'\n',{flag:'wx'});
|
|
121
|
-
if(!json){console.log(`Created ${configFile}. No checks were executed
|
|
234
|
+
if(!json){console.log(`Created ${configFile}. ${trials?'Checks ran inside and outside the sandbox to choose their recipe; no results were recorded.':'No checks were executed.'}`);
|
|
122
235
|
console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
|
|
123
236
|
console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
|
|
124
|
-
console.log('
|
|
237
|
+
console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
|
|
238
|
+
console.log('Then redue status says whether each result applies to the files as they are now.');}
|
|
125
239
|
} else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
|
|
126
240
|
if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
|
|
127
|
-
|
|
241
|
+
await exit(0);
|
|
128
242
|
}
|
|
129
243
|
function printLifecycle(output,action,machine){
|
|
130
244
|
if(machine){process.stdout.write(output);return;}
|
|
@@ -141,11 +255,12 @@ function printLifecycle(output,action,machine){
|
|
|
141
255
|
if(stateOverride&&['stop','remove-state'].includes(command)){
|
|
142
256
|
const runtime=path.join(stateOverride,'project-runtime-v1.json');
|
|
143
257
|
if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
|
|
258
|
+
mark('client.engine_submit');
|
|
144
259
|
const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
|
|
145
260
|
{cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
|
|
146
261
|
if(result.error)error(result.error.message);
|
|
147
262
|
if(result.stdout)printLifecycle(result.stdout,command,json);
|
|
148
|
-
|
|
263
|
+
await exit(result.status??2);
|
|
149
264
|
}
|
|
150
265
|
let publicConfig;
|
|
151
266
|
try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
|
|
@@ -157,10 +272,7 @@ for(const key of Object.keys(publicConfig))
|
|
|
157
272
|
if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
|
|
158
273
|
if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
|
|
159
274
|
const root=realObservedPath(path.resolve(path.dirname(configFile),publicConfig.root||'.'));
|
|
160
|
-
const
|
|
161
|
-
const state=stateOverride||path.join(stateBase,'vstate',
|
|
162
|
-
`vstate-${createHash('sha256').update(stateIdentity(root)+'\0'+
|
|
163
|
-
stateIdentity(configFile)).digest('hex').slice(0,16)}`);
|
|
275
|
+
const state=stateOverride||projectState(root,configFile);
|
|
164
276
|
if(!/^(?:vstate|redue)-/.test(path.basename(state)))
|
|
165
277
|
error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
|
|
166
278
|
const runtimeFile=path.join(state,'project-runtime-v1.json');
|
|
@@ -169,9 +281,18 @@ function which(name){
|
|
|
169
281
|
if(found)return found;
|
|
170
282
|
error(`cannot resolve executable ${name} on PATH`);
|
|
171
283
|
}
|
|
172
|
-
function token(value){
|
|
284
|
+
function token(value,cwd='.'){
|
|
173
285
|
if(value==='@node')return process.execPath;
|
|
174
286
|
if(value==='@project')return root;
|
|
287
|
+
if(value==='@typescript-compiler:.'){
|
|
288
|
+
try{return directCompiler(root,{verify:false}).entry;}catch(e){error(`direct compiler unavailable: ${e.code||e.message}`);}
|
|
289
|
+
}
|
|
290
|
+
if(value.startsWith('@test-runner:')){
|
|
291
|
+
// Qualification reports a missing runner; status and stop still work.
|
|
292
|
+
try{return testRunnerAt(root,cwd,value.slice('@test-runner:'.length)).entry;}
|
|
293
|
+
catch{return path.join(root,cwd,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
|
|
294
|
+
}
|
|
295
|
+
if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
|
|
175
296
|
if(value.startsWith('@typescript-bin:')){
|
|
176
297
|
const dir=value.slice('@typescript-bin:'.length);
|
|
177
298
|
if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
|
|
@@ -192,10 +313,10 @@ function token(value){
|
|
|
192
313
|
if(value.startsWith('@which:'))return which(value.slice(7));
|
|
193
314
|
return value;
|
|
194
315
|
}
|
|
195
|
-
function argv(values,label){
|
|
316
|
+
function argv(values,label,cwd){
|
|
196
317
|
if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
|
|
197
318
|
error(`${label} must be nonempty exact argv`);
|
|
198
|
-
return values.map(token);
|
|
319
|
+
return values.map(value=>token(value,cwd));
|
|
199
320
|
}
|
|
200
321
|
const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
|
|
201
322
|
'probeContinuity'];
|
|
@@ -208,8 +329,12 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
208
329
|
for(const category of categories)if(check.coverage?.[category]&&
|
|
209
330
|
typeof check.coverageReview?.[category]!=='string')
|
|
210
331
|
error(`${check.name}: reviewed coverage ${category} needs a rationale`);
|
|
332
|
+
const npmDirect=check.qualification===directQualification;
|
|
211
333
|
const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
|
|
212
|
-
|
|
334
|
+
const testsDirect=check.qualification===testsQualification;
|
|
335
|
+
const generalScript=check.qualification===scriptQualification;
|
|
336
|
+
const sandboxed=isSandboxed(check.qualification);
|
|
337
|
+
if(check.script&&check.command&&!pnpmTypecheck&&!npmDirect&&!testsDirect)
|
|
213
338
|
error(`${check.name}: choose script or command, not both`);
|
|
214
339
|
const manager=publicConfig.packageManager;
|
|
215
340
|
if(check.script&&!['npm','yarn','pnpm'].includes(manager))
|
|
@@ -218,8 +343,24 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
218
343
|
[`@which:${manager}`,'run',check.script]:null;
|
|
219
344
|
const npmAutomatic=check.qualification==='typescript-noemit-v1';
|
|
220
345
|
const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
|
|
221
|
-
if(check.qualification&&!npmAutomatic&&!yarnWorkspace&&!pnpmTypecheck)
|
|
346
|
+
if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck&&!testsDirect&&!generalScript&&!sandboxed)
|
|
222
347
|
error(`${check.name}: unknown qualification contract`);
|
|
348
|
+
// The general recipe runs the root package script through the package manager.
|
|
349
|
+
if(generalScript&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.command||check.cwd&&check.cwd!=='.'||
|
|
350
|
+
check.workspace||!scriptKinds.includes(check.kind)))
|
|
351
|
+
error(`${check.name}: the general recipe needs a root typecheck, test or lint script, without command or cwd`);
|
|
352
|
+
// The sandboxed recipe runs a root package script, or a tool from the project's .venv.
|
|
353
|
+
if(sandboxed&&(check.cwd&&check.cwd!=='.'||check.workspace||!sandboxKinds.includes(check.kind)||
|
|
354
|
+
(check.script?!['npm','pnpm','yarn'].includes(manager):!pythonTool(check))))
|
|
355
|
+
error(`${check.name}: the sandboxed recipe needs a root ${sandboxKinds.join(' or ')} script, or a command that runs a tool from .venv/bin, without cwd`);
|
|
356
|
+
if(npmDirect&&(manager!=='npm'||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
|
|
357
|
+
check.command[0]!=='@node'||check.command[1]!=='@typescript-compiler:.'||
|
|
358
|
+
!tscRun({},{script:null,args:check.command.slice(2)}).run?.noEmit))
|
|
359
|
+
error(`${check.name}: direct recipe requires its explicit local compiler command`);
|
|
360
|
+
// The script's own arguments follow the runner; qualification compares them with the script.
|
|
361
|
+
if(testsDirect&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.cwd&&check.cwd!=='.'&&!check.workspace||!Array.isArray(check.command)||
|
|
362
|
+
check.command[0]!=='@node'||!/^@test-runner:(?:vitest|jest)$/.test(check.command[1]||'')))
|
|
363
|
+
error(`${check.name}: test recipe requires its explicit local test runner command`);
|
|
223
364
|
if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
|
|
224
365
|
error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
|
|
225
366
|
if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
|
|
@@ -227,25 +368,33 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
227
368
|
check.command[1]!=='workspace'||check.command[2]!==check.workspace||
|
|
228
369
|
check.command[3]!=='run'))
|
|
229
370
|
error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
|
|
230
|
-
if(pnpmTypecheck&&(manager!=='pnpm'||!
|
|
371
|
+
if(pnpmTypecheck&&(manager!=='pnpm'||!Array.isArray(check.command)||
|
|
231
372
|
check.command[0]!=='@node'||
|
|
232
373
|
check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
|
|
233
374
|
check.workspace&&check.cwd==='.'||
|
|
234
|
-
!check.command.slice(2).
|
|
375
|
+
!tscRun({},{script:null,args:check.command.slice(2)}).run))
|
|
235
376
|
error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
|
|
236
377
|
const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
|
|
237
378
|
check.kind==='lint'?'lint plugins, config and resolver inputs need review':
|
|
238
379
|
check.kind==='build'?'build environment and generated inputs need review':
|
|
239
380
|
'check inputs need review';
|
|
240
|
-
|
|
381
|
+
// A chained script has one check per tsc run: each check's run counts them,
|
|
382
|
+
// so a run added to or removed from the script invalidates them all.
|
|
383
|
+
const runCount=(npmDirect||pnpmTypecheck)&&check.script?publicConfig.checks.filter(other=>
|
|
384
|
+
other.qualification===check.qualification&&other.script===check.script&&
|
|
385
|
+
(other.cwd||'.')===(check.cwd||'.')&&(other.workspace||null)===(check.workspace||null)).length:1;
|
|
386
|
+
if(runCount>16)error(`${check.name}: more than 16 checks for script ${check.script}`);
|
|
387
|
+
const run=npmDirect||pnpmTypecheck?runArgv({script:check.script??null,args:check.command.slice(2),count:runCount}):null;
|
|
388
|
+
const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',...run]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
|
|
241
389
|
'@project',check.script,'@which:npm']]:yarnWorkspace?
|
|
242
390
|
[['@node','@vstate/yarn-workspace-typecheck-probe','@project',
|
|
243
391
|
check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
|
|
244
392
|
[['@node','@vstate/pnpm-typecheck-probe','@project',
|
|
245
|
-
check.workspace||'.'
|
|
393
|
+
check.workspace||'.',...run]]:[];
|
|
246
394
|
const windowsContext=process.platform==='win32'?
|
|
247
395
|
['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
|
|
248
|
-
const autoEnvironment=
|
|
396
|
+
const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:generalScript?scriptEnvironment(manager):
|
|
397
|
+
sandboxed?sandboxEnvironment(Boolean(pythonTool(check)),check.script?manager:null,check.kind):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
|
|
249
398
|
'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
|
|
250
399
|
pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
|
|
251
400
|
{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
|
|
@@ -254,10 +403,11 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
254
403
|
{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
|
|
255
404
|
prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
|
|
256
405
|
pathExecutables:['node'],executableIdentity:true}:{};
|
|
257
|
-
return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command
|
|
406
|
+
return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`,
|
|
407
|
+
typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
|
|
258
408
|
cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
|
|
259
409
|
installedInputs:check.installedInputs||(
|
|
260
|
-
npmAutomatic?['node_modules/**']:[]),
|
|
410
|
+
npmDirect?[...directInstalledInputs]:npmAutomatic||testsDirect?['node_modules/**']:[]),
|
|
261
411
|
allowedExternalRoots:check.allowedExternalRoots||[],
|
|
262
412
|
probes:[...(check.probes||[]),...autoProbes]
|
|
263
413
|
.map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
|
|
@@ -266,18 +416,36 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
266
416
|
coverageReasons:check.coverageReasons||Object.fromEntries(
|
|
267
417
|
categories.map(category=>[category,reason])),
|
|
268
418
|
qualification:check.qualification||null,script:check.script||null,
|
|
419
|
+
...(runCount>1?{runCount}:{}),
|
|
420
|
+
...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
|
|
421
|
+
...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
|
|
422
|
+
...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
|
|
423
|
+
...(generalScript?{qualificationInterpretation:scriptInterpretation}:{}),
|
|
424
|
+
...(sandboxed?{qualificationInterpretation:sandboxInterpretationOf(check.qualification,check.kind)}:{}),
|
|
269
425
|
workspace:check.workspace||null,kind:check.kind||null};
|
|
270
426
|
});
|
|
271
427
|
const runtime={schema:1,provider:'declared-project@1',root,state,checks};
|
|
272
428
|
try{assertOwnedStatePlacement(runtime);}catch(e){error(e.message);}
|
|
273
429
|
const serialized=JSON.stringify(runtime,null,2)+'\n';
|
|
430
|
+
if(command==='stats'){
|
|
431
|
+
const {readStats,renderStats}=await import('../src/stats.mjs');
|
|
432
|
+
const value=readStats(state,publicConfig.checks.map(item=>item.name));
|
|
433
|
+
console.log(json?JSON.stringify(value):renderStats(value));
|
|
434
|
+
await exit(0);
|
|
435
|
+
}
|
|
274
436
|
const maintenance=command==='stop'||command==='remove-state';
|
|
275
437
|
if(maintenance&&!fs.existsSync(runtimeFile)){
|
|
276
438
|
console.log(`No REDUE state exists for ${configFile}.`);
|
|
277
439
|
process.exit(0);
|
|
278
440
|
}
|
|
279
441
|
if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
|
|
280
|
-
|
|
442
|
+
// Two commands can meet on fresh state (SessionStart's start and the first
|
|
443
|
+
// status): whichever creates the runtime file first wins, and the other
|
|
444
|
+
// treats it as existing.
|
|
445
|
+
let created=false;
|
|
446
|
+
if(!maintenance&&!fs.existsSync(runtimeFile))
|
|
447
|
+
try{fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});created=true;}catch(e){if(e.code!=='EEXIST')throw e;}
|
|
448
|
+
if(!maintenance&&!created){
|
|
281
449
|
const previous=fs.readFileSync(runtimeFile,'utf8');
|
|
282
450
|
if(previous!==serialized){
|
|
283
451
|
const pidFile=path.join(state,'observer.lock','pid');
|
|
@@ -287,18 +455,41 @@ if(!maintenance&&fs.existsSync(runtimeFile)){
|
|
|
287
455
|
}
|
|
288
456
|
fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
|
|
289
457
|
}
|
|
290
|
-
}
|
|
458
|
+
}
|
|
291
459
|
|
|
292
460
|
if(command==='status'&&!sync){
|
|
293
461
|
const value=readCachedStatus(state,publicConfig.checks);
|
|
294
462
|
console.log(json?JSON.stringify(value):renderStatus(value,{short}));
|
|
295
|
-
|
|
463
|
+
await exit(0);
|
|
296
464
|
}
|
|
297
465
|
|
|
298
466
|
const details=command==='detail'||command==='explain';
|
|
299
467
|
const internal=command==='remove-state'?'uninstall':
|
|
300
468
|
details?'sync':command==='status'&&sync?'sync':command;
|
|
301
|
-
|
|
469
|
+
// On macOS and Linux the engine runs in this process, which saves a Node startup
|
|
470
|
+
// on every run; its own handlers receive SIGINT and SIGTERM. Windows keeps a
|
|
471
|
+
// child engine because cancellation reaches it over IPC.
|
|
472
|
+
// Asked when this part of a command runs, after earlier parts may have
|
|
473
|
+
// changed files. REDUE_FORCE=1 runs it anyway.
|
|
474
|
+
if(ifDue&&process.env.REDUE_FORCE!=='1'){
|
|
475
|
+
const [{verdict},{note,skipNote,dueNote}]=await Promise.all([import('../src/claude-hook.mjs'),import('../src/stats.mjs')]);
|
|
476
|
+
const asked=spawnSync(process.execPath,[fileURLToPath(import.meta.url),'--config',configFile,...(stateOverride?['--state-dir',stateOverride]:[]),
|
|
477
|
+
'status','--sync','--json'],{encoding:'utf8',stdio:['ignore','pipe','ignore'],env:process.env,timeout:600_000});
|
|
478
|
+
let status=null;try{status=JSON.parse(asked.stdout);}catch{}
|
|
479
|
+
const due=dueNote(status,check);
|
|
480
|
+
if(due)note(state,due);
|
|
481
|
+
if(verdict(status,check)){
|
|
482
|
+
note(state,skipNote(status,check,'if-due'));
|
|
483
|
+
const row=status.checks.find(item=>item.name===check);
|
|
484
|
+
if(json)console.log(JSON.stringify({check,skipped:true,result:'PASS',run_id:row.invocation.runId}));
|
|
485
|
+
else console.error(`REDUE: ${check} passed on these exact inputs (run ${row.invocation.runId}); not run again. REDUE_FORCE=1 runs it anyway.`);
|
|
486
|
+
await exit(0);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if(command==='run'&&process.platform!=='win32'){
|
|
490
|
+
process.argv=[process.argv[0],engine,runtimeFile,internal,check,...(!json?['--human']:[])];
|
|
491
|
+
await import(pathToFileURL(engine).href);
|
|
492
|
+
}else if(command==='run'){
|
|
302
493
|
const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
|
|
303
494
|
{stdio:['inherit','inherit','inherit','ipc'],env:process.env});
|
|
304
495
|
const forward=signal=>{if(child.exitCode!==null)return;
|
|
@@ -313,17 +504,32 @@ if(command==='run'){
|
|
|
313
504
|
process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
|
|
314
505
|
if(outcome.error)error(outcome.error.message);
|
|
315
506
|
process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
|
|
507
|
+
}else if(internal==='sync'&&process.platform!=='win32'){
|
|
508
|
+
// As for run, the synchronized status engine runs in this process.
|
|
509
|
+
process.argv=[process.argv[0],engine,runtimeFile,internal,'','--value'];
|
|
510
|
+
mark('client.engine_submit',{inProcess:true});
|
|
511
|
+
const synchronized=await import(pathToFileURL(engine).href);
|
|
512
|
+
await synchronized.finished;
|
|
513
|
+
mark('client.engine_complete',{exit:process.exitCode??0});
|
|
514
|
+
if(synchronized.syncValue)showSynchronized(synchronized.syncValue);
|
|
515
|
+
await exit(process.exitCode??0);
|
|
516
|
+
}else{
|
|
517
|
+
mark('client.engine_submit');
|
|
518
|
+
const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
|
|
519
|
+
{stdio:['inherit','pipe','inherit'],
|
|
520
|
+
encoding:'utf8',env:traceEnvironment()});
|
|
521
|
+
mark('client.engine_complete',{exit:result.status,signal:result.signal});
|
|
522
|
+
if(result.error)error(result.error.message);
|
|
523
|
+
if(result.stdout){
|
|
524
|
+
if(['status','detail','explain'].includes(command)){
|
|
525
|
+
let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
|
|
526
|
+
showSynchronized(value);
|
|
527
|
+
}else printLifecycle(result.stdout,command,json);
|
|
528
|
+
}
|
|
529
|
+
await exit(result.status??1);
|
|
316
530
|
}
|
|
317
|
-
|
|
318
|
-
{
|
|
319
|
-
|
|
320
|
-
|
|
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);
|
|
531
|
+
function showSynchronized(value){
|
|
532
|
+
if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
|
|
533
|
+
if(json)console.log(JSON.stringify(value));
|
|
534
|
+
else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
|
|
328
535
|
}
|
|
329
|
-
process.exit(result.status??1);
|
package/docs/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
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)
|
|
8
|
+
- [Sandboxed recipe: typecheck and lint in any language, Python included](user/sandbox.md)
|
|
7
9
|
- [Configuration](user/configuration.md)
|
|
8
|
-
- [
|
|
10
|
+
- [Connect coding agents and apply the decision policy](user/agents.md)
|
|
9
11
|
- [JSON contract](user/json.md)
|
|
10
12
|
- [Shell usage](user/shell.md)
|
|
11
13
|
|