@mjasnikovs/pi-task 0.40.50 → 0.41.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 +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -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 +199 -38
- 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/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 +39 -57
- package/dist/task/task-gates.js +115 -195
- 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 +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -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,7 +1,9 @@
|
|
|
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';
|
|
7
9
|
// The debt ledger is reached through the injected `recordDebt` dep (so it stays
|
|
@@ -12,38 +14,6 @@ import { crossTaskDeletionReason } from './accept-debt.js';
|
|
|
12
14
|
import { clampOutput } from './clamp-output.js';
|
|
13
15
|
import { cancelCheckpoint } from './cancel-points.js';
|
|
14
16
|
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
17
|
// The trail-side output ceiling lives in clamp-output.ts, so the render probe's
|
|
48
18
|
// evidence clamps identically — one ceiling, one implementation.
|
|
49
19
|
/**
|
|
@@ -70,9 +40,9 @@ export async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
|
70
40
|
}
|
|
71
41
|
/**
|
|
72
42
|
* The VERIFY resolution loop: run the task's verification against the finished
|
|
73
|
-
* work, and
|
|
74
|
-
*
|
|
75
|
-
* or terminates.
|
|
43
|
+
* work, and carry out what gate-resolution.ts's decision table says to do with a
|
|
44
|
+
* FAIL (bounded lint fix first, then the table's autofix / accept / ask) until it
|
|
45
|
+
* verifies, is accepted, or terminates.
|
|
76
46
|
*
|
|
77
47
|
* Split from `runGatesForTask` at the single boolean that crosses to the ENFORCE
|
|
78
48
|
* half (`cleanPass`). This loop has four terminal exits and carries the whole
|
|
@@ -85,7 +55,23 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
85
55
|
v.reason ?
|
|
86
56
|
`verify: PASS (${v.reason})`
|
|
87
57
|
: 'verify: PASS'
|
|
88
|
-
: `verify: FAIL — ${v.reason
|
|
58
|
+
: `verify: FAIL — ${v.reason}`;
|
|
59
|
+
/**
|
|
60
|
+
* The loop's ONLY debt-bearing exit. Every way out of a FAIL that leaves the
|
|
61
|
+
* task FINISHED — accepted by a human, accepted unattended, blocked by a spec
|
|
62
|
+
* contradiction, or dismissed at the picker — goes through here, so a defect
|
|
63
|
+
* cannot leave this gate with no ledger entry (five did in AUTO_0002). The
|
|
64
|
+
* exits that leave the task UNFINISHED and resumable (a cancel, a re-run that
|
|
65
|
+
* could not start) are deliberately not debts: that work is coming back.
|
|
66
|
+
*/
|
|
67
|
+
const settleDebt = async (origin, reason) => {
|
|
68
|
+
try {
|
|
69
|
+
await deps.recordDebt?.(p.cwd, p.taskId, reason, origin);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// recording must never break the gate sequence
|
|
73
|
+
}
|
|
74
|
+
};
|
|
89
75
|
// GATE: actually RUN the task's verification against the just-finished work
|
|
90
76
|
// BEFORE it is checked off or committed. Whether this produced a GENUINE clean
|
|
91
77
|
// pass (a real signal ran and the work met it) also decides how the enforce pass
|
|
@@ -97,24 +83,12 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
97
83
|
notifyBoth(active, `${p.tag}: verifying "${p.title}"…`, 'info');
|
|
98
84
|
let verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
99
85
|
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
86
|
let lintFixAttempted = false;
|
|
106
87
|
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;
|
|
88
|
+
// A contradiction outlives the round that proved it: it is a fact about the
|
|
89
|
+
// SPEC, not about one verdict. Re-deriving it per round is how a loop spends
|
|
90
|
+
// its whole budget on a freeze that was already named in round one (0053).
|
|
91
|
+
let contradiction = null;
|
|
118
92
|
while (!verified.ok) {
|
|
119
93
|
// SAFE CHECKPOINT (before a resolution round): a round is a bounded
|
|
120
94
|
// lint fix, a research child and possibly a whole implementation
|
|
@@ -134,12 +108,12 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
134
108
|
await updateTaskFrontMatter(p.cwd, p.taskId, { state: 'cancelled' }).catch(() => { });
|
|
135
109
|
return { stop: { kind: 'cancelled', ctx: active } };
|
|
136
110
|
}
|
|
137
|
-
const failReason = verified.reason
|
|
111
|
+
const failReason = verified.reason;
|
|
138
112
|
// GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
|
|
139
113
|
// bounded fix attempt before the picker — smallest tool first. Applied →
|
|
140
114
|
// 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);
|
|
115
|
+
// trip, no convergence) → fall through to the decision table unchanged.
|
|
116
|
+
const failClass = verifyFailClass(verified) ?? 'model-verdict';
|
|
143
117
|
if (!lintFixAttempted && deps.lintFix && failClass === 'repo-health') {
|
|
144
118
|
lintFixAttempted = true;
|
|
145
119
|
notifyRun(active, `${p.tag}: static findings on "${p.title}" — attempting bounded lint fix…`, 'info');
|
|
@@ -152,144 +126,73 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
152
126
|
await rec(verdictLine(verified));
|
|
153
127
|
continue;
|
|
154
128
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
}
|
|
129
|
+
if (fix.contradiction)
|
|
130
|
+
contradiction = fix.contradiction;
|
|
158
131
|
}
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// AUTO-RESOLVE the AUTOFIX path: when the research says the work is
|
|
208
|
-
// genuinely wrong, re-run the fix WITHOUT prompting the user. The picker is
|
|
209
|
-
// reserved for the ACCEPT recommendation (the human decides whether to bless
|
|
210
|
-
// an artifact the gate FAILed) and for the bounded fallback: after
|
|
211
|
-
// MAX_AUTO_AUTOFIX consecutive unattended attempts that still FAIL, hand
|
|
212
|
-
// control back so a person can break a non-converging loop.
|
|
213
|
-
const autoFixNow = !isUnobserved
|
|
214
|
-
&& !isFrozenBlocked
|
|
215
|
-
&& recOutcome.recommend === 'autofix'
|
|
216
|
-
&& autoFixCount < MAX_AUTO_AUTOFIX
|
|
217
|
-
// A rescue attempt that still FAILed is terminal under YOLO: the
|
|
218
|
-
// recommendation that got us here was ACCEPT, so a later flip to
|
|
219
|
-
// AUTOFIX must not bootstrap the full budget from it.
|
|
220
|
-
&& !yoloRescueUsed;
|
|
221
|
-
// YOLO, THE RESCUE BRANCH: the recommendation is ACCEPT, nobody can be
|
|
222
|
-
// asked, and the unattended budget is UNTOUCHED. Accepting here would
|
|
223
|
-
// ship a defect having attempted nothing, on a judgement the human who
|
|
224
|
-
// would normally weigh it never saw. So spend ONE attempt first.
|
|
225
|
-
// Bounded by construction: one, not MAX_AUTO_AUTOFIX, so an ACCEPT
|
|
226
|
-
// recommendation can never restart a full loop; if it still FAILs the
|
|
227
|
-
// next turn falls through to the same auto-ACCEPT and the same debt.
|
|
228
|
-
const yoloRescueNow = isYoloMode()
|
|
229
|
-
&& !yoloRescueUsed
|
|
230
|
-
&& !isUnobserved
|
|
231
|
-
&& !isFrozenBlocked
|
|
232
|
-
&& recOutcome.recommend === 'accept'
|
|
233
|
-
&& autoFixCount === 0;
|
|
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());
|
|
132
|
+
// Two FAILs make the recommendation research MOOT before it runs. An
|
|
133
|
+
// UNOBSERVED one (rule 5c) is a check that could not RUN for want of
|
|
134
|
+
// tooling, and a contradiction has already been PROVEN deterministically
|
|
135
|
+
// — asking a model to re-derive either costs a child and decides nothing.
|
|
136
|
+
const unobserved = verified.unobserved === true;
|
|
137
|
+
const judge = contradiction === null && !unobserved ? deps.recommend : undefined;
|
|
138
|
+
const recOutcome = judge ?
|
|
139
|
+
await judge(active, p.cwd, p.title, p.taskId, failReason)
|
|
140
|
+
: { recommend: 'autofix', rationale: failReason };
|
|
141
|
+
if (judge)
|
|
142
|
+
await rec(`resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
143
|
+
contradiction ??= recOutcome.contradiction ?? null;
|
|
144
|
+
const unattended = isYoloMode();
|
|
145
|
+
const disposition = resolveDisposition({
|
|
146
|
+
failClass,
|
|
147
|
+
recommend: recOutcome.recommend,
|
|
148
|
+
unobserved,
|
|
149
|
+
contradiction,
|
|
150
|
+
attempts: autoFixCount,
|
|
151
|
+
unattended
|
|
152
|
+
});
|
|
241
153
|
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) {
|
|
154
|
+
if (disposition.action === 'autofix') {
|
|
255
155
|
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');
|
|
156
|
+
await rec(`resolution: auto-AUTOFIX (${disposition.reason})`);
|
|
157
|
+
notifyRun(active, `${p.tag}: verify FAIL on "${p.title}" — auto-fixing (${disposition.reason})…`, 'info');
|
|
263
158
|
choice = { action: 'autofix' };
|
|
264
159
|
}
|
|
160
|
+
else if (disposition.action === 'accept') {
|
|
161
|
+
// NAME the rule that decided. This line is the durable record of why
|
|
162
|
+
// a defect shipped, and the rules that reach it have spent wildly
|
|
163
|
+
// different amounts of the budget — some none at all.
|
|
164
|
+
await rec(`resolution: auto-ACCEPTED despite verify FAIL — ${disposition.reason}${unattended ? `, nobody to ask ${YOLO_STAMP}` : ''}`);
|
|
165
|
+
choice = { action: 'accept' };
|
|
166
|
+
}
|
|
265
167
|
else {
|
|
168
|
+
// Why the picker is up, when that is not already the line above:
|
|
169
|
+
// the judge-accept row's reason only restates the recommendation.
|
|
170
|
+
if (disposition.rule !== 'judge-accept') {
|
|
171
|
+
await rec(`resolution: asking the human — ${disposition.reason}`);
|
|
172
|
+
}
|
|
266
173
|
choice = await askVerifyResolution(active, p.title, failReason, recOutcome);
|
|
267
174
|
}
|
|
268
175
|
if (choice.action === 'cancel') {
|
|
269
176
|
await rec('resolution: user dismissed the verify-FAIL picker — paused');
|
|
177
|
+
await settleDebt('dismissed', failReason);
|
|
270
178
|
return { stop: { kind: 'paused', ctx: active, reason: failReason } };
|
|
271
179
|
}
|
|
272
180
|
if (choice.action === 'accept') {
|
|
273
|
-
|
|
274
|
-
|
|
181
|
+
// Provenance splits here, mandatorily. The table's own accept carries
|
|
182
|
+
// the origin that names the rule which decided; only a PICKER answer
|
|
183
|
+
// earns the plain 'accepted' class, which asserts a human weighed
|
|
184
|
+
// this failing artifact.
|
|
185
|
+
const origin = disposition.action === 'accept' ? disposition.debtOrigin : 'accepted';
|
|
186
|
+
if (origin === 'accepted') {
|
|
275
187
|
await rec('resolution: user ACCEPTED the work despite verify FAIL');
|
|
276
|
-
// Durable debt: the human blessed a FAILing artifact as-is, so the
|
|
277
|
-
// defect ships and nothing else in this task revisits it. Record it
|
|
278
|
-
// to the run ledger; the final integration gate re-checks it at run
|
|
279
|
-
// end and surfaces it if still open. Best-effort.
|
|
280
|
-
// The frozen-blocked routing above already recorded this defect (with
|
|
281
|
-
// the contradiction named) — don't double-enter it in the ledger.
|
|
282
|
-
if (!frozenDebtRecorded) {
|
|
283
|
-
try {
|
|
284
|
-
// Provenance splits here, mandatorily: an auto-pick writes the
|
|
285
|
-
// 'yolo-accepted' origin, never the plain 'accepted' one that
|
|
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
|
-
}
|
|
292
188
|
}
|
|
189
|
+
// Durable debt: the defect ships and nothing else in this task
|
|
190
|
+
// revisits it. The final integration gate re-checks it at run end and
|
|
191
|
+
// surfaces it if still open. The reason LEADS with the fail text so
|
|
192
|
+
// its minted class prefix still routes the run-end re-check.
|
|
193
|
+
await settleDebt(origin, disposition.rule === 'spec-contradiction' ?
|
|
194
|
+
`${failReason} — ${disposition.reason}`
|
|
195
|
+
: failReason);
|
|
293
196
|
// ROOT CAUSE: an accepted FAIL that some OTHER task's file caused is
|
|
294
197
|
// not fixed by accepting it — every later task keeps tripping over
|
|
295
198
|
// the same bug. Queue the scoped repair so the plan closes it.
|
|
@@ -306,7 +209,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
306
209
|
// recording must never break the gate sequence
|
|
307
210
|
}
|
|
308
211
|
}
|
|
309
|
-
notifyRun(active, `${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.${
|
|
212
|
+
notifyRun(active, `${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.${unattended ? ` ${YOLO_STAMP}` : ''}`, 'warning');
|
|
310
213
|
break;
|
|
311
214
|
}
|
|
312
215
|
// AUTOFIX: re-run the implementation turn with the failure (and any typed
|
|
@@ -315,24 +218,31 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
315
218
|
// hand its diagnosis to the re-run rather than making it re-derive the
|
|
316
219
|
// cause from the bare FAIL line. Skipped when there is no researched
|
|
317
220
|
// rationale beyond the failure text itself.
|
|
318
|
-
// Only the picker branch may claim a person chose this: the
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
if (
|
|
221
|
+
// Only the picker branch may claim a person chose this: the unattended
|
|
222
|
+
// branch already recorded itself one line above, and a trail that says
|
|
223
|
+
// "user chose" when nobody was asked is the same lie the accept line
|
|
224
|
+
// used to tell.
|
|
225
|
+
if (disposition.action === 'ask') {
|
|
323
226
|
await rec('resolution: user chose AUTOFIX — re-running the implementation turn');
|
|
324
227
|
}
|
|
325
228
|
notifyBoth(active, `${p.tag}: autofixing "${p.title}"…`, 'info');
|
|
326
229
|
const diagnosis = (recOutcome.recommend === 'autofix'
|
|
327
230
|
&& recOutcome.rationale.length > 0
|
|
328
231
|
&& recOutcome.rationale !== failReason) ?
|
|
329
|
-
|
|
232
|
+
recOutcome.rationale
|
|
330
233
|
: '';
|
|
331
|
-
const fixInstruction = `${failReason}${diagnosis}${choice.guidance ? `\n\nUser guidance: ${choice.guidance}` : ''}`;
|
|
332
234
|
const fixRes = await deps.runTask(active, p.cwd, p.title, {
|
|
333
235
|
resumeId: p.taskId,
|
|
334
236
|
planContext: p.planContext,
|
|
335
|
-
|
|
237
|
+
fixContext: {
|
|
238
|
+
outcome: verified,
|
|
239
|
+
disposition,
|
|
240
|
+
probes: verified.probes ?? {},
|
|
241
|
+
attempt: autoFixCount,
|
|
242
|
+
contradiction,
|
|
243
|
+
...(diagnosis === '' ? {} : { diagnosis }),
|
|
244
|
+
...(choice.guidance === undefined ? {} : { guidance: choice.guidance })
|
|
245
|
+
}
|
|
336
246
|
});
|
|
337
247
|
active = fixRes.ctx ?? active;
|
|
338
248
|
// The re-run's ending, mapped to this loop's own terminal kinds. A
|
|
@@ -364,6 +274,15 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
364
274
|
verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
365
275
|
await rec(verdictLine(verified));
|
|
366
276
|
}
|
|
277
|
+
// Static checks the repo was ALREADY failing when this task started (see
|
|
278
|
+
// health-baseline.ts). They did not fail this gate — that is the point — but
|
|
279
|
+
// dropping them here is how a sibling's defect vanishes until the run-end
|
|
280
|
+
// gate rediscovers it with nobody's name on it. Recorded once, after the
|
|
281
|
+
// loop, whatever the verdict was: a task can verify perfectly in a red repo.
|
|
282
|
+
if (verified.inheritedHealth) {
|
|
283
|
+
await rec(`accept-debt: inherited repo health — ${verified.inheritedHealth}`);
|
|
284
|
+
await settleDebt('inherited-health', verified.inheritedHealth);
|
|
285
|
+
}
|
|
367
286
|
// Loop exited because the work verified OR the user accepted the artifact. A
|
|
368
287
|
// genuine clean pass is ok===true with NO reason; a no-op pass or an
|
|
369
288
|
// accept-override (verified.ok still false at break) is NOT a guardable signal.
|
|
@@ -448,12 +367,13 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
448
367
|
let enforceEditsBlocked = false;
|
|
449
368
|
if (mode === 'edit' && deps.repoHealth && editsMade !== false) {
|
|
450
369
|
const after = await deps.repoHealth(active, p.cwd, p.title);
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
370
|
+
// The SAME differential the verify gate runs (health-baseline.ts), per
|
|
371
|
+
// command rather than per overall verdict — two runs can both be red
|
|
372
|
+
// while a different check failed in each. A missing baseline is no
|
|
373
|
+
// longer read as a clean one: that default silently turned this
|
|
374
|
+
// differential back into the absolute check it exists to replace.
|
|
375
|
+
const delta = classifyHealthDelta(healthBefore ?? null, after);
|
|
376
|
+
if (delta === 'regressed') {
|
|
457
377
|
enforceEditsBlocked = true;
|
|
458
378
|
const outputTail = after.output ? ` — output:\n${clampOutput(after.output)}` : '';
|
|
459
379
|
if (deps.discardEdits) {
|
|
@@ -465,7 +385,7 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
|
|
|
465
385
|
}
|
|
466
386
|
notifyRun(active, `${p.tag}: guideline edits on "${p.title}" regressed repo health (${after.reason.slice(0, 120)}) — discarded before commit.`, 'warning');
|
|
467
387
|
}
|
|
468
|
-
else if (
|
|
388
|
+
else if (delta === 'pre-existing') {
|
|
469
389
|
// Failing both before and after → not enforce's fault. Keep the edits;
|
|
470
390
|
// record that the repo entered the gate already unhealthy so the trail
|
|
471
391
|
// 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
|
}
|
|
@@ -13,19 +13,18 @@ const FRONT_MATTER_KEYS = [
|
|
|
13
13
|
'updated_at',
|
|
14
14
|
'title',
|
|
15
15
|
'label',
|
|
16
|
+
'plan_key',
|
|
16
17
|
'reason'
|
|
17
18
|
];
|
|
19
|
+
/** Front-matter keys omitted entirely when empty rather than emitted blank. */
|
|
20
|
+
const OPTIONAL_KEYS = new Set(['label', 'plan_key', 'reason']);
|
|
18
21
|
// ─── Front matter ────────────────────────────────────────────────────────────
|
|
19
22
|
export function emitFrontMatter(fm) {
|
|
20
23
|
const lines = ['---'];
|
|
21
24
|
for (const k of FRONT_MATTER_KEYS) {
|
|
22
25
|
const v = fm[k];
|
|
23
|
-
if (v === undefined || v === '')
|
|
24
|
-
|
|
25
|
-
// as a blank `key:` line.
|
|
26
|
-
if (k === 'reason' || k === 'label')
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
26
|
+
if ((v === undefined || v === '') && OPTIONAL_KEYS.has(k))
|
|
27
|
+
continue;
|
|
29
28
|
lines.push(`${k}: ${typeof v === 'string' ? v : String(v)}`);
|
|
30
29
|
}
|
|
31
30
|
lines.push('---');
|
|
@@ -59,6 +58,7 @@ export function parseFrontMatter(content) {
|
|
|
59
58
|
updated_at: obj.updated_at ?? obj.created_at,
|
|
60
59
|
title: obj.title ?? '',
|
|
61
60
|
label: obj.label || undefined,
|
|
61
|
+
plan_key: obj.plan_key || undefined,
|
|
62
62
|
reason: obj.reason || undefined
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -21,6 +21,13 @@ export interface TaskFrontMatter {
|
|
|
21
21
|
* of `title` (`titleForDisplay`, parsers.ts). See title-label.ts.
|
|
22
22
|
*/
|
|
23
23
|
label?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The /task-auto plan entry this task implements (`TaskEntry.key`). It is
|
|
26
|
+
* what the owned-requirements ledger joins on, and it lives here rather than
|
|
27
|
+
* in memory because a resumed run reconstructs the task from this file alone.
|
|
28
|
+
* Absent on a bare /task and on any task planned before keys existed.
|
|
29
|
+
*/
|
|
30
|
+
plan_key?: string;
|
|
24
31
|
reason?: string;
|
|
25
32
|
}
|
|
26
33
|
export declare const PHASE_ORDER: PhaseName[];
|
package/dist/task/timings.d.ts
CHANGED
|
@@ -18,3 +18,14 @@ export interface TimingEntry {
|
|
|
18
18
|
}
|
|
19
19
|
export declare function formatMs(ms: number): string;
|
|
20
20
|
export declare function formatTimings(entries: ReadonlyArray<TimingEntry>): string;
|
|
21
|
+
/**
|
|
22
|
+
* The `## phase timings` body once this attempt's phases are added to it.
|
|
23
|
+
*
|
|
24
|
+
* A run re-entered for an autofix resumes at `phase: done` and therefore runs no
|
|
25
|
+
* phases at all, so rendering only what IT timed wrote an empty block over the
|
|
26
|
+
* five phases the first attempt measured — the timings vanished for exactly the
|
|
27
|
+
* tasks that took longest. Each attempt gets its own labelled block instead, and
|
|
28
|
+
* an unlabelled body written before this grammar is adopted as attempt 1 rather
|
|
29
|
+
* than discarded.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mergeTimings(prev: string | null, next: ReadonlyArray<TimingEntry>): string;
|