@mjasnikovs/pi-task 0.32.0 → 0.33.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.
@@ -285,6 +285,29 @@ export type GateResult = {
285
285
  * regardless of this count (blessing an artifact as-is is a human's call).
286
286
  */
287
287
  export declare const MAX_AUTO_AUTOFIX = 3;
288
+ /** Which of the four disjoint branches sent a FAIL to the terminal YOLO ACCEPT. */
289
+ export interface YoloAcceptContext {
290
+ /** Rule 5c: the spec-required check could not run (tooling absent). */
291
+ isUnobserved: boolean;
292
+ /** Cross-task contradiction: the repo-health fix needs a spec-frozen path. */
293
+ isFrozenBlocked: boolean;
294
+ /** What the resolution research recommended, when it was consulted at all. */
295
+ recommend: ResolutionOutcome['recommend'];
296
+ /** Unattended AUTOFIX attempts already spent on this task. */
297
+ autoFixCount: number;
298
+ }
299
+ /**
300
+ * The reason an auto-ACCEPT is being written — NAMED, not assumed.
301
+ *
302
+ * The line this replaces asserted "autofix budget spent" on every branch. It was
303
+ * measured false in 2709 of 2709 recorded accepts (scripts/yolo-accept-baserate.ts):
304
+ * the budget has never once reached MAX_AUTO_AUTOFIX anywhere in the corpus — 30%
305
+ * of accepts are UNOBSERVED (the research is never even consulted) and 70% are an
306
+ * ACCEPT recommendation with the budget fully untouched. A durable trail that
307
+ * misstates why a defect shipped is worse than no trail: mx5 run 19's TASK_0009
308
+ * reads as an exhausted fixer when nothing was ever attempted.
309
+ */
310
+ export declare function yoloAcceptReason(c: YoloAcceptContext): string;
288
311
  /**
289
312
  * Show the boxed two-choice picker after a verify FAIL and return what the user
290
313
  * decided. The model-recommended card is placed first so the renderer tints it
@@ -13,6 +13,29 @@ import { attributeEnforceFailure } from './enforce-attribution.js';
13
13
  * regardless of this count (blessing an artifact as-is is a human's call).
14
14
  */
15
15
  export const MAX_AUTO_AUTOFIX = 3;
16
+ /**
17
+ * The reason an auto-ACCEPT is being written — NAMED, not assumed.
18
+ *
19
+ * The line this replaces asserted "autofix budget spent" on every branch. It was
20
+ * measured false in 2709 of 2709 recorded accepts (scripts/yolo-accept-baserate.ts):
21
+ * the budget has never once reached MAX_AUTO_AUTOFIX anywhere in the corpus — 30%
22
+ * of accepts are UNOBSERVED (the research is never even consulted) and 70% are an
23
+ * ACCEPT recommendation with the budget fully untouched. A durable trail that
24
+ * misstates why a defect shipped is worse than no trail: mx5 run 19's TASK_0009
25
+ * reads as an exhausted fixer when nothing was ever attempted.
26
+ */
27
+ export function yoloAcceptReason(c) {
28
+ if (c.isUnobserved)
29
+ return 'verify UNOBSERVED — tooling absent, an unattended re-run cannot provision it';
30
+ if (c.isFrozenBlocked)
31
+ return 'repo-health blocked by a spec-frozen path — an impl re-run under the same freeze cannot converge';
32
+ if (c.recommend === 'autofix') {
33
+ return `autofix budget spent (${c.autoFixCount}/${MAX_AUTO_AUTOFIX})`;
34
+ }
35
+ return c.autoFixCount === 0 ?
36
+ `judge recommended ACCEPT (autofix budget 0/${MAX_AUTO_AUTOFIX} unused)`
37
+ : `judge recommended ACCEPT (autofix budget ${c.autoFixCount}/${MAX_AUTO_AUTOFIX} already spent)`;
38
+ }
16
39
  /**
17
40
  * Bound a captured health-check output before it is embedded in a gate-trail line.
18
41
  * appendGateRecord flattens newlines to spaces, so the trail stays one line per
@@ -134,6 +157,8 @@ export async function runGatesForTask(ctxIn, deps, p) {
134
157
  // defect is recorded as a durable debt for the final gate.
135
158
  let frozenContradiction = null;
136
159
  let frozenDebtRecorded = false;
160
+ // YOLO only: has the one-attempt rescue below already been spent on this task?
161
+ let yoloRescueUsed = false;
137
162
  while (!verified.ok) {
138
163
  const failReason = verified.reason ?? 'did not verify';
139
164
  // GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
@@ -213,23 +238,54 @@ export async function runGatesForTask(ctxIn, deps, p) {
213
238
  const autoFixNow = !isUnobserved
214
239
  && !isFrozenBlocked
215
240
  && recOutcome.recommend === 'autofix'
216
- && autoFixCount < MAX_AUTO_AUTOFIX;
217
- // YOLO: the picker is unreachable with nobody watching, and by the time
218
- // we are here the unattended AUTOFIX budget is ALREADY spent (autoFixNow
219
- // is false) so the only option left that terminates is ACCEPT, recorded
220
- // as its own 'yolo-accepted' debt. Deliberately NOT a re-entry into
221
- // autofix: MAX_AUTO_AUTOFIX exists to break a non-converging loop, and an
241
+ && autoFixCount < MAX_AUTO_AUTOFIX
242
+ // A rescue attempt that still FAILed is terminal under YOLO: the
243
+ // recommendation that got us here was ACCEPT, so a later flip to
244
+ // AUTOFIX must not bootstrap the full budget from it.
245
+ && !yoloRescueUsed;
246
+ // YOLO, THE RESCUE BRANCH: the recommendation is ACCEPT, nobody can be
247
+ // asked, and the unattended budget is UNTOUCHED. Accepting here ships a
248
+ // defect having attempted nothing — mx5 run 19 did exactly that twice,
249
+ // and the final-gate autofix later fixed one of the two in a single pass
250
+ // (`0 pass 130 fail` → `121 pass 0 fail`). So spend ONE attempt first.
251
+ // Bounded by construction: one, not MAX_AUTO_AUTOFIX, so an ACCEPT
252
+ // recommendation can never restart a full loop; if it still FAILs the
253
+ // next turn falls through to the same auto-ACCEPT and the same debt.
254
+ const yoloRescueNow = isYoloMode()
255
+ && !yoloRescueUsed
256
+ && !isUnobserved
257
+ && !isFrozenBlocked
258
+ && recOutcome.recommend === 'accept'
259
+ && autoFixCount === 0;
260
+ // YOLO: the picker is unreachable with nobody watching, and every
261
+ // unattended attempt this task may make has been made — so the only
262
+ // option left that terminates is ACCEPT, recorded as its own
263
+ // 'yolo-accepted' debt. Deliberately NOT a re-entry into autofix:
264
+ // MAX_AUTO_AUTOFIX exists to break a non-converging loop, and an
222
265
  // auto-pick here would restart the budget from the site that proves it ran out.
223
- const yoloChoice = autoFixNow ? null : yoloVerifyResolution(isYoloMode());
266
+ const yoloChoice = autoFixNow || yoloRescueNow ? null : yoloVerifyResolution(isYoloMode());
224
267
  let choice;
225
268
  if (yoloChoice !== null) {
226
269
  choice = yoloChoice;
227
- await rec(`resolution: auto-ACCEPTED despite verify FAIL autofix budget spent, nobody to ask ${YOLO_STAMP}`);
270
+ // NAME the branch. This line is the durable record of why a defect
271
+ // shipped; asserting a spent budget on all four branches made run
272
+ // 19's zero-attempt accepts read as an exhausted fixer.
273
+ await rec(`resolution: auto-ACCEPTED despite verify FAIL — ${yoloAcceptReason({
274
+ isUnobserved,
275
+ isFrozenBlocked,
276
+ recommend: recOutcome.recommend,
277
+ autoFixCount
278
+ })}, nobody to ask ${YOLO_STAMP}`);
228
279
  }
229
- else if (autoFixNow) {
280
+ else if (autoFixNow || yoloRescueNow) {
230
281
  autoFixCount += 1;
231
- await rec(`resolution: auto-AUTOFIX (recommended, unattended ${autoFixCount}/${MAX_AUTO_AUTOFIX})`);
232
- active.ui.notify(`${p.tag}: verify FAIL on "${p.title}" — auto-fixing (recommended, ${autoFixCount}/${MAX_AUTO_AUTOFIX})…`, 'info');
282
+ if (yoloRescueNow)
283
+ yoloRescueUsed = true;
284
+ await rec(yoloRescueNow ?
285
+ `resolution: auto-AUTOFIX (${YOLO_STAMP} rescue — judge recommended ACCEPT with the unattended `
286
+ + `budget unspent; one attempt, ${autoFixCount}/${MAX_AUTO_AUTOFIX})`
287
+ : `resolution: auto-AUTOFIX (recommended, unattended ${autoFixCount}/${MAX_AUTO_AUTOFIX})`);
288
+ active.ui.notify(`${p.tag}: verify FAIL on "${p.title}" — auto-fixing (${yoloRescueNow ? `${YOLO_STAMP} one attempt before accepting` : 'recommended'}, ${autoFixCount}/${MAX_AUTO_AUTOFIX})…`, 'info');
233
289
  choice = { action: 'autofix' };
234
290
  }
235
291
  else {
@@ -292,7 +348,13 @@ export async function runGatesForTask(ctxIn, deps, p) {
292
348
  // hand that diagnosis to the re-run so it fixes the located cause
293
349
  // instead of re-deriving it from the bare FAIL line. Skipped when there
294
350
  // is no researched rationale beyond the failure text itself.
295
- await rec('resolution: user chose AUTOFIX re-running the implementation turn');
351
+ // Only the picker branch may claim a person chose this: the two
352
+ // unattended branches already recorded themselves one line above, and a
353
+ // trail that says "user chose" when nobody was asked is the same lie the
354
+ // accept line used to tell.
355
+ if (!autoFixNow && !yoloRescueNow) {
356
+ await rec('resolution: user chose AUTOFIX — re-running the implementation turn');
357
+ }
296
358
  active.ui.notify(`${p.tag}: autofixing "${p.title}"…`, 'info');
297
359
  const diagnosis = (recOutcome.recommend === 'autofix'
298
360
  && recOutcome.rationale.length > 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",