@mjasnikovs/pi-task 0.38.2 → 0.38.3
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/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.js +57 -205
- 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
|
@@ -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.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as fsp from 'node:fs/promises';
|
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import { tasksDir, readTaskFile, appendGateRecord } from './task-io.js';
|
|
20
20
|
import { gitCommitAll, gitDropLastCommit, git } from './auto-commit.js';
|
|
21
|
-
import { runGuidelineEnforcement
|
|
21
|
+
import { runGuidelineEnforcement } from './enforce-guidelines.js';
|
|
22
22
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
23
23
|
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
24
24
|
import { readContracts } from './contracts.js';
|
|
@@ -41,11 +41,11 @@ import { findScriptEscapesInManifest, scriptEscapeVerifyFindings } from './scrip
|
|
|
41
41
|
import { assessRunnerGlobs, runnerGlobVerifyFindings } from './runner-globs.js';
|
|
42
42
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
43
43
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
44
|
-
import { formatLoopHint } from './child-runner.js';
|
|
45
44
|
import { getConfig } from '../config/config.js';
|
|
46
45
|
import { makeDebugAppender } from './debug-log.js';
|
|
47
46
|
import { startAutoLoader } from './widget.js';
|
|
48
47
|
import { resolveContextUsage } from './context-usage.js';
|
|
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;
|
|
51
51
|
/**
|
|
@@ -474,136 +474,51 @@ export function buildGateDeps(params) {
|
|
|
474
474
|
await git(cwd2, ['checkout', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
475
475
|
await git(cwd2, ['clean', '-fd', '-e', '.pi-tasks'], signal);
|
|
476
476
|
};
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
log(`=== ${kind} start: ${taskTitle} ===`);
|
|
498
|
-
// GIT-STATE GUARD: these children are read-only BY CONTRACT, but the
|
|
499
|
-
// contract is prompt-level and the live model breaks it (mx5 run 6: the
|
|
500
|
-
// verify child `git stash`ed the task's uncommitted work and never popped
|
|
501
|
-
// — the impl was destroyed and the orphan stash detonated 2 days later).
|
|
502
|
-
// Snapshot before, deterministically restore after; lint-fix is excluded
|
|
503
|
-
// because editing is its job (it carries its own revert guard).
|
|
504
|
-
const guardSnapshot = kind === 'verify' || kind === 'recommend' ? await captureGitState(cwd2, sig) : null;
|
|
505
|
-
const stopLoader = opts.loader === false ?
|
|
506
|
-
() => { }
|
|
507
|
-
: startAutoLoader(gateCtx, () => ({
|
|
508
|
-
title: taskTitle,
|
|
509
|
-
kind,
|
|
510
|
-
step: kind,
|
|
511
|
-
stepNum: 1,
|
|
512
|
-
stepTotal: 1,
|
|
513
|
-
startedAt,
|
|
514
|
-
lastLine,
|
|
515
|
-
contextUsage
|
|
516
|
-
}));
|
|
517
|
-
try {
|
|
518
|
-
let r;
|
|
519
|
-
try {
|
|
520
|
-
r = await runWorker({
|
|
521
|
-
prompt,
|
|
522
|
-
cwd: cwd2,
|
|
523
|
-
signal: sig,
|
|
524
|
-
tools,
|
|
525
|
-
timeoutMs: 0,
|
|
526
|
-
// The gate child runs to completion (timeoutMs 0), but a
|
|
527
|
-
// single command inside it must still be bounded: pi's bash
|
|
528
|
-
// tool has no default timeout, so a `bun run dev` / hung
|
|
529
|
-
// check the model forgot to bound wedges the gate forever.
|
|
530
|
-
// The stall guard cannot see it — a reachable model endpoint
|
|
531
|
-
// reads as proof of life while the command blocks. Same
|
|
532
|
-
// ceiling the main session uses, so one /task-config knob
|
|
533
|
-
// covers implementation and gates alike.
|
|
534
|
-
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
535
|
-
// Same reasoning one level up: a gate child with no
|
|
536
|
-
// wall-clock cap also needs the HUNG-STREAM bound, which
|
|
537
|
-
// the probe-based stall guard structurally cannot supply
|
|
538
|
-
// (a healthy endpoint reads as proof of life).
|
|
539
|
-
streamInactivityMs: getConfig().streamInactivityMs,
|
|
540
|
-
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
541
|
-
// A discarded attempt is otherwise invisible here too: the
|
|
542
|
-
// returned exitCode/text describe the FINAL attempt, so a
|
|
543
|
-
// gate child that burned two attempts and its wall clock
|
|
544
|
-
// reads exactly like one that ran clean.
|
|
545
|
-
onRestart: rs => log(`=== ${kind} RESTART (attempt ${rs.attempt} discarded)`
|
|
546
|
-
+ ` reason=${rs.reason} wall=${rs.wallMs}ms`
|
|
547
|
-
+ (rs.detail ? ` — ${rs.detail}` : '')
|
|
548
|
-
+ ' ==='),
|
|
549
|
-
onLine: line => {
|
|
550
|
-
// `lastLine` feeds the LIVE status widget and is not
|
|
551
|
-
// logging — it stays outside the gate, or a quiet
|
|
552
|
-
// trail would also blank the progress display.
|
|
553
|
-
lastLine = line;
|
|
554
|
-
log(line, 'stream');
|
|
555
|
-
},
|
|
556
|
-
// Log tool OUTPUTS, not just the command (mx5 run 10 item 6):
|
|
557
|
-
// without the result "verify claimed curl PASS on a server that
|
|
558
|
-
// cannot serve" is undecidable from the log. Truncated, tail-kept
|
|
559
|
-
// (a bind failure / status usually lands at the end), error-flagged.
|
|
560
|
-
onToolResult: ({ name, isError, text }) => log(`↳ ${name} [${isError ? 'ERR' : 'ok'}]: ${truncateToolResult(text)}`, 'stream'),
|
|
561
|
-
onContextUsage: snapshot => {
|
|
562
|
-
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
563
|
-
}
|
|
564
|
-
});
|
|
477
|
+
// Adapter onto the shared gate-child runner (gate-child.ts). What survives
|
|
478
|
+
// here is WIRING — which context, which log file, which config knobs, and
|
|
479
|
+
// where the live-widget state lives; the ritual and the per-kind policy are
|
|
480
|
+
// the table's. The enforce child below goes through the same call.
|
|
481
|
+
const gateChild = (gateCtx, cwd2, taskTitle, kind, logFile, opts = {}) => {
|
|
482
|
+
// An accessor box, not a copy: the runner writes these fields and the
|
|
483
|
+
// loader snapshot reads them on every tick, so both must see the same
|
|
484
|
+
// closure state the rest of buildGateDeps already shares.
|
|
485
|
+
const widget = {
|
|
486
|
+
get lastLine() {
|
|
487
|
+
return lastLine;
|
|
488
|
+
},
|
|
489
|
+
set lastLine(v) {
|
|
490
|
+
lastLine = v;
|
|
491
|
+
},
|
|
492
|
+
get contextUsage() {
|
|
493
|
+
return contextUsage;
|
|
494
|
+
},
|
|
495
|
+
set contextUsage(v) {
|
|
496
|
+
contextUsage = v;
|
|
565
497
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
log(`=== ${kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
589
|
-
gateCtx.ui.notify(`${taskTitle}: ${kind} worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
590
|
-
}
|
|
591
|
-
const failure = classifyEnforceChildFailure(r);
|
|
592
|
-
log(failure ? `=== ${kind} end: FAIL — ${failure} ===` : `=== ${kind} end: ok ===`);
|
|
593
|
-
if (failure)
|
|
594
|
-
throw new Error(failure);
|
|
595
|
-
// CAPABILITY-LEVEL diff capture (mx5 run 11): any WRITE-capable
|
|
596
|
-
// child — decided by its tools, not by which phase spawned it —
|
|
597
|
-
// gets its tree changes logged, so a future write-capable kind
|
|
598
|
-
// cannot run invisibly the way the final-fix child's `rm` did.
|
|
599
|
-
if (/\b(?:edit|bash|write)\b/.test(tools)) {
|
|
600
|
-
log(`=== ${kind} tree changes: ${formatTreeChanges(await collectTreeChanges(cwd2, sig))} ===`);
|
|
498
|
+
};
|
|
499
|
+
return makeGateChild({
|
|
500
|
+
ctx: gateCtx,
|
|
501
|
+
cwd: cwd2,
|
|
502
|
+
taskTitle,
|
|
503
|
+
kind,
|
|
504
|
+
logPath: path.join(tasksDir(cwd2), logFile),
|
|
505
|
+
...(opts.loader === undefined ? {} : { loader: opts.loader }),
|
|
506
|
+
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
507
|
+
streamInactivityMs: getConfig().streamInactivityMs,
|
|
508
|
+
parentContextWindow,
|
|
509
|
+
runWorker,
|
|
510
|
+
makeDebugAppender,
|
|
511
|
+
startAutoLoader,
|
|
512
|
+
captureGitState,
|
|
513
|
+
reconcileGitState,
|
|
514
|
+
describeTreeChanges: async (c, sig) => formatTreeChanges(await collectTreeChanges(c, sig)),
|
|
515
|
+
resolveContextUsage,
|
|
516
|
+
truncateToolResult,
|
|
517
|
+
widget,
|
|
518
|
+
onReconcile: rec => {
|
|
519
|
+
lastGuardReconcile = rec;
|
|
601
520
|
}
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
finally {
|
|
605
|
-
stopLoader();
|
|
606
|
-
}
|
|
521
|
+
});
|
|
607
522
|
};
|
|
608
523
|
return {
|
|
609
524
|
runTask,
|
|
@@ -710,77 +625,14 @@ export function buildGateDeps(params) {
|
|
|
710
625
|
// research-worker guards mislabel that as a runaway and kill good work
|
|
711
626
|
// (proven on mx5 TASK_0002). classifyEnforceChildFailure still blocks
|
|
712
627
|
// on a real failure (non-zero exit, leaked tool call) or a user cancel.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
title: taskTitle,
|
|
722
|
-
kind: 'enforce',
|
|
723
|
-
step: 'guidelines',
|
|
724
|
-
stepNum: 1,
|
|
725
|
-
stepTotal: 1,
|
|
726
|
-
startedAt,
|
|
727
|
-
lastLine,
|
|
728
|
-
contextUsage
|
|
729
|
-
}));
|
|
730
|
-
try {
|
|
731
|
-
const r = await runWorker({
|
|
732
|
-
prompt,
|
|
733
|
-
cwd: cwd2,
|
|
734
|
-
signal: sig,
|
|
735
|
-
tools,
|
|
736
|
-
timeoutMs: 0, // no wall-clock timeout — run to completion
|
|
737
|
-
// …but still bound any SINGLE command (see makeGateChild).
|
|
738
|
-
// enforce is read,edit today, so nothing here can hang on
|
|
739
|
-
// bash — wired anyway so a future tool grant can't quietly
|
|
740
|
-
// re-open the hole.
|
|
741
|
-
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
742
|
-
// Unbounded wall clock here too — the hung-stream
|
|
743
|
-
// bound is the only thing that ends a dead stream.
|
|
744
|
-
streamInactivityMs: getConfig().streamInactivityMs,
|
|
745
|
-
// Exact-match loop guard only: pathThreshold Infinity
|
|
746
|
-
// disables the path-revisit heuristic, so revisiting one
|
|
747
|
-
// file (which IS this pass's job) never trips — only a
|
|
748
|
-
// literally-identical call repeated past threshold does.
|
|
749
|
-
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
750
|
-
// Same reasoning as the gate child: without this a
|
|
751
|
-
// discarded attempt leaves no trace anywhere.
|
|
752
|
-
onRestart: rs => logEnforce(`=== enforce RESTART (attempt ${rs.attempt} discarded)`
|
|
753
|
-
+ ` reason=${rs.reason} wall=${rs.wallMs}ms`
|
|
754
|
-
+ (rs.detail ? ` — ${rs.detail}` : '')
|
|
755
|
-
+ ' ==='),
|
|
756
|
-
onLine: line => {
|
|
757
|
-
// `lastLine` drives the live widget, not the trail.
|
|
758
|
-
lastLine = line;
|
|
759
|
-
logEnforce(line, 'stream');
|
|
760
|
-
},
|
|
761
|
-
onContextUsage: snapshot => {
|
|
762
|
-
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
// A loop that survived the restart-with-hint nudges is a
|
|
766
|
-
// warning, not a failure: log it and tell the user, but let the
|
|
767
|
-
// verdict gate be the only thing that can block.
|
|
768
|
-
if (r.loopHit) {
|
|
769
|
-
logEnforce(`=== enforce LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
770
|
-
enforceCtx.ui.notify(`${taskTitle}: enforce worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
771
|
-
}
|
|
772
|
-
const failure = classifyEnforceChildFailure(r);
|
|
773
|
-
logEnforce(failure ?
|
|
774
|
-
`=== enforce end: FAIL — ${failure} ===`
|
|
775
|
-
: '=== enforce end: verdict captured ===');
|
|
776
|
-
if (failure)
|
|
777
|
-
throw new Error(failure);
|
|
778
|
-
return r.text;
|
|
779
|
-
}
|
|
780
|
-
finally {
|
|
781
|
-
stopLoader();
|
|
782
|
-
}
|
|
783
|
-
}
|
|
628
|
+
//
|
|
629
|
+
// This used to be an inline ~85-line copy of the gate-child ritual,
|
|
630
|
+
// differing only in the four things GATE_CHILD_KINDS now carries as
|
|
631
|
+
// row data: no git-state guard (editing is this pass's job), no
|
|
632
|
+
// tool-result logging, no tree-change capture, and its own end
|
|
633
|
+
// marker. Its own debug log stays — the enforce child is otherwise
|
|
634
|
+
// unobservable.
|
|
635
|
+
runChild: gateChild(enforceCtx, cwd2, taskTitle, 'enforce', 'enforce-debug.log')
|
|
784
636
|
});
|
|
785
637
|
},
|
|
786
638
|
verify: async (verifyCtx, cwd2, taskTitle, taskId) => {
|
|
@@ -836,7 +688,7 @@ export function buildGateDeps(params) {
|
|
|
836
688
|
// The child renders no loader of its own: the gate-wide one above is
|
|
837
689
|
// already live and reads the same `lastLine`/`contextUsage` the child
|
|
838
690
|
// feeds, so a second widget on the same key would only fight it.
|
|
839
|
-
runChild:
|
|
691
|
+
runChild: gateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log', {
|
|
840
692
|
loader: deadAirBaseline
|
|
841
693
|
}),
|
|
842
694
|
// Names the deterministic step in the live status line.
|
|
@@ -956,7 +808,7 @@ export function buildGateDeps(params) {
|
|
|
956
808
|
cwd: cwd2,
|
|
957
809
|
signal,
|
|
958
810
|
failReason,
|
|
959
|
-
runChild:
|
|
811
|
+
runChild: gateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
|
|
960
812
|
repoHealth: () => runRepoHealthCheckAsync(cwd2, { signal }),
|
|
961
813
|
git: async (args) => {
|
|
962
814
|
const r = await git(cwd2, args, signal);
|
|
@@ -1005,7 +857,7 @@ export function buildGateDeps(params) {
|
|
|
1005
857
|
cwd: cwd2,
|
|
1006
858
|
signal,
|
|
1007
859
|
failReason,
|
|
1008
|
-
runChild:
|
|
860
|
+
runChild: gateChild(fixCtx, cwd2, 'final integration gate', 'final-fix', 'final-gate-debug.log'),
|
|
1009
861
|
// The gate re-run is the only arbiter of convergence, and the
|
|
1010
862
|
// shrink guard's discovery is the gate's own (see final-gate.ts).
|
|
1011
863
|
gate: c => runFinalIntegrationGate(c),
|
|
@@ -1055,7 +907,7 @@ export function buildGateDeps(params) {
|
|
|
1055
907
|
signal,
|
|
1056
908
|
spec,
|
|
1057
909
|
failReason,
|
|
1058
|
-
runChild:
|
|
910
|
+
runChild: gateChild(recCtx, cwd2, taskTitle, 'recommend', 'verify-debug.log')
|
|
1059
911
|
});
|
|
1060
912
|
}
|
|
1061
913
|
};
|