@mjasnikovs/pi-task 0.38.11 → 0.38.12
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/README.md +7 -3
- package/dist/shared/child-process.d.ts +8 -0
- package/dist/shared/command-watchdog.d.ts +1 -1
- package/dist/shared/command-watchdog.js +1 -1
- package/dist/task/accept-debt.d.ts +47 -0
- package/dist/task/accept-debt.js +127 -28
- package/dist/task/auto-orchestrator.js +91 -114
- package/dist/task/child-runner.d.ts +39 -25
- package/dist/task/child-runner.js +59 -31
- package/dist/task/child-status.d.ts +95 -0
- package/dist/task/child-status.js +99 -0
- package/dist/task/command-run.d.ts +36 -0
- package/dist/task/command-run.js +48 -1
- package/dist/task/command-watchdog.js +1 -1
- package/dist/task/context-usage.d.ts +4 -3
- package/dist/task/context-usage.js +4 -3
- package/dist/task/contracts.js +18 -35
- package/dist/task/deep-render-check.d.ts +47 -0
- package/dist/task/deep-render-check.js +110 -65
- package/dist/task/env-notes.d.ts +3 -3
- package/dist/task/env-notes.js +24 -35
- package/dist/task/final-gate-fix.d.ts +1 -1
- package/dist/task/final-gate-fix.js +1 -1
- package/dist/task/final-gate.d.ts +5 -151
- package/dist/task/final-gate.js +81 -379
- package/dist/task/gate-child.d.ts +8 -10
- package/dist/task/gate-child.js +15 -19
- package/dist/task/gate-deps.d.ts +29 -0
- package/dist/task/gate-deps.js +192 -206
- package/dist/task/gate-tally.d.ts +189 -0
- package/dist/task/gate-tally.js +249 -0
- package/dist/task/implementation-turn.d.ts +201 -0
- package/dist/task/implementation-turn.js +263 -0
- package/dist/task/launch-contract.js +27 -43
- package/dist/task/ledger.d.ts +38 -0
- package/dist/task/ledger.js +83 -0
- package/dist/task/loop-detector.d.ts +14 -8
- package/dist/task/loop-detector.js +36 -12
- package/dist/task/orchestrator.d.ts +61 -126
- package/dist/task/orchestrator.js +67 -294
- package/dist/task/plan-orchestrator.js +34 -33
- package/dist/task/requirements.d.ts +1 -1
- package/dist/task/requirements.js +50 -66
- package/dist/task/root-cause-repair.js +20 -32
- package/dist/task/run-bracket.d.ts +75 -0
- package/dist/task/run-bracket.js +41 -0
- package/dist/task/stall-detector.d.ts +110 -0
- package/dist/task/stall-detector.js +159 -0
- package/dist/task/verify-work.d.ts +53 -67
- package/dist/task/verify-work.js +15 -11
- package/dist/workers/single-read-extension.d.ts +1 -1
- package/dist/workers/single-read-extension.js +5 -4
- package/dist/workers/single-read-guard.d.ts +32 -10
- package/dist/workers/single-read-guard.js +67 -16
- package/package.json +1 -1
package/dist/task/final-gate.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import { existsSync, readFileSync } from 'node:fs';
|
|
49
49
|
import * as path from 'node:path';
|
|
50
50
|
import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.js';
|
|
51
|
-
import {
|
|
51
|
+
import { deriveOpenDebts, rerunDebtVerifyCommand } from './accept-debt.js';
|
|
52
52
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
53
53
|
import { readLaunchManifest, inertLaunchContractNote, packageScripts, makeHasTarget } from './launch-manifest.js';
|
|
54
54
|
import { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners, recoverOrphanPort, defaultFindPortHolder } from './boot-probe.js';
|
|
@@ -63,6 +63,7 @@ import { findDanglingArtifacts, danglingGateFailureText } from './artifact-closu
|
|
|
63
63
|
import { findMissingEnvDeclarations, envGateFailureText, scanEnvTemplateClosure, inertClosure, trackedFiles } from './env-template-closure.js';
|
|
64
64
|
import { findMissingServeEntry, serveEntryGateFailureText } from './serve-entry.js';
|
|
65
65
|
import { makefileRecipe } from './command-shrink.js';
|
|
66
|
+
import { GateTally, observabilityGapFailure, unobservedVerdict } from './gate-tally.js';
|
|
66
67
|
/**
|
|
67
68
|
* The project's OWN whole-repo integration commands (test, then build — test
|
|
68
69
|
* first because it is the richer signal and the more common script). First
|
|
@@ -272,127 +273,13 @@ run = spawnCommand) {
|
|
|
272
273
|
}
|
|
273
274
|
return verdict;
|
|
274
275
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Re-run one VERIFY-block command line (nexttask 5) under the gate's existing
|
|
286
|
-
* env-gap contract, so a debt whose reason NAMES that command can be closed by the
|
|
287
|
-
* command itself rather than by a judgement about it.
|
|
288
|
-
*
|
|
289
|
-
* Runs through `sh -c` because a VERIFY line is a shell line, not an argv: run 19's
|
|
290
|
-
* is `AGENT=1 bun test test/listings.test.ts`, and env prefixes, `&&` and redirects
|
|
291
|
-
* are all ordinary there. The leading command word is still resolved through
|
|
292
|
-
* runner-resolve so a login-shell-stripped PATH cannot make every re-run look like a
|
|
293
|
-
* gap (mx5 run 16's blindness, one level down).
|
|
294
|
-
*
|
|
295
|
-
* The asymmetry is the point: only exit 0 is conclusive. Every other ending — real
|
|
296
|
-
* failure, missing tool, unreachable database, timeout, no POSIX shell — leaves the
|
|
297
|
-
* debt exactly as open as it was.
|
|
298
|
-
*/
|
|
299
|
-
export function runVerifyCommandLine(cwd, line, timeoutMs, extraGapRe,
|
|
300
|
-
/** The spawner. Injected so a re-run's outcome can be tested without one. */
|
|
301
|
-
run = spawnCommand) {
|
|
302
|
-
const bin = leadingBin(line);
|
|
303
|
-
const runner = bin === null ? null : resolveRunner(bin);
|
|
304
|
-
// A VERIFY line is a SHELL line, not an argv — env prefixes, `&&` and
|
|
305
|
-
// redirects are all ordinary there — so the runner spawns `sh -c`.
|
|
306
|
-
const verdict = classifyCommandRun(run({
|
|
307
|
-
cwd,
|
|
308
|
-
bin: 'sh',
|
|
309
|
-
args: ['-c', line],
|
|
310
|
-
timeoutMs,
|
|
311
|
-
env: runner ? runnerEnv(runner) : { ...process.env }
|
|
312
|
-
}),
|
|
313
|
-
// Infrastructure counts as a gap on EVERY debt re-run, not only on
|
|
314
|
-
// request: an unreachable database cannot tell us whether the code is
|
|
315
|
-
// fixed, and the asymmetry below means an inconclusive re-run simply
|
|
316
|
-
// leaves the debt as open as it was.
|
|
317
|
-
extraGapRe ? [INFRA_GAP_OUTPUT_RE, extraGapRe] : [INFRA_GAP_OUTPUT_RE]);
|
|
318
|
-
if (verdict.outcome === 'gap')
|
|
319
|
-
return { outcome: 'gap', detail: verdict.detail };
|
|
320
|
-
return verdict;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* The full-skip blindness guard (mx5 run 16, validated): dynamic commands were
|
|
324
|
-
* DISCOVERED but every single one skipped as an environment gap, so the gate
|
|
325
|
-
* decided on statics alone and stamped a permanently blank app green. Per-command
|
|
326
|
-
* env-gap skips stay legitimate (a missing browser must not fail a suite); what
|
|
327
|
-
* may never happen again is ALL of them skipping while the gate still reports
|
|
328
|
-
* PASS — a gate that observed nothing dynamic has no basis to vouch for the
|
|
329
|
-
* assembled app. Pure so the semantics are unit-tested; the caller feeds it the
|
|
330
|
-
* attempt/observation counters and runner resolvability.
|
|
331
|
-
*/
|
|
332
|
-
export function observabilityGapFailure(args) {
|
|
333
|
-
if (args.attempted === 0 || args.observed > 0)
|
|
334
|
-
return null;
|
|
335
|
-
if (args.spawnFailures < args.attempted)
|
|
336
|
-
return null;
|
|
337
|
-
const unresolvable = args.runnerBins.filter(b => !args.runnerResolvable(b));
|
|
338
|
-
const runnerNote = unresolvable.length > 0 ?
|
|
339
|
-
` — the project's own runner ${unresolvable
|
|
340
|
-
.map(b => `\`${b}\``)
|
|
341
|
-
.join(', ')} is not spawnable here (not on PATH nor any known install location)`
|
|
342
|
-
: '';
|
|
343
|
-
return (`observability gap: ${args.attempted} integration/boot command(s) exist but NONE `
|
|
344
|
-
+ `could even spawn in this environment${runnerNote}; `
|
|
345
|
-
+ `the gate observed nothing dynamic and cannot vouch for the assembled app`);
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* The THIRD verdict. observabilityGapFailure above covers "commands were DISCOVERED
|
|
349
|
-
* but every one failed to spawn" — a rank-0 FAIL. It deliberately returns null for
|
|
350
|
-
* `attempted === 0`, and until now that silence fell straight through to
|
|
351
|
-
* `PASS — no integration command found (statics passed)`: the run-16 blindness class
|
|
352
|
-
* entering through a different door, where "we never checked" reads exactly like "we
|
|
353
|
-
* checked and it was fine". Measured 2026-07-27: IAR1 (C++/CMake, no package.json)
|
|
354
|
-
* shipped that verdict TWICE while carrying 2 and 3 open verify-FAIL debts, and
|
|
355
|
-
* godot-engine (package.json whose only script is `verify`) reproduces it live today.
|
|
356
|
-
*
|
|
357
|
-
* So: observed anything dynamic ⇒ PASS; discovered-but-all-spawn-failed ⇒ the
|
|
358
|
-
* existing FAIL; observed NOTHING ⇒ this note, carried on an `ok: true` outcome.
|
|
359
|
-
*
|
|
360
|
-
* WHY NON-BLOCKING (decided, not deferred — the evidence cuts both ways and this is
|
|
361
|
-
* the resolution):
|
|
362
|
-
* - Blocking's case: both real occurrences also carried open verify-FAIL debt, so
|
|
363
|
-
* the runs with no dynamic evidence were exactly the runs already known to be
|
|
364
|
-
* carrying defects.
|
|
365
|
-
* - Against, and decisive: (1) that debt is ALREADY surfaced unconditionally at the
|
|
366
|
-
* gate moment, on PASS as on FAIL — the IAR1 records literally read "PASS — no
|
|
367
|
-
* integration command found … UNRESOLVED VERIFY-FAIL DEBT still open (2)". The
|
|
368
|
-
* missing signal was never the debt, it was the word PASS endorsing the run, and
|
|
369
|
-
* that is what this fixes. (2) `ok: false` routes into the autofix picker, whose
|
|
370
|
-
* seed is `reason`; "no integration command is discoverable" is not fixable by
|
|
371
|
-
* editing code, so the highest-probability child response is to FABRICATE a
|
|
372
|
-
* runnable command to satisfy the gate — the same fabrication class that refuted
|
|
373
|
-
* the `## verified tooling` harvest (see discoverIntegrationCommands) and that had
|
|
374
|
-
* run 11's fix child `rm` a sibling's deliverable. (3) That harvest being refuted
|
|
375
|
-
* means IAR1 and godot-engine can NEVER discover a command, so blocking would end
|
|
376
|
-
* every non-npm run in `failed` permanently, with no remedy — the task's own I3
|
|
377
|
-
* ("show blocking does not block IAR1/godot post-Task-1") is unsatisfiable, and
|
|
378
|
-
* its stated consequence is to downgrade to a warning and say so. This is that.
|
|
379
|
-
* The teeth are elsewhere and are real: the verdict word changes, the gate trail says
|
|
380
|
-
* UNOBSERVED, and the caller records a durable final-gate debt that the NEXT run's
|
|
381
|
-
* gate re-surfaces (it can never auto-close — it is not static-class).
|
|
382
|
-
*/
|
|
383
|
-
export function unobservedVerdict(args) {
|
|
384
|
-
if (args.observed > 0)
|
|
385
|
-
return null;
|
|
386
|
-
// Kept short ON PURPOSE: the run-level trail line slices the reason at 300 chars,
|
|
387
|
-
// and the whole point of this verdict is that the durable record carries it.
|
|
388
|
-
const why = args.discovered === 0 ?
|
|
389
|
-
'no integration, lockfile or boot command was discoverable here, so the gate ran '
|
|
390
|
-
+ 'nothing at all'
|
|
391
|
-
: `all ${args.discovered} discovered command(s) skipped as environment gaps, so the `
|
|
392
|
-
+ 'gate ran nothing observable';
|
|
393
|
-
return (`UNOBSERVED — NOT a pass: ${why}; statics passed, but this run produced NO evidence `
|
|
394
|
-
+ 'that the assembled product builds, boots or works.');
|
|
395
|
-
}
|
|
276
|
+
// `runVerifyCommandLine` and its outcome type live in command-run.ts with the
|
|
277
|
+
// other command drivers; re-exported so existing importers keep working.
|
|
278
|
+
export { runVerifyCommandLine } from './command-run.js';
|
|
279
|
+
// The two verdict predicates — the run-16 full-blindness FAIL and the third,
|
|
280
|
+
// non-blocking UNOBSERVED verdict — live with the counters they read, in
|
|
281
|
+
// gate-tally.ts (GateTally). Re-exported so every existing importer keeps working.
|
|
282
|
+
export { observabilityGapFailure, unobservedVerdict };
|
|
396
283
|
// File → introducing-task provenance moved to task-provenance.ts (mx5 run-12
|
|
397
284
|
// PROMPT 2 extracted it for the cross-task deletion guards); re-exported so
|
|
398
285
|
// existing importers keep working.
|
|
@@ -402,110 +289,10 @@ export { taskThatIntroduced };
|
|
|
402
289
|
// always imported exactly this surface and nothing else from the gate — keep
|
|
403
290
|
// working unchanged. Same pattern as taskThatIntroduced above.
|
|
404
291
|
export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
* task fixed it) and pruned from the ledger; every other debt cannot be proven
|
|
410
|
-
* resolved deterministically, so it stays OPEN and is surfaced — a run may not
|
|
411
|
-
* complete silently carrying an accepted defect. FP-safe by construction (see
|
|
412
|
-
* accept-debt.ts). Best-effort: a ledger read/write failure must never break the
|
|
413
|
-
* caller.
|
|
414
|
-
*
|
|
415
|
-
* FACTORED OUT of runFinalIntegrationGate (nexttask 6): the derivation has to be
|
|
416
|
-
* runnable at a SECOND moment — after a converged final-gate autofix, where the
|
|
417
|
-
* orchestrator used to rebuild its gate outcome as a bare `{ok, reason}` and drop
|
|
418
|
-
* `openDebts` entirely. The report a run ends on has to be derived from the tree
|
|
419
|
-
* the run ends with, not from the tree as it was before the fix pass.
|
|
420
|
-
*
|
|
421
|
-
* `staticOk` is the caller's claim about the CURRENT statics, and it is the only
|
|
422
|
-
* thing that can auto-close a static-class debt — so a caller that does not know
|
|
423
|
-
* must pass `false` (unprovable ⇒ stays open), never a guess.
|
|
424
|
-
*/
|
|
425
|
-
export async function deriveOpenDebts(cwd, staticOk) {
|
|
426
|
-
const { open: openRaw, resolved, trail } = recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
427
|
-
staticOk,
|
|
428
|
-
// Cross-task-deletion debts auto-close iff the deleted file is back in the
|
|
429
|
-
// tree — a deterministic existence check, corroborating the per-file
|
|
430
|
-
// provenance the record already carries.
|
|
431
|
-
fileExists: rel => existsSync(path.join(cwd, rel)),
|
|
432
|
-
// VERIFY-COMMAND class (nexttask 5): a debt that NAMES a command is settled
|
|
433
|
-
// by running that command, under the gate's own env-gap contract and behind
|
|
434
|
-
// the no-write guard below.
|
|
435
|
-
rerunVerify: cmd => rerunDebtVerifyCommand(cwd, cmd)
|
|
436
|
-
});
|
|
437
|
-
if (resolved.length > 0)
|
|
438
|
-
await writeAcceptDebts(cwd, openRaw);
|
|
439
|
-
// Conflicting-claim annotation (mx5 run 11): an existence-as-failure debt whose
|
|
440
|
-
// named file is another task's committed deliverable is a plan defect — surface
|
|
441
|
-
// the contradiction with the debt so nobody (human or child) treats the claim as
|
|
442
|
-
// a deletion instruction. Pure git-history lookup; degrades to no annotation.
|
|
443
|
-
const openDebts = annotateDebtConflicts(openRaw, p => taskThatIntroduced(cwd, p));
|
|
444
|
-
const debtNote = buildAcceptDebtNote(openDebts);
|
|
445
|
-
return { openDebts, ...(debtNote ? { debtNote } : {}), ...(trail.length > 0 ? { trail } : {}) };
|
|
446
|
-
}
|
|
447
|
-
/** Per-command ceiling for a debt re-run (`inv-bounded`). */
|
|
448
|
-
const DEBT_RERUN_TIMEOUT_MS = 300_000;
|
|
449
|
-
/**
|
|
450
|
-
* Extra infrastructure-gap shapes recognised ONLY when re-running a debt's command,
|
|
451
|
-
* never in the gate's own verdicts. A driver that reports its connection simply
|
|
452
|
-
* closed (`ERR_POSTGRES_CONNECTION_CLOSED` — what bun's SQL client says when the
|
|
453
|
-
* database is not there at all, as on this box with the mx5 container stopped) is an
|
|
454
|
-
* absent dependency, and calling that "the defect is still present" would be a
|
|
455
|
-
* finding the environment invented. Kept out of INFRA_GAP_OUTPUT_RE on purpose: in a
|
|
456
|
-
* gate verdict the same wording can be a real fault the suite must own, and only the
|
|
457
|
-
* debt re-check needs the conservative reading — where it costs nothing, because gap
|
|
458
|
-
* and fail both leave the debt open.
|
|
459
|
-
*/
|
|
460
|
-
const DEBT_INFRA_GAP_RE = /ERR_POSTGRES_CONNECTION_CLOSED|ERR_MYSQL_CONNECTION|ECONNRESET/i;
|
|
461
|
-
/**
|
|
462
|
-
* Re-run ONE debt's stored VERIFY command for the re-check, with the no-write guard
|
|
463
|
-
* (`inv-no-write`) wrapped around it.
|
|
464
|
-
*
|
|
465
|
-
* A VERIFY command is the project's own command and may legitimately write (a build
|
|
466
|
-
* emits `dist/`, a suite writes a snapshot). What it may NOT do is turn the tree into
|
|
467
|
-
* a passing tree and have that count as the debt being fixed — the run would then be
|
|
468
|
-
* certifying its own side effect. So tracked state is captured before and after, and
|
|
469
|
-
* a pass that came with a tracked change is downgraded to INCONCLUSIVE with the
|
|
470
|
-
* change named. Untracked output is left alone: it is what a build legitimately
|
|
471
|
-
* produces, and `git status --porcelain` in a repo with the usual ignores does not
|
|
472
|
-
* see it.
|
|
473
|
-
*
|
|
474
|
-
* A repository the guard cannot read (no git, git absent) is not a licence to skip
|
|
475
|
-
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
476
|
-
* assumption rather than an observation.
|
|
477
|
-
*/
|
|
478
|
-
export function rerunDebtVerifyCommand(cwd, command,
|
|
479
|
-
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
480
|
-
* the guard's four outcomes are testable without a repo or a real command. */
|
|
481
|
-
run = spawnCommand) {
|
|
482
|
-
const tracked = () => {
|
|
483
|
-
const r = run({
|
|
484
|
-
cwd,
|
|
485
|
-
bin: 'git',
|
|
486
|
-
args: ['status', '--porcelain', '--untracked-files=no'],
|
|
487
|
-
timeoutMs: 60_000
|
|
488
|
-
});
|
|
489
|
-
return r.failedToStart || r.status !== 0 ? null : r.stdout;
|
|
490
|
-
};
|
|
491
|
-
const before = tracked();
|
|
492
|
-
const r = runVerifyCommandLine(cwd, command, DEBT_RERUN_TIMEOUT_MS, DEBT_INFRA_GAP_RE, run);
|
|
493
|
-
if (r.outcome === 'fail')
|
|
494
|
-
return { outcome: 'fail', detail: `exit ${r.status} — ${r.tail}` };
|
|
495
|
-
if (r.outcome === 'gap')
|
|
496
|
-
return { outcome: 'gap', detail: r.detail };
|
|
497
|
-
const after = tracked();
|
|
498
|
-
if (before === null || after === null) {
|
|
499
|
-
return { outcome: 'gap', detail: 'tracked-state guard could not read git status' };
|
|
500
|
-
}
|
|
501
|
-
if (before !== after) {
|
|
502
|
-
return {
|
|
503
|
-
outcome: 'gap',
|
|
504
|
-
detail: 'the re-run itself CHANGED tracked files — a command that edits the tree into a pass proves nothing'
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
return { outcome: 'pass' };
|
|
508
|
-
}
|
|
292
|
+
// The ACCEPT-debt re-check (`deriveOpenDebts`, `rerunDebtVerifyCommand`) lives in
|
|
293
|
+
// accept-debt.ts with the ledger it reads and writes; re-exported so the
|
|
294
|
+
// orchestrator and the harnesses under scripts/ keep working unchanged.
|
|
295
|
+
export { deriveOpenDebts, rerunDebtVerifyCommand };
|
|
509
296
|
/**
|
|
510
297
|
* The run-level closure scans, in emission order within their stage.
|
|
511
298
|
*
|
|
@@ -608,33 +395,17 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
608
395
|
}
|
|
609
396
|
}, trackedFiles: trackedFilesFn = trackedFiles } = opts;
|
|
610
397
|
const stat = runRepoHealthCheck(cwd);
|
|
611
|
-
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
// Aggregated failures across ALL sections (mx5 run 13 — see the function doc).
|
|
621
|
-
// rank 0 = boot/render ("does not serve/render" is the most load-bearing
|
|
622
|
-
// signal); rank 1 = everything else, kept in execution order by stable sort.
|
|
623
|
-
const failures = [];
|
|
624
|
-
const fail = (text, rank = 1) => {
|
|
625
|
-
failures.push({ rank, text });
|
|
626
|
-
};
|
|
627
|
-
/**
|
|
628
|
-
* A failure a PROBE returned after observing (nexttask 19A — see
|
|
629
|
-
* FinalGateOutcome.observedFailures). Used by exactly one caller: the boot
|
|
630
|
-
* section, whose `fail` outcome can only arise from a probe that looked. Every
|
|
631
|
-
* other `fail()` keeps today's class, so nothing else changes.
|
|
632
|
-
*/
|
|
633
|
-
const failObserved = (text, rank = 1) => {
|
|
634
|
-
failures.push({ rank, text, observed: true });
|
|
635
|
-
};
|
|
398
|
+
// Debts are derived once, before any section runs, and ride on every verdict
|
|
399
|
+
// shape (GateTally.verdict): `reason` stays the mechanical failure because it
|
|
400
|
+
// seeds the autofix child's prompt — run 11's fix child executed a recorded
|
|
401
|
+
// claim as an instruction.
|
|
402
|
+
const debts = await deriveOpenDebts(cwd, stat.ok);
|
|
403
|
+
// Every section below RECORDS into the tally (failures ranked, the four
|
|
404
|
+
// dynamic counters, the notes) and the verdict is assembled ONCE at the end —
|
|
405
|
+
// see gate-tally.ts for what each method means.
|
|
406
|
+
const tally = new GateTally();
|
|
636
407
|
if (!stat.ok)
|
|
637
|
-
fail(`static checks: ${stat.reason}`);
|
|
408
|
+
tally.fail(`static checks: ${stat.reason}`);
|
|
638
409
|
// Launch-contract diff (mx5 run 10 item 4): the design declared `migrate`/`seed`
|
|
639
410
|
// scripts that fell through decompose and shipped missing, unchecked. Diff the
|
|
640
411
|
// plan-time-extracted declared scripts against the manifest; a missing one is a
|
|
@@ -650,73 +421,43 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
650
421
|
// readLaunchManifest resolves package.json, else a Makefile's targets, else
|
|
651
422
|
// nothing — and nothing means no failure plus a note, never a silent pass.
|
|
652
423
|
const declared = await readDeclaredScripts(cwd);
|
|
653
|
-
const contractNotes = [];
|
|
654
424
|
if (declared.length > 0) {
|
|
655
425
|
const manifest = readLaunchManifest(cwd);
|
|
656
426
|
if (manifest.kind === 'none') {
|
|
657
|
-
|
|
427
|
+
tally.contractNote(inertLaunchContractNote(declared, manifest));
|
|
658
428
|
}
|
|
659
429
|
else {
|
|
660
430
|
const missing = missingDeclaredScripts(declared, manifest.names);
|
|
661
431
|
if (missing.length > 0) {
|
|
662
|
-
fail(`launch contract: the design declares script(s) the shipped ${manifest.file} does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`);
|
|
432
|
+
tally.fail(`launch contract: the design declares script(s) the shipped ${manifest.file} does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`);
|
|
663
433
|
}
|
|
664
434
|
}
|
|
665
435
|
}
|
|
666
436
|
// Run-level closure scans that must be decided BEFORE the zero-discovery early
|
|
667
437
|
// return below — a static check needs no runner (CLOSURE_SCANS: 'pre-discovery').
|
|
668
|
-
runClosureScans('pre-discovery', { cwd, planText }, fail);
|
|
438
|
+
runClosureScans('pre-discovery', { cwd, planText }, (t, r) => tally.fail(t, r));
|
|
669
439
|
const lockCmds = discoverLockfileChecks(cwd);
|
|
670
440
|
const { cmds } = discoverIntegrationCommands(cwd);
|
|
671
441
|
const boot = discoverBootCommand(cwd);
|
|
672
|
-
// ZERO DISCOVERY IS UNOBSERVED, NEVER A PASS (see unobservedVerdict). Nothing was
|
|
673
|
-
// discovered, so nothing ran, so observabilityGapFailure (attempted === 0 → null) does
|
|
674
|
-
// not fire — and this outcome used to be reported as `PASS — no integration command
|
|
675
|
-
// found (statics passed)`, i.e. "we never checked" reading identically to "we checked
|
|
676
|
-
// and it was fine". IAR1 shipped that verdict TWICE while carrying open verify-FAIL
|
|
677
|
-
// debt (its .pi-tasks/TASK_AUTO_0001.md:31 and TASK_AUTO_0002.md:37). The outcome stays
|
|
678
|
-
// `ok: true` (non-blocking, justified at unobservedVerdict) but is now labelled, trailed
|
|
679
|
-
// and carried as debt by the caller. It needs no new command source, so unlike the
|
|
680
|
-
// harvest lever refuted at discoverIntegrationCommands it cannot inject a fabricated
|
|
681
|
-
// failure.
|
|
682
|
-
if (lockCmds.length === 0 && cmds.length === 0 && !boot && failures.length === 0) {
|
|
683
|
-
// The inert-contract note rides here too: a non-npm project carrying a launch
|
|
684
|
-
// contract usually discovers no command either, and that is exactly the run
|
|
685
|
-
// whose silence must not read as "the contract was checked and was fine".
|
|
686
|
-
const note = [unobservedVerdict({ discovered: 0, observed: 0 }) ?? '', ...contractNotes]
|
|
687
|
-
.filter(n => n !== '')
|
|
688
|
-
.join(' ');
|
|
689
|
-
return withDebts({ ok: true, unobserved: note, reason: note });
|
|
690
|
-
}
|
|
691
|
-
const ran = [];
|
|
692
|
-
// Full-skip blindness counters (mx5 run 16): every dynamic spawn counts an
|
|
693
|
-
// attempt; a real pass OR a real fail counts an observation; skips observe
|
|
694
|
-
// nothing. If everything discovered ends up skipped, observabilityGapFailure
|
|
695
|
-
// turns the silence into a rank-0 failure instead of a static-only PASS.
|
|
696
|
-
let dynAttempted = 0;
|
|
697
|
-
let dynObserved = 0;
|
|
698
|
-
let dynSpawnFailures = 0;
|
|
699
|
-
const dynBins = new Set();
|
|
700
442
|
for (const { prefix, list } of [
|
|
701
443
|
{ prefix: 'lockfile check: ', list: lockCmds },
|
|
702
444
|
{ prefix: '', list: cmds }
|
|
703
445
|
]) {
|
|
704
446
|
for (const cmd of list) {
|
|
705
447
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
706
|
-
|
|
707
|
-
dynBins.add(cmd[0]);
|
|
448
|
+
tally.attempted(cmd[0]);
|
|
708
449
|
const r = runGateCommand(cwd, cmd, timeoutMs, undefined, undefined, runCmd);
|
|
709
450
|
if (r.outcome === 'skip') {
|
|
710
451
|
if (r.spawnFailed)
|
|
711
|
-
|
|
452
|
+
tally.spawnFailure(cmd[0]);
|
|
712
453
|
continue;
|
|
713
454
|
}
|
|
714
|
-
|
|
455
|
+
tally.observed();
|
|
715
456
|
if (r.outcome === 'fail') {
|
|
716
|
-
fail(`${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
|
|
457
|
+
tally.fail(`${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
|
|
717
458
|
continue;
|
|
718
459
|
}
|
|
719
|
-
ran
|
|
460
|
+
tally.ran(label);
|
|
720
461
|
}
|
|
721
462
|
}
|
|
722
463
|
// EXECUTE the launch contract (mx5 run 11): every declared script that is
|
|
@@ -729,12 +470,6 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
729
470
|
// note (F7) is surfaced as an UNOBSERVED warning — the note may be covering a
|
|
730
471
|
// real defect the gate could not reach here (run 11's "pre-existing .rows
|
|
731
472
|
// bug" note excused the exact scripts that shipped broken).
|
|
732
|
-
const warnings = [];
|
|
733
|
-
/** UNOBSERVED notes for launch scripts reclassified as CONFIG GAPS (run 20).
|
|
734
|
-
* They ride in `unobserved`, not `warnings`, so the caller's existing
|
|
735
|
-
* `recordDebt(cwd, id, fin.unobserved, 'final-gate')` writes the debt —
|
|
736
|
-
* never a PASS. */
|
|
737
|
-
const configGapNotes = [];
|
|
738
473
|
if (declared.length > 0) {
|
|
739
474
|
const covered = cmds.flatMap(([bin, args]) => (bin === 'bun' || bin === 'npm') && args[0] === 'run' && args[1] ? [args[1]] : []);
|
|
740
475
|
const skippedLaunch = [];
|
|
@@ -760,16 +495,15 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
760
495
|
continue;
|
|
761
496
|
const cmd = ['bun', ['run', name]];
|
|
762
497
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
763
|
-
|
|
764
|
-
dynBins.add(cmd[0]);
|
|
498
|
+
tally.attempted(cmd[0]);
|
|
765
499
|
const r = runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, undefined, runCmd);
|
|
766
500
|
if (r.outcome === 'skip') {
|
|
767
501
|
if (r.spawnFailed)
|
|
768
|
-
|
|
502
|
+
tally.spawnFailure(cmd[0]);
|
|
769
503
|
skippedLaunch.push(name);
|
|
770
504
|
continue;
|
|
771
505
|
}
|
|
772
|
-
|
|
506
|
+
tally.observed();
|
|
773
507
|
if (r.outcome === 'fail') {
|
|
774
508
|
// A CONFIG GAP IS NOT A CODE FAULT (mx5 run 20). The run died on
|
|
775
509
|
// `bun run seed` exiting 1 because ADMIN_PHONE — which the project's
|
|
@@ -796,16 +530,16 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
796
530
|
// Nothing about this script was OBSERVED: the real run could
|
|
797
531
|
// not reach it and the probe run is a diagnostic, never an
|
|
798
532
|
// observation. So it un-counts, exactly like a skip.
|
|
799
|
-
|
|
533
|
+
tally.unobserve();
|
|
800
534
|
skippedLaunch.push(name);
|
|
801
|
-
|
|
535
|
+
tally.configGap(configGapUnobservedNote(gap));
|
|
802
536
|
continue;
|
|
803
537
|
}
|
|
804
538
|
}
|
|
805
|
-
fail(`launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
|
|
539
|
+
tally.fail(`launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
|
|
806
540
|
continue;
|
|
807
541
|
}
|
|
808
|
-
ran
|
|
542
|
+
tally.ran(label);
|
|
809
543
|
}
|
|
810
544
|
if (skippedLaunch.length > 0) {
|
|
811
545
|
const notes = parseEnvNotes(await readEnvNotes(cwd)).filter(n => isExcuseNote(n.fact));
|
|
@@ -813,7 +547,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
813
547
|
const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
814
548
|
const excuse = notes.find(n => re.test(n.fact));
|
|
815
549
|
if (excuse) {
|
|
816
|
-
|
|
550
|
+
tally.warn(`launch script \`${name}\` could not run here (environment gap) and a `
|
|
817
551
|
+ `standing excuse note covers it ("${excuse.fact.slice(0, 160)}") — `
|
|
818
552
|
+ `UNOBSERVED: verify it by hand before trusting the launch surface`);
|
|
819
553
|
}
|
|
@@ -824,13 +558,36 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
824
558
|
// construction, and it carries the run's most load-bearing signal — earlier
|
|
825
559
|
// failures no longer shadow it. Its failures rank FIRST in the aggregate.
|
|
826
560
|
// A boot that never RAN is its own verdict (mx5 run 18 — see bootSkipVerdict);
|
|
827
|
-
// it lives outside the
|
|
561
|
+
// it lives outside the tally's dynamic counters on purpose, so the test/build
|
|
828
562
|
// commands that did run cannot cancel it.
|
|
829
|
-
|
|
563
|
+
//
|
|
564
|
+
// ZERO DISCOVERY IS UNOBSERVED, NEVER A PASS (see unobservedVerdict, and the
|
|
565
|
+
// zero-attempts branch of GateTally.verdict). Nothing was discovered, so nothing
|
|
566
|
+
// ran, so the blindness guard below (attempted === 0 → null) does not fire — and
|
|
567
|
+
// this outcome used to be reported as `PASS — no integration command found
|
|
568
|
+
// (statics passed)`, i.e. "we never checked" reading identically to "we checked
|
|
569
|
+
// and it was fine". IAR1 shipped that verdict TWICE while carrying open
|
|
570
|
+
// verify-FAIL debt (its .pi-tasks/TASK_AUTO_0001.md:31 and TASK_AUTO_0002.md:37).
|
|
571
|
+
// The outcome stays `ok: true` (non-blocking, justified at unobservedVerdict) but
|
|
572
|
+
// is labelled, trailed and carried as debt by the caller. It needs no new command
|
|
573
|
+
// source, so unlike the harvest lever refuted at discoverIntegrationCommands it
|
|
574
|
+
// cannot inject a fabricated failure. The inert-contract note rides on it too: a
|
|
575
|
+
// non-npm project carrying a launch contract usually discovers no command either,
|
|
576
|
+
// and that is exactly the run whose silence must not read as "the contract was
|
|
577
|
+
// checked and was fine".
|
|
578
|
+
//
|
|
579
|
+
// This return sits AFTER the launch-script loop, not before it: it used to fire
|
|
580
|
+
// first, so a DECLARED launch script never ran on a tree with no discoverable
|
|
581
|
+
// integration command (found and left unfixed in f5d7110). "Nothing to observe"
|
|
582
|
+
// is a fact about the tally — no attempt, no failure — not about discovery, and
|
|
583
|
+
// asking the tally makes the two paths see the same state. It still returns
|
|
584
|
+
// before the boot `else` branch and the post-boot closure scans, whose stage is a
|
|
585
|
+
// statement about when they are meaningful.
|
|
586
|
+
if (!boot && tally.silent())
|
|
587
|
+
return tally.verdict(debts);
|
|
830
588
|
if (boot) {
|
|
831
589
|
const label = `${boot[0]} ${boot[1].join(' ')}`;
|
|
832
|
-
|
|
833
|
-
dynBins.add(boot[0]);
|
|
590
|
+
tally.attempted(boot[0]);
|
|
834
591
|
const expectServer = detectsServedApp(cwd, planText);
|
|
835
592
|
// Render check (mx5 runs 8/11): for a served app, load the live page in a
|
|
836
593
|
// headless browser and judge the RENDERED DOM — curl can't run JS, so a
|
|
@@ -859,14 +616,14 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
859
616
|
b = await recoverOrphanPort(cwd, boot, b, bootGraceMs, bootDepsWithRender, expectServer);
|
|
860
617
|
}
|
|
861
618
|
if (b.outcome !== 'skip')
|
|
862
|
-
|
|
619
|
+
tally.observed();
|
|
863
620
|
else if (b.spawnFailed)
|
|
864
|
-
|
|
865
|
-
bootUnobserved
|
|
621
|
+
tally.spawnFailure(boot[0]);
|
|
622
|
+
tally.bootUnobserved(bootSkipVerdict({
|
|
866
623
|
label,
|
|
867
624
|
skipped: b.outcome === 'skip',
|
|
868
625
|
expectServer
|
|
869
|
-
});
|
|
626
|
+
}));
|
|
870
627
|
if (b.outcome === 'fail') {
|
|
871
628
|
// OBSERVED (nexttask 19A). Every path that produces `fail` here is a
|
|
872
629
|
// probe that looked: the render judge saw an empty body, the deep
|
|
@@ -875,7 +632,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
875
632
|
// condition that means "we could not look" — no ss/netstat/lsof, mx5
|
|
876
633
|
// run 14 — returns PASS stamped UNOBSERVED and never reaches here
|
|
877
634
|
// (`b0f90a7`, final-gate.ts `if (!canEnumerate) return passAndKill(…)`).
|
|
878
|
-
failObserved(`boot check: \`${label}\` ${b.detail}`, 0);
|
|
635
|
+
tally.failObserved(`boot check: \`${label}\` ${b.detail}`, 0);
|
|
879
636
|
}
|
|
880
637
|
else if (b.outcome === 'orphan-port') {
|
|
881
638
|
// Could not clear the port. Distinct HARNESS diagnosis, never a bare app
|
|
@@ -884,14 +641,14 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
884
641
|
const who = holder ? ` — held by an orphaned process (pid ${holder.pid}: ${holder.command})`
|
|
885
642
|
: b.port !== null ? ` — port ${b.port} is held by another process`
|
|
886
643
|
: '';
|
|
887
|
-
fail(`boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`, 0);
|
|
644
|
+
tally.fail(`boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`, 0);
|
|
888
645
|
}
|
|
889
646
|
else if (b.outcome === 'pass') {
|
|
890
|
-
ran
|
|
647
|
+
tally.ran(label);
|
|
891
648
|
// A listener that served, but whose page could not be OBSERVED to render
|
|
892
649
|
// (no browser, undeterminable port) → UNOBSERVED warning, not a silent pass.
|
|
893
650
|
if (b.renderNote)
|
|
894
|
-
|
|
651
|
+
tally.warn(b.renderNote);
|
|
895
652
|
}
|
|
896
653
|
}
|
|
897
654
|
else {
|
|
@@ -900,77 +657,22 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
900
657
|
// project with no launch surface, and it must not degrade into silence.
|
|
901
658
|
const rejected = rejectedLaunchScript(cwd);
|
|
902
659
|
if (rejected && detectsServedApp(cwd, planText)) {
|
|
903
|
-
bootUnobserved
|
|
904
|
-
`
|
|
905
|
-
|
|
906
|
-
+ 'observed to run.';
|
|
660
|
+
tally.bootUnobserved(`boot check: this project's only launch script (\`${rejected.name}\`) is not a `
|
|
661
|
+
+ `launch — ${rejected.reason} — so nothing was started and the app was never `
|
|
662
|
+
+ 'observed to run.');
|
|
907
663
|
}
|
|
908
664
|
}
|
|
909
665
|
// Full-skip blindness guard (mx5 run 16): commands were discovered but every
|
|
910
666
|
// one skipped → rank-0 failure, never a static-only PASS. Runner resolvability
|
|
911
667
|
// is checked through resolveRunner so the failure text can name the missing
|
|
912
668
|
// runner when that is the cause (the run-16 shape: login-shell PATH lost bun).
|
|
913
|
-
const gap =
|
|
914
|
-
attempted: dynAttempted,
|
|
915
|
-
observed: dynObserved,
|
|
916
|
-
spawnFailures: dynSpawnFailures,
|
|
917
|
-
runnerBins: [...dynBins],
|
|
918
|
-
runnerResolvable: b => resolveRunner(b).ok
|
|
919
|
-
});
|
|
669
|
+
const gap = tally.blindness(b => resolveRunner(b).ok);
|
|
920
670
|
if (gap)
|
|
921
|
-
fail(gap, 0);
|
|
671
|
+
tally.fail(gap, 0);
|
|
922
672
|
// The remaining run-level closure scans — "the shipped tree references or
|
|
923
673
|
// requires something it does not contain" — after every dynamic section, so
|
|
924
674
|
// their failures keep their historical place in the aggregate (CLOSURE_SCANS:
|
|
925
675
|
// 'post-boot').
|
|
926
|
-
runClosureScans('post-boot', { cwd, planText }, fail);
|
|
927
|
-
|
|
928
|
-
// Stable sort: boot/render (rank 0) leads, everything else keeps execution
|
|
929
|
-
// order. One failure keeps the exact single-failure wording; several become
|
|
930
|
-
// a numbered list so the trail, the ACCEPT picker, and the autofix seed all
|
|
931
|
-
// carry the complete ranked picture.
|
|
932
|
-
const ranked = [...failures].sort((a, b) => a.rank - b.rank);
|
|
933
|
-
const texts = ranked.map(f => f.text);
|
|
934
|
-
// The observed subset rides along by exact text identity (19A) — the demote
|
|
935
|
-
// decision downstream reads THIS, instead of re-deriving observability from
|
|
936
|
-
// the failure string.
|
|
937
|
-
const observed = ranked.filter(f => f.observed === true).map(f => f.text);
|
|
938
|
-
return withDebts({
|
|
939
|
-
ok: false,
|
|
940
|
-
reason: texts.length === 1 ?
|
|
941
|
-
texts[0]
|
|
942
|
-
: `${texts.length} failures (ranked, most load-bearing first):\n${texts
|
|
943
|
-
.map((t, i) => `${i + 1}. ${t}`)
|
|
944
|
-
.join('\n')}`,
|
|
945
|
-
failures: texts,
|
|
946
|
-
...(observed.length > 0 ? { observedFailures: observed } : {})
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
const warningNote = warnings.length > 0 ? ` — WARNING: ${warnings.join('; WARNING: ')}` : '';
|
|
950
|
-
// The same three-way verdict at the other zero-observation door: commands WERE
|
|
951
|
-
// discovered, none spawn-failed (so the run-16 guard correctly stayed silent — every
|
|
952
|
-
// skip was a tool-level env gap), and yet nothing ran. That was `statics passed
|
|
953
|
-
// (integration commands not runnable here)`, which is the identical "we never checked"
|
|
954
|
-
// silence wearing different words. Unchanged when anything at all was observed, so a
|
|
955
|
-
// project with runnable commands is byte-for-byte unaffected.
|
|
956
|
-
// Two independent UNOBSERVED notes, either or both of which may apply: the boot
|
|
957
|
-
// never ran (run 18), and/or NOTHING dynamic ran at all. The boot note leads
|
|
958
|
-
// because it names a concrete command and the trail line is sliced at 300 chars.
|
|
959
|
-
const unobserved = [
|
|
960
|
-
bootUnobserved,
|
|
961
|
-
unobservedVerdict({ discovered: dynAttempted, observed: dynObserved }),
|
|
962
|
-
...configGapNotes,
|
|
963
|
-
...contractNotes
|
|
964
|
-
]
|
|
965
|
-
.filter(n => n !== null)
|
|
966
|
-
.join(' ');
|
|
967
|
-
return withDebts({
|
|
968
|
-
ok: true,
|
|
969
|
-
...(unobserved ? { unobserved } : {}),
|
|
970
|
-
reason: (unobserved ? `${unobserved} — ` : '')
|
|
971
|
-
+ (ran.length > 0 ?
|
|
972
|
-
`statics + ${ran.map(c => `\`${c}\``).join(', ')} passed`
|
|
973
|
-
: 'statics passed (integration commands not runnable here)')
|
|
974
|
-
+ warningNote
|
|
975
|
-
});
|
|
676
|
+
runClosureScans('post-boot', { cwd, planText }, (t, r) => tally.fail(t, r));
|
|
677
|
+
return tally.verdict(debts);
|
|
976
678
|
}
|