@mjasnikovs/pi-task 0.40.50 → 0.42.0
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 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +31 -1
- package/dist/task/accept-debt.js +49 -3
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +66 -7
- package/dist/task/auto-io.js +152 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +184 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +201 -39
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/health-repair.d.ts +68 -0
- package/dist/task/health-repair.js +124 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +52 -58
- package/dist/task/task-gates.js +161 -199
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +32 -6
- package/dist/task/verify-resolution.js +78 -15
- package/dist/task/verify-work.d.ts +105 -35
- package/dist/task/verify-work.js +232 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
package/dist/task/gate-deps.js
CHANGED
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
import { existsSync, readFileSync } from 'node:fs';
|
|
23
23
|
import * as fsp from 'node:fs/promises';
|
|
24
24
|
import * as path from 'node:path';
|
|
25
|
-
import {
|
|
25
|
+
import { readTaskFile, appendGateRecord, readSection, setTaskSection } from './task-io.js';
|
|
26
|
+
import { makeGit } from '../shared/git-runner.js';
|
|
27
|
+
import { worktreeTreeHash } from './tree-hash.js';
|
|
28
|
+
import { captureHealthBaseline, formatHealthBaseline, lazyHealthBaseline, parseHealthBaseline, HEALTH_BASELINE_SECTION } from './health-baseline.js';
|
|
26
29
|
import { gitCommitAll, gitDropLastCommit, git } from './auto-commit.js';
|
|
27
30
|
import { runGuidelineEnforcement } from './enforce-guidelines.js';
|
|
28
31
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
29
32
|
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
33
|
+
import { currentRunContext } from './run-context.js';
|
|
34
|
+
import { runGateEvidence, evidenceVerifyFindings } from './gate-evidence.js';
|
|
30
35
|
import { readContracts } from './contracts.js';
|
|
31
|
-
import { recordDebt } from './accept-debt.js';
|
|
36
|
+
import { closeHealthDebts, recordDebt } from './accept-debt.js';
|
|
32
37
|
import { recordRepairCandidate } from './root-cause-repair.js';
|
|
33
38
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
34
39
|
import { runFinalIntegrationGate, discoverGateCommandLabels, discoverGateCommandBodies } from './final-gate.js';
|
|
@@ -45,12 +50,17 @@ import { runBoundedLintFix } from './lint-fix.js';
|
|
|
45
50
|
import { findForeignPaths, foreignPathVerifyFindings, repairForeignPaths } from './foreign-path.js';
|
|
46
51
|
import { findScriptEscapesInManifest, scriptEscapeVerifyFindings } from './script-escape.js';
|
|
47
52
|
import { assessRunnerGlobs, runnerGlobVerifyFindings } from './runner-globs.js';
|
|
53
|
+
import { compileSuppressionPatterns, findSuppressionWidening, suppressionPatternsFor } from './suppression-probe.js';
|
|
54
|
+
import { detectEcosystems } from '../workers/docs-ecosystems.js';
|
|
55
|
+
import { qaKindsFromRecord } from './qa-transcript.js';
|
|
56
|
+
import { extractSection } from './task-parsers.js';
|
|
48
57
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
49
58
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
50
59
|
import { getConfig } from '../config/config.js';
|
|
51
60
|
import { groupChildArgs } from '../config/group-args.js';
|
|
52
61
|
import { contextWindowForGroup } from './context-usage.js';
|
|
53
62
|
import { makeDebugAppender } from './debug-log.js';
|
|
63
|
+
import { runLogPath } from './state-dir.js';
|
|
54
64
|
import { startAutoLoader } from './widget.js';
|
|
55
65
|
import { ChildStatus } from './child-status.js';
|
|
56
66
|
import { makeGateChild } from './gate-child.js';
|
|
@@ -494,6 +504,50 @@ export async function readSpecForVerification(cwd, taskId) {
|
|
|
494
504
|
return null;
|
|
495
505
|
}
|
|
496
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* The task's rendered Q&A, which is what a constraint's `[from: Q<n>]` tag
|
|
509
|
+
* resolves against (see qa-transcript's `qaKindsFromRecord`). Null when the task
|
|
510
|
+
* asked nothing, or the file cannot be read — both mean every tagged constraint
|
|
511
|
+
* reads as `derived`, and `constraint-policy.ts` says what that is worth.
|
|
512
|
+
*/
|
|
513
|
+
export async function readQaRecordForVerification(cwd, taskId) {
|
|
514
|
+
try {
|
|
515
|
+
const { body } = await readTaskFile(cwd, taskId);
|
|
516
|
+
return extractSection(body, 'grill Q&A');
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* The health baseline the verify gate's differential needs: the one stored on the
|
|
524
|
+
* task file, or — for a task file that has none — one established lazily from a
|
|
525
|
+
* DETACHED WORKTREE at HEAD and written back, so the next resolution round does
|
|
526
|
+
* not pay for it again.
|
|
527
|
+
*
|
|
528
|
+
* Everything here is best-effort. A baseline that cannot be established is null,
|
|
529
|
+
* and `classifyHealthDelta` already states what an absent baseline means; failing
|
|
530
|
+
* the gate because the differential could not run would be the harness blaming
|
|
531
|
+
* the work for its own gap.
|
|
532
|
+
*/
|
|
533
|
+
export async function healthBaselineFor(cwd, taskId, signal) {
|
|
534
|
+
try {
|
|
535
|
+
const stored = parseHealthBaseline(await readSection(cwd, taskId, HEALTH_BASELINE_SECTION));
|
|
536
|
+
if (stored)
|
|
537
|
+
return stored;
|
|
538
|
+
const fresh = await lazyHealthBaseline({
|
|
539
|
+
git: makeGit(cwd, signal),
|
|
540
|
+
runHealthIn: dir => runRepoHealthCheck(dir, { signal })
|
|
541
|
+
});
|
|
542
|
+
if (fresh) {
|
|
543
|
+
await setTaskSection(cwd, taskId, HEALTH_BASELINE_SECTION, formatHealthBaseline(fresh)).catch(() => { });
|
|
544
|
+
}
|
|
545
|
+
return fresh;
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
497
551
|
/**
|
|
498
552
|
* Bind the deterministic verify probes — THE one place the collectors above meet
|
|
499
553
|
* the probe table in verify-work.ts. One entry per `BoundProbeKey`; the table row
|
|
@@ -507,8 +561,30 @@ export async function readSpecForVerification(cwd, taskId) {
|
|
|
507
561
|
* verify debug appender for the one probe that repairs as well as reports.
|
|
508
562
|
*/
|
|
509
563
|
export function buildVerifyProbes(params) {
|
|
510
|
-
const { cwd, signal, taskId, spec, log } = params;
|
|
564
|
+
const { cwd, signal, taskId, spec, qaRecord, log, onCommand, run } = params;
|
|
565
|
+
const kinds = qaRecord ? qaKindsFromRecord(qaRecord) : [];
|
|
511
566
|
return {
|
|
567
|
+
// The project's own check and build commands, run ONCE per tree by the
|
|
568
|
+
// parent (gate-evidence.ts) and handed to the child as a result to read.
|
|
569
|
+
// Cached on the RUN's context, so every gate child of this session shares
|
|
570
|
+
// one answer and a lint-fix that moves the tree costs exactly one re-run.
|
|
571
|
+
// Outside a run bracket (a direct runSingleTask, a test) there is no run
|
|
572
|
+
// to share and each ask gets a fresh context, so the cache spans one call.
|
|
573
|
+
evidence: () => {
|
|
574
|
+
const rc = currentRunContext(cwd);
|
|
575
|
+
return rc
|
|
576
|
+
.gateEvidenceFor((commands, tree) => runGateEvidence({
|
|
577
|
+
cwd,
|
|
578
|
+
runId: rc.runId,
|
|
579
|
+
commands,
|
|
580
|
+
treeHash: tree,
|
|
581
|
+
timeoutMs: getConfig().requestTimeoutMs,
|
|
582
|
+
...(signal === undefined ? {} : { signal }),
|
|
583
|
+
...(onCommand === undefined ? {} : { onCommand }),
|
|
584
|
+
...(run === undefined ? {} : { run })
|
|
585
|
+
}))
|
|
586
|
+
.then(evidenceVerifyFindings);
|
|
587
|
+
},
|
|
512
588
|
// Deterministic self-verification probe: test files the task itself
|
|
513
589
|
// authored/changed become prompt-level findings mandating the child
|
|
514
590
|
// to drive the real artifact before trusting their green result.
|
|
@@ -518,7 +594,7 @@ export function buildVerifyProbes(params) {
|
|
|
518
594
|
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
519
595
|
// diff` and cannot even see the violation.
|
|
520
596
|
prohibition: () => {
|
|
521
|
-
const banned = spec ? extractProhibitions(spec) : [];
|
|
597
|
+
const banned = spec ? extractProhibitions(spec, n => kinds[n - 1] ?? null) : [];
|
|
522
598
|
if (banned.length === 0)
|
|
523
599
|
return Promise.resolve([]);
|
|
524
600
|
return collectChangedFiles(cwd, signal).then(files => findProhibitionViolations(banned, files));
|
|
@@ -555,9 +631,65 @@ export function buildVerifyProbes(params) {
|
|
|
555
631
|
// production wiring — importing the leaf modules the shipped entry
|
|
556
632
|
// composes and assembling their own copy — become rule-3f findings,
|
|
557
633
|
// so the child drives the REAL assembly and not the copy.
|
|
558
|
-
testAssembly: () => collectChangedFiles(cwd, signal).then(changed => collectTestAssemblyFindings(cwd, changed, signal))
|
|
634
|
+
testAssembly: () => collectChangedFiles(cwd, signal).then(changed => collectTestAssemblyFindings(cwd, changed, signal)),
|
|
635
|
+
// Deterministic suppression-widening probe: checker suppressions this
|
|
636
|
+
// task NET added. Injected under rule 4i — the child cannot find these
|
|
637
|
+
// by running the checks, because the suppressions are why they pass.
|
|
638
|
+
suppressionWidening: () => collectSuppressionHits(cwd, signal)
|
|
559
639
|
};
|
|
560
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* The task's net-new suppressions: the files its work touched (the same tree
|
|
643
|
+
* shape the write-guard reads), diffed against the checkpoint HEAD, counted per
|
|
644
|
+
* pattern.
|
|
645
|
+
*
|
|
646
|
+
* The pattern set follows the repo's detected ecosystems, so a Go project is not
|
|
647
|
+
* scanned for `@ts-expect-error` and a TypeScript one is not scanned for
|
|
648
|
+
* `//nolint`; `suppressionPatterns` in the config adds project-specific rows.
|
|
649
|
+
* Failures degrade to no findings — a sharpener, never a blocker.
|
|
650
|
+
*/
|
|
651
|
+
export async function collectSuppressionHits(cwd, signal) {
|
|
652
|
+
const changes = await collectTreeChanges(cwd, signal);
|
|
653
|
+
const files = [...changes.modified, ...changes.added];
|
|
654
|
+
if (files.length === 0)
|
|
655
|
+
return [];
|
|
656
|
+
const patterns = suppressionPatternsFor(detectEcosystems(cwd), compileSuppressionPatterns(getConfig().suppressionPatterns));
|
|
657
|
+
const diff = await git(cwd, ['diff', ...DIFF_PREFIX_ARGS, 'HEAD', '--', ...files], signal);
|
|
658
|
+
const lines = diff.exitCode === 0 ? parseDiffLines(diff.stdout) : [];
|
|
659
|
+
const untracked = await Promise.all(changes.added.map(async (rel) => {
|
|
660
|
+
try {
|
|
661
|
+
const text = await fsp.readFile(path.join(cwd, rel), 'utf8');
|
|
662
|
+
return text.split('\n').map(t => ({ path: rel, text: t, added: true }));
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
}));
|
|
668
|
+
return findSuppressionWidening([...lines, ...untracked.flat()], patterns);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* A unified diff's content lines, tagged with their side and their file. The `+++`
|
|
672
|
+
* header names the file and the `---`/`+++` headers themselves are excluded, which
|
|
673
|
+
* is the one thing a naive `startsWith('+')` scan gets wrong — it counts every
|
|
674
|
+
* changed file's own header as an added line.
|
|
675
|
+
*/
|
|
676
|
+
export function parseDiffLines(diff) {
|
|
677
|
+
const out = [];
|
|
678
|
+
let file = '';
|
|
679
|
+
for (const line of diff.split('\n')) {
|
|
680
|
+
if (line.startsWith('+++ ')) {
|
|
681
|
+
file = line.slice(4).replace(/^b\//, '').trim();
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (line.startsWith('--- ') || line.startsWith('@@'))
|
|
685
|
+
continue;
|
|
686
|
+
if (line.startsWith('+'))
|
|
687
|
+
out.push({ path: file, text: line.slice(1), added: true });
|
|
688
|
+
else if (line.startsWith('-'))
|
|
689
|
+
out.push({ path: file, text: line.slice(1), added: false });
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
561
693
|
/**
|
|
562
694
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
563
695
|
* implementation re-runner, injected by the caller. The returned object also drives
|
|
@@ -581,13 +713,37 @@ export function buildGateDeps(params) {
|
|
|
581
713
|
// mutationCheck dep to discard a verdict computed on a mutated tree.
|
|
582
714
|
let lastGuardReconcile = null;
|
|
583
715
|
// Restore tracked files to HEAD and drop files a pass created. The `.pi-tasks`
|
|
584
|
-
// trail
|
|
585
|
-
//
|
|
716
|
+
// trail survives both: the checkout excludes that directory by pathspec and the
|
|
717
|
+
// clean excludes it with `-e`. Debug logs are outside the tree. Shared by the
|
|
586
718
|
// enforce pre-commit gate (discardEdits) and the final-gate autofix guards.
|
|
587
719
|
const discardTreeEdits = async (cwd2) => {
|
|
588
720
|
await git(cwd2, ['checkout', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
589
721
|
await git(cwd2, ['clean', '-fd', '-e', '.pi-tasks'], signal);
|
|
590
722
|
};
|
|
723
|
+
// The project's own statics, under a live loader naming the running command.
|
|
724
|
+
// Each run is as long as that command, and a gate step that long with no widget
|
|
725
|
+
// is indistinguishable from a hang. Shared by the enforce pre-commit gate (a
|
|
726
|
+
// baseline before the edit pass, a differential after it) and by the
|
|
727
|
+
// pre-task baseline capture.
|
|
728
|
+
const runHealthUnderLoader = (healthCtx, cwd2, label) => {
|
|
729
|
+
const startedAt = Date.now();
|
|
730
|
+
let running;
|
|
731
|
+
const stop = startAutoLoader(healthCtx, () => ({
|
|
732
|
+
title: label,
|
|
733
|
+
kind: 'enforce',
|
|
734
|
+
step: 'repo health',
|
|
735
|
+
stepNum: 1,
|
|
736
|
+
stepTotal: 1,
|
|
737
|
+
startedAt,
|
|
738
|
+
lastLine: running ? `repo health · ${running}` : 'repo health'
|
|
739
|
+
}));
|
|
740
|
+
return runRepoHealthCheck(cwd2, {
|
|
741
|
+
signal,
|
|
742
|
+
onCommand: c => {
|
|
743
|
+
running = c;
|
|
744
|
+
}
|
|
745
|
+
}).finally(stop);
|
|
746
|
+
};
|
|
591
747
|
// Adapter onto the shared gate-child runner (gate-child.ts). What survives
|
|
592
748
|
// here is WIRING — which context, which log file, which config knobs, and
|
|
593
749
|
// the shared live status; the ritual and the per-kind policy are the
|
|
@@ -597,7 +753,7 @@ export function buildGateDeps(params) {
|
|
|
597
753
|
cwd: cwd2,
|
|
598
754
|
taskTitle,
|
|
599
755
|
kind,
|
|
600
|
-
logPath:
|
|
756
|
+
logPath: runLogPath(cwd2, logFile),
|
|
601
757
|
...(opts.loader === undefined ? {} : { loader: opts.loader }),
|
|
602
758
|
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
603
759
|
streamInactivityMs: getConfig().streamInactivityMs,
|
|
@@ -630,6 +786,7 @@ export function buildGateDeps(params) {
|
|
|
630
786
|
// origin, and the final integration gate re-checks each one at run end.
|
|
631
787
|
recordDebt,
|
|
632
788
|
recordRepairCandidate: (cwd2, candidate) => recordRepairCandidate(cwd2, candidate),
|
|
789
|
+
closeHealthDebts,
|
|
633
790
|
// file → introducing task, the provenance half of the discriminator.
|
|
634
791
|
introducedBy: (cwd2, rel) => Promise.resolve(taskThatIntroduced(cwd2, rel)),
|
|
635
792
|
// Tracked paths, used only to resolve a bare file name a FAIL text names
|
|
@@ -733,6 +890,10 @@ export function buildGateDeps(params) {
|
|
|
733
890
|
// file. A task that never reached compose has no spec section —
|
|
734
891
|
// runWorkVerification treats a null spec as a no-op pass.
|
|
735
892
|
const spec = await readSpecForVerification(cwd2, taskId);
|
|
893
|
+
const qaRecord = await readQaRecordForVerification(cwd2, taskId);
|
|
894
|
+
// One id for the whole gate: read and append must agree on which run
|
|
895
|
+
// these facts belong to.
|
|
896
|
+
const { runId } = currentRunContext(cwd2);
|
|
736
897
|
// DEAD AIR (the reason this loader exists). The gate's DETERMINISTIC
|
|
737
898
|
// stage — repo health plus the nine PROBE_ADAPTERS rows — runs before
|
|
738
899
|
// the verify child, and the child's own loader only starts once the
|
|
@@ -788,6 +949,10 @@ export function buildGateDeps(params) {
|
|
|
788
949
|
stageLine = `repo health · ${c}`;
|
|
789
950
|
}
|
|
790
951
|
}),
|
|
952
|
+
// What those checks said before the task started, so a red one
|
|
953
|
+
// is attributed rather than absolutely failed. Read only when
|
|
954
|
+
// the check above comes back red.
|
|
955
|
+
healthBaseline: () => healthBaselineFor(cwd2, taskId, signal),
|
|
791
956
|
// The deterministic probes, bound in one place (buildVerifyProbes
|
|
792
957
|
// above); the PROBE_ADAPTERS table in verify-work.ts runs them.
|
|
793
958
|
probes: buildVerifyProbes({
|
|
@@ -795,8 +960,15 @@ export function buildGateDeps(params) {
|
|
|
795
960
|
signal,
|
|
796
961
|
taskId,
|
|
797
962
|
spec,
|
|
798
|
-
|
|
963
|
+
qaRecord,
|
|
964
|
+
log: makeDebugAppender(runLogPath(cwd2, 'verify-debug.log')),
|
|
965
|
+
// The evidence row runs the project's own checks, so it is
|
|
966
|
+
// the longest step of the stage the loader above exists for.
|
|
967
|
+
onCommand: c => {
|
|
968
|
+
stageLine = `project checks · ${c}`;
|
|
969
|
+
}
|
|
799
970
|
}),
|
|
971
|
+
...(qaRecord === null ? {} : { qaRecord }),
|
|
800
972
|
// Git-state guard result of the most recent child run: a verdict
|
|
801
973
|
// computed on a tree the child itself mutated is discarded — but ONLY
|
|
802
974
|
// when the mutation touched graded state (verdictTainted). A child
|
|
@@ -812,11 +984,13 @@ export function buildGateDeps(params) {
|
|
|
812
984
|
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
813
985
|
// next one, stamped with this task's id as their origin so a
|
|
814
986
|
// later child sees a cited fact is second-hand and must
|
|
815
|
-
// re-validate before excusing a failure
|
|
816
|
-
//
|
|
987
|
+
// re-validate before excusing a failure, and with the RUN's id so
|
|
988
|
+
// the facts measured against this tree lead the block. Facts
|
|
989
|
+
// only — verdict rules unaffected.
|
|
817
990
|
envNotes: {
|
|
818
991
|
read: () => readEnvNotes(cwd2),
|
|
819
|
-
append: notes => appendEnvNotes(cwd2, notes, taskId)
|
|
992
|
+
append: notes => appendEnvNotes(cwd2, notes, taskId, runId),
|
|
993
|
+
runId
|
|
820
994
|
},
|
|
821
995
|
// Per-run cross-slice contract registry under `.pi-tasks/`: the
|
|
822
996
|
// verbatim interface facts the design pins that multiple slices
|
|
@@ -854,31 +1028,17 @@ export function buildGateDeps(params) {
|
|
|
854
1028
|
introducedBy: rel => Promise.resolve(taskThatIntroduced(cwd2, rel))
|
|
855
1029
|
});
|
|
856
1030
|
},
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
kind: 'enforce',
|
|
869
|
-
step: 'repo health',
|
|
870
|
-
stepNum: 1,
|
|
871
|
-
stepTotal: 1,
|
|
872
|
-
startedAt,
|
|
873
|
-
lastLine: running ? `repo health · ${running}` : 'repo health'
|
|
874
|
-
}));
|
|
875
|
-
return runRepoHealthCheck(cwd2, {
|
|
876
|
-
signal,
|
|
877
|
-
onCommand: c => {
|
|
878
|
-
running = c;
|
|
879
|
-
}
|
|
880
|
-
}).finally(stop);
|
|
881
|
-
},
|
|
1031
|
+
// task-gates.ts calls this up to twice per task in `edit` mode — a baseline
|
|
1032
|
+
// before the enforce edit pass, and a differential check after it.
|
|
1033
|
+
repoHealth: runHealthUnderLoader,
|
|
1034
|
+
captureHealthBaseline: (healthCtx, cwd2, label) => captureHealthBaseline({
|
|
1035
|
+
runHealth: () => runHealthUnderLoader(healthCtx, cwd2, label),
|
|
1036
|
+
treeHash: () => worktreeTreeHash(makeGit(cwd2, signal)),
|
|
1037
|
+
// A project's `lint` script commonly runs `--fix`, so the baseline
|
|
1038
|
+
// can leave the tree edited. Undone here, or the task starts on a
|
|
1039
|
+
// tree the baseline authored.
|
|
1040
|
+
discardEdits: () => discardTreeEdits(cwd2)
|
|
1041
|
+
}).catch(() => null),
|
|
882
1042
|
dirty: async (cwd2) => {
|
|
883
1043
|
const r = await git(cwd2, ['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
884
1044
|
return r.exitCode === 0 && r.stdout.trim().length > 0;
|
|
@@ -919,17 +1079,19 @@ export function buildGateDeps(params) {
|
|
|
919
1079
|
// proven to depend on it.
|
|
920
1080
|
ignoredSnapshot: () => collectIgnoredSnapshot(cwd2, signal),
|
|
921
1081
|
...(ignoredKnown && ignoredKnown.length > 0 ? { ignoredKnown } : {}),
|
|
922
|
-
gateWithoutIgnored: paths => gatePassesWithoutIgnored(cwd2, paths, c => runFinalIntegrationGate(c, { signal }), makeDebugAppender(
|
|
923
|
-
log: makeDebugAppender(
|
|
1082
|
+
gateWithoutIgnored: paths => gatePassesWithoutIgnored(cwd2, paths, c => runFinalIntegrationGate(c, { signal }), makeDebugAppender(runLogPath(cwd2, 'final-gate-debug.log'))),
|
|
1083
|
+
log: makeDebugAppender(runLogPath(cwd2, 'final-gate-debug.log'))
|
|
924
1084
|
}),
|
|
925
1085
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
926
1086
|
// Read the same composed spec the verify gate judged against, so the
|
|
927
1087
|
// recommendation reasons over the real contract (degrade to the bare title).
|
|
928
1088
|
const spec = (await readSpecForVerification(cwd2, taskId)) ?? taskTitle;
|
|
1089
|
+
const qaRecord = await readQaRecordForVerification(cwd2, taskId);
|
|
929
1090
|
return researchResolution({
|
|
930
1091
|
cwd: cwd2,
|
|
931
1092
|
signal,
|
|
932
1093
|
spec,
|
|
1094
|
+
...(qaRecord === null ? {} : { qaRecord }),
|
|
933
1095
|
failReason,
|
|
934
1096
|
runChild: gateChild(recCtx, cwd2, taskTitle, 'recommend', 'verify-debug.log')
|
|
935
1097
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type CommandRunner } from './command-run.js';
|
|
2
|
+
import { type VerifiedCommand } from './run-context.js';
|
|
3
|
+
/** One command the parent ran for the children, and where its full output went. */
|
|
4
|
+
export interface EvidenceCommand {
|
|
5
|
+
cmd: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** Its real exit code, or {@link NOT_RUN} when `gap` says nothing was observed. */
|
|
8
|
+
exitCode: number;
|
|
9
|
+
/** Absolute path — outside the worktree — of the file holding the full output. */
|
|
10
|
+
outputPath: string;
|
|
11
|
+
/** The tree it was measured against; null when git could not say (see treeHash). */
|
|
12
|
+
treeHash: string | null;
|
|
13
|
+
/** The env-gap ladder's reason nothing was observed. Absent ⇒ the command ran. */
|
|
14
|
+
gap?: string;
|
|
15
|
+
}
|
|
16
|
+
/** What one gate session hands its children in place of a command to run. */
|
|
17
|
+
export interface GateEvidence {
|
|
18
|
+
commands: EvidenceCommand[];
|
|
19
|
+
}
|
|
20
|
+
export interface EvidenceRunDeps {
|
|
21
|
+
cwd: string;
|
|
22
|
+
/** Whose state dir the output files land in. */
|
|
23
|
+
runId: string;
|
|
24
|
+
/** Already filtered to the classes that terminate (see RunContext). */
|
|
25
|
+
commands: readonly VerifiedCommand[];
|
|
26
|
+
treeHash: string | null;
|
|
27
|
+
/** Per-command ceiling. The same one a gate child's own bash tool gets: a
|
|
28
|
+
* command pre-run here must not be bounded more tightly than the same command
|
|
29
|
+
* run by the child, or the child would see a timeout the child cannot reproduce. */
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
/** Called with each command as it STARTS. These are the project's own whole-repo
|
|
33
|
+
* checks and they run before the child exists, so without a live line naming
|
|
34
|
+
* the running one the gate's longest stage is also its quietest. */
|
|
35
|
+
onCommand?: (cmd: string) => void;
|
|
36
|
+
/** The spawner. Injected so a session's command count is assertable. */
|
|
37
|
+
run?: CommandRunner;
|
|
38
|
+
}
|
|
39
|
+
/** Where a run's evidence output lives. */
|
|
40
|
+
export declare function evidenceDir(cwd: string, runId: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Run each command and write its output to the run's state dir.
|
|
43
|
+
*
|
|
44
|
+
* Sequential: these are the project's own whole-repo checks, and running a
|
|
45
|
+
* typecheck, a build and a test suite at once on the machine the user is working on
|
|
46
|
+
* would make each of them slower and their results less reproducible.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runGateEvidence(deps: EvidenceRunDeps): Promise<GateEvidence>;
|
|
49
|
+
/** One prompt line per command: what ran, how it ended, and the file the child
|
|
50
|
+
* reads instead of running it again. */
|
|
51
|
+
export declare function evidenceVerifyFindings(evidence: GateEvidence): string[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-evidence — the project's own check and build commands, run ONCE per tree by
|
|
3
|
+
* the parent, so a gate child reads a result instead of producing one.
|
|
4
|
+
*
|
|
5
|
+
* The failure this closes: the verify child is told to run the project's own
|
|
6
|
+
* commands; so is the lint-fix child after it; so is the verify child after that.
|
|
7
|
+
* One real run spent 2755 s inside gate children re-running the same suites 49
|
|
8
|
+
* times, and a single gate session ran one of them seven times over a tree that
|
|
9
|
+
* changed twice.
|
|
10
|
+
*
|
|
11
|
+
* What makes pre-supplying safe is the CLASS column the run context already holds:
|
|
12
|
+
* `check` and `build` terminate on their own, `serve` does not, and a dev server
|
|
13
|
+
* launched here would hang the gate before the child ever started. So `serve` is
|
|
14
|
+
* never run — the filter lives in `RunContext.gateEvidenceFor`, which owns the
|
|
15
|
+
* verdicts.
|
|
16
|
+
*
|
|
17
|
+
* Nothing observed is nothing claimed. A command that could not run on this machine
|
|
18
|
+
* is recorded SKIPPED with the env-gap ladder's own reason (command-run.ts), never
|
|
19
|
+
* as a failure: the ladder is the single statement of what an ending MEANS, and a
|
|
20
|
+
* second statement of it here would drift from the gate's.
|
|
21
|
+
*
|
|
22
|
+
* The output files live in the run's state dir, OUTSIDE the worktree. The children
|
|
23
|
+
* that read them run under the git-state guard, which restores the tree around
|
|
24
|
+
* them; evidence written into the tree would be reverted between being produced and
|
|
25
|
+
* being read.
|
|
26
|
+
*/
|
|
27
|
+
import * as fsp from 'node:fs/promises';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { classifyCommandRun, leadingBin, spawnCommand } from './command-run.js';
|
|
30
|
+
import { resolveRunner, runnerEnv } from './runner-resolve.js';
|
|
31
|
+
import { NOT_RUN } from './run-context.js';
|
|
32
|
+
import { stateDir } from './state-dir.js';
|
|
33
|
+
/** Where a run's evidence output lives. */
|
|
34
|
+
export function evidenceDir(cwd, runId) {
|
|
35
|
+
return path.join(stateDir(cwd, runId), 'evidence');
|
|
36
|
+
}
|
|
37
|
+
/** The output file, readable on its own once the session that produced it is gone. */
|
|
38
|
+
function evidenceFile(cmd, ending, stdout, stderr) {
|
|
39
|
+
return [`$ ${cmd}`, ending, '', stdout, stderr].join('\n');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run each command and write its output to the run's state dir.
|
|
43
|
+
*
|
|
44
|
+
* Sequential: these are the project's own whole-repo checks, and running a
|
|
45
|
+
* typecheck, a build and a test suite at once on the machine the user is working on
|
|
46
|
+
* would make each of them slower and their results less reproducible.
|
|
47
|
+
*/
|
|
48
|
+
export async function runGateEvidence(deps) {
|
|
49
|
+
if (deps.commands.length === 0)
|
|
50
|
+
return { commands: [] };
|
|
51
|
+
const dir = evidenceDir(deps.cwd, deps.runId);
|
|
52
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
53
|
+
const run = deps.run ?? spawnCommand;
|
|
54
|
+
const commands = [];
|
|
55
|
+
for (const [i, v] of deps.commands.entries()) {
|
|
56
|
+
const outputPath = path.join(dir, `${i + 1}.out`);
|
|
57
|
+
deps.onCommand?.(v.cmd);
|
|
58
|
+
// A verified command is a SHELL line, not an argv (`bun run lint`, `make -j2
|
|
59
|
+
// build`), so it runs through `sh -c`; its leading word still resolves through
|
|
60
|
+
// runner-resolve, or a login-shell-stripped PATH makes every command a gap.
|
|
61
|
+
const bin = leadingBin(v.cmd);
|
|
62
|
+
const runner = bin === null ? null : resolveRunner(bin);
|
|
63
|
+
const r = await run({
|
|
64
|
+
cwd: v.cwd,
|
|
65
|
+
bin: 'sh',
|
|
66
|
+
args: ['-c', v.cmd],
|
|
67
|
+
timeoutMs: deps.timeoutMs,
|
|
68
|
+
env: runner ? runnerEnv(runner) : { ...process.env },
|
|
69
|
+
...(deps.signal === undefined ? {} : { signal: deps.signal })
|
|
70
|
+
});
|
|
71
|
+
const verdict = classifyCommandRun(r);
|
|
72
|
+
const gap = verdict.outcome === 'gap' ? verdict.detail : undefined;
|
|
73
|
+
await fsp.writeFile(outputPath, evidenceFile(v.cmd, gap === undefined ? `exit ${r.status}` : `skipped — ${gap}`, r.stdout, r.stderr), 'utf8');
|
|
74
|
+
commands.push({
|
|
75
|
+
cmd: v.cmd,
|
|
76
|
+
cwd: v.cwd,
|
|
77
|
+
exitCode: gap === undefined ? (r.status ?? NOT_RUN) : NOT_RUN,
|
|
78
|
+
outputPath,
|
|
79
|
+
treeHash: deps.treeHash,
|
|
80
|
+
...(gap === undefined ? {} : { gap })
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return { commands };
|
|
84
|
+
}
|
|
85
|
+
/** One prompt line per command: what ran, how it ended, and the file the child
|
|
86
|
+
* reads instead of running it again. */
|
|
87
|
+
export function evidenceVerifyFindings(evidence) {
|
|
88
|
+
return evidence.commands.map(c => `\`${c.cmd}\` — ${c.gap === undefined ? `exit ${c.exitCode}` : `SKIPPED (${c.gap})`}`
|
|
89
|
+
+ ` — full output: ${c.outputPath}`);
|
|
90
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-resolution — what to DO about a verify FAIL, as one ordered decision table.
|
|
3
|
+
*
|
|
4
|
+
* WHY A TABLE. The gate loop used to answer this with three overlapping booleans
|
|
5
|
+
* over five predicates, and then RE-DERIVE the same branches afterwards to name
|
|
6
|
+
* them in the trail. In AUTO_0002 that shape cost: a correct judge ACCEPT
|
|
7
|
+
* overridden by a "rescue" attempt (41 minutes, 16 suppressions shipped, no
|
|
8
|
+
* debt), a spec contradiction that looped three rounds with no exit, and five
|
|
9
|
+
* defects that left the gate with no ledger entry at all. One row per decision in
|
|
10
|
+
* precedence order is the whole policy, and the row that fired NAMES itself — so
|
|
11
|
+
* the durable trail can no longer disagree with the branch that produced it.
|
|
12
|
+
*
|
|
13
|
+
* THERE IS NO RESCUE ACTION. A judge ACCEPT comes from a pass that read the real
|
|
14
|
+
* workspace and ran the real commands; spending an implementation re-run "just in
|
|
15
|
+
* case" overrides that verdict with nothing.
|
|
16
|
+
*
|
|
17
|
+
* Same ordered-data shape as `FAILURE_RULES` (src/workers/worker-failure.ts):
|
|
18
|
+
* first match wins, and row order IS the precedence.
|
|
19
|
+
*/
|
|
20
|
+
import type { VerifyFailClass } from './verify-work.js';
|
|
21
|
+
import type { ResolutionRecommendation } from './verify-resolution.js';
|
|
22
|
+
import type { DebtOrigin } from './accept-debt.js';
|
|
23
|
+
/**
|
|
24
|
+
* A criterion the task CANNOT meet without an edit its own spec forbids.
|
|
25
|
+
*
|
|
26
|
+
* Both gates that can prove one mint this shape: the bounded lint fix, whose
|
|
27
|
+
* static findings trace to a frozen path, and the resolution judge, through its
|
|
28
|
+
* `VERIFY-RESOLUTION: BLOCKED-BY-FROZEN` marker. It is the one input no re-run
|
|
29
|
+
* and no human ACCEPT-vs-AUTOFIX choice can move — the fix the check demands is
|
|
30
|
+
* the edit the spec denies — so it outranks every other row below.
|
|
31
|
+
*/
|
|
32
|
+
export interface SpecContradiction {
|
|
33
|
+
/** The acceptance criterion (or static check) that cannot be satisfied. May be
|
|
34
|
+
* empty when the judge named only the path. */
|
|
35
|
+
criterion: string;
|
|
36
|
+
/** The spec-frozen path whose edit is the only way to satisfy it. */
|
|
37
|
+
frozenPath: string;
|
|
38
|
+
}
|
|
39
|
+
/** Everything the decision depends on. Nothing else may enter the table. */
|
|
40
|
+
export interface ResolutionInput {
|
|
41
|
+
failClass: VerifyFailClass;
|
|
42
|
+
/** What the resolution judge recommended, or the default when it was not consulted. */
|
|
43
|
+
recommend: ResolutionRecommendation;
|
|
44
|
+
/** Rule 5c: a spec-required check could not RUN because its tooling is absent. */
|
|
45
|
+
unobserved: boolean;
|
|
46
|
+
contradiction: SpecContradiction | null;
|
|
47
|
+
/** Unattended AUTOFIX attempts already spent on this task. */
|
|
48
|
+
attempts: number;
|
|
49
|
+
/** Nobody can be asked (YOLO mode) — the picker is unreachable. */
|
|
50
|
+
unattended: boolean;
|
|
51
|
+
}
|
|
52
|
+
export type ResolutionRuleId = 'spec-contradiction' | 'judge-accept' | 'no-autofix-budget' | 'budget-spent' | 'autofix';
|
|
53
|
+
/**
|
|
54
|
+
* What the gate loop does next. `debtOrigin` is non-null exactly on `accept`,
|
|
55
|
+
* which is the loop's terminal exit: a defect may not leave the gate without a
|
|
56
|
+
* ledger entry, and the type — not a runtime check — is what enforces it. `ask`
|
|
57
|
+
* and `autofix` carry null because the human's answer (or the next round) names
|
|
58
|
+
* the origin instead.
|
|
59
|
+
*/
|
|
60
|
+
export type Disposition = {
|
|
61
|
+
rule: ResolutionRuleId;
|
|
62
|
+
action: 'autofix';
|
|
63
|
+
debtOrigin: null;
|
|
64
|
+
reason: string;
|
|
65
|
+
} | {
|
|
66
|
+
rule: ResolutionRuleId;
|
|
67
|
+
action: 'accept';
|
|
68
|
+
debtOrigin: DebtOrigin;
|
|
69
|
+
reason: string;
|
|
70
|
+
} | {
|
|
71
|
+
rule: ResolutionRuleId;
|
|
72
|
+
action: 'ask';
|
|
73
|
+
debtOrigin: null;
|
|
74
|
+
reason: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* How many UNATTENDED implementation re-runs each FAIL class is worth.
|
|
78
|
+
*
|
|
79
|
+
* Zero is a statement, not a disabled feature: an absent tool is not installed by
|
|
80
|
+
* re-running the work that needed it, and a verification pass that could not run
|
|
81
|
+
* is not repaired by changing the code it failed to judge. Spending three turns
|
|
82
|
+
* on either is how a run burns an hour to arrive where round one already was.
|
|
83
|
+
*/
|
|
84
|
+
export declare const AUTOFIX_BUDGET: Record<VerifyFailClass, number>;
|
|
85
|
+
/**
|
|
86
|
+
* How many times the /task-auto loop will START one plan entry before abandoning it.
|
|
87
|
+
*
|
|
88
|
+
* Derived, not chosen: one run to reach the gate at all, plus the re-runs the most
|
|
89
|
+
* forgiving fail class is worth inside that run. An entry that comes back for a
|
|
90
|
+
* further attempt has already spent a full autofix budget without converging, and
|
|
91
|
+
* the plan's remaining entries are worth more than its next round. Raising a class
|
|
92
|
+
* budget raises this with it, which is the relationship that should hold.
|
|
93
|
+
*/
|
|
94
|
+
export declare const ENTRY_ATTEMPT_BUDGET: number;
|
|
95
|
+
/** The budget in force, honouring an UNOBSERVED flag on any class. */
|
|
96
|
+
export declare function autofixBudget(i: Pick<ResolutionInput, 'failClass' | 'unobserved'>): number;
|
|
97
|
+
export declare function contradictionReason(c: SpecContradiction): string;
|
|
98
|
+
/**
|
|
99
|
+
* The ordered ladder. FIRST MATCH WINS, and this order is the only statement of
|
|
100
|
+
* the policy:
|
|
101
|
+
*
|
|
102
|
+
* 1. `spec-contradiction` — the fix the check demands is the edit the spec
|
|
103
|
+
* forbids. First, because every row below would otherwise spend a budget,
|
|
104
|
+
* a human's attention or both on a loop that cannot converge (0053 ran three
|
|
105
|
+
* rounds past this exact fact).
|
|
106
|
+
* 2. `judge-accept` — before any budget arithmetic, because that arithmetic is
|
|
107
|
+
* precisely what used to override a correct ACCEPT with a rescue attempt.
|
|
108
|
+
* 3. `no-autofix-budget` — a class worth zero attempts. Its own row, not folded
|
|
109
|
+
* into the one below, because a budget that was never offered must not be
|
|
110
|
+
* reported as one that was spent.
|
|
111
|
+
* 4. `budget-spent` — consecutive unattended attempts that all still FAIL. A
|
|
112
|
+
* person breaks the loop, or (unattended) it terminates with a debt.
|
|
113
|
+
* 5. `autofix` — unconditional, so the table is total.
|
|
114
|
+
*/
|
|
115
|
+
export declare const RESOLUTION_RULES: ReadonlyArray<{
|
|
116
|
+
id: ResolutionRuleId;
|
|
117
|
+
match: (i: ResolutionInput) => Disposition | null;
|
|
118
|
+
}>;
|
|
119
|
+
export declare function resolveDisposition(i: ResolutionInput): Disposition;
|