@mjasnikovs/pi-task 0.40.50 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +31 -1
- package/dist/task/accept-debt.js +49 -3
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +66 -7
- package/dist/task/auto-io.js +152 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +184 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +201 -39
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/health-repair.d.ts +68 -0
- package/dist/task/health-repair.js +124 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +52 -58
- package/dist/task/task-gates.js +161 -199
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +32 -6
- package/dist/task/verify-resolution.js +78 -15
- package/dist/task/verify-work.d.ts +105 -35
- package/dist/task/verify-work.js +232 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
package/dist/task/task-gates.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { verifyFailClass } from './verify-work.js';
|
|
2
2
|
import { resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
|
|
3
|
+
import { resolveDisposition } from './gate-resolution.js';
|
|
4
|
+
import { classifyHealthDelta } from './health-baseline.js';
|
|
3
5
|
import { SessionUI, notifyBoth, notifyRun } from '../remote/bridge.js';
|
|
4
|
-
import { isYoloMode,
|
|
6
|
+
import { isYoloMode, YOLO_STAMP } from './yolo.js';
|
|
5
7
|
import { extractFailingCommand, findRepairCandidate, summariseDefect } from './root-cause-repair.js';
|
|
6
8
|
import { attributeEnforceFailure } from './enforce-attribution.js';
|
|
9
|
+
import { healthRedSubject, parseHealthRepairTitle } from './health-repair.js';
|
|
7
10
|
// The debt ledger is reached through the injected `recordDebt` dep (so it stays
|
|
8
11
|
// absent-in-tests); only the origin TYPE and the cross-task-deletion reason SHAPE
|
|
9
12
|
// come from accept-debt.ts directly — the latter because its writer and its
|
|
@@ -12,38 +15,6 @@ import { crossTaskDeletionReason } from './accept-debt.js';
|
|
|
12
15
|
import { clampOutput } from './clamp-output.js';
|
|
13
16
|
import { cancelCheckpoint } from './cancel-points.js';
|
|
14
17
|
import { updateTaskFrontMatter } from './task-io.js';
|
|
15
|
-
/**
|
|
16
|
-
* How many times a verify FAIL may be auto-fixed UNATTENDED (the research
|
|
17
|
-
* recommended AUTOFIX, so pi re-runs the impl turn without prompting) before the
|
|
18
|
-
* loop falls back to the human picker. Each AUTOFIX is a full implementation
|
|
19
|
-
* re-run, so a non-converging loop must not run forever with nobody able to break
|
|
20
|
-
* it — after this many consecutive auto attempts that still FAIL, the picker is
|
|
21
|
-
* shown so a person can decide. A recommendation to ACCEPT always shows the picker
|
|
22
|
-
* regardless of this count (blessing an artifact as-is is a human's call).
|
|
23
|
-
*/
|
|
24
|
-
export const MAX_AUTO_AUTOFIX = 3;
|
|
25
|
-
/**
|
|
26
|
-
* The reason an auto-ACCEPT is being written — NAMED, not assumed.
|
|
27
|
-
*
|
|
28
|
-
* Four disjoint branches reach the terminal auto-ACCEPT, and only ONE of them has
|
|
29
|
-
* spent the autofix budget. An UNOBSERVED FAIL never consults the research; a
|
|
30
|
-
* frozen-blocked one is a contradiction no re-run can resolve; an ACCEPT
|
|
31
|
-
* recommendation can arrive with the budget untouched. A single "autofix budget
|
|
32
|
-
* spent" line would be false on three of the four, and a durable trail that
|
|
33
|
-
* misstates why a defect shipped reads as an exhausted fixer that never tried.
|
|
34
|
-
*/
|
|
35
|
-
export function yoloAcceptReason(c) {
|
|
36
|
-
if (c.isUnobserved)
|
|
37
|
-
return 'verify UNOBSERVED — tooling absent, an unattended re-run cannot provision it';
|
|
38
|
-
if (c.isFrozenBlocked)
|
|
39
|
-
return 'repo-health blocked by a spec-frozen path — an impl re-run under the same freeze cannot converge';
|
|
40
|
-
if (c.recommend === 'autofix') {
|
|
41
|
-
return `autofix budget spent (${c.autoFixCount}/${MAX_AUTO_AUTOFIX})`;
|
|
42
|
-
}
|
|
43
|
-
return c.autoFixCount === 0 ?
|
|
44
|
-
`judge recommended ACCEPT (autofix budget 0/${MAX_AUTO_AUTOFIX} unused)`
|
|
45
|
-
: `judge recommended ACCEPT (autofix budget ${c.autoFixCount}/${MAX_AUTO_AUTOFIX} already spent)`;
|
|
46
|
-
}
|
|
47
18
|
// The trail-side output ceiling lives in clamp-output.ts, so the render probe's
|
|
48
19
|
// evidence clamps identically — one ceiling, one implementation.
|
|
49
20
|
/**
|
|
@@ -53,10 +24,13 @@ export function yoloAcceptReason(c) {
|
|
|
53
24
|
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker against
|
|
54
25
|
* a remote answer, with the two actions also surfaced as remote buttons.
|
|
55
26
|
*/
|
|
56
|
-
export async function askVerifyResolution(ctx, title, failReason, rec
|
|
57
|
-
|
|
27
|
+
export async function askVerifyResolution(ctx, title, failReason, rec,
|
|
28
|
+
/** The repair ACCEPT would queue (a regressed health check), if any. */
|
|
29
|
+
acceptQueues) {
|
|
30
|
+
const options = resolutionOptions(rec.recommend, acceptQueues);
|
|
58
31
|
const question = `Verification FAILED for "${title}".\n\n${failReason}\n\n`
|
|
59
|
-
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}
|
|
32
|
+
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}`
|
|
33
|
+
+ (acceptQueues ? `\n\nACCEPT queues ${acceptQueues} before the next task.` : '');
|
|
60
34
|
const answer = await new SessionUI(ctx).ask({
|
|
61
35
|
localTitle: 'Verification failed — how should pi proceed?',
|
|
62
36
|
displayQuestion: question,
|
|
@@ -68,11 +42,17 @@ export async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
|
68
42
|
});
|
|
69
43
|
return classifyResolutionAnswer(answer);
|
|
70
44
|
}
|
|
45
|
+
/** The repair a red health check earns, as the picker and the trail name it. */
|
|
46
|
+
function describeHealthRepair(red) {
|
|
47
|
+
return red.files.length > 0 ?
|
|
48
|
+
`a repair for ${red.files.join(', ')} (\`${red.command}\`)`
|
|
49
|
+
: `a repair for \`${red.command}\``;
|
|
50
|
+
}
|
|
71
51
|
/**
|
|
72
52
|
* The VERIFY resolution loop: run the task's verification against the finished
|
|
73
|
-
* work, and
|
|
74
|
-
*
|
|
75
|
-
* or terminates.
|
|
53
|
+
* work, and carry out what gate-resolution.ts's decision table says to do with a
|
|
54
|
+
* FAIL (bounded lint fix first, then the table's autofix / accept / ask) until it
|
|
55
|
+
* verifies, is accepted, or terminates.
|
|
76
56
|
*
|
|
77
57
|
* Split from `runGatesForTask` at the single boolean that crosses to the ENFORCE
|
|
78
58
|
* half (`cleanPass`). This loop has four terminal exits and carries the whole
|
|
@@ -85,7 +65,31 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
85
65
|
v.reason ?
|
|
86
66
|
`verify: PASS (${v.reason})`
|
|
87
67
|
: 'verify: PASS'
|
|
88
|
-
: `verify: FAIL — ${v.reason
|
|
68
|
+
: `verify: FAIL — ${v.reason}`;
|
|
69
|
+
/**
|
|
70
|
+
* The loop's ONLY debt-bearing exit. Every way out of a FAIL that leaves the
|
|
71
|
+
* task FINISHED — accepted by a human, accepted unattended, blocked by a spec
|
|
72
|
+
* contradiction, or dismissed at the picker — goes through here, so a defect
|
|
73
|
+
* cannot leave this gate with no ledger entry (five did in AUTO_0002). The
|
|
74
|
+
* exits that leave the task UNFINISHED and resumable (a cancel, a re-run that
|
|
75
|
+
* could not start) are deliberately not debts: that work is coming back.
|
|
76
|
+
*/
|
|
77
|
+
const settleDebt = async (origin, reason) => {
|
|
78
|
+
try {
|
|
79
|
+
await deps.recordDebt?.(p.cwd, p.taskId, reason, origin);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// recording must never break the gate sequence
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const healthRedOf = async (health) => {
|
|
86
|
+
try {
|
|
87
|
+
return healthRedSubject(health, p.cwd, (await deps.repoFiles?.(p.cwd)) ?? null);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
89
93
|
// GATE: actually RUN the task's verification against the just-finished work
|
|
90
94
|
// BEFORE it is checked off or committed. Whether this produced a GENUINE clean
|
|
91
95
|
// pass (a real signal ran and the work met it) also decides how the enforce pass
|
|
@@ -97,24 +101,12 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
97
101
|
notifyBoth(active, `${p.tag}: verifying "${p.title}"…`, 'info');
|
|
98
102
|
let verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
99
103
|
await rec(verdictLine(verified));
|
|
100
|
-
// A FAIL no longer dead-stops. When the research recommends AUTOFIX, pi
|
|
101
|
-
// re-runs the impl turn UNATTENDED (no picker) — the human is consulted only
|
|
102
|
-
// when the recommendation is ACCEPT (blessing the artifact as-is). The
|
|
103
|
-
// unattended fix is BOUNDED by MAX_AUTO_AUTOFIX: once that many consecutive
|
|
104
|
-
// auto attempts still FAIL, the picker returns so a person can break the loop.
|
|
105
104
|
let lintFixAttempted = false;
|
|
106
105
|
let autoFixCount = 0;
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
// CANNOT converge and are skipped;
|
|
112
|
-
// the picker is forced with the cross-task contradiction named, and the
|
|
113
|
-
// defect is recorded as a durable debt for the final gate.
|
|
114
|
-
let frozenContradiction = null;
|
|
115
|
-
let frozenDebtRecorded = false;
|
|
116
|
-
// YOLO only: has the one-attempt rescue below already been spent on this task?
|
|
117
|
-
let yoloRescueUsed = false;
|
|
106
|
+
// A contradiction outlives the round that proved it: it is a fact about the
|
|
107
|
+
// SPEC, not about one verdict. Re-deriving it per round is how a loop spends
|
|
108
|
+
// its whole budget on a freeze that was already named in round one (0053).
|
|
109
|
+
let contradiction = null;
|
|
118
110
|
while (!verified.ok) {
|
|
119
111
|
// SAFE CHECKPOINT (before a resolution round): a round is a bounded
|
|
120
112
|
// lint fix, a research child and possibly a whole implementation
|
|
@@ -134,12 +126,12 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
134
126
|
await updateTaskFrontMatter(p.cwd, p.taskId, { state: 'cancelled' }).catch(() => { });
|
|
135
127
|
return { stop: { kind: 'cancelled', ctx: active } };
|
|
136
128
|
}
|
|
137
|
-
const failReason = verified.reason
|
|
129
|
+
const failReason = verified.reason;
|
|
138
130
|
// GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
|
|
139
131
|
// bounded fix attempt before the picker — smallest tool first. Applied →
|
|
140
132
|
// re-verify and re-enter the loop on the fresh verdict; not applied (guard
|
|
141
|
-
// trip, no convergence) → fall through to the
|
|
142
|
-
const failClass = verifyFailClass(verified);
|
|
133
|
+
// trip, no convergence) → fall through to the decision table unchanged.
|
|
134
|
+
const failClass = verifyFailClass(verified) ?? 'model-verdict';
|
|
143
135
|
if (!lintFixAttempted && deps.lintFix && failClass === 'repo-health') {
|
|
144
136
|
lintFixAttempted = true;
|
|
145
137
|
notifyRun(active, `${p.tag}: static findings on "${p.title}" — attempting bounded lint fix…`, 'info');
|
|
@@ -152,143 +144,81 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
152
144
|
await rec(verdictLine(verified));
|
|
153
145
|
continue;
|
|
154
146
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// UNOBSERVED (rule 5c): a spec-required behavioral check could not run because
|
|
160
|
-
// its observation tooling is absent. An unattended AUTOFIX re-run cannot install
|
|
161
|
-
// a missing tool, so it would only burn MAX_AUTO_AUTOFIX turns and re-FAIL — the
|
|
162
|
-
// decision (provision the tool, or accept the unproven behavior) is the human's.
|
|
163
|
-
// Skip the (moot) recommendation research and force the picker.
|
|
164
|
-
const isUnobserved = verified.unobserved === true;
|
|
165
|
-
// FROZEN-BLOCKED (cross-task contradiction): skip the recommendation
|
|
166
|
-
// research too — it would only re-derive what the deterministic lint-fix
|
|
167
|
-
// rejection already proved. The picker shows the contradiction; ACCEPT
|
|
168
|
-
// records the (already-recorded) defect as the human's call. Applies
|
|
169
|
-
// only while the FAIL is still the repo-health one the contradiction
|
|
170
|
-
// explains — a later, different FAIL gets the ordinary resolution path.
|
|
171
|
-
const isFrozenBlocked = frozenContradiction !== null && failClass === 'repo-health';
|
|
172
|
-
const recOutcome = isUnobserved ? { recommend: 'autofix', rationale: failReason }
|
|
173
|
-
: isFrozenBlocked ?
|
|
174
|
-
{
|
|
175
|
-
recommend: 'accept',
|
|
176
|
-
rationale: `${frozenContradiction} — the failing static check can only be fixed by `
|
|
177
|
-
+ `editing a path this task's spec freezes (a cross-task contradiction: `
|
|
178
|
-
+ `the spec forbids the very edit the repo needs; the "owning" earlier `
|
|
179
|
-
+ `step already completed). An implementation re-run under the same `
|
|
180
|
-
+ `freeze cannot converge. ACCEPT records it as durable debt the final `
|
|
181
|
-
+ `gate re-checks; fixing it needs a plan-level change, not a re-run.`
|
|
182
|
-
}
|
|
183
|
-
: deps.recommend ?
|
|
184
|
-
await deps.recommend(active, p.cwd, p.title, p.taskId, failReason)
|
|
185
|
-
: { recommend: 'autofix', rationale: failReason };
|
|
186
|
-
await rec(isUnobserved ?
|
|
187
|
-
'resolution: verify UNOBSERVED — spec-required check could not run (tooling absent); '
|
|
188
|
-
+ 'forcing the human picker, an unattended re-run cannot provision it'
|
|
189
|
-
: isFrozenBlocked ?
|
|
190
|
-
'resolution: repo-health FAIL is blocked by spec-frozen path(s) — cross-task '
|
|
191
|
-
+ 'contradiction; unattended AUTOFIX skipped (an impl re-run under the same '
|
|
192
|
-
+ 'freeze cannot converge), forcing the human picker'
|
|
193
|
-
: `resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
194
|
-
if (isFrozenBlocked && !frozenDebtRecorded) {
|
|
195
|
-
frozenDebtRecorded = true;
|
|
196
|
-
// Durable regardless of what the human picks next: the contradiction
|
|
197
|
-
// is real, cross-task, and outside this task's power to fix — the
|
|
198
|
-
// final gate must surface it at run end (static-class: it auto-closes
|
|
199
|
-
// iff the run-end static check passes).
|
|
200
|
-
try {
|
|
201
|
-
await deps.recordDebt?.(p.cwd, p.taskId, `${failReason} — ${frozenContradiction}`, 'frozen-blocked');
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
// recording must never break the gate sequence
|
|
205
|
-
}
|
|
147
|
+
if (fix.contradiction)
|
|
148
|
+
contradiction = fix.contradiction;
|
|
206
149
|
}
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// YOLO: the picker is unreachable with nobody watching, and every
|
|
235
|
-
// unattended attempt this task may make has been made — so the only
|
|
236
|
-
// option left that terminates is ACCEPT, recorded as its own
|
|
237
|
-
// 'yolo-accepted' debt. Deliberately NOT a re-entry into autofix:
|
|
238
|
-
// MAX_AUTO_AUTOFIX exists to break a non-converging loop, and an
|
|
239
|
-
// auto-pick here would restart the budget from the site that proves it ran out.
|
|
240
|
-
const yoloChoice = autoFixNow || yoloRescueNow ? null : yoloVerifyResolution(isYoloMode());
|
|
150
|
+
// Two FAILs make the recommendation research MOOT before it runs. An
|
|
151
|
+
// UNOBSERVED one (rule 5c) is a check that could not RUN for want of
|
|
152
|
+
// tooling, and a contradiction has already been PROVEN deterministically
|
|
153
|
+
// — asking a model to re-derive either costs a child and decides nothing.
|
|
154
|
+
const unobserved = verified.unobserved === true;
|
|
155
|
+
const judge = contradiction === null && !unobserved ? deps.recommend : undefined;
|
|
156
|
+
const recOutcome = judge ?
|
|
157
|
+
await judge(active, p.cwd, p.title, p.taskId, failReason)
|
|
158
|
+
: { recommend: 'autofix', rationale: failReason };
|
|
159
|
+
if (judge)
|
|
160
|
+
await rec(`resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
161
|
+
contradiction ??= recOutcome.contradiction ?? null;
|
|
162
|
+
const unattended = isYoloMode();
|
|
163
|
+
// A regressed health check is THIS task's red. Accepting it ships the
|
|
164
|
+
// red, and the next checkpoint splices a repair before anything builds
|
|
165
|
+
// on it (health-repair.ts) — named here so a human choosing ACCEPT
|
|
166
|
+
// sees what the choice queues, and the trail says the same.
|
|
167
|
+
const regression = verified.health ? await healthRedOf(verified.health) : null;
|
|
168
|
+
const acceptQueues = regression ? describeHealthRepair(regression) : undefined;
|
|
169
|
+
const disposition = resolveDisposition({
|
|
170
|
+
failClass,
|
|
171
|
+
recommend: recOutcome.recommend,
|
|
172
|
+
unobserved,
|
|
173
|
+
contradiction,
|
|
174
|
+
attempts: autoFixCount,
|
|
175
|
+
unattended
|
|
176
|
+
});
|
|
241
177
|
let choice;
|
|
242
|
-
if (
|
|
243
|
-
choice = yoloChoice;
|
|
244
|
-
// NAME the branch. This line is the durable record of why a defect
|
|
245
|
-
// shipped, and three of the four branches that reach it have spent
|
|
246
|
-
// no budget at all.
|
|
247
|
-
await rec(`resolution: auto-ACCEPTED despite verify FAIL — ${yoloAcceptReason({
|
|
248
|
-
isUnobserved,
|
|
249
|
-
isFrozenBlocked,
|
|
250
|
-
recommend: recOutcome.recommend,
|
|
251
|
-
autoFixCount
|
|
252
|
-
})}, nobody to ask ${YOLO_STAMP}`);
|
|
253
|
-
}
|
|
254
|
-
else if (autoFixNow || yoloRescueNow) {
|
|
178
|
+
if (disposition.action === 'autofix') {
|
|
255
179
|
autoFixCount += 1;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await rec(yoloRescueNow ?
|
|
259
|
-
`resolution: auto-AUTOFIX (${YOLO_STAMP} rescue — judge recommended ACCEPT with the unattended `
|
|
260
|
-
+ `budget unspent; one attempt, ${autoFixCount}/${MAX_AUTO_AUTOFIX})`
|
|
261
|
-
: `resolution: auto-AUTOFIX (recommended, unattended ${autoFixCount}/${MAX_AUTO_AUTOFIX})`);
|
|
262
|
-
notifyRun(active, `${p.tag}: verify FAIL on "${p.title}" — auto-fixing (${yoloRescueNow ? `${YOLO_STAMP} one attempt before accepting` : 'recommended'}, ${autoFixCount}/${MAX_AUTO_AUTOFIX})…`, 'info');
|
|
180
|
+
await rec(`resolution: auto-AUTOFIX (${disposition.reason})`);
|
|
181
|
+
notifyRun(active, `${p.tag}: verify FAIL on "${p.title}" — auto-fixing (${disposition.reason})…`, 'info');
|
|
263
182
|
choice = { action: 'autofix' };
|
|
264
183
|
}
|
|
184
|
+
else if (disposition.action === 'accept') {
|
|
185
|
+
// NAME the rule that decided. This line is the durable record of why
|
|
186
|
+
// a defect shipped, and the rules that reach it have spent wildly
|
|
187
|
+
// different amounts of the budget — some none at all.
|
|
188
|
+
await rec(`resolution: auto-ACCEPTED despite verify FAIL — ${disposition.reason}${unattended ? `, nobody to ask ${YOLO_STAMP}` : ''}`);
|
|
189
|
+
choice = { action: 'accept' };
|
|
190
|
+
}
|
|
265
191
|
else {
|
|
266
|
-
|
|
192
|
+
// Why the picker is up, when that is not already the line above:
|
|
193
|
+
// the judge-accept row's reason only restates the recommendation.
|
|
194
|
+
if (disposition.rule !== 'judge-accept') {
|
|
195
|
+
await rec(`resolution: asking the human — ${disposition.reason}`);
|
|
196
|
+
}
|
|
197
|
+
choice = await askVerifyResolution(active, p.title, failReason, recOutcome, acceptQueues);
|
|
267
198
|
}
|
|
268
199
|
if (choice.action === 'cancel') {
|
|
269
200
|
await rec('resolution: user dismissed the verify-FAIL picker — paused');
|
|
201
|
+
await settleDebt('dismissed', failReason);
|
|
270
202
|
return { stop: { kind: 'paused', ctx: active, reason: failReason } };
|
|
271
203
|
}
|
|
272
204
|
if (choice.action === 'accept') {
|
|
273
|
-
|
|
274
|
-
|
|
205
|
+
// Provenance splits here, mandatorily. The table's own accept carries
|
|
206
|
+
// the origin that names the rule which decided; only a PICKER answer
|
|
207
|
+
// earns the plain 'accepted' class, which asserts a human weighed
|
|
208
|
+
// this failing artifact.
|
|
209
|
+
const origin = disposition.action === 'accept' ? disposition.debtOrigin : 'accepted';
|
|
210
|
+
if (origin === 'accepted') {
|
|
275
211
|
await rec('resolution: user ACCEPTED the work despite verify FAIL');
|
|
276
|
-
|
|
277
|
-
// defect ships and nothing else in this task
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// asserts a human weighed the failing artifact.
|
|
287
|
-
await deps.recordDebt?.(p.cwd, p.taskId, failReason, byYolo ? 'yolo-accepted' : 'accepted');
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// recording must never break the gate sequence
|
|
291
|
-
}
|
|
212
|
+
}
|
|
213
|
+
// Durable debt: the defect ships and nothing else in this task
|
|
214
|
+
// revisits it. The final integration gate re-checks it at run end and
|
|
215
|
+
// surfaces it if still open. The reason LEADS with the fail text so
|
|
216
|
+
// its minted class prefix still routes the run-end re-check.
|
|
217
|
+
await settleDebt(origin, disposition.rule === 'spec-contradiction' ?
|
|
218
|
+
`${failReason} — ${disposition.reason}`
|
|
219
|
+
: failReason);
|
|
220
|
+
if (acceptQueues) {
|
|
221
|
+
await rec(`accept: repo health regressed by this task — ${acceptQueues} is spliced before the next task`);
|
|
292
222
|
}
|
|
293
223
|
// ROOT CAUSE: an accepted FAIL that some OTHER task's file caused is
|
|
294
224
|
// not fixed by accepting it — every later task keeps tripping over
|
|
@@ -306,7 +236,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
306
236
|
// recording must never break the gate sequence
|
|
307
237
|
}
|
|
308
238
|
}
|
|
309
|
-
notifyRun(active, `${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.${
|
|
239
|
+
notifyRun(active, `${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.${unattended ? ` ${YOLO_STAMP}` : ''}`, 'warning');
|
|
310
240
|
break;
|
|
311
241
|
}
|
|
312
242
|
// AUTOFIX: re-run the implementation turn with the failure (and any typed
|
|
@@ -315,24 +245,31 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
315
245
|
// hand its diagnosis to the re-run rather than making it re-derive the
|
|
316
246
|
// cause from the bare FAIL line. Skipped when there is no researched
|
|
317
247
|
// rationale beyond the failure text itself.
|
|
318
|
-
// Only the picker branch may claim a person chose this: the
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
if (
|
|
248
|
+
// Only the picker branch may claim a person chose this: the unattended
|
|
249
|
+
// branch already recorded itself one line above, and a trail that says
|
|
250
|
+
// "user chose" when nobody was asked is the same lie the accept line
|
|
251
|
+
// used to tell.
|
|
252
|
+
if (disposition.action === 'ask') {
|
|
323
253
|
await rec('resolution: user chose AUTOFIX — re-running the implementation turn');
|
|
324
254
|
}
|
|
325
255
|
notifyBoth(active, `${p.tag}: autofixing "${p.title}"…`, 'info');
|
|
326
256
|
const diagnosis = (recOutcome.recommend === 'autofix'
|
|
327
257
|
&& recOutcome.rationale.length > 0
|
|
328
258
|
&& recOutcome.rationale !== failReason) ?
|
|
329
|
-
|
|
259
|
+
recOutcome.rationale
|
|
330
260
|
: '';
|
|
331
|
-
const fixInstruction = `${failReason}${diagnosis}${choice.guidance ? `\n\nUser guidance: ${choice.guidance}` : ''}`;
|
|
332
261
|
const fixRes = await deps.runTask(active, p.cwd, p.title, {
|
|
333
262
|
resumeId: p.taskId,
|
|
334
263
|
planContext: p.planContext,
|
|
335
|
-
|
|
264
|
+
fixContext: {
|
|
265
|
+
outcome: verified,
|
|
266
|
+
disposition,
|
|
267
|
+
probes: verified.probes ?? {},
|
|
268
|
+
attempt: autoFixCount,
|
|
269
|
+
contradiction,
|
|
270
|
+
...(diagnosis === '' ? {} : { diagnosis }),
|
|
271
|
+
...(choice.guidance === undefined ? {} : { guidance: choice.guidance })
|
|
272
|
+
}
|
|
336
273
|
});
|
|
337
274
|
active = fixRes.ctx ?? active;
|
|
338
275
|
// The re-run's ending, mapped to this loop's own terminal kinds. A
|
|
@@ -364,6 +301,30 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
364
301
|
verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
365
302
|
await rec(verdictLine(verified));
|
|
366
303
|
}
|
|
304
|
+
// Static checks the repo was ALREADY failing when this task started (see
|
|
305
|
+
// health-baseline.ts). They did not fail this gate — that is the point — but
|
|
306
|
+
// dropping them here is how a sibling's defect vanishes until the run-end
|
|
307
|
+
// gate rediscovers it with nobody's name on it. Recorded once, after the
|
|
308
|
+
// loop, whatever the verdict was: a task can verify perfectly in a red repo.
|
|
309
|
+
if (verified.inheritedHealth) {
|
|
310
|
+
await rec(`accept-debt: inherited repo health — ${verified.inheritedHealth}`);
|
|
311
|
+
await settleDebt('inherited-health', verified.inheritedHealth);
|
|
312
|
+
}
|
|
313
|
+
// A health repair that verified CLEAN — the check it exists for ran green,
|
|
314
|
+
// nothing inherited — closes the debts that check opened, under its own id.
|
|
315
|
+
const repair = parseHealthRepairTitle(p.title);
|
|
316
|
+
if (repair && verified.ok && !verified.reason && !verified.inheritedHealth) {
|
|
317
|
+
try {
|
|
318
|
+
const closed = await deps.closeHealthDebts?.(p.cwd, repair.command, p.taskId);
|
|
319
|
+
if (closed && closed.length > 0) {
|
|
320
|
+
await rec(`accept-debt: closed ${closed.length} debt(s) on \`${repair.command}\` — `
|
|
321
|
+
+ `${closed.map(d => d.taskId).join(', ')} — repaired by ${p.taskId}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// closing debts must never break the gate sequence
|
|
326
|
+
}
|
|
327
|
+
}
|
|
367
328
|
// Loop exited because the work verified OR the user accepted the artifact. A
|
|
368
329
|
// genuine clean pass is ok===true with NO reason; a no-op pass or an
|
|
369
330
|
// accept-override (verified.ok still false at break) is NOT a guardable signal.
|
|
@@ -448,12 +409,13 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
448
409
|
let enforceEditsBlocked = false;
|
|
449
410
|
if (mode === 'edit' && deps.repoHealth && editsMade !== false) {
|
|
450
411
|
const after = await deps.repoHealth(active, p.cwd, p.title);
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
412
|
+
// The SAME differential the verify gate runs (health-baseline.ts), per
|
|
413
|
+
// command rather than per overall verdict — two runs can both be red
|
|
414
|
+
// while a different check failed in each. A missing baseline is no
|
|
415
|
+
// longer read as a clean one: that default silently turned this
|
|
416
|
+
// differential back into the absolute check it exists to replace.
|
|
417
|
+
const delta = classifyHealthDelta(healthBefore ?? null, after);
|
|
418
|
+
if (delta === 'regressed') {
|
|
457
419
|
enforceEditsBlocked = true;
|
|
458
420
|
const outputTail = after.output ? ` — output:\n${clampOutput(after.output)}` : '';
|
|
459
421
|
if (deps.discardEdits) {
|
|
@@ -465,7 +427,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
465
427
|
}
|
|
466
428
|
notifyRun(active, `${p.tag}: guideline edits on "${p.title}" regressed repo health (${after.reason.slice(0, 120)}) — discarded before commit.`, 'warning');
|
|
467
429
|
}
|
|
468
|
-
else if (
|
|
430
|
+
else if (delta === 'pre-existing') {
|
|
469
431
|
// Failing both before and after → not enforce's fault. Keep the edits;
|
|
470
432
|
// record that the repo entered the gate already unhealthy so the trail
|
|
471
433
|
// explains why a still-failing repo did NOT trigger a discard here.
|
package/dist/task/task-io.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare function writeTaskFile(cwd: string, fm: TaskFrontMatter, body: st
|
|
|
21
21
|
export declare function updateTaskFrontMatter(cwd: string, id: string, patch: Partial<TaskFrontMatter>): Promise<void>;
|
|
22
22
|
export declare function readSection(cwd: string, id: string, heading: string): Promise<string | null>;
|
|
23
23
|
export declare function setTaskSection(cwd: string, id: string, heading: string, content: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite a section from its own current contents.
|
|
26
|
+
*
|
|
27
|
+
* The read-then-write pair a caller would otherwise inline, kept whole here
|
|
28
|
+
* because the RE-ENTRY case is where it goes wrong: a resumed run that reaches a
|
|
29
|
+
* section it did not fully regenerate (an autofix re-entry runs zero phases)
|
|
30
|
+
* calls `setTaskSection` with what THIS pass produced and erases what the first
|
|
31
|
+
* pass proved. `merge` receives null when the section is absent.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mergeTaskSection(cwd: string, id: string, heading: string, merge: (old: string | null) => string): Promise<void>;
|
|
24
34
|
/**
|
|
25
35
|
* Append one timestamped line to the task's `## gates` section — the durable
|
|
26
36
|
* per-task trail of gate outcomes (verify verdicts, enforce mode/verdict, commit
|
package/dist/task/task-io.js
CHANGED
|
@@ -69,19 +69,51 @@ export async function readTaskFile(cwd, id) {
|
|
|
69
69
|
const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
70
70
|
return { frontMatter: fm, body };
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* One writer per task file at a time. Research workers in graph mode, the
|
|
74
|
+
* loop-events trail and the gate recorder all rewrite the same file from
|
|
75
|
+
* overlapping continuations; without this, two read-modify-write pairs interleave
|
|
76
|
+
* and the second one erases the first, or a reader catches a half-written file
|
|
77
|
+
* and reports it as malformed front matter.
|
|
78
|
+
*/
|
|
79
|
+
const fileChains = new Map();
|
|
80
|
+
async function withTaskFile(file, fn) {
|
|
81
|
+
const prev = fileChains.get(file) ?? Promise.resolve();
|
|
82
|
+
const next = prev.then(fn, fn);
|
|
83
|
+
fileChains.set(file, next.catch(() => { }));
|
|
84
|
+
try {
|
|
85
|
+
return await next;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (fileChains.get(file) === next)
|
|
89
|
+
fileChains.delete(file);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
72
92
|
export async function writeTaskFile(cwd, fm, body) {
|
|
73
93
|
await ensureTasksDir(cwd);
|
|
94
|
+
const file = taskFilePath(cwd, fm.id);
|
|
74
95
|
const content = `${emitFrontMatter(fm)}\n${body}`;
|
|
75
|
-
|
|
96
|
+
// Written beside and renamed over: a reader never sees a truncated file.
|
|
97
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
98
|
+
await fsp.writeFile(tmp, content, 'utf8');
|
|
99
|
+
try {
|
|
100
|
+
await fsp.rename(tmp, file);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
await fsp.rm(tmp, { force: true }).catch(() => { });
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
76
106
|
}
|
|
77
107
|
export async function updateTaskFrontMatter(cwd, id, patch) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
await withTaskFile(taskFilePath(cwd, id), async () => {
|
|
109
|
+
const { frontMatter, body } = await readTaskFile(cwd, id);
|
|
110
|
+
const next = {
|
|
111
|
+
...frontMatter,
|
|
112
|
+
...patch,
|
|
113
|
+
updated_at: new Date().toISOString()
|
|
114
|
+
};
|
|
115
|
+
await writeTaskFile(cwd, next, body);
|
|
116
|
+
});
|
|
85
117
|
}
|
|
86
118
|
// ─── Section read/write (append if absent, rewrite if present) ───────────────
|
|
87
119
|
export async function readSection(cwd, id, heading) {
|
|
@@ -89,9 +121,15 @@ export async function readSection(cwd, id, heading) {
|
|
|
89
121
|
const m = sectionRegex(heading).exec(body);
|
|
90
122
|
return m ? m[2].trim() : null;
|
|
91
123
|
}
|
|
92
|
-
export
|
|
124
|
+
export function setTaskSection(cwd, id, heading, content) {
|
|
125
|
+
return withTaskFile(taskFilePath(cwd, id), () => rewriteSection(cwd, id, heading, () => content));
|
|
126
|
+
}
|
|
127
|
+
/** The read-modify-write itself; callers hold the file's chain. */
|
|
128
|
+
async function rewriteSection(cwd, id, heading, render) {
|
|
93
129
|
const { frontMatter, body } = await readTaskFile(cwd, id);
|
|
94
130
|
const re = sectionRegex(heading);
|
|
131
|
+
const m = re.exec(body);
|
|
132
|
+
const content = render(m ? m[2].trim() : null);
|
|
95
133
|
let next;
|
|
96
134
|
if (re.test(body)) {
|
|
97
135
|
// Use a replacer FUNCTION, not a replacement string: `content` is
|
|
@@ -112,6 +150,18 @@ export async function setTaskSection(cwd, id, heading, content) {
|
|
|
112
150
|
}
|
|
113
151
|
await writeTaskFile(cwd, { ...frontMatter, updated_at: new Date().toISOString() }, next);
|
|
114
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Rewrite a section from its own current contents.
|
|
155
|
+
*
|
|
156
|
+
* The read-then-write pair a caller would otherwise inline, kept whole here
|
|
157
|
+
* because the RE-ENTRY case is where it goes wrong: a resumed run that reaches a
|
|
158
|
+
* section it did not fully regenerate (an autofix re-entry runs zero phases)
|
|
159
|
+
* calls `setTaskSection` with what THIS pass produced and erases what the first
|
|
160
|
+
* pass proved. `merge` receives null when the section is absent.
|
|
161
|
+
*/
|
|
162
|
+
export function mergeTaskSection(cwd, id, heading, merge) {
|
|
163
|
+
return withTaskFile(taskFilePath(cwd, id), () => rewriteSection(cwd, id, heading, merge));
|
|
164
|
+
}
|
|
115
165
|
/**
|
|
116
166
|
* Append one timestamped line to the task's `## gates` section — the durable
|
|
117
167
|
* per-task trail of gate outcomes (verify verdicts, enforce mode/verdict, commit
|
|
@@ -124,19 +174,20 @@ export async function appendGateRecord(cwd, id, line) {
|
|
|
124
174
|
try {
|
|
125
175
|
const stamp = new Date().toISOString();
|
|
126
176
|
const entry = `- ${stamp} ${line.replace(/\s*\n\s*/g, ' ').trim()}`;
|
|
127
|
-
|
|
128
|
-
await setTaskSection(cwd, id, 'gates', existing ? `${existing}\n${entry}` : entry);
|
|
177
|
+
await mergeTaskSection(cwd, id, 'gates', existing => existing ? `${existing}\n${entry}` : entry);
|
|
129
178
|
}
|
|
130
179
|
catch {
|
|
131
180
|
// Recording is observability, not control flow — never propagate.
|
|
132
181
|
}
|
|
133
182
|
}
|
|
134
183
|
/** Remove a section (heading + body) if present; a no-op when it's absent. */
|
|
135
|
-
export
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
184
|
+
export function removeTaskSection(cwd, id, heading) {
|
|
185
|
+
return withTaskFile(taskFilePath(cwd, id), async () => {
|
|
186
|
+
const { frontMatter, body } = await readTaskFile(cwd, id);
|
|
187
|
+
const re = sectionRegex(heading);
|
|
188
|
+
if (!re.test(body))
|
|
189
|
+
return;
|
|
190
|
+
const next = body.replace(re, '');
|
|
191
|
+
await writeTaskFile(cwd, { ...frontMatter, updated_at: new Date().toISOString() }, next);
|
|
192
|
+
});
|
|
142
193
|
}
|