@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
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
32
32
|
import type { RunWorkerInput, RunWorkerResult } from '../workers/pi-worker-core.js';
|
|
33
33
|
import type { GitStateSnapshot, ReconcileResult } from './git-state-guard.js';
|
|
34
|
-
import type {
|
|
35
|
-
import type { AutoLoaderState } from './widget.js';
|
|
34
|
+
import type { ChildStatus } from './child-status.js';
|
|
36
35
|
/** Which gate child this is. */
|
|
37
36
|
export type GateChildKind = 'verify' | 'recommend' | 'lint-fix' | 'final-fix' | 'enforce';
|
|
38
37
|
export interface GateChildRow {
|
|
@@ -76,21 +75,20 @@ export interface GateChildDeps {
|
|
|
76
75
|
commandTimeoutMs: number;
|
|
77
76
|
/** Hung-stream bound; the probe-based stall guard cannot supply it. */
|
|
78
77
|
streamInactivityMs: number;
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The live widget state this child feeds and its loader reads. SHARED with
|
|
80
|
+
* the caller — the verify gate's own loader reads the same status while this
|
|
81
|
+
* child runs with `loader: false` — so it is the caller's object, not a copy.
|
|
82
|
+
* It also owns the loader ritual and the context-usage resolution.
|
|
83
|
+
*/
|
|
84
|
+
status: ChildStatus;
|
|
80
85
|
runWorker: (input: RunWorkerInput) => Promise<RunWorkerResult>;
|
|
81
86
|
makeDebugAppender: (path: string) => (line: string, level?: 'event' | 'stream') => void;
|
|
82
|
-
startAutoLoader: (ctx: ExtensionCommandContext, getState: () => AutoLoaderState | null) => () => void;
|
|
83
87
|
captureGitState: (cwd: string, signal?: AbortSignal) => Promise<GitStateSnapshot>;
|
|
84
88
|
reconcileGitState: (cwd: string, snapshot: GitStateSnapshot, signal?: AbortSignal) => Promise<ReconcileResult>;
|
|
85
89
|
/** Tree changes for a WRITE-capable child, already formatted. */
|
|
86
90
|
describeTreeChanges: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
87
|
-
resolveContextUsage: (snapshot: ContextSnapshot, prev: ContextSnapshot | undefined, parentContextWindow: number) => ContextSnapshot;
|
|
88
91
|
truncateToolResult: (text: string) => string;
|
|
89
|
-
/** Where the live widget reads `lastLine` / `contextUsage` from. */
|
|
90
|
-
widget: {
|
|
91
|
-
lastLine?: string;
|
|
92
|
-
contextUsage?: ContextSnapshot;
|
|
93
|
-
};
|
|
94
92
|
/** Set to the last reconcile so the caller can discard a tainted verdict. */
|
|
95
93
|
onReconcile?: (rec: ReconcileResult) => void;
|
|
96
94
|
}
|
package/dist/task/gate-child.js
CHANGED
|
@@ -57,8 +57,11 @@ export const GATE_CHILD_KINDS = {
|
|
|
57
57
|
export function makeGateChild(deps) {
|
|
58
58
|
const row = GATE_CHILD_KINDS[deps.kind];
|
|
59
59
|
return async (tools, prompt, sig) => {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Reset FIRST — before the start marker and the guard snapshot, not just
|
|
61
|
+
// inside `track` — so a previous child's trailer never shows under this
|
|
62
|
+
// one while the git snapshot is still being taken (the verify gate's own
|
|
63
|
+
// loader is already reading this status by then).
|
|
64
|
+
deps.status.reset();
|
|
62
65
|
const startedAt = Date.now();
|
|
63
66
|
// Every marker below (start/end, the guard's restore, the loop warning, a
|
|
64
67
|
// write-capable child's tree changes) is a guard record that survives at
|
|
@@ -67,19 +70,17 @@ export function makeGateChild(deps) {
|
|
|
67
70
|
const log = deps.makeDebugAppender(deps.logPath);
|
|
68
71
|
log(`=== ${deps.kind} start: ${deps.taskTitle} ===`);
|
|
69
72
|
const guardSnapshot = row.guarded ? await deps.captureGitState(deps.cwd, sig) : null;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
:
|
|
73
|
+
const frame = deps.loader === false ?
|
|
74
|
+
null
|
|
75
|
+
: () => ({
|
|
73
76
|
title: deps.taskTitle,
|
|
74
77
|
kind: deps.kind,
|
|
75
78
|
step: row.step,
|
|
76
79
|
stepNum: 1,
|
|
77
80
|
stepTotal: 1,
|
|
78
|
-
startedAt
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}));
|
|
82
|
-
try {
|
|
81
|
+
startedAt
|
|
82
|
+
});
|
|
83
|
+
return deps.status.track(deps.ctx, frame, async () => {
|
|
83
84
|
let r;
|
|
84
85
|
try {
|
|
85
86
|
r = await deps.runWorker({
|
|
@@ -106,10 +107,10 @@ export function makeGateChild(deps) {
|
|
|
106
107
|
+ (rs.detail ? ` — ${rs.detail}` : '')
|
|
107
108
|
+ ' ==='),
|
|
108
109
|
onLine: line => {
|
|
109
|
-
//
|
|
110
|
+
// The status feeds the LIVE widget and is not logging — it
|
|
110
111
|
// stays outside the gate, or a quiet trail would also blank
|
|
111
112
|
// the progress display.
|
|
112
|
-
deps.
|
|
113
|
+
deps.status.onLine(line);
|
|
113
114
|
log(line, 'stream');
|
|
114
115
|
},
|
|
115
116
|
...(row.logToolResults ?
|
|
@@ -118,9 +119,7 @@ export function makeGateChild(deps) {
|
|
|
118
119
|
+ deps.truncateToolResult(text), 'stream')
|
|
119
120
|
}
|
|
120
121
|
: {}),
|
|
121
|
-
onContextUsage: snapshot =>
|
|
122
|
-
deps.widget.contextUsage = deps.resolveContextUsage(snapshot, deps.widget.contextUsage, deps.parentContextWindow);
|
|
123
|
-
}
|
|
122
|
+
onContextUsage: snapshot => deps.status.onContextUsage(snapshot)
|
|
124
123
|
});
|
|
125
124
|
}
|
|
126
125
|
finally {
|
|
@@ -169,9 +168,6 @@ export function makeGateChild(deps) {
|
|
|
169
168
|
+ `${await deps.describeTreeChanges(deps.cwd, sig)} ===`);
|
|
170
169
|
}
|
|
171
170
|
return r.text;
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
stopLoader();
|
|
175
|
-
}
|
|
171
|
+
});
|
|
176
172
|
};
|
|
177
173
|
}
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { GateDeps } from './task-gates.js';
|
|
3
|
+
import { type VerifyProbes } from './verify-work.js';
|
|
3
4
|
import { type FinalFixResult } from './final-gate-fix.js';
|
|
4
5
|
import { type AddedLine } from './probe-gaming.js';
|
|
5
6
|
import { type ChangedFile } from './substitution-probe.js';
|
|
@@ -135,6 +136,34 @@ export declare function collectTaskTreeChanges(cwd: string, signal?: AbortSignal
|
|
|
135
136
|
* costs one extra tracked-file listing, not a second diff.
|
|
136
137
|
*/
|
|
137
138
|
export declare function collectTestAssemblyFindings(cwd: string, changed: ChangedFile[], signal?: AbortSignal): Promise<string[]>;
|
|
139
|
+
/**
|
|
140
|
+
* The composed spec a gate judges against: the `## spec` section of the task file
|
|
141
|
+
* (see extractSpecForVerification). A task that never reached compose has no spec
|
|
142
|
+
* section, and an unreadable task file is the same case — both are null, and every
|
|
143
|
+
* caller already treats null as "nothing to hold the work to" (verify no-ops,
|
|
144
|
+
* frozen paths are empty, recommend falls back to the title). Never throws. This
|
|
145
|
+
* used to be the same four-line try/catch at four gate sites.
|
|
146
|
+
*/
|
|
147
|
+
export declare function readSpecForVerification(cwd: string, taskId: string): Promise<string | null>;
|
|
148
|
+
/**
|
|
149
|
+
* Bind the deterministic verify probes — THE one place the collectors above meet
|
|
150
|
+
* the probe table in verify-work.ts. One entry per `BoundProbeKey`; the table row
|
|
151
|
+
* with that key reads it, and the table's loop owns the ritual (skip when absent,
|
|
152
|
+
* degrade to the row's empty value when the thunk throws), so nothing here needs a
|
|
153
|
+
* try/catch and a fault in one probe cannot reach another. Each thunk is LAZY:
|
|
154
|
+
* building the record runs no git.
|
|
155
|
+
*
|
|
156
|
+
* `spec` is the composed spec (see readSpecForVerification): the prohibition probe
|
|
157
|
+
* reads it and skips the git call when the spec forbids nothing. `log` is the
|
|
158
|
+
* verify debug appender for the one probe that repairs as well as reports.
|
|
159
|
+
*/
|
|
160
|
+
export declare function buildVerifyProbes(params: {
|
|
161
|
+
cwd: string;
|
|
162
|
+
signal?: AbortSignal;
|
|
163
|
+
taskId: string;
|
|
164
|
+
spec: string | null;
|
|
165
|
+
log?: (msg: string) => void;
|
|
166
|
+
}): VerifyProbes;
|
|
138
167
|
/**
|
|
139
168
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
140
169
|
* implementation re-runner, injected by the caller. The returned object also drives
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -44,7 +44,7 @@ import { runWorker } from '../workers/pi-worker-core.js';
|
|
|
44
44
|
import { getConfig } from '../config/config.js';
|
|
45
45
|
import { makeDebugAppender } from './debug-log.js';
|
|
46
46
|
import { startAutoLoader } from './widget.js';
|
|
47
|
-
import {
|
|
47
|
+
import { ChildStatus } from './child-status.js';
|
|
48
48
|
import { makeGateChild } from './gate-child.js';
|
|
49
49
|
/** Max chars of a tool result kept in the gate debug log (mx5 run 10 item 6). */
|
|
50
50
|
const TOOL_RESULT_LOG_LIMIT = 300;
|
|
@@ -457,6 +457,90 @@ export async function collectTestAssemblyFindings(cwd, changed, signal) {
|
|
|
457
457
|
}
|
|
458
458
|
return testAssemblyVerifyFindings(findTestRebuiltAssemblies(testFiles, production));
|
|
459
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* The composed spec a gate judges against: the `## spec` section of the task file
|
|
462
|
+
* (see extractSpecForVerification). A task that never reached compose has no spec
|
|
463
|
+
* section, and an unreadable task file is the same case — both are null, and every
|
|
464
|
+
* caller already treats null as "nothing to hold the work to" (verify no-ops,
|
|
465
|
+
* frozen paths are empty, recommend falls back to the title). Never throws. This
|
|
466
|
+
* used to be the same four-line try/catch at four gate sites.
|
|
467
|
+
*/
|
|
468
|
+
export async function readSpecForVerification(cwd, taskId) {
|
|
469
|
+
try {
|
|
470
|
+
const { body } = await readTaskFile(cwd, taskId);
|
|
471
|
+
return extractSpecForVerification(body);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Bind the deterministic verify probes — THE one place the collectors above meet
|
|
479
|
+
* the probe table in verify-work.ts. One entry per `BoundProbeKey`; the table row
|
|
480
|
+
* with that key reads it, and the table's loop owns the ritual (skip when absent,
|
|
481
|
+
* degrade to the row's empty value when the thunk throws), so nothing here needs a
|
|
482
|
+
* try/catch and a fault in one probe cannot reach another. Each thunk is LAZY:
|
|
483
|
+
* building the record runs no git.
|
|
484
|
+
*
|
|
485
|
+
* `spec` is the composed spec (see readSpecForVerification): the prohibition probe
|
|
486
|
+
* reads it and skips the git call when the spec forbids nothing. `log` is the
|
|
487
|
+
* verify debug appender for the one probe that repairs as well as reports.
|
|
488
|
+
*/
|
|
489
|
+
export function buildVerifyProbes(params) {
|
|
490
|
+
const { cwd, signal, taskId, spec, log } = params;
|
|
491
|
+
return {
|
|
492
|
+
// Deterministic self-verification probe: test files the task itself
|
|
493
|
+
// authored/changed become prompt-level findings mandating the child
|
|
494
|
+
// to drive the real artifact before trusting their green result.
|
|
495
|
+
substitution: () => collectChangedFiles(cwd, signal).then(findSubstitutionSuspects),
|
|
496
|
+
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
497
|
+
// that the task's diff modified anyway become prompt-level findings
|
|
498
|
+
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
499
|
+
// diff` and cannot even see the violation.
|
|
500
|
+
prohibition: () => {
|
|
501
|
+
const banned = spec ? extractProhibitions(spec) : [];
|
|
502
|
+
if (banned.length === 0)
|
|
503
|
+
return Promise.resolve([]);
|
|
504
|
+
return collectChangedFiles(cwd, signal).then(files => findProhibitionViolations(banned, files));
|
|
505
|
+
},
|
|
506
|
+
// Deterministic cross-task deletion probe (mx5 run 12 PROMPT 2):
|
|
507
|
+
// tracked files this task's diff DELETES whose introducing commit
|
|
508
|
+
// belongs to a DIFFERENT task — a sibling's committed deliverable
|
|
509
|
+
// destroyed (typically to green a check). Injected under rule 4d and
|
|
510
|
+
// carried on a FAIL so an ACCEPT records durable debts.
|
|
511
|
+
crossTaskDeletion: () => collectTaskTreeChanges(cwd, signal).then(changes => findCrossTaskDeletions(changes, taskId, rel => taskThatIntroduced(cwd, rel))),
|
|
512
|
+
// Deterministic probe-gaming probe (F6): added lines whose stated
|
|
513
|
+
// purpose is to make a check pass rather than meet the requirement
|
|
514
|
+
// ("return 401 so the verification test passes") become rule-4c
|
|
515
|
+
// findings so the child verifies the real requirement, not the check.
|
|
516
|
+
probeGaming: () => collectAddedLines(cwd, signal).then(findProbeGaming),
|
|
517
|
+
// Deterministic sandbox-path-leak probe (mx5 run 13 PROMPT 4 item
|
|
518
|
+
// 1): absolute paths committed from the authoring child's own
|
|
519
|
+
// environment (`/workspace/src/shared`) that resolve nowhere here.
|
|
520
|
+
// Repaired deterministically where the relative form provably
|
|
521
|
+
// resolves; the remainder is injected under rule 4e, whose point is
|
|
522
|
+
// that such a path breaks the BUILD — so the checks that would have
|
|
523
|
+
// caught it report nothing rather than failing.
|
|
524
|
+
foreignPath: () => collectForeignPathFindings(cwd, signal, log),
|
|
525
|
+
// Deterministic neutered-check-script probe (mx5 run 13 PROMPT 4
|
|
526
|
+
// item 4): a check script this task authored that cannot fail
|
|
527
|
+
// (`… || true`, an inverted-grep launder). Injected under rule 4f,
|
|
528
|
+
// because the child provably cannot find this by running the
|
|
529
|
+
// script — it passes, which IS the defect.
|
|
530
|
+
scriptEscape: () => collectScriptEscapeFindings(cwd, signal),
|
|
531
|
+
// Deterministic runner glob-collision probe (mx5 runs 7 AND 13,
|
|
532
|
+
// PROMPT 4 item 2): both `bun test` and `playwright test` declared
|
|
533
|
+
// with no proof their file sets are disjoint. Injected under rule
|
|
534
|
+
// 4g — the collision kills the suite during COLLECTION, which does
|
|
535
|
+
// not look like a test failure.
|
|
536
|
+
runnerGlob: () => collectRunnerGlobFindings(cwd),
|
|
537
|
+
// Deterministic test-assembly probe (F4): authored test files that
|
|
538
|
+
// rebuild production wiring — importing the leaf modules the shipped
|
|
539
|
+
// entry composes and assembling their own copy — become rule-3f
|
|
540
|
+
// findings so the child drives the REAL assembly, not the copy.
|
|
541
|
+
testAssembly: () => collectChangedFiles(cwd, signal).then(changed => collectTestAssemblyFindings(cwd, changed, signal))
|
|
542
|
+
};
|
|
543
|
+
}
|
|
460
544
|
/**
|
|
461
545
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
462
546
|
* implementation re-runner, injected by the caller. The returned object also drives
|
|
@@ -471,10 +555,11 @@ export function buildGateDeps(params) {
|
|
|
471
555
|
// loader across the deterministic stage — so the dead air can be measured in the
|
|
472
556
|
// SAME binary rather than against a remembered baseline. Unset in every real run.
|
|
473
557
|
const deadAirBaseline = process.env.DEADAIR_AB_ARM === 'baseline';
|
|
474
|
-
//
|
|
475
|
-
// output line and context usage
|
|
476
|
-
|
|
477
|
-
|
|
558
|
+
// ONE live status for every gate child, so the widget mirrors the running
|
|
559
|
+
// child's latest output line and context usage exactly like the single-task
|
|
560
|
+
// phase widget — and so the verify gate's own loader (below) reads what its
|
|
561
|
+
// loader-less child feeds.
|
|
562
|
+
const status = new ChildStatus({ parentContextWindow });
|
|
478
563
|
// What the git-state guard had to restore after the MOST RECENT verify/recommend
|
|
479
564
|
// child run (see git-state-guard.ts). runWorkVerification reads this through its
|
|
480
565
|
// mutationCheck dep to discard a verdict computed on a mutated tree.
|
|
@@ -488,50 +573,28 @@ export function buildGateDeps(params) {
|
|
|
488
573
|
};
|
|
489
574
|
// Adapter onto the shared gate-child runner (gate-child.ts). What survives
|
|
490
575
|
// here is WIRING — which context, which log file, which config knobs, and
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
const gateChild = (gateCtx, cwd2, taskTitle, kind, logFile, opts = {}) => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
cwd: cwd2,
|
|
514
|
-
taskTitle,
|
|
515
|
-
kind,
|
|
516
|
-
logPath: path.join(tasksDir(cwd2), logFile),
|
|
517
|
-
...(opts.loader === undefined ? {} : { loader: opts.loader }),
|
|
518
|
-
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
519
|
-
streamInactivityMs: getConfig().streamInactivityMs,
|
|
520
|
-
parentContextWindow,
|
|
521
|
-
runWorker,
|
|
522
|
-
makeDebugAppender,
|
|
523
|
-
startAutoLoader,
|
|
524
|
-
captureGitState,
|
|
525
|
-
reconcileGitState,
|
|
526
|
-
describeTreeChanges: async (c, sig) => formatTreeChanges(await collectTreeChanges(c, sig)),
|
|
527
|
-
resolveContextUsage,
|
|
528
|
-
truncateToolResult,
|
|
529
|
-
widget,
|
|
530
|
-
onReconcile: rec => {
|
|
531
|
-
lastGuardReconcile = rec;
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
};
|
|
576
|
+
// the shared live status; the ritual and the per-kind policy are the
|
|
577
|
+
// table's. The enforce child below goes through the same call.
|
|
578
|
+
const gateChild = (gateCtx, cwd2, taskTitle, kind, logFile, opts = {}) => makeGateChild({
|
|
579
|
+
ctx: gateCtx,
|
|
580
|
+
cwd: cwd2,
|
|
581
|
+
taskTitle,
|
|
582
|
+
kind,
|
|
583
|
+
logPath: path.join(tasksDir(cwd2), logFile),
|
|
584
|
+
...(opts.loader === undefined ? {} : { loader: opts.loader }),
|
|
585
|
+
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
586
|
+
streamInactivityMs: getConfig().streamInactivityMs,
|
|
587
|
+
status,
|
|
588
|
+
runWorker,
|
|
589
|
+
makeDebugAppender,
|
|
590
|
+
captureGitState,
|
|
591
|
+
reconcileGitState,
|
|
592
|
+
describeTreeChanges: async (c, sig) => formatTreeChanges(await collectTreeChanges(c, sig)),
|
|
593
|
+
truncateToolResult,
|
|
594
|
+
onReconcile: rec => {
|
|
595
|
+
lastGuardReconcile = rec;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
535
598
|
return {
|
|
536
599
|
runTask,
|
|
537
600
|
// Durable per-task gate trail: every verdict/decision lands in the task
|
|
@@ -604,15 +667,7 @@ export function buildGateDeps(params) {
|
|
|
604
667
|
// enforce EDIT pass makes to them before those edits are committed. Reads the
|
|
605
668
|
// same composed spec + extractProhibitions the verify probe consumes; empty on
|
|
606
669
|
// a spec that froze nothing → the guard is a no-op.
|
|
607
|
-
frozenPaths: async (cwd2, taskId) =>
|
|
608
|
-
try {
|
|
609
|
-
const { body } = await readTaskFile(cwd2, taskId);
|
|
610
|
-
return frozenPathsFromSpec(extractSpecForVerification(body));
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
return [];
|
|
614
|
-
}
|
|
615
|
-
},
|
|
670
|
+
frozenPaths: async (cwd2, taskId) => frozenPathsFromSpec(await readSpecForVerification(cwd2, taskId)),
|
|
616
671
|
// Restore those frozen paths to HEAD, discarding a gate child's edits to them;
|
|
617
672
|
// returns the files actually reverted (for the trail). Best-effort git shape.
|
|
618
673
|
revertFrozenPaths: (cwd2, paths) => revertFrozenPaths(paths, async (args) => {
|
|
@@ -654,14 +709,7 @@ export function buildGateDeps(params) {
|
|
|
654
709
|
// The spec to verify against is the composed spec committed in the task
|
|
655
710
|
// file. A task that never reached compose has no spec section —
|
|
656
711
|
// runWorkVerification treats a null spec as a no-op pass.
|
|
657
|
-
|
|
658
|
-
try {
|
|
659
|
-
const { body } = await readTaskFile(cwd2, taskId);
|
|
660
|
-
spec = extractSpecForVerification(body);
|
|
661
|
-
}
|
|
662
|
-
catch {
|
|
663
|
-
spec = null;
|
|
664
|
-
}
|
|
712
|
+
const spec = await readSpecForVerification(cwd2, taskId);
|
|
665
713
|
// DEAD AIR (the reason this loader exists). The gate's DETERMINISTIC
|
|
666
714
|
// stage — repo health plus ten probes — runs before the verify child,
|
|
667
715
|
// and the child's own loader only starts once the child does. The impl
|
|
@@ -674,133 +722,84 @@ export function buildGateDeps(params) {
|
|
|
674
722
|
// deterministic stage and the child — so the run is never silent.
|
|
675
723
|
const gateStartedAt = Date.now();
|
|
676
724
|
let stageLine;
|
|
677
|
-
//
|
|
678
|
-
// deterministic stage has no child of its own, so a stale `↳`
|
|
679
|
-
// the last task's enforce pass would otherwise sit under the
|
|
680
|
-
// status block as if it were live.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
725
|
+
// `track` clears the PREVIOUS child's trailer before the loader goes
|
|
726
|
+
// up: the deterministic stage has no child of its own, so a stale `↳`
|
|
727
|
+
// line from the last task's enforce pass would otherwise sit under the
|
|
728
|
+
// new status block as if it were live. The frame's `lastLine` wins
|
|
729
|
+
// over the status's own, which is how the stage label shows until
|
|
730
|
+
// the child has a line.
|
|
731
|
+
const gateFrame = deadAirBaseline ? null : (() => ({
|
|
732
|
+
title: taskTitle,
|
|
733
|
+
kind: 'verify',
|
|
734
|
+
step: 'verify',
|
|
735
|
+
stepNum: 1,
|
|
736
|
+
stepTotal: 1,
|
|
737
|
+
startedAt: gateStartedAt,
|
|
738
|
+
lastLine: status.snapshot().lastLine ?? stageLine
|
|
739
|
+
}));
|
|
740
|
+
return status.track(verifyCtx, gateFrame, () => runWorkVerification({
|
|
741
|
+
cwd: cwd2,
|
|
742
|
+
signal,
|
|
743
|
+
spec,
|
|
744
|
+
// The child renders no loader of its own: the gate-wide one above is
|
|
745
|
+
// already live and reads the same status the child feeds, so a
|
|
746
|
+
// second widget on the same key would only fight it.
|
|
747
|
+
runChild: gateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log', {
|
|
748
|
+
loader: deadAirBaseline
|
|
749
|
+
}),
|
|
750
|
+
// Names the deterministic step in the live status line.
|
|
751
|
+
onStage: label => {
|
|
752
|
+
stageLine = label;
|
|
753
|
+
},
|
|
754
|
+
// Deterministic whole-repo static-analysis gate — runs the project's
|
|
755
|
+
// own lint/typecheck and fails on a real non-zero exit, independent of
|
|
756
|
+
// the model-authored VERIFY block (which may not lint at all). ASYNC:
|
|
757
|
+
// the sync runner froze the event loop for the whole lint (see above).
|
|
758
|
+
repoHealth: () => deadAirBaseline ?
|
|
759
|
+
Promise.resolve(runRepoHealthCheck(cwd2))
|
|
760
|
+
: runRepoHealthCheckAsync(cwd2, {
|
|
761
|
+
signal,
|
|
762
|
+
onCommand: c => {
|
|
763
|
+
stageLine = `repo health · ${c}`;
|
|
764
|
+
}
|
|
765
|
+
}),
|
|
766
|
+
// The deterministic probes, bound in one place (buildVerifyProbes
|
|
767
|
+
// above); the PROBE_ADAPTERS table in verify-work.ts runs them.
|
|
768
|
+
probes: buildVerifyProbes({
|
|
697
769
|
cwd: cwd2,
|
|
698
770
|
signal,
|
|
771
|
+
taskId,
|
|
699
772
|
spec,
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
testAssemblyProbe: () => collectChangedFiles(cwd2, signal).then(changed => collectTestAssemblyFindings(cwd2, changed, signal)),
|
|
731
|
-
// Deterministic probe-gaming probe (F6): added lines whose stated
|
|
732
|
-
// purpose is to make a check pass rather than meet the requirement
|
|
733
|
-
// ("return 401 so the verification test passes") become rule-4c
|
|
734
|
-
// findings so the child verifies the real requirement, not the check.
|
|
735
|
-
probeGamingProbe: () => collectAddedLines(cwd2, signal).then(findProbeGaming),
|
|
736
|
-
// Deterministic cross-task deletion probe (mx5 run 12 PROMPT 2):
|
|
737
|
-
// tracked files this task's diff DELETES whose introducing commit
|
|
738
|
-
// belongs to a DIFFERENT task — a sibling's committed deliverable
|
|
739
|
-
// destroyed (typically to green a check). Injected under rule 4d and
|
|
740
|
-
// carried on a FAIL so an ACCEPT records durable debts.
|
|
741
|
-
crossTaskDeletionProbe: () => collectTaskTreeChanges(cwd2, signal).then(changes => findCrossTaskDeletions(changes, taskId, rel => taskThatIntroduced(cwd2, rel))),
|
|
742
|
-
// Deterministic sandbox-path-leak probe (mx5 run 13 PROMPT 4 item
|
|
743
|
-
// 1): absolute paths committed from the authoring child's own
|
|
744
|
-
// environment (`/workspace/src/shared`) that resolve nowhere here.
|
|
745
|
-
// Repaired deterministically where the relative form provably
|
|
746
|
-
// resolves; the remainder is injected under rule 4e, whose point is
|
|
747
|
-
// that such a path breaks the BUILD — so the checks that would have
|
|
748
|
-
// caught it report nothing rather than failing.
|
|
749
|
-
foreignPathProbe: () => collectForeignPathFindings(cwd2, signal, makeDebugAppender(path.join(tasksDir(cwd2), 'verify-debug.log'))),
|
|
750
|
-
// Deterministic neutered-check-script probe (mx5 run 13 PROMPT 4
|
|
751
|
-
// item 4): a check script this task authored that cannot fail
|
|
752
|
-
// (`… || true`, an inverted-grep launder). Injected under rule 4f,
|
|
753
|
-
// because the child provably cannot find this by running the
|
|
754
|
-
// script — it passes, which IS the defect.
|
|
755
|
-
scriptEscapeProbe: () => collectScriptEscapeFindings(cwd2, signal),
|
|
756
|
-
// Deterministic runner glob-collision probe (mx5 runs 7 AND 13,
|
|
757
|
-
// PROMPT 4 item 2): both `bun test` and `playwright test` declared
|
|
758
|
-
// with no proof their file sets are disjoint. Injected under rule
|
|
759
|
-
// 4g — the collision kills the suite during COLLECTION, which does
|
|
760
|
-
// not look like a test failure.
|
|
761
|
-
runnerGlobProbe: () => collectRunnerGlobFindings(cwd2),
|
|
762
|
-
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
763
|
-
// that the task's diff modified anyway become prompt-level findings
|
|
764
|
-
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
765
|
-
// diff` and cannot even see the violation.
|
|
766
|
-
prohibitionProbe: () => {
|
|
767
|
-
const banned = spec ? extractProhibitions(spec) : [];
|
|
768
|
-
if (banned.length === 0)
|
|
769
|
-
return Promise.resolve([]);
|
|
770
|
-
return collectChangedFiles(cwd2, signal).then(files => findProhibitionViolations(banned, files));
|
|
771
|
-
},
|
|
772
|
-
// Git-state guard result of the most recent child run: a verdict
|
|
773
|
-
// computed on a tree the child itself mutated is discarded — but ONLY
|
|
774
|
-
// when the mutation touched graded state (verdictTainted). A child
|
|
775
|
-
// that merely left test-runner output behind (test-results/,
|
|
776
|
-
// playwright-report/ …) judged an equivalent tree; its verdict stands
|
|
777
|
-
// and the artifacts were still cleaned (mx5 run 9 lost 7 verdicts this
|
|
778
|
-
// way — see git-state-guard.ts).
|
|
779
|
-
mutationCheck: () => lastGuardReconcile?.verdictTainted ?
|
|
780
|
-
{ mutated: true, detail: lastGuardReconcile.actions.join('; ') }
|
|
781
|
-
: { mutated: false, detail: '' },
|
|
782
|
-
// Per-run environment-facts cache under .pi-tasks/ (survives
|
|
783
|
-
// discardEdits): earlier children's discoveries save this child
|
|
784
|
-
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
785
|
-
// next one, stamped with this task's id as their origin so a
|
|
786
|
-
// later child sees a cited fact is second-hand and must
|
|
787
|
-
// re-validate before excusing a failure (F7). Facts only —
|
|
788
|
-
// verdict rules unaffected.
|
|
789
|
-
envNotes: {
|
|
790
|
-
read: () => readEnvNotes(cwd2),
|
|
791
|
-
append: notes => appendEnvNotes(cwd2, notes, taskId)
|
|
792
|
-
},
|
|
793
|
-
// Per-run cross-slice contract registry under .pi-tasks/ (F3): the
|
|
794
|
-
// verbatim interface facts the design pins that multiple slices
|
|
795
|
-
// share, so the verify child checks this slice's boundary against
|
|
796
|
-
// them. Empty on single-`/task` runs or a design with no shared
|
|
797
|
-
// boundary → no block.
|
|
798
|
-
contracts: () => readContracts(cwd2)
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
finally {
|
|
802
|
-
stopGateLoader();
|
|
803
|
-
}
|
|
773
|
+
log: makeDebugAppender(path.join(tasksDir(cwd2), 'verify-debug.log'))
|
|
774
|
+
}),
|
|
775
|
+
// Git-state guard result of the most recent child run: a verdict
|
|
776
|
+
// computed on a tree the child itself mutated is discarded — but ONLY
|
|
777
|
+
// when the mutation touched graded state (verdictTainted). A child
|
|
778
|
+
// that merely left test-runner output behind (test-results/,
|
|
779
|
+
// playwright-report/ …) judged an equivalent tree; its verdict stands
|
|
780
|
+
// and the artifacts were still cleaned (mx5 run 9 lost 7 verdicts this
|
|
781
|
+
// way — see git-state-guard.ts).
|
|
782
|
+
mutationCheck: () => lastGuardReconcile?.verdictTainted ?
|
|
783
|
+
{ mutated: true, detail: lastGuardReconcile.actions.join('; ') }
|
|
784
|
+
: { mutated: false, detail: '' },
|
|
785
|
+
// Per-run environment-facts cache under .pi-tasks/ (survives
|
|
786
|
+
// discardEdits): earlier children's discoveries save this child
|
|
787
|
+
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
788
|
+
// next one, stamped with this task's id as their origin so a
|
|
789
|
+
// later child sees a cited fact is second-hand and must
|
|
790
|
+
// re-validate before excusing a failure (F7). Facts only —
|
|
791
|
+
// verdict rules unaffected.
|
|
792
|
+
envNotes: {
|
|
793
|
+
read: () => readEnvNotes(cwd2),
|
|
794
|
+
append: notes => appendEnvNotes(cwd2, notes, taskId)
|
|
795
|
+
},
|
|
796
|
+
// Per-run cross-slice contract registry under .pi-tasks/ (F3): the
|
|
797
|
+
// verbatim interface facts the design pins that multiple slices
|
|
798
|
+
// share, so the verify child checks this slice's boundary against
|
|
799
|
+
// them. Empty on single-`/task` runs or a design with no shared
|
|
800
|
+
// boundary → no block.
|
|
801
|
+
contracts: () => readContracts(cwd2)
|
|
802
|
+
}));
|
|
804
803
|
},
|
|
805
804
|
lintFix: async (fixCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
806
805
|
// Same frozen extraction the enforce guard and the verify rule-4b
|
|
@@ -808,14 +807,8 @@ export function buildGateDeps(params) {
|
|
|
808
807
|
// tsconfig.json because ESLint's own error text instructed it, then
|
|
809
808
|
// verify failed the TASK for that edit — the child must know the
|
|
810
809
|
// spec's do-not-touch list AND be mechanically denied it).
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const { body } = await readTaskFile(cwd2, taskId);
|
|
814
|
-
frozenPaths = frozenPathsFromSpec(extractSpecForVerification(body));
|
|
815
|
-
}
|
|
816
|
-
catch {
|
|
817
|
-
// spec unreadable → no frozen paths; the guard degrades to a no-op
|
|
818
|
-
}
|
|
810
|
+
// (spec unreadable → no frozen paths; the guard degrades to a no-op)
|
|
811
|
+
const frozenPaths = frozenPathsFromSpec(await readSpecForVerification(cwd2, taskId));
|
|
819
812
|
return runBoundedLintFix({
|
|
820
813
|
cwd: cwd2,
|
|
821
814
|
signal,
|
|
@@ -906,14 +899,7 @@ export function buildGateDeps(params) {
|
|
|
906
899
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
907
900
|
// Read the same composed spec the verify gate judged against, so the
|
|
908
901
|
// recommendation reasons over the real contract (degrade to the bare title).
|
|
909
|
-
|
|
910
|
-
try {
|
|
911
|
-
const { body } = await readTaskFile(cwd2, taskId);
|
|
912
|
-
spec = extractSpecForVerification(body) ?? taskTitle;
|
|
913
|
-
}
|
|
914
|
-
catch {
|
|
915
|
-
spec = taskTitle;
|
|
916
|
-
}
|
|
902
|
+
const spec = (await readSpecForVerification(cwd2, taskId)) ?? taskTitle;
|
|
917
903
|
return researchResolution({
|
|
918
904
|
cwd: cwd2,
|
|
919
905
|
signal,
|