@redue/cli 0.1.0-alpha.10 → 0.1.0-alpha.11
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 +53 -0
- package/README.md +1 -1
- package/bin/redue.mjs +35 -19
- package/docs/user/agents.md +47 -1
- package/docs/user/direct-typescript.md +18 -0
- package/docs/user/pilot.md +9 -6
- package/package.json +1 -1
- package/src/agent-integration.mjs +59 -13
- package/src/claude-hook.mjs +115 -0
- package/src/cli.mjs +51 -11
- package/src/daemon.mjs +236 -82
- package/src/decision-validation.mjs +10 -0
- package/src/direct-run-certificate.mjs +161 -16
- package/src/direct-typescript-probe.mjs +6 -40
- package/src/direct-typescript.mjs +48 -15
- package/src/history-worker.mjs +19 -16
- package/src/observation.mjs +32 -12
- package/src/probe-result.mjs +7 -3
- package/src/run-recorder.cjs +78 -0
- package/src/typescript-input-facts.mjs +5 -2
- package/src/typescript-list.mjs +76 -7
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import {directQualification,directInterpretation,observedNpmContext} from './direct-typescript.mjs';
|
|
4
|
-
import {directRunCertificateAllowed,
|
|
4
|
+
import {directRunCertificateAllowed,directRunCertificate,directRunRecording,validateRunRecord} from './direct-run-certificate.mjs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import net from 'node:net';
|
|
7
7
|
import os from 'node:os';
|
|
@@ -225,7 +225,7 @@ async function execute() {
|
|
|
225
225
|
timing('cli.start_checkpoint',startSnapshotStarted,{available:before?.snapshots?.[name]?1:0});
|
|
226
226
|
// Missing discovery metadata can recover through a full probe. A missing
|
|
227
227
|
// healthy start boundary cannot: freeze this decision before any recovery.
|
|
228
|
-
// Only the reviewed
|
|
228
|
+
// Only the reviewed direct recipe on macOS and Linux enters this shortcut.
|
|
229
229
|
const unobservedDirectRun=callerCertificate&&
|
|
230
230
|
before?.snapshots?.[name]?.observationHealthy!==true;
|
|
231
231
|
if(unobservedDirectRun){
|
|
@@ -235,7 +235,19 @@ async function execute() {
|
|
|
235
235
|
await testRunBoundary('after-start-checkpoint');
|
|
236
236
|
// Start the observed interval before caller validation, not after it. Any
|
|
237
237
|
// intervening edit (even restored before launch) remains disqualifying.
|
|
238
|
-
|
|
238
|
+
// With a certificate, the compile records its own answers instead of two probes.
|
|
239
|
+
// Without one after an edit, the recording can become the certificate at the
|
|
240
|
+
// end checkpoint; that checkpoint probes in full if it cannot.
|
|
241
|
+
let certificate=null,recording=null;
|
|
242
|
+
if(callerCertificate&&!unobservedDirectRun){
|
|
243
|
+
certificate=directRunCertificate(plan,selected,before?.snapshots?.[name],root);
|
|
244
|
+
if(!certificate)recording=directRunRecording(plan,selected,before?.snapshots?.[name],root);
|
|
245
|
+
probeBefore=certificate?.value??(recording?null:probeHash(probePlan));
|
|
246
|
+
}
|
|
247
|
+
// The record arrives on fd 3, so the compiler's environment is unchanged.
|
|
248
|
+
const launched=certificate||recording?[command[0],'--require',path.join(here,'run-recorder.cjs'),
|
|
249
|
+
...command.slice(1)]:command;
|
|
250
|
+
const record=[];
|
|
239
251
|
// A Windows full reconciliation may finish while queued watcher callbacks
|
|
240
252
|
// are still arriving. Fold callbacks delivered before process launch into
|
|
241
253
|
// the starting checkpoint; callbacks after launch still disqualify reuse.
|
|
@@ -256,10 +268,11 @@ async function execute() {
|
|
|
256
268
|
if(requestedSignal){resolve({status:'interrupted',exitCode:null,signal:requestedSignal});return;}
|
|
257
269
|
let child,spawnError=null;
|
|
258
270
|
try{runLock.phase('launching');
|
|
259
|
-
const launch=windowsLaunch(
|
|
271
|
+
const launch=windowsLaunch(launched);
|
|
260
272
|
child=spawn(launch.file,launch.args,{cwd:plan.cwd,
|
|
261
|
-
stdio:'inherit',env:process.env,
|
|
262
|
-
|
|
273
|
+
stdio:certificate||recording?['inherit','inherit','inherit','pipe']:'inherit',env:process.env,
|
|
274
|
+
detached:process.platform==='linux',...launch.options});
|
|
275
|
+
child.stdio[3]?.on('data',chunk=>record.push(chunk));}
|
|
263
276
|
catch(e){resolve({status:'start_failed',exitCode:null,signal:null,errorCode:e.code||e.name});return;}
|
|
264
277
|
activeChild=child;runLock.phase('running',child.pid??null);
|
|
265
278
|
mark('cli.child_started',{childPid:child.pid});
|
|
@@ -293,18 +306,44 @@ async function execute() {
|
|
|
293
306
|
const target={source:'direct-command',status:invocation.status==='exited'?'direct_executed':'unknown'};
|
|
294
307
|
const afterContextStarted=performance.now(),environmentAfter=contextHashes()[name];
|
|
295
308
|
timing('cli.context_after',afterContextStarted);
|
|
296
|
-
|
|
309
|
+
let probeAfter=null;
|
|
310
|
+
const recordText=Buffer.concat(record).toString('utf8');
|
|
311
|
+
if(certificate){
|
|
312
|
+
const issue=validateRunRecord(recordText,certificate);
|
|
313
|
+
if(issue)captureIssues.push(`compiler inputs differ from discovery certificate (${issue})`);
|
|
314
|
+
probeAfter=certificate.value;
|
|
315
|
+
}else if(!unobservedDirectRun&&!recording)probeAfter=probeHash(probePlan);
|
|
316
|
+
// The daemon reads the recording from owned state; control requests are small.
|
|
317
|
+
const recordFile=recording&&path.join(state,`run-record-${recording}.json`);
|
|
318
|
+
if(recordFile)try{fs.writeFileSync(recordFile,recordText,{mode:0o600});}catch{}
|
|
297
319
|
const endSnapshotStarted=performance.now();
|
|
298
320
|
if(!unobservedDirectRun){
|
|
299
|
-
try{after=await call({action:'snapshot',name,contextHashes:contextHashes()
|
|
321
|
+
try{after=await call({action:'snapshot',name,contextHashes:contextHashes(),
|
|
322
|
+
...(recording?{runCertificate:1,adopt:recording}:{})},captureTimeout);}
|
|
300
323
|
catch{captureIssues.push('end observation unavailable');}
|
|
324
|
+
finally{if(recordFile)fs.rmSync(recordFile,{force:true});}
|
|
301
325
|
timing('cli.end_checkpoint',endSnapshotStarted,{available:after?.snapshots?.[name]?1:0});
|
|
302
326
|
}
|
|
303
327
|
const end=after?.snapshots?.[name];
|
|
328
|
+
// The end certificate, recorded or probed, must cover what this compile received.
|
|
329
|
+
// With no input event, gap or content change in between, it also holds at the start.
|
|
330
|
+
if(recording&&end?.recordingAccepted===true)probeBefore=probeAfter=end.probeHash;
|
|
331
|
+
else if(recording){
|
|
332
|
+
const endCertificate=directRunCertificate(plan,selected,end,root);
|
|
333
|
+
if(!endCertificate)captureIssues.push('no discovery certificate at the end checkpoint');
|
|
334
|
+
else{
|
|
335
|
+
const issue=validateRunRecord(recordText,endCertificate);
|
|
336
|
+
if(issue)captureIssues.push(`compiler inputs differ from discovery certificate (${issue})`);
|
|
337
|
+
probeBefore=probeAfter=endCertificate.value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if(recording&&snap?.observationGaps!==end?.observationGaps)
|
|
341
|
+
captureIssues.push('observation gap during execution');
|
|
304
342
|
if(!unobservedDirectRun&&(!end||!end.observationHealthy))captureIssues.push('end checkpoint unavailable');
|
|
305
343
|
if(JSON.stringify(environmentBefore)!==JSON.stringify(environmentAfter))
|
|
306
344
|
captureIssues.push('declared environment changed during execution');
|
|
307
|
-
if(!unobservedDirectRun&&(!probeBefore||!probeAfter||probeBefore!==snap?.probeHash||
|
|
345
|
+
if(!unobservedDirectRun&&(!probeBefore||!probeAfter||probeBefore!==(recording?end:snap)?.probeHash||
|
|
346
|
+
probeAfter!==end?.probeHash))
|
|
308
347
|
captureIssues.push('declared state probe unavailable or changed');
|
|
309
348
|
if(before?.observation?.generation!==after?.observation?.generation)
|
|
310
349
|
captureIssues.push('observer generation changed during execution');
|
|
@@ -312,7 +351,8 @@ async function execute() {
|
|
|
312
351
|
captureIssues.push('input plan rebuilt during execution');
|
|
313
352
|
if(snap?.inputEventSerial!==end?.inputEventSerial)
|
|
314
353
|
captureIssues.push('declared input event occurred during execution');
|
|
315
|
-
|
|
354
|
+
// A recorded certificate changes only the probe part of the fingerprint.
|
|
355
|
+
if(!snap||!end||snap.planId!==end.planId||(recording?snap.inputKey!==end.inputKey:snap.fingerprint!==end.fingerprint)||
|
|
316
356
|
snap.revision!==end.revision)captureIssues.push('declared inputs changed or were not observed');
|
|
317
357
|
const stable=captureIssues.length===0;
|
|
318
358
|
const receipt={result,invocation,target,stable,fingerprintVersion:1,
|
|
@@ -323,7 +363,7 @@ async function execute() {
|
|
|
323
363
|
...(process.env.REDUE_PUBLIC_INPUT_TRACE==='1'?{
|
|
324
364
|
startInputEventSerial:snap?.inputEventSerial??null,
|
|
325
365
|
endInputEventSerial:end?.inputEventSerial??null}: {})},
|
|
326
|
-
planId:snap?.planId??plan.id,fingerprint:snap?.fingerprint??null,
|
|
366
|
+
planId:snap?.planId??plan.id,fingerprint:(recording?end:snap)?.fingerprint??null,
|
|
327
367
|
provider:plan.provider,environmentHashes:environmentBefore,
|
|
328
368
|
...(observedContext?{observedContext,verificationRecipe:directInterpretation}:{}),
|
|
329
369
|
files:snap?.files??null,observerGeneration:before?.observation?.generation??null,
|