@mjasnikovs/pi-task 0.38.2 → 0.38.4
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/dist/config/config.d.ts +7 -0
- package/dist/config/config.js +10 -4
- package/dist/config/register.d.ts +37 -0
- package/dist/config/register.js +89 -114
- package/dist/remote/events.js +0 -3
- package/dist/remote/register.js +12 -3
- package/dist/task/auto-orchestrator.js +119 -94
- package/dist/task/command-run.d.ts +104 -0
- package/dist/task/command-run.js +138 -0
- package/dist/task/coverage-loop.d.ts +45 -0
- package/dist/task/critique-probes.d.ts +82 -0
- package/dist/task/critique-probes.js +156 -0
- package/dist/task/deep-render-check.d.ts +30 -0
- package/dist/task/deep-render-check.js +19 -11
- package/dist/task/enforce-guidelines.d.ts +14 -17
- package/dist/task/enforce-guidelines.js +44 -31
- package/dist/task/final-gate.d.ts +8 -10
- package/dist/task/final-gate.js +36 -74
- package/dist/task/gate-child.d.ts +104 -0
- package/dist/task/gate-child.js +177 -0
- package/dist/task/gate-deps.d.ts +13 -0
- package/dist/task/gate-deps.js +72 -208
- package/dist/task/orchestrator.js +13 -22
- package/dist/task/phases.js +109 -182
- package/dist/task/plan-session.d.ts +4 -22
- package/dist/task/plan-session.js +4 -33
- package/dist/task/question-dialog.d.ts +71 -0
- package/dist/task/question-dialog.js +89 -0
- package/dist/task/terminal-outcome.d.ts +67 -0
- package/dist/task/terminal-outcome.js +76 -0
- package/dist/task/type-only-answer.js +2 -3
- package/dist/workers/abstention.d.ts +71 -0
- package/dist/workers/abstention.js +108 -0
- package/dist/workers/docs-chunk.d.ts +74 -0
- package/dist/workers/docs-chunk.js +143 -0
- package/dist/workers/docs-core.d.ts +10 -1
- package/dist/workers/docs-core.js +22 -19
- package/dist/workers/docs-index.js +2 -69
- package/dist/workers/docs-project.d.ts +15 -1
- package/dist/workers/docs-project.js +27 -66
- package/dist/workers/fetch-core.d.ts +1 -1
- package/dist/workers/fetch-core.js +2 -1
- package/dist/workers/pi-worker-core.js +157 -86
- package/dist/workers/pi-worker-docs.js +5 -10
- package/dist/workers/pi-worker-fetch.js +8 -1
- package/dist/workers/typeonly-log.js +2 -10
- package/dist/workers/worker-failure.d.ts +91 -0
- package/dist/workers/worker-failure.js +82 -0
- package/package.json +1 -1
package/dist/task/final-gate.js
CHANGED
|
@@ -57,6 +57,7 @@ import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
|
57
57
|
import { runRenderCheck } from './render-check.js';
|
|
58
58
|
import { collectProjectEnv, pinnedLocalPort, runDeepRenderCheck } from './deep-render-check.js';
|
|
59
59
|
import { resolveRunner, runnerEnv, isCommandNotFound } from './runner-resolve.js';
|
|
60
|
+
import { classifyCommandRun, spawnCommand, outputTail, INFRA_GAP_OUTPUT_RE } from './command-run.js';
|
|
60
61
|
import { findLaunchConfigGap, probeEnv, configGapUnobservedNote } from './launch-config-gap.js';
|
|
61
62
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
62
63
|
import { findDanglingArtifacts, danglingGateFailureText } from './artifact-closure.js';
|
|
@@ -978,30 +979,6 @@ function resolveCommandBody(bin, args, scripts, makefile) {
|
|
|
978
979
|
}
|
|
979
980
|
return null;
|
|
980
981
|
}
|
|
981
|
-
/** Last ~`limit` chars of the command's combined output, one line, for the reason. */
|
|
982
|
-
function outputTail(stdout, stderr, limit = 400) {
|
|
983
|
-
const combined = `${stdout}\n${stderr}`.trim();
|
|
984
|
-
if (combined.length === 0)
|
|
985
|
-
return '';
|
|
986
|
-
const tail = combined.slice(-limit).replace(/\s+/g, ' ').trim();
|
|
987
|
-
return combined.length > limit ? `…${tail}` : tail;
|
|
988
|
-
}
|
|
989
|
-
/**
|
|
990
|
-
* A non-zero exit whose output shows an EXTERNAL runtime dependency is missing, not
|
|
991
|
-
* a code fault: a browser suite (Playwright/Cypress) whose browser binaries or system
|
|
992
|
-
* libraries were never installed here (mx5 run 10 item 2: `test:ct` must run in the
|
|
993
|
-
* gate, but on a box with no Playwright browsers it is an environment gap, not a FAIL).
|
|
994
|
-
* These exit non-zero (not 127), so they need output-shape recognition to skip.
|
|
995
|
-
*/
|
|
996
|
-
const ENV_GAP_OUTPUT_RE = /Executable doesn't exist|playwright install|browserType\.\w+: Executable|(?:wasn't|weren't) installed|Host system is missing dependencies|No usable sandbox|Cypress verification|Cypress executable (?:not found|was not found)|browser(?:s)? (?:is|are)? ?not installed/i;
|
|
997
|
-
/**
|
|
998
|
-
* A non-zero exit whose output shows the EXTERNAL INFRASTRUCTURE a launch script
|
|
999
|
-
* talks to is absent HERE — a database/daemon that is not running or not
|
|
1000
|
-
* installed — rather than a fault in the script itself. Applied ONLY to
|
|
1001
|
-
* launch-contract scripts (a migrate/seed against no DB is an environment gap on
|
|
1002
|
-
* this box; the same wording in a `test` run is a real failure the suite must own).
|
|
1003
|
-
*/
|
|
1004
|
-
export const INFRA_GAP_OUTPUT_RE = /ECONNREFUSED|connection refused|ENOTFOUND|EAI_AGAIN|is the server running|could not connect|cannot connect to the docker daemon|connect: connection|no such host/i;
|
|
1005
982
|
/**
|
|
1006
983
|
* Run one gate command with the env-gap contract: tool missing, timeout, or
|
|
1007
984
|
* command-not-found inside the script chain (127) → environment gap, not a code
|
|
@@ -1013,36 +990,20 @@ export const INFRA_GAP_OUTPUT_RE = /ECONNREFUSED|connection refused|ENOTFOUND|EA
|
|
|
1013
990
|
function runGateCommand(cwd, [bin, args], timeoutMs, extraGapRe,
|
|
1014
991
|
/** Replaces the child's environment wholesale (config-gap probe re-run only —
|
|
1015
992
|
* see launch-config-gap.ts). Absent ⇒ `runnerEnv(runner)`, i.e. unchanged. */
|
|
1016
|
-
envOverride
|
|
993
|
+
envOverride,
|
|
994
|
+
/** The spawner. Injected so the gate's own tests can script a verdict. */
|
|
995
|
+
run = spawnCommand) {
|
|
1017
996
|
// Runner resolution (mx5 run 16): a login-shell-stripped PATH left `bun`
|
|
1018
997
|
// unspawnable, so every dynamic check skipped and the gate went blind. The
|
|
1019
998
|
// resolved binary is spawned, and its directory rides on the child's PATH so
|
|
1020
999
|
// the SCRIPT CHAIN can re-invoke the runner (`bun run test` runs `bun test`
|
|
1021
1000
|
// inside — a bare 127 there is the same blindness one level down).
|
|
1022
|
-
// env passed explicitly: bun's spawnSync resolves the binary against a
|
|
1023
|
-
// startup snapshot of the environment, not the live process.env.
|
|
1024
1001
|
const runner = resolveRunner(bin);
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
timeout: timeoutMs,
|
|
1029
|
-
env: envOverride ?? runnerEnv(runner)
|
|
1030
|
-
});
|
|
1031
|
-
if (r.error)
|
|
1032
|
-
return { outcome: 'skip', spawnFailed: true };
|
|
1033
|
-
if (r.status === null)
|
|
1034
|
-
return { outcome: 'skip', spawnFailed: false };
|
|
1035
|
-
if (r.status !== 0) {
|
|
1036
|
-
const output = `${r.stdout ?? ''}\n${r.stderr ?? ''}`;
|
|
1037
|
-
if (isCommandNotFound(r.status, output))
|
|
1038
|
-
return { outcome: 'skip', spawnFailed: false };
|
|
1039
|
-
if (ENV_GAP_OUTPUT_RE.test(output))
|
|
1040
|
-
return { outcome: 'skip', spawnFailed: false };
|
|
1041
|
-
if (extraGapRe?.test(output))
|
|
1042
|
-
return { outcome: 'skip', spawnFailed: false };
|
|
1043
|
-
return { outcome: 'fail', status: r.status, tail: outputTail(r.stdout ?? '', r.stderr ?? '') };
|
|
1002
|
+
const verdict = classifyCommandRun(run({ cwd, bin: runner.bin, args, timeoutMs, env: envOverride ?? runnerEnv(runner) }), extraGapRe ? [extraGapRe] : []);
|
|
1003
|
+
if (verdict.outcome === 'gap') {
|
|
1004
|
+
return { outcome: 'skip', spawnFailed: verdict.gap === 'spawn-failed' };
|
|
1044
1005
|
}
|
|
1045
|
-
return
|
|
1006
|
+
return verdict;
|
|
1046
1007
|
}
|
|
1047
1008
|
/** The command word of a shell line, past any leading `VAR=value` assignments. */
|
|
1048
1009
|
function leadingBin(line) {
|
|
@@ -1068,31 +1029,28 @@ function leadingBin(line) {
|
|
|
1068
1029
|
* failure, missing tool, unreachable database, timeout, no POSIX shell — leaves the
|
|
1069
1030
|
* debt exactly as open as it was.
|
|
1070
1031
|
*/
|
|
1071
|
-
export function runVerifyCommandLine(cwd, line, timeoutMs, extraGapRe
|
|
1032
|
+
export function runVerifyCommandLine(cwd, line, timeoutMs, extraGapRe,
|
|
1033
|
+
/** The spawner. Injected so a re-run's outcome can be tested without one. */
|
|
1034
|
+
run = spawnCommand) {
|
|
1072
1035
|
const bin = leadingBin(line);
|
|
1073
1036
|
const runner = bin === null ? null : resolveRunner(bin);
|
|
1074
|
-
|
|
1037
|
+
// A VERIFY line is a SHELL line, not an argv — env prefixes, `&&` and
|
|
1038
|
+
// redirects are all ordinary there — so the runner spawns `sh -c`.
|
|
1039
|
+
const verdict = classifyCommandRun(run({
|
|
1075
1040
|
cwd,
|
|
1076
|
-
|
|
1077
|
-
|
|
1041
|
+
bin: 'sh',
|
|
1042
|
+
args: ['-c', line],
|
|
1043
|
+
timeoutMs,
|
|
1078
1044
|
env: runner ? runnerEnv(runner) : { ...process.env }
|
|
1079
|
-
})
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (
|
|
1086
|
-
return { outcome: '
|
|
1087
|
-
|
|
1088
|
-
return { outcome: 'gap', detail: 'command not found (127)' };
|
|
1089
|
-
}
|
|
1090
|
-
if (ENV_GAP_OUTPUT_RE.test(output))
|
|
1091
|
-
return { outcome: 'gap', detail: 'missing browser/runtime' };
|
|
1092
|
-
if (INFRA_GAP_OUTPUT_RE.test(output) || extraGapRe?.test(output) === true) {
|
|
1093
|
-
return { outcome: 'gap', detail: 'external infrastructure unreachable' };
|
|
1094
|
-
}
|
|
1095
|
-
return { outcome: 'fail', status: r.status, tail: outputTail(r.stdout ?? '', r.stderr ?? '') };
|
|
1045
|
+
}),
|
|
1046
|
+
// Infrastructure counts as a gap on EVERY debt re-run, not only on
|
|
1047
|
+
// request: an unreachable database cannot tell us whether the code is
|
|
1048
|
+
// fixed, and the asymmetry below means an inconclusive re-run simply
|
|
1049
|
+
// leaves the debt as open as it was.
|
|
1050
|
+
extraGapRe ? [INFRA_GAP_OUTPUT_RE, extraGapRe] : [INFRA_GAP_OUTPUT_RE]);
|
|
1051
|
+
if (verdict.outcome === 'gap')
|
|
1052
|
+
return { outcome: 'gap', detail: verdict.detail };
|
|
1053
|
+
return verdict;
|
|
1096
1054
|
}
|
|
1097
1055
|
/**
|
|
1098
1056
|
* The full-skip blindness guard (mx5 run 16, validated): dynamic commands were
|
|
@@ -1312,17 +1270,21 @@ const DEBT_INFRA_GAP_RE = /ERR_POSTGRES_CONNECTION_CLOSED|ERR_MYSQL_CONNECTION|E
|
|
|
1312
1270
|
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
1313
1271
|
* assumption rather than an observation.
|
|
1314
1272
|
*/
|
|
1315
|
-
export function rerunDebtVerifyCommand(cwd, command
|
|
1273
|
+
export function rerunDebtVerifyCommand(cwd, command,
|
|
1274
|
+
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
1275
|
+
* the guard's four outcomes are testable without a repo or a real command. */
|
|
1276
|
+
run = spawnCommand) {
|
|
1316
1277
|
const tracked = () => {
|
|
1317
|
-
const r =
|
|
1278
|
+
const r = run({
|
|
1318
1279
|
cwd,
|
|
1319
|
-
|
|
1320
|
-
|
|
1280
|
+
bin: 'git',
|
|
1281
|
+
args: ['status', '--porcelain', '--untracked-files=no'],
|
|
1282
|
+
timeoutMs: 60_000
|
|
1321
1283
|
});
|
|
1322
|
-
return r.
|
|
1284
|
+
return r.failedToStart || r.status !== 0 ? null : r.stdout;
|
|
1323
1285
|
};
|
|
1324
1286
|
const before = tracked();
|
|
1325
|
-
const r = runVerifyCommandLine(cwd, command, DEBT_RERUN_TIMEOUT_MS, DEBT_INFRA_GAP_RE);
|
|
1287
|
+
const r = runVerifyCommandLine(cwd, command, DEBT_RERUN_TIMEOUT_MS, DEBT_INFRA_GAP_RE, run);
|
|
1326
1288
|
if (r.outcome === 'fail')
|
|
1327
1289
|
return { outcome: 'fail', detail: `exit ${r.status} — ${r.tail}` };
|
|
1328
1290
|
if (r.outcome === 'gap')
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-child — running ONE gate child pi, and the table of what each kind of gate
|
|
3
|
+
* child is allowed to do.
|
|
4
|
+
*
|
|
5
|
+
* Five children run under the gates: `verify`, `recommend`, `lint-fix`,
|
|
6
|
+
* `final-fix` and `enforce`. All five share one ritual — reset the widget state,
|
|
7
|
+
* stamp the start, open a per-gate debug log, write a start marker, raise a
|
|
8
|
+
* status loader, call `runWorker` unguarded (no wall clock, exact-match loop
|
|
9
|
+
* guard only), warn on a surviving loop, classify the failure, write an end
|
|
10
|
+
* marker, throw on failure, and stop the loader in a `finally`. That was ~85
|
|
11
|
+
* lines, and it was written TWICE: once as `makeGateChild` and once as an inline
|
|
12
|
+
* closure for `enforce`, whose comments repeated the originals verbatim.
|
|
13
|
+
*
|
|
14
|
+
* The enforce copy differed in exactly four things — no git-state guard, no
|
|
15
|
+
* tool-result logging, no tree-change capture, and a different end marker — which
|
|
16
|
+
* is why they are row data here rather than a forked body. Apply the deletion
|
|
17
|
+
* test to that copy and it passes: routing enforce through this concentrates the
|
|
18
|
+
* differences into a table instead of moving them.
|
|
19
|
+
*
|
|
20
|
+
* The second reason for the module is that all of it used to live inside
|
|
21
|
+
* `buildGateDeps`'s closure, so nothing about it was reachable from a test:
|
|
22
|
+
* `buildGateDeps` is ~700 lines and is never called by the suite. The
|
|
23
|
+
* git-state-guard wiring in particular — snapshot, restore-in-`finally`,
|
|
24
|
+
* `verdictTainted` — is the mechanism that discards a verify verdict computed on
|
|
25
|
+
* a tree the child mutated (mx5 run 6, where the verify child `git stash`ed the
|
|
26
|
+
* task's uncommitted work and never popped it), and it could only be checked
|
|
27
|
+
* indirectly through a fake `mutationCheck` one layer up. Here `runWorker` and
|
|
28
|
+
* the git helpers are injected, so the ordering, the trail lines and the
|
|
29
|
+
* throwing-child path are all directly assertable.
|
|
30
|
+
*/
|
|
31
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
32
|
+
import type { RunWorkerInput, RunWorkerResult } from '../workers/pi-worker-core.js';
|
|
33
|
+
import type { GitStateSnapshot, ReconcileResult } from './git-state-guard.js';
|
|
34
|
+
import type { ContextSnapshot } from '../shared/child-process.js';
|
|
35
|
+
import type { AutoLoaderState } from './widget.js';
|
|
36
|
+
/** Which gate child this is. */
|
|
37
|
+
export type GateChildKind = 'verify' | 'recommend' | 'lint-fix' | 'final-fix' | 'enforce';
|
|
38
|
+
export interface GateChildRow {
|
|
39
|
+
/**
|
|
40
|
+
* Snapshot the tree before and restore after. These children are read-only BY
|
|
41
|
+
* CONTRACT, but the contract is prompt-level and the live model breaks it.
|
|
42
|
+
* `lint-fix` and `final-fix` are excluded because editing is their job (they
|
|
43
|
+
* carry their own revert guards), and `enforce` because it edits too.
|
|
44
|
+
*/
|
|
45
|
+
guarded: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Log tool OUTPUTS, not just the calls (mx5 run 10 item 6): without the result,
|
|
48
|
+
* "verify claimed curl PASS on a server that cannot serve" is undecidable from
|
|
49
|
+
* the log. Off for `enforce`, whose log is a per-pass verdict trail.
|
|
50
|
+
*/
|
|
51
|
+
logToolResults: boolean;
|
|
52
|
+
/** The loader's step label. */
|
|
53
|
+
step: string;
|
|
54
|
+
/** The end-marker written on success. */
|
|
55
|
+
okMarker: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What each kind may do. Adding a child is a row; it cannot be added without
|
|
59
|
+
* deciding all four questions, which is the point.
|
|
60
|
+
*/
|
|
61
|
+
export declare const GATE_CHILD_KINDS: Record<GateChildKind, GateChildRow>;
|
|
62
|
+
/** Everything the runner needs that is not a property of the KIND. */
|
|
63
|
+
export interface GateChildDeps {
|
|
64
|
+
ctx: ExtensionCommandContext;
|
|
65
|
+
cwd: string;
|
|
66
|
+
taskTitle: string;
|
|
67
|
+
kind: GateChildKind;
|
|
68
|
+
/** Absolute path of the debug log for this gate. */
|
|
69
|
+
logPath: string;
|
|
70
|
+
/**
|
|
71
|
+
* `false` when the CALLER already renders a loader spanning this child (the
|
|
72
|
+
* verify gate does). Two loaders on one widget key only fight each other.
|
|
73
|
+
*/
|
|
74
|
+
loader?: boolean;
|
|
75
|
+
/** Per-command ceiling; pi's bash tool has no default timeout. */
|
|
76
|
+
commandTimeoutMs: number;
|
|
77
|
+
/** Hung-stream bound; the probe-based stall guard cannot supply it. */
|
|
78
|
+
streamInactivityMs: number;
|
|
79
|
+
parentContextWindow: number;
|
|
80
|
+
runWorker: (input: RunWorkerInput) => Promise<RunWorkerResult>;
|
|
81
|
+
makeDebugAppender: (path: string) => (line: string, level?: 'event' | 'stream') => void;
|
|
82
|
+
startAutoLoader: (ctx: ExtensionCommandContext, getState: () => AutoLoaderState | null) => () => void;
|
|
83
|
+
captureGitState: (cwd: string, signal?: AbortSignal) => Promise<GitStateSnapshot>;
|
|
84
|
+
reconcileGitState: (cwd: string, snapshot: GitStateSnapshot, signal?: AbortSignal) => Promise<ReconcileResult>;
|
|
85
|
+
/** Tree changes for a WRITE-capable child, already formatted. */
|
|
86
|
+
describeTreeChanges: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
87
|
+
resolveContextUsage: (snapshot: ContextSnapshot, prev: ContextSnapshot | undefined, parentContextWindow: number) => ContextSnapshot;
|
|
88
|
+
truncateToolResult: (text: string) => string;
|
|
89
|
+
/** Where the live widget reads `lastLine` / `contextUsage` from. */
|
|
90
|
+
widget: {
|
|
91
|
+
lastLine?: string;
|
|
92
|
+
contextUsage?: ContextSnapshot;
|
|
93
|
+
};
|
|
94
|
+
/** Set to the last reconcile so the caller can discard a tainted verdict. */
|
|
95
|
+
onReconcile?: (rec: ReconcileResult) => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the `runChild` closure the gate steps expect.
|
|
99
|
+
*
|
|
100
|
+
* The `finally` ordering is load-bearing: the git-state restore runs BEFORE any
|
|
101
|
+
* verdict or failure is acted on, and it runs even when the child THREW — a
|
|
102
|
+
* crashed child must not skip the restore.
|
|
103
|
+
*/
|
|
104
|
+
export declare function makeGateChild(deps: GateChildDeps): (tools: string, prompt: string, sig?: AbortSignal) => Promise<string>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-child — running ONE gate child pi, and the table of what each kind of gate
|
|
3
|
+
* child is allowed to do.
|
|
4
|
+
*
|
|
5
|
+
* Five children run under the gates: `verify`, `recommend`, `lint-fix`,
|
|
6
|
+
* `final-fix` and `enforce`. All five share one ritual — reset the widget state,
|
|
7
|
+
* stamp the start, open a per-gate debug log, write a start marker, raise a
|
|
8
|
+
* status loader, call `runWorker` unguarded (no wall clock, exact-match loop
|
|
9
|
+
* guard only), warn on a surviving loop, classify the failure, write an end
|
|
10
|
+
* marker, throw on failure, and stop the loader in a `finally`. That was ~85
|
|
11
|
+
* lines, and it was written TWICE: once as `makeGateChild` and once as an inline
|
|
12
|
+
* closure for `enforce`, whose comments repeated the originals verbatim.
|
|
13
|
+
*
|
|
14
|
+
* The enforce copy differed in exactly four things — no git-state guard, no
|
|
15
|
+
* tool-result logging, no tree-change capture, and a different end marker — which
|
|
16
|
+
* is why they are row data here rather than a forked body. Apply the deletion
|
|
17
|
+
* test to that copy and it passes: routing enforce through this concentrates the
|
|
18
|
+
* differences into a table instead of moving them.
|
|
19
|
+
*
|
|
20
|
+
* The second reason for the module is that all of it used to live inside
|
|
21
|
+
* `buildGateDeps`'s closure, so nothing about it was reachable from a test:
|
|
22
|
+
* `buildGateDeps` is ~700 lines and is never called by the suite. The
|
|
23
|
+
* git-state-guard wiring in particular — snapshot, restore-in-`finally`,
|
|
24
|
+
* `verdictTainted` — is the mechanism that discards a verify verdict computed on
|
|
25
|
+
* a tree the child mutated (mx5 run 6, where the verify child `git stash`ed the
|
|
26
|
+
* task's uncommitted work and never popped it), and it could only be checked
|
|
27
|
+
* indirectly through a fake `mutationCheck` one layer up. Here `runWorker` and
|
|
28
|
+
* the git helpers are injected, so the ordering, the trail lines and the
|
|
29
|
+
* throwing-child path are all directly assertable.
|
|
30
|
+
*/
|
|
31
|
+
import { formatLoopHint } from './child-runner.js';
|
|
32
|
+
import { classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
33
|
+
/**
|
|
34
|
+
* What each kind may do. Adding a child is a row; it cannot be added without
|
|
35
|
+
* deciding all four questions, which is the point.
|
|
36
|
+
*/
|
|
37
|
+
export const GATE_CHILD_KINDS = {
|
|
38
|
+
verify: { guarded: true, logToolResults: true, step: 'verify', okMarker: 'ok' },
|
|
39
|
+
recommend: { guarded: true, logToolResults: true, step: 'recommend', okMarker: 'ok' },
|
|
40
|
+
// Editing is lint-fix's job, so the guard would revert its work.
|
|
41
|
+
'lint-fix': { guarded: false, logToolResults: true, step: 'lint-fix', okMarker: 'ok' },
|
|
42
|
+
'final-fix': { guarded: false, logToolResults: true, step: 'final-fix', okMarker: 'ok' },
|
|
43
|
+
enforce: {
|
|
44
|
+
guarded: false,
|
|
45
|
+
logToolResults: false,
|
|
46
|
+
step: 'guidelines',
|
|
47
|
+
okMarker: 'verdict captured'
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Build the `runChild` closure the gate steps expect.
|
|
52
|
+
*
|
|
53
|
+
* The `finally` ordering is load-bearing: the git-state restore runs BEFORE any
|
|
54
|
+
* verdict or failure is acted on, and it runs even when the child THREW — a
|
|
55
|
+
* crashed child must not skip the restore.
|
|
56
|
+
*/
|
|
57
|
+
export function makeGateChild(deps) {
|
|
58
|
+
const row = GATE_CHILD_KINDS[deps.kind];
|
|
59
|
+
return async (tools, prompt, sig) => {
|
|
60
|
+
deps.widget.lastLine = undefined;
|
|
61
|
+
deps.widget.contextUsage = undefined;
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
// Every marker below (start/end, the guard's restore, the loop warning, a
|
|
64
|
+
// write-capable child's tree changes) is a guard record that survives at
|
|
65
|
+
// the default level. Only the child's own stdout and its tool results
|
|
66
|
+
// pass 'stream'.
|
|
67
|
+
const log = deps.makeDebugAppender(deps.logPath);
|
|
68
|
+
log(`=== ${deps.kind} start: ${deps.taskTitle} ===`);
|
|
69
|
+
const guardSnapshot = row.guarded ? await deps.captureGitState(deps.cwd, sig) : null;
|
|
70
|
+
const stopLoader = deps.loader === false ?
|
|
71
|
+
() => { }
|
|
72
|
+
: deps.startAutoLoader(deps.ctx, () => ({
|
|
73
|
+
title: deps.taskTitle,
|
|
74
|
+
kind: deps.kind,
|
|
75
|
+
step: row.step,
|
|
76
|
+
stepNum: 1,
|
|
77
|
+
stepTotal: 1,
|
|
78
|
+
startedAt,
|
|
79
|
+
lastLine: deps.widget.lastLine,
|
|
80
|
+
contextUsage: deps.widget.contextUsage
|
|
81
|
+
}));
|
|
82
|
+
try {
|
|
83
|
+
let r;
|
|
84
|
+
try {
|
|
85
|
+
r = await deps.runWorker({
|
|
86
|
+
prompt,
|
|
87
|
+
cwd: deps.cwd,
|
|
88
|
+
...(sig ? { signal: sig } : {}),
|
|
89
|
+
tools,
|
|
90
|
+
// Run to completion: these passes legitimately read and edit the
|
|
91
|
+
// same file many times, and the research-worker guards mislabel
|
|
92
|
+
// that as a runaway and kill good work (mx5 TASK_0002).
|
|
93
|
+
timeoutMs: 0,
|
|
94
|
+
commandTimeoutMs: deps.commandTimeoutMs,
|
|
95
|
+
streamInactivityMs: deps.streamInactivityMs,
|
|
96
|
+
// Exact-match loop guard only: pathThreshold Infinity disables
|
|
97
|
+
// the path-revisit heuristic, so revisiting one file (which IS
|
|
98
|
+
// the job) never trips — only a literally-identical call
|
|
99
|
+
// repeated past threshold does.
|
|
100
|
+
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
101
|
+
// A discarded attempt is otherwise invisible: the returned
|
|
102
|
+
// exitCode/text describe the FINAL attempt, so a child that
|
|
103
|
+
// burned two attempts reads exactly like one that ran clean.
|
|
104
|
+
onRestart: rs => log(`=== ${deps.kind} RESTART (attempt ${rs.attempt} discarded)`
|
|
105
|
+
+ ` reason=${rs.reason} wall=${rs.wallMs}ms`
|
|
106
|
+
+ (rs.detail ? ` — ${rs.detail}` : '')
|
|
107
|
+
+ ' ==='),
|
|
108
|
+
onLine: line => {
|
|
109
|
+
// `lastLine` feeds the LIVE widget and is not logging — it
|
|
110
|
+
// stays outside the gate, or a quiet trail would also blank
|
|
111
|
+
// the progress display.
|
|
112
|
+
deps.widget.lastLine = line;
|
|
113
|
+
log(line, 'stream');
|
|
114
|
+
},
|
|
115
|
+
...(row.logToolResults ?
|
|
116
|
+
{
|
|
117
|
+
onToolResult: ({ name, isError, text }) => log(`↳ ${name} [${isError ? 'ERR' : 'ok'}]: `
|
|
118
|
+
+ deps.truncateToolResult(text), 'stream')
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
121
|
+
onContextUsage: snapshot => {
|
|
122
|
+
deps.widget.contextUsage = deps.resolveContextUsage(snapshot, deps.widget.contextUsage, deps.parentContextWindow);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
// Restore whatever the child moved BEFORE any verdict or failure is
|
|
128
|
+
// acted on — a crashed child must not skip the restore either.
|
|
129
|
+
if (guardSnapshot) {
|
|
130
|
+
const rec = await deps.reconcileGitState(deps.cwd, guardSnapshot, sig);
|
|
131
|
+
deps.onReconcile?.(rec);
|
|
132
|
+
if (rec.mutated) {
|
|
133
|
+
// Distinguish the two outcomes in the trail: a tainting
|
|
134
|
+
// mutation (graded work altered → verdict discarded) vs
|
|
135
|
+
// benign cleanup (test-runner output the child left behind
|
|
136
|
+
// → verdict stands).
|
|
137
|
+
const label = rec.verdictTainted ?
|
|
138
|
+
'child mutated graded state (verdict discarded)'
|
|
139
|
+
: 'cleaned child test-runner artifacts (verdict kept)';
|
|
140
|
+
log(`=== ${deps.kind} GIT-STATE GUARD — ${label}; `
|
|
141
|
+
+ `restored: ${rec.actions.join('; ')} ===`);
|
|
142
|
+
if (rec.verdictTainted) {
|
|
143
|
+
deps.ctx.ui.notify(`${deps.taskTitle}: ${deps.kind} child mutated repo state — `
|
|
144
|
+
+ `restored (${rec.actions.join('; ').slice(0, 140)}).`, 'warning');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// A loop that survived the restart-with-hint nudges is a WARNING, not a
|
|
150
|
+
// failure: log it and tell the user, but let the verdict gate be the
|
|
151
|
+
// only thing that can block.
|
|
152
|
+
if (r.loopHit) {
|
|
153
|
+
log(`=== ${deps.kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
154
|
+
deps.ctx.ui.notify(`${deps.taskTitle}: ${deps.kind} worker looped past the nudges — `
|
|
155
|
+
+ 'continuing (not blocked).', 'warning');
|
|
156
|
+
}
|
|
157
|
+
const failure = classifyEnforceChildFailure(r);
|
|
158
|
+
log(failure ?
|
|
159
|
+
`=== ${deps.kind} end: FAIL — ${failure} ===`
|
|
160
|
+
: `=== ${deps.kind} end: ${row.okMarker} ===`);
|
|
161
|
+
if (failure)
|
|
162
|
+
throw new Error(failure);
|
|
163
|
+
// CAPABILITY-LEVEL diff capture (mx5 run 11): any WRITE-capable child —
|
|
164
|
+
// decided by its TOOLS, not by which phase spawned it — gets its tree
|
|
165
|
+
// changes logged, so a future write-capable kind cannot run invisibly
|
|
166
|
+
// the way the final-fix child's `rm` did.
|
|
167
|
+
if (/\b(?:edit|bash|write)\b/.test(tools)) {
|
|
168
|
+
log(`=== ${deps.kind} tree changes: `
|
|
169
|
+
+ `${await deps.describeTreeChanges(deps.cwd, sig)} ===`);
|
|
170
|
+
}
|
|
171
|
+
return r.text;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
stopLoader();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -38,6 +38,19 @@ export declare function collectChangedFiles(cwd: string, signal?: AbortSignal):
|
|
|
38
38
|
* never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
|
|
39
39
|
*/
|
|
40
40
|
export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Promise<AddedLine[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic sandbox-path-leak pass (see foreign-path.ts, mx5 run 13 PROMPT 4
|
|
43
|
+
* item 1): find absolute paths the task committed that resolve nowhere on this
|
|
44
|
+
* machine while the real file sits in the repo, REPAIR the ones whose relative
|
|
45
|
+
* form provably resolves, and return verify findings for whatever is left.
|
|
46
|
+
*
|
|
47
|
+
* The repair runs here, before the verify child, for the same reason lint-fix
|
|
48
|
+
* does: the defect is mechanical and the correct target is already known, so
|
|
49
|
+
* spending an AUTOFIX round (or a human) on a path substitution is waste. What it
|
|
50
|
+
* cannot repair still reaches the child under rule 4e. Failures degrade to no
|
|
51
|
+
* findings — a sharpener, never a blocker.
|
|
52
|
+
*/
|
|
53
|
+
export declare function collectForeignPathFindings(cwd: string, signal?: AbortSignal, logDebug?: (m: string) => void): Promise<string[]>;
|
|
41
54
|
/**
|
|
42
55
|
* Deterministic neutered-check-script pass (see script-escape.ts, mx5 run 13 PROMPT
|
|
43
56
|
* 4 item 4): check-class scripts that cannot report failure, in a manifest THIS
|