@mjasnikovs/pi-task 0.18.29 → 0.18.31
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 +15 -0
- package/dist/config/config.js +8 -1
- package/dist/config/register.js +10 -0
- package/dist/task/accept-debt.d.ts +13 -1
- package/dist/task/accept-debt.js +18 -1
- package/dist/task/auto-orchestrator.d.ts +8 -0
- package/dist/task/auto-orchestrator.js +127 -22
- package/dist/task/final-gate-fix.d.ts +25 -0
- package/dist/task/final-gate-fix.js +33 -0
- package/dist/task/foreign-path.d.ts +96 -0
- package/dist/task/foreign-path.js +0 -0
- package/dist/task/gate-deps.js +143 -1
- package/dist/task/parsers.d.ts +18 -0
- package/dist/task/parsers.js +4 -3
- package/dist/task/phases.js +56 -10
- package/dist/task/runner-globs.d.ts +74 -0
- package/dist/task/runner-globs.js +155 -0
- package/dist/task/script-escape.d.ts +83 -0
- package/dist/task/script-escape.js +189 -0
- package/dist/task/task-gates.d.ts +8 -0
- package/dist/task/task-gates.js +26 -4
- package/dist/task/verify-work.d.ts +39 -1
- package/dist/task/verify-work.js +126 -2
- package/dist/task/yolo.d.ts +74 -0
- package/dist/task/yolo.js +112 -0
- package/package.json +1 -1
package/dist/config/config.d.ts
CHANGED
|
@@ -61,6 +61,21 @@ export interface PiTaskConfig {
|
|
|
61
61
|
* a true hang doesn't cost half an hour of dead time.
|
|
62
62
|
*/
|
|
63
63
|
requestTimeoutMs: number;
|
|
64
|
+
/**
|
|
65
|
+
* UNATTENDED AUTO-PICK (see task/yolo.ts): wherever pi-task would stop and ask,
|
|
66
|
+
* take the option it already marks RECOMMENDED, stamp the artifact `(YOLO)` so
|
|
67
|
+
* an audit can tell a machine decided, and never notify. Lets a local model run
|
|
68
|
+
* a throwaway/test project end to end with nobody watching.
|
|
69
|
+
*
|
|
70
|
+
* Decided PER SITE, before the prompt is built — so the lone prompt notification
|
|
71
|
+
* (SessionUI.ask) is suppressed structurally, and the existing unattended budgets
|
|
72
|
+
* (MAX_AUTO_AUTOFIX, MAX_FINAL_GATE_AUTOFIX) still bound the loops they were
|
|
73
|
+
* written to bound. An auto-pick may cost time, never work: a question with no
|
|
74
|
+
* recommendation, or one the anti-synthesis guard demoted, is SKIPPED, not
|
|
75
|
+
* invented.
|
|
76
|
+
* DEFAULT OFF — this is never the behaviour of a normal, watched run.
|
|
77
|
+
*/
|
|
78
|
+
yoloMode: boolean;
|
|
64
79
|
}
|
|
65
80
|
/**
|
|
66
81
|
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
package/dist/config/config.js
CHANGED
|
@@ -38,7 +38,9 @@ const DEFAULTS = {
|
|
|
38
38
|
researchCache: true,
|
|
39
39
|
searchProvider: 'exa',
|
|
40
40
|
extensionWhitelist: [],
|
|
41
|
-
requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS
|
|
41
|
+
requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
|
|
42
|
+
// OFF: auto-answering is for unattended throwaway runs only.
|
|
43
|
+
yoloMode: false
|
|
42
44
|
};
|
|
43
45
|
/**
|
|
44
46
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
@@ -67,6 +69,11 @@ if (!G.loaded) {
|
|
|
67
69
|
delete parsed.searchProvider;
|
|
68
70
|
parsed.extensionWhitelist = sanitizeExtensionWhitelist(parsed.extensionWhitelist);
|
|
69
71
|
parsed.requestTimeoutMs = sanitizeRequestTimeoutMs(parsed.requestTimeoutMs);
|
|
72
|
+
// A hand-edited `"yoloMode": "false"` is a truthy string — it must not
|
|
73
|
+
// silently switch a watched run into unattended auto-pick. Only a real
|
|
74
|
+
// boolean counts; anything else falls back to the OFF default.
|
|
75
|
+
if (typeof parsed.yoloMode !== 'boolean')
|
|
76
|
+
delete parsed.yoloMode;
|
|
70
77
|
G.config = { ...DEFAULTS, ...parsed };
|
|
71
78
|
}
|
|
72
79
|
catch {
|
package/dist/config/register.js
CHANGED
|
@@ -107,6 +107,16 @@ const ITEMS = [
|
|
|
107
107
|
+ 'unbounded',
|
|
108
108
|
// Display human labels; the stored config value stays the ms number.
|
|
109
109
|
values: COMMAND_TIMEOUT_OPTIONS.map(o => o.label)
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'yoloMode',
|
|
113
|
+
label: 'yolo mode',
|
|
114
|
+
description: 'UNATTENDED: auto-answer every question with the option pi already recommends, '
|
|
115
|
+
+ 'and show no prompts at all — clarify/grill answers, the verify-FAIL picker '
|
|
116
|
+
+ '(auto-ACCEPT, recorded as a yolo debt), and the final-gate picker (autofix '
|
|
117
|
+
+ 'while the budget lasts, then leave the run FAILED). Every auto-pick is stamped '
|
|
118
|
+
+ '(YOLO) in the task file and debt ledger. For THROWAWAY/TEST projects you are '
|
|
119
|
+
+ 'not watching — a real run should decide these itself'
|
|
110
120
|
}
|
|
111
121
|
];
|
|
112
122
|
/** Human label for the stored command-timeout ms (falls back to the raw ms). */
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* created files need a tsconfig registration every spec forbids). Cross-task
|
|
12
12
|
* contradiction: no unattended re-run can converge, so the gate loop records the
|
|
13
13
|
* defect and routes to the human picker instead of burning AUTOFIX rounds.
|
|
14
|
+
* - 'yolo-accepted' — YOLO MODE (unattended auto-pick, see yolo.ts) took the
|
|
15
|
+
* verify-FAIL picker's terminal option with nobody watching. It is NOT the
|
|
16
|
+
* 'accepted' class and must never collapse into it: 'accepted' asserts a HUMAN
|
|
17
|
+
* weighed this failing artifact and blessed it, which is exactly the assurance
|
|
18
|
+
* an auto-pick cannot give. Same re-check treatment, honest provenance.
|
|
14
19
|
* - 'cross-task-deletion' — the task's work DELETED a sibling task's committed
|
|
15
20
|
* deliverable (mx5 run 12 PROMPT 2: a fix child deleted TASK_0020's playwright ct
|
|
16
21
|
* files to green a lint) and the user ACCEPTed the verify-FAIL anyway, so the
|
|
@@ -18,7 +23,7 @@
|
|
|
18
23
|
* resolved iff the named file is back in the tree (a later task restored it),
|
|
19
24
|
* otherwise surfaced.
|
|
20
25
|
*/
|
|
21
|
-
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion';
|
|
26
|
+
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion' | 'yolo-accepted';
|
|
22
27
|
/** One recorded defect: the task, why its VERIFY failed, and how it was recorded. */
|
|
23
28
|
export interface AcceptDebt {
|
|
24
29
|
taskId: string;
|
|
@@ -77,6 +82,13 @@ export declare function recordCrossTaskDeletionDebt(cwd: string, taskId: string,
|
|
|
77
82
|
path: string;
|
|
78
83
|
owner: string;
|
|
79
84
|
}): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Record a YOLO-ACCEPTED debt: unattended auto-pick took the verify-FAIL picker's
|
|
87
|
+
* ACCEPT branch because there was nobody to ask (yolo.ts). Its own origin — and
|
|
88
|
+
* therefore its own line in the final gate's surfaced report — so a later audit
|
|
89
|
+
* reading only the artifacts can never read it as "a human decided this".
|
|
90
|
+
*/
|
|
91
|
+
export declare function recordYoloAcceptDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
80
92
|
/**
|
|
81
93
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
82
94
|
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -76,7 +76,8 @@ export function parseAcceptDebts(raw) {
|
|
|
76
76
|
reason: parts[1].trim(),
|
|
77
77
|
...((origin === 'enforce-revert'
|
|
78
78
|
|| origin === 'frozen-blocked'
|
|
79
|
-
|| origin === 'cross-task-deletion'
|
|
79
|
+
|| origin === 'cross-task-deletion'
|
|
80
|
+
|| origin === 'yolo-accepted') ?
|
|
80
81
|
{ origin: origin }
|
|
81
82
|
: {})
|
|
82
83
|
});
|
|
@@ -173,6 +174,19 @@ export async function recordCrossTaskDeletionDebt(cwd, taskId, deletion) {
|
|
|
173
174
|
origin: 'cross-task-deletion'
|
|
174
175
|
});
|
|
175
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Record a YOLO-ACCEPTED debt: unattended auto-pick took the verify-FAIL picker's
|
|
179
|
+
* ACCEPT branch because there was nobody to ask (yolo.ts). Its own origin — and
|
|
180
|
+
* therefore its own line in the final gate's surfaced report — so a later audit
|
|
181
|
+
* reading only the artifacts can never read it as "a human decided this".
|
|
182
|
+
*/
|
|
183
|
+
export async function recordYoloAcceptDebt(cwd, taskId, reason) {
|
|
184
|
+
await appendDebt(cwd, {
|
|
185
|
+
taskId: taskId.trim(),
|
|
186
|
+
reason: normaliseReason(reason),
|
|
187
|
+
origin: 'yolo-accepted'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
176
190
|
/**
|
|
177
191
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
178
192
|
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
@@ -324,5 +338,8 @@ export function describeDebt(d) {
|
|
|
324
338
|
if (d.origin === 'cross-task-deletion') {
|
|
325
339
|
return "a sibling task's committed deliverable was DELETED by this task's work and the deletion was accepted (still missing from the tree)";
|
|
326
340
|
}
|
|
341
|
+
if (d.origin === 'yolo-accepted') {
|
|
342
|
+
return 'auto-ACCEPTED by YOLO mode despite verify-FAIL (unattended — no human weighed this)';
|
|
343
|
+
}
|
|
327
344
|
return 'accepted despite verify-FAIL';
|
|
328
345
|
}
|
|
@@ -45,6 +45,14 @@ export interface AutoDeps extends GateDeps {
|
|
|
45
45
|
* Leave-failed / Accept, exactly the pre-autofix behavior.
|
|
46
46
|
*/
|
|
47
47
|
finalGateFix?: FinalGateFixFn;
|
|
48
|
+
/**
|
|
49
|
+
* Paths currently uncommitted in the working tree (`git status` shape), used to
|
|
50
|
+
* detect SUB-FIXES a non-converging final-gate autofix left behind (mx5 run 13
|
|
51
|
+
* PROMPT 4 item 3). Every task is committed by the time the final gate runs, so
|
|
52
|
+
* anything dirty here is the fix pass's own work. Absent → the stranded-fix
|
|
53
|
+
* handling is skipped entirely (prior behavior).
|
|
54
|
+
*/
|
|
55
|
+
pendingChanges?: (cwd: string) => Promise<string[]>;
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
58
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -24,13 +24,14 @@ import { SessionUI, registerBridgeCommand, publishLifecycleNotice } from '../rem
|
|
|
24
24
|
import { pushNotify } from '../remote/push.js';
|
|
25
25
|
import { startAutoLoader } from './widget.js';
|
|
26
26
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
27
|
-
import { buildGateDeps } from './gate-deps.js';
|
|
27
|
+
import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
|
|
28
28
|
import { runGatesForTask } from './task-gates.js';
|
|
29
29
|
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
30
30
|
import { runFinalIntegrationGate } from './final-gate.js';
|
|
31
31
|
import { describeDebt } from './accept-debt.js';
|
|
32
|
-
import { classifyFinalGateAnswer, MAX_FINAL_GATE_AUTOFIX, FINAL_LEAVE_LABEL, FINAL_LEAVE_VALUE, FINAL_ACCEPT_LABEL, FINAL_ACCEPT_VALUE, FINAL_AUTOFIX_LABEL, FINAL_AUTOFIX_VALUE } from './final-gate-fix.js';
|
|
32
|
+
import { classifyFinalGateAnswer, MAX_FINAL_GATE_AUTOFIX, FINAL_LEAVE_LABEL, FINAL_LEAVE_VALUE, FINAL_ACCEPT_LABEL, FINAL_ACCEPT_VALUE, FINAL_AUTOFIX_LABEL, FINAL_AUTOFIX_VALUE, STRANDED_FIX_COMMIT, strandedFixNote } from './final-gate-fix.js';
|
|
33
33
|
import { getConfig } from '../config/config.js';
|
|
34
|
+
import { isYoloMode, yoloPickAnswer, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
|
|
34
35
|
import { configureResearchRun } from '../workers/research-cache.js';
|
|
35
36
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
36
37
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
@@ -409,6 +410,20 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
409
410
|
// the recommended one tinted green; an open question shows the bare text
|
|
410
411
|
// prompt. No verbose "Recommended:" / "press Enter to accept" scaffolding.
|
|
411
412
|
const twoOption = plainSuggested !== undefined && plainAlt !== undefined;
|
|
413
|
+
// YOLO: take the recommended option (index 0 / the green card) without ever
|
|
414
|
+
// building the prompt. Clarify has no anti-synthesis channel — it runs before
|
|
415
|
+
// any research — so the only step-aside here is a question that carries no
|
|
416
|
+
// recommendation to take; that one is skipped rather than guessed.
|
|
417
|
+
const yolo = yoloPickAnswer(isYoloMode(), {
|
|
418
|
+
...(plainSuggested !== undefined && { suggested: plainSuggested }),
|
|
419
|
+
...(plainAlt !== undefined && { alt: plainAlt })
|
|
420
|
+
});
|
|
421
|
+
if (yolo !== null) {
|
|
422
|
+
const auto = yolo.kind === 'answer' ? yolo.answer : `(skipped — ${yolo.note})`;
|
|
423
|
+
answers.push(`Q${answers.length + 1}: ${plainQ}\n`
|
|
424
|
+
+ `A${answers.length + 1}: ${auto} ${YOLO_STAMP}`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
412
427
|
const options = twoOption ?
|
|
413
428
|
[
|
|
414
429
|
{
|
|
@@ -889,7 +904,14 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
889
904
|
// run-level half of the same verification story.
|
|
890
905
|
finalGate: (cwd2, planText) => getConfig().verifyWork ?
|
|
891
906
|
runFinalIntegrationGate(cwd2, undefined, undefined, undefined, planText)
|
|
892
|
-
: Promise.resolve({ ok: true, reason: 'disabled' })
|
|
907
|
+
: Promise.resolve({ ok: true, reason: 'disabled' }),
|
|
908
|
+
// Uncommitted paths, for the stranded-sub-fix handling around the final-gate
|
|
909
|
+
// picker (mx5 run 13 PROMPT 4 item 3). Every task is committed by the time
|
|
910
|
+
// the gate runs, so whatever is dirty here belongs to the fix pass.
|
|
911
|
+
pendingChanges: async (cwd2) => {
|
|
912
|
+
const changes = await collectTreeChanges(cwd2, signal);
|
|
913
|
+
return [...changes.modified, ...changes.added, ...changes.deleted].sort();
|
|
914
|
+
}
|
|
893
915
|
};
|
|
894
916
|
}
|
|
895
917
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
@@ -994,6 +1016,21 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
994
1016
|
// after MAX_FINAL_GATE_AUTOFIX attempts that still FAIL the
|
|
995
1017
|
// autofix card is withdrawn so the loop cannot run unbounded.
|
|
996
1018
|
let fixAttempts = 0;
|
|
1019
|
+
// Sub-fixes a non-converging autofix attempt left uncommitted.
|
|
1020
|
+
// Refreshed after every attempt; drives the picker note and the
|
|
1021
|
+
// accept-time commit (mx5 run 13 PROMPT 4 item 3).
|
|
1022
|
+
let stranded = [];
|
|
1023
|
+
const refreshStranded = async () => {
|
|
1024
|
+
if (!deps.pendingChanges)
|
|
1025
|
+
return;
|
|
1026
|
+
try {
|
|
1027
|
+
stranded = await deps.pendingChanges(cwd);
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// Inconclusive: say nothing rather than claim a clean tree.
|
|
1031
|
+
stranded = [];
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
997
1034
|
while (!fin.ok) {
|
|
998
1035
|
const canAutofix = deps.finalGateFix !== undefined && fixAttempts < MAX_FINAL_GATE_AUTOFIX;
|
|
999
1036
|
// The picker question shows the debts (the HUMAN weighs them);
|
|
@@ -1004,26 +1041,73 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1004
1041
|
+ '(the project’s own test/build/static commands, run unaided).'
|
|
1005
1042
|
+ (fixAttempts > 0 ?
|
|
1006
1043
|
`\n\nAutofix attempts so far: ${fixAttempts}/${MAX_FINAL_GATE_AUTOFIX}.`
|
|
1007
|
-
: '')
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1044
|
+
: '')
|
|
1045
|
+
// Never let a partial repair be invisible at the moment
|
|
1046
|
+
// the human decides (run 13: a bunfig fix that made
|
|
1047
|
+
// `bun run test` pass 116/116 was stranded by an ACCEPT).
|
|
1048
|
+
+ strandedFixNote(stranded);
|
|
1049
|
+
// YOLO: keep autofixing WHILE the card is still offered — the
|
|
1050
|
+
// loop withdraws it after MAX_FINAL_GATE_AUTOFIX, so the cap
|
|
1051
|
+
// that bounds a non-converging fix pass still bounds this —
|
|
1052
|
+
// then LEAVE the run failed. Never 'accept': an unattended run
|
|
1053
|
+
// that could not green the whole-repo gate has not produced a
|
|
1054
|
+
// working project, and mx5 run 13 shows what an accepted FAIL
|
|
1055
|
+
// looks like afterwards (a shipped app that 404s at `/`).
|
|
1056
|
+
const yoloFinal = yoloFinalGateChoice(isYoloMode(), canAutofix);
|
|
1057
|
+
if (yoloFinal !== null) {
|
|
1058
|
+
await recGate(`final-gate: auto-chose ${yoloFinal.action.toUpperCase()} ${YOLO_STAMP}`);
|
|
1059
|
+
}
|
|
1060
|
+
const answer = yoloFinal !== null ?
|
|
1061
|
+
yoloFinal.action === 'autofix' ?
|
|
1062
|
+
FINAL_AUTOFIX_VALUE
|
|
1063
|
+
: FINAL_LEAVE_VALUE
|
|
1064
|
+
: await new SessionUI(active).ask({
|
|
1065
|
+
localTitle: 'Final integration gate failed — how should pi proceed?',
|
|
1066
|
+
displayQuestion: question,
|
|
1067
|
+
question,
|
|
1068
|
+
recommended: FINAL_LEAVE_LABEL,
|
|
1069
|
+
recommended2: canAutofix ? FINAL_AUTOFIX_LABEL : FINAL_ACCEPT_LABEL,
|
|
1070
|
+
allowSkip: false,
|
|
1071
|
+
options: [
|
|
1072
|
+
{ label: FINAL_LEAVE_LABEL, value: FINAL_LEAVE_VALUE },
|
|
1073
|
+
...(canAutofix ?
|
|
1074
|
+
[
|
|
1075
|
+
{
|
|
1076
|
+
label: FINAL_AUTOFIX_LABEL,
|
|
1077
|
+
value: FINAL_AUTOFIX_VALUE
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
: []),
|
|
1081
|
+
{ label: FINAL_ACCEPT_LABEL, value: FINAL_ACCEPT_VALUE }
|
|
1082
|
+
]
|
|
1083
|
+
});
|
|
1023
1084
|
const choice = classifyFinalGateAnswer(answer);
|
|
1024
1085
|
if (choice.action === 'accept') {
|
|
1025
1086
|
await recGate('final-gate: FAIL accepted by user');
|
|
1026
|
-
|
|
1087
|
+
// STRANDED SUB-FIXES: the run completes here, so anything
|
|
1088
|
+
// the fix pass repaired but never committed would be lost
|
|
1089
|
+
// to the next `git checkout` while HEAD keeps the defect
|
|
1090
|
+
// it fixed. Commit it as its own, named commit — the
|
|
1091
|
+
// ACCEPT is a decision about the FAILING gate, never an
|
|
1092
|
+
// instruction to throw away work (mx5 run 13 item 3).
|
|
1093
|
+
if (stranded.length > 0) {
|
|
1094
|
+
try {
|
|
1095
|
+
const sha = await deps.commit(cwd, STRANDED_FIX_COMMIT(id));
|
|
1096
|
+
await recGate(`final-gate: committed ${stranded.length} stranded fix-pass change(s)`
|
|
1097
|
+
+ `${sha ? ` as ${sha}` : ''} — ${stranded.slice(0, 8).join(', ')}`);
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
// Never break the completion path over this — but
|
|
1101
|
+
// say so, so the changes are not silently lost.
|
|
1102
|
+
await recGate(`final-gate: could NOT commit ${stranded.length} stranded fix-pass `
|
|
1103
|
+
+ `change(s) (${err instanceof Error ? err.message : String(err)}) — `
|
|
1104
|
+
+ `they remain UNCOMMITTED in the working tree: ${stranded.slice(0, 8).join(', ')}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
active.ui.notify(`${id}: final integration gate FAIL accepted by user — completing.`
|
|
1108
|
+
+ (stranded.length > 0 ?
|
|
1109
|
+
` ${stranded.length} uncommitted fix-pass change(s) committed separately.`
|
|
1110
|
+
: ''), 'warning');
|
|
1027
1111
|
break;
|
|
1028
1112
|
}
|
|
1029
1113
|
if (choice.action === 'autofix' && canAutofix) {
|
|
@@ -1042,6 +1126,14 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1042
1126
|
break;
|
|
1043
1127
|
}
|
|
1044
1128
|
await recGate(`final-gate: autofix attempt ${fixAttempts} failed — ${fix.reason.slice(0, 200)}`);
|
|
1129
|
+
// The attempt's edits survive a non-convergence (only a
|
|
1130
|
+
// guard trip discards). Find out what they are NOW, so
|
|
1131
|
+
// the next picker shows them and an ACCEPT can commit them.
|
|
1132
|
+
await refreshStranded();
|
|
1133
|
+
if (stranded.length > 0) {
|
|
1134
|
+
await recGate(`final-gate: autofix attempt ${fixAttempts} left ${stranded.length} `
|
|
1135
|
+
+ `uncommitted change(s) — ${stranded.slice(0, 8).join(', ')}`);
|
|
1136
|
+
}
|
|
1045
1137
|
active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
|
|
1046
1138
|
// Work from the FRESH gate failure when the fix pass got
|
|
1047
1139
|
// as far as re-running the gate; otherwise keep the last.
|
|
@@ -1064,9 +1156,22 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1064
1156
|
}
|
|
1065
1157
|
// Leave failed — the dismissal default, unchanged from the
|
|
1066
1158
|
// two-option picker (an unavailable autofix demotes here too).
|
|
1067
|
-
await recGate(
|
|
1159
|
+
await recGate(yoloFinal !== null ?
|
|
1160
|
+
`final-gate: left failed — autofix budget spent, nobody to ask ${YOLO_STAMP}`
|
|
1161
|
+
: 'final-gate: left failed (user)');
|
|
1162
|
+
// Leaving the run failed hands the working tree back to the
|
|
1163
|
+
// user, so uncommitted fix-pass edits are theirs to keep or
|
|
1164
|
+
// drop — but they must be VISIBLE, not discovered later by a
|
|
1165
|
+
// stray `git status` (mx5 run 13 item 3).
|
|
1166
|
+
if (stranded.length > 0) {
|
|
1167
|
+
await recGate(`final-gate: ${stranded.length} uncommitted fix-pass change(s) left in the `
|
|
1168
|
+
+ `working tree — ${stranded.slice(0, 8).join(', ')}`);
|
|
1169
|
+
}
|
|
1068
1170
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
1069
|
-
announceDone(active, `${id} finished all tasks but FAILED the final integration gate — ${fin.reason.slice(0, 200)} — fix and /task-auto-resume (the gate re-runs)
|
|
1171
|
+
announceDone(active, `${id} finished all tasks but FAILED the final integration gate — ${fin.reason.slice(0, 200)} — fix and /task-auto-resume (the gate re-runs).`
|
|
1172
|
+
+ (stranded.length > 0 ?
|
|
1173
|
+
` NOTE: ${stranded.length} uncommitted fix-pass change(s) are in your working tree (${stranded.slice(0, 4).join(', ')}).`
|
|
1174
|
+
: ''), 'error');
|
|
1070
1175
|
return;
|
|
1071
1176
|
}
|
|
1072
1177
|
}
|
|
@@ -58,6 +58,31 @@ export declare function parseFinalFixMarker(text: string): {
|
|
|
58
58
|
blocked: boolean;
|
|
59
59
|
note?: string;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* STRANDED SUB-FIXES (mx5 run 13, PROMPT 4 item 3).
|
|
63
|
+
*
|
|
64
|
+
* A fix attempt that does not converge keeps its edits: they are NOT discarded
|
|
65
|
+
* (only a guard trip discards), and `deps.commit` runs only on `fix.ok`. So a
|
|
66
|
+
* partial fix that genuinely repaired something sits in the working tree, uncommitted,
|
|
67
|
+
* and if the user then ACCEPTs the FAIL the run completes around it — leaving HEAD
|
|
68
|
+
* broken while the repair is invisible unless someone runs `git status`.
|
|
69
|
+
*
|
|
70
|
+
* That is exactly what run 13 shipped: the fix child's bunfig.toml change made
|
|
71
|
+
* `bun run test` pass 116/116, attempt 1 did not converge overall, the user accepted
|
|
72
|
+
* the FAIL, and the tree still shows the file modified while HEAD's `bun run test` is
|
|
73
|
+
* broken. The repair and the breakage were BOTH real; only the repair was discarded
|
|
74
|
+
* by default.
|
|
75
|
+
*
|
|
76
|
+
* The rule: a partial fix is either committed (its own commit, named in the trail) or
|
|
77
|
+
* explicitly surfaced — never silently stranded.
|
|
78
|
+
*/
|
|
79
|
+
/** Commit subject for partial fixes committed alongside an accepted gate FAIL. */
|
|
80
|
+
export declare const STRANDED_FIX_COMMIT: (runId: string) => string;
|
|
81
|
+
/**
|
|
82
|
+
* The picker/trail line describing what a non-converging fix pass left behind.
|
|
83
|
+
* Empty string when the tree is clean — the caller then says nothing at all.
|
|
84
|
+
*/
|
|
85
|
+
export declare function strandedFixNote(paths: string[]): string;
|
|
61
86
|
export interface FinalFixResult {
|
|
62
87
|
/** true → the fix child ran AND the re-run gate passed. */
|
|
63
88
|
ok: boolean;
|
|
@@ -161,6 +161,39 @@ export function parseFinalFixMarker(text) {
|
|
|
161
161
|
}
|
|
162
162
|
return { blocked: false, note: last[2].trim() || undefined };
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* STRANDED SUB-FIXES (mx5 run 13, PROMPT 4 item 3).
|
|
166
|
+
*
|
|
167
|
+
* A fix attempt that does not converge keeps its edits: they are NOT discarded
|
|
168
|
+
* (only a guard trip discards), and `deps.commit` runs only on `fix.ok`. So a
|
|
169
|
+
* partial fix that genuinely repaired something sits in the working tree, uncommitted,
|
|
170
|
+
* and if the user then ACCEPTs the FAIL the run completes around it — leaving HEAD
|
|
171
|
+
* broken while the repair is invisible unless someone runs `git status`.
|
|
172
|
+
*
|
|
173
|
+
* That is exactly what run 13 shipped: the fix child's bunfig.toml change made
|
|
174
|
+
* `bun run test` pass 116/116, attempt 1 did not converge overall, the user accepted
|
|
175
|
+
* the FAIL, and the tree still shows the file modified while HEAD's `bun run test` is
|
|
176
|
+
* broken. The repair and the breakage were BOTH real; only the repair was discarded
|
|
177
|
+
* by default.
|
|
178
|
+
*
|
|
179
|
+
* The rule: a partial fix is either committed (its own commit, named in the trail) or
|
|
180
|
+
* explicitly surfaced — never silently stranded.
|
|
181
|
+
*/
|
|
182
|
+
/** Commit subject for partial fixes committed alongside an accepted gate FAIL. */
|
|
183
|
+
export const STRANDED_FIX_COMMIT = (runId) => `FINAL GATE PARTIAL FIX (${runId}) — accepted with gate still failing`;
|
|
184
|
+
/**
|
|
185
|
+
* The picker/trail line describing what a non-converging fix pass left behind.
|
|
186
|
+
* Empty string when the tree is clean — the caller then says nothing at all.
|
|
187
|
+
*/
|
|
188
|
+
export function strandedFixNote(paths) {
|
|
189
|
+
if (paths.length === 0)
|
|
190
|
+
return '';
|
|
191
|
+
const shown = paths.slice(0, 8).join(', ');
|
|
192
|
+
return (`\n\nUNCOMMITTED: the fix pass left ${paths.length} change(s) in the working tree `
|
|
193
|
+
+ `(${shown}${paths.length > 8 ? ', …' : ''}). These are NOT in HEAD. Accepting will `
|
|
194
|
+
+ `commit them as their own commit so they are not lost; leaving the run failed keeps `
|
|
195
|
+
+ `them in your working tree.`);
|
|
196
|
+
}
|
|
164
197
|
/**
|
|
165
198
|
* Run one bounded final-gate fix attempt: snapshot discovery → child → write-guard
|
|
166
199
|
* stack (diff capture → frozen-path revert → deletion guard → shrink guard → probe
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AddedLine } from './probe-gaming.js';
|
|
2
|
+
/** One leaked absolute path found in a committed file. */
|
|
3
|
+
export interface ForeignPathFinding {
|
|
4
|
+
/** Repo-relative file the leaked path was committed into. */
|
|
5
|
+
file: string;
|
|
6
|
+
/** The absolute path exactly as written in the file. */
|
|
7
|
+
absolute: string;
|
|
8
|
+
/** The repo-relative path its tail resolves to (the repair target). */
|
|
9
|
+
repoPath: string;
|
|
10
|
+
/** The leading segments that are foreign to this host (e.g. `/workspace`). */
|
|
11
|
+
foreignPrefix: string;
|
|
12
|
+
/** The verbatim line carrying the leak, trimmed. */
|
|
13
|
+
line: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a repo-relative path to the real thing it names, honouring the
|
|
17
|
+
* extensionless module specifiers config files use (`src/client/api` →
|
|
18
|
+
* `src/client/api.ts`). Returns the resolved repo-relative path, or null.
|
|
19
|
+
*
|
|
20
|
+
* This mirrors how the tools that CONSUME these paths (vite/tsconfig aliases,
|
|
21
|
+
* bundler resolvers) look them up — without it the mx5 true positive
|
|
22
|
+
* `/workspace/src/client/api` would be missed, since only `src/client/api.ts` exists.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveRepoPath(rel: string, exists: (rel: string) => boolean): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Scan a task's added lines for sandbox-leaked absolute paths.
|
|
27
|
+
*
|
|
28
|
+
* `existsOnHost` answers for an ABSOLUTE path; `existsInRepo` for a REPO-RELATIVE
|
|
29
|
+
* one. Both are injected so the detector is pure and unit-testable against a
|
|
30
|
+
* synthetic tree.
|
|
31
|
+
*
|
|
32
|
+
* The tail search strips the FEWEST leading segments that still resolve, so the
|
|
33
|
+
* finding names the smallest foreign prefix (`/workspace`, not `/workspace/src`)
|
|
34
|
+
* and the largest repo-relative target — the reading that matches how a mount
|
|
35
|
+
* actually shadows a tree.
|
|
36
|
+
*/
|
|
37
|
+
export declare function findForeignPaths(lines: AddedLine[], existsOnHost: (abs: string) => boolean, existsInRepo: (rel: string) => boolean): ForeignPathFinding[];
|
|
38
|
+
/**
|
|
39
|
+
* The repair for one finding: the leaked absolute path rewritten relative to the
|
|
40
|
+
* FILE that carries it, in the `./`-prefixed form config resolvers expect. A target
|
|
41
|
+
* in a parent directory keeps its `../` form (already relative, no prefix needed).
|
|
42
|
+
*/
|
|
43
|
+
export declare function relativeRepairFor(finding: ForeignPathFinding): string;
|
|
44
|
+
/**
|
|
45
|
+
* Apply every finding for ONE file to that file's text, as literal substitutions.
|
|
46
|
+
* Returns the new text and the substitutions made; `text` is returned untouched
|
|
47
|
+
* when nothing applied (the caller then writes nothing).
|
|
48
|
+
*
|
|
49
|
+
* Deliberately a plain string replacement of a path literal for a path literal: it
|
|
50
|
+
* cannot reflow, reformat, or restructure the file, so a rewrite that turns out to
|
|
51
|
+
* be semantically wrong is still trivially reviewable in the diff. A finding whose
|
|
52
|
+
* absolute string is no longer present (the file moved on) is skipped.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyForeignPathRepairs(text: string, findings: ForeignPathFinding[]): {
|
|
55
|
+
text: string;
|
|
56
|
+
applied: Array<{
|
|
57
|
+
from: string;
|
|
58
|
+
to: string;
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
/** IO seam for the repair pass; injected so the orchestration is unit-testable. */
|
|
62
|
+
export interface ForeignPathRepairIO {
|
|
63
|
+
readFile: (rel: string) => Promise<string>;
|
|
64
|
+
writeFile: (rel: string, text: string) => Promise<void>;
|
|
65
|
+
/** Does this repo-relative path exist? Used to RE-VALIDATE each repair. */
|
|
66
|
+
existsInRepo: (rel: string) => boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface ForeignPathRepairResult {
|
|
69
|
+
/** Human-readable repairs actually written, e.g. `cfg.ts: /workspace/src → ./src`. */
|
|
70
|
+
repaired: string[];
|
|
71
|
+
/** Findings left alone — these become the verify finding. */
|
|
72
|
+
remaining: ForeignPathFinding[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Deterministically repair what can be repaired, and hand back the rest.
|
|
76
|
+
*
|
|
77
|
+
* Every repair is RE-VALIDATED before it is written: the file-relative form is
|
|
78
|
+
* resolved back to a repo path and must land on the same real file the finding
|
|
79
|
+
* named. A repair that does not re-resolve is dropped and its finding stays in
|
|
80
|
+
* `remaining`, where the verify block will raise it for a human or an AUTOFIX
|
|
81
|
+
* round. An unreadable or unwritable file does the same. Nothing here can turn a
|
|
82
|
+
* working path into a broken one — the only edit it makes is swapping a path that
|
|
83
|
+
* provably does not resolve for one that provably does.
|
|
84
|
+
*/
|
|
85
|
+
export declare function repairForeignPaths(findings: ForeignPathFinding[], io: ForeignPathRepairIO): Promise<ForeignPathRepairResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Verify-child prompt lines: one per leak, naming the file, the leaked path, and
|
|
88
|
+
* the real repo path it shadows. Empty findings → empty array (caller emits no
|
|
89
|
+
* block), matching every other probe's contract.
|
|
90
|
+
*/
|
|
91
|
+
export declare function foreignPathVerifyFindings(findings: ForeignPathFinding[]): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Render findings as a critique/enforce defect block — the same shape as
|
|
94
|
+
* skipEscapeDefectText: a numbered list the rewrite must resolve.
|
|
95
|
+
*/
|
|
96
|
+
export declare function foreignPathDefectText(findings: ForeignPathFinding[]): string;
|
|
Binary file
|