@mjasnikovs/pi-task 0.32.0 → 0.34.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/dist/task/phases.d.ts +21 -0
- package/dist/task/phases.js +52 -8
- package/dist/task/refuted-constraint.d.ts +82 -0
- package/dist/task/refuted-constraint.js +281 -0
- package/dist/task/research-fanout-budget.d.ts +46 -10
- package/dist/task/research-fanout-budget.js +52 -11
- package/dist/task/task-gates.d.ts +23 -0
- package/dist/task/task-gates.js +74 -12
- package/package.json +1 -1
package/dist/task/phases.d.ts
CHANGED
|
@@ -158,6 +158,27 @@ export interface PhaseAutoAnswerDeps {
|
|
|
158
158
|
}
|
|
159
159
|
export declare function phaseAutoAnswer(deps: PhaseDeps, refined: string, research: string, question: string, autoDeps?: PhaseAutoAnswerDeps): Promise<AutoAnswer>;
|
|
160
160
|
export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext, widgetState: WidgetState, refined: string, research: string): Promise<string>;
|
|
161
|
+
/**
|
|
162
|
+
* A refutation is a DELETION. Where the run's own research explicitly says a
|
|
163
|
+
* dependency refine invented is not needed, drop that token from CONSTRAINTS —
|
|
164
|
+
* compose cannot forbid the design's own API "because the refined task
|
|
165
|
+
* explicitly requires `argon2`" if the refined task no longer requires it.
|
|
166
|
+
*
|
|
167
|
+
* Applied to the REFINED TASK ITSELF, not to compose's copy of it, and that is
|
|
168
|
+
* load-bearing: critique receives the refined task as GROUND TRUTH and is told
|
|
169
|
+
* its CONSTRAINTS "MUST be preserved in spirit — do not silently drop or weaken
|
|
170
|
+
* them", so a deletion visible only to compose is restored one phase later. Both
|
|
171
|
+
* spec-producing phases have to see the same text.
|
|
172
|
+
*
|
|
173
|
+
* Purely subtractive and never touches an owned line (task/refuted-constraint.ts).
|
|
174
|
+
* Idempotent, so a resumed run re-deriving `refined` from the task file lands in
|
|
175
|
+
* the same place. The task file's `## refined prompt` is deliberately left as
|
|
176
|
+
* refine wrote it; the drop is recorded on the `## gates` trail with both source
|
|
177
|
+
* lines quoted, so the decision stays auditable after the fact.
|
|
178
|
+
*
|
|
179
|
+
* STEP 0 `scripts/refuted-constraint-baserate.ts`; A/B-1 `…-ab.ts` (PASS).
|
|
180
|
+
*/
|
|
181
|
+
export declare function dropRefutedConstraints(deps: PhaseDeps, refined: string, research: string): Promise<string>;
|
|
161
182
|
export declare function phaseCompose(deps: PhaseDeps, refined: string, research: string, qa: string): Promise<string>;
|
|
162
183
|
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string, planContext?: string, research?: string,
|
|
163
184
|
/**
|
package/dist/task/phases.js
CHANGED
|
@@ -23,7 +23,8 @@ import { resolve } from 'node:path';
|
|
|
23
23
|
import { formatServiceBlock, formatFreshnessSkippedBlock } from './service-blocks.js';
|
|
24
24
|
import { gatherExternalContext } from './external-context.js';
|
|
25
25
|
import { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, MAX_GRILL_QUESTIONS, appendNoThink } from './prompts.js';
|
|
26
|
-
import { readSection, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
26
|
+
import { appendGateRecord, readSection, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
27
|
+
import { applyRefutations } from './refuted-constraint.js';
|
|
27
28
|
import { spawnSync } from 'node:child_process';
|
|
28
29
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
29
30
|
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
@@ -639,13 +640,21 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
639
640
|
// the pattern nexxtasks exists to prevent. To re-run the experiment, restore the block
|
|
640
641
|
// this comment replaces — see the git history of this file and the PROMPT 4 entry in
|
|
641
642
|
// nexxtasks.txt RESULTS.
|
|
642
|
-
// nexttask 5B fan-out bounds.
|
|
643
|
-
// every worker in a run sees the same policy and a harness cannot
|
|
644
|
-
// an arm
|
|
643
|
+
// nexttask 5B fan-out bounds. All four read their env ONCE per research
|
|
644
|
+
// phase, so every worker in a run sees the same policy and a harness cannot
|
|
645
|
+
// half-apply an arm. CAP, SCALE and carry-forward are null/false in the
|
|
646
|
+
// shipped configuration; the progress deadline shipped ON (nexttask 9).
|
|
645
647
|
const fanoutBudget = projectDocsBudget();
|
|
646
648
|
const fanoutTimeout = fanoutTimeoutPolicy();
|
|
647
649
|
const carryForward = workerCarryForward();
|
|
648
650
|
const progressCeilingMs = workerProgressCeilingMs();
|
|
651
|
+
// Which deadline policy was in force is a fact about how every number below
|
|
652
|
+
// was produced. Run 18's 120 discarded minutes were only recoverable because
|
|
653
|
+
// 5A started writing down what the workers actually did; a run whose logs do
|
|
654
|
+
// not say which policy it ran under cannot be compared with one that does.
|
|
655
|
+
deps.logDebug?.(progressCeilingMs === null ?
|
|
656
|
+
'phase:research: worker deadline = fixed elapsed cap (progress deadline DISABLED)'
|
|
657
|
+
: `phase:research: worker deadline = no-progress, ceiling ${progressCeilingMs}ms`);
|
|
649
658
|
let doneCount = 0;
|
|
650
659
|
const updateProgress = () => {
|
|
651
660
|
doneCount++;
|
|
@@ -842,9 +851,11 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
842
851
|
// 5B SCALE arm — null unless both env vars are set. Only the
|
|
843
852
|
// docs-capable worker can fan out, so only it can be scaled.
|
|
844
853
|
...(spec.fanoutBounded && fanoutTimeout ? { fanoutTimeout } : {}),
|
|
845
|
-
// 5B RESCUE
|
|
846
|
-
//
|
|
847
|
-
//
|
|
854
|
+
// 5B RESCUE. Applies to EVERY research worker, not just the
|
|
855
|
+
// docs-capable one: any worker that gets killed loses its work
|
|
856
|
+
// the same way. carry-forward stays OFF unless asked for
|
|
857
|
+
// (measured harmful on its own); the progress deadline SHIPPED
|
|
858
|
+
// ON in nexttask 9 and is null only when explicitly disabled.
|
|
848
859
|
...(carryForward ? { carryForward: true } : {}),
|
|
849
860
|
...(progressCeilingMs !== null ?
|
|
850
861
|
{ progressTimeoutCeilingMs: progressCeilingMs }
|
|
@@ -1324,6 +1335,36 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1324
1335
|
return '(no questions produced)';
|
|
1325
1336
|
return out.join('\n');
|
|
1326
1337
|
}
|
|
1338
|
+
/**
|
|
1339
|
+
* A refutation is a DELETION. Where the run's own research explicitly says a
|
|
1340
|
+
* dependency refine invented is not needed, drop that token from CONSTRAINTS —
|
|
1341
|
+
* compose cannot forbid the design's own API "because the refined task
|
|
1342
|
+
* explicitly requires `argon2`" if the refined task no longer requires it.
|
|
1343
|
+
*
|
|
1344
|
+
* Applied to the REFINED TASK ITSELF, not to compose's copy of it, and that is
|
|
1345
|
+
* load-bearing: critique receives the refined task as GROUND TRUTH and is told
|
|
1346
|
+
* its CONSTRAINTS "MUST be preserved in spirit — do not silently drop or weaken
|
|
1347
|
+
* them", so a deletion visible only to compose is restored one phase later. Both
|
|
1348
|
+
* spec-producing phases have to see the same text.
|
|
1349
|
+
*
|
|
1350
|
+
* Purely subtractive and never touches an owned line (task/refuted-constraint.ts).
|
|
1351
|
+
* Idempotent, so a resumed run re-deriving `refined` from the task file lands in
|
|
1352
|
+
* the same place. The task file's `## refined prompt` is deliberately left as
|
|
1353
|
+
* refine wrote it; the drop is recorded on the `## gates` trail with both source
|
|
1354
|
+
* lines quoted, so the decision stays auditable after the fact.
|
|
1355
|
+
*
|
|
1356
|
+
* STEP 0 `scripts/refuted-constraint-baserate.ts`; A/B-1 `…-ab.ts` (PASS).
|
|
1357
|
+
*/
|
|
1358
|
+
export async function dropRefutedConstraints(deps, refined, research) {
|
|
1359
|
+
const refuted = applyRefutations(refined, research);
|
|
1360
|
+
if (refuted.trail.length === 0)
|
|
1361
|
+
return refined;
|
|
1362
|
+
for (const line of refuted.trail) {
|
|
1363
|
+
deps.logDebug?.(`compose: ${line}`);
|
|
1364
|
+
await appendGateRecord(deps.cwd, deps.taskId, line).catch(() => { });
|
|
1365
|
+
}
|
|
1366
|
+
return refuted.refined;
|
|
1367
|
+
}
|
|
1327
1368
|
export async function phaseCompose(deps, refined, research, qa) {
|
|
1328
1369
|
// CLAIM before the belt is built: an obligation an earlier task had to
|
|
1329
1370
|
// detach (its own spec froze the only file that could satisfy it) becomes
|
|
@@ -1627,7 +1668,10 @@ export const PHASES = [
|
|
|
1627
1668
|
name: 'compose',
|
|
1628
1669
|
section: 'spec',
|
|
1629
1670
|
field: 'spec',
|
|
1630
|
-
run: (d, p) =>
|
|
1671
|
+
run: async (d, p) => {
|
|
1672
|
+
p.refined = await dropRefutedConstraints(d, p.refined, p.research);
|
|
1673
|
+
return await phaseCompose(d, p.refined, p.research, p.qa);
|
|
1674
|
+
}
|
|
1631
1675
|
},
|
|
1632
1676
|
{
|
|
1633
1677
|
name: 'critique',
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A refutation is a DELETION, not an addition.
|
|
3
|
+
*
|
|
4
|
+
* THE LEAD (mx5 run 19, `~/hub/mx5 @ dfbdd6f`, TASK_0001). Refine invented a
|
|
5
|
+
* dependency the design explicitly rules out; the run's own research said so, in
|
|
6
|
+
* writing, in the same file; the composed spec then turned the invention into a
|
|
7
|
+
* capitalised prohibition against the design's own API:
|
|
8
|
+
*
|
|
9
|
+
* :21 refine CONSTRAINTS "Add only new entries the task requires (e.g.,
|
|
10
|
+
* `hono`, `bun-sql`-equivalent, …, `argon2`, …)"
|
|
11
|
+
* :92 research CONTEXT "Password hashing uses `Bun.password` (built-in
|
|
12
|
+
* argon2id) — no external `argon2` or
|
|
13
|
+
* `@node-rs/argon2` dependency needed despite the
|
|
14
|
+
* task's mention of it."
|
|
15
|
+
* :132 spec CONSTRAINTS "Do NOT use built-in `Bun.password` for hashing —
|
|
16
|
+
* the refined task explicitly requires `argon2`."
|
|
17
|
+
*
|
|
18
|
+
* `argon2@^0.41.0` shipped as a runtime dependency of a repo that never imports
|
|
19
|
+
* it, locked in by a VERIFY assertion, next to a spec telling the next
|
|
20
|
+
* implementer not to use `Bun.sql` — the API the whole data layer is built on.
|
|
21
|
+
*
|
|
22
|
+
* This is `memory/phantom-correction-additive-not-subtractive.md` at spec scale:
|
|
23
|
+
* the correction was APPENDED to research CONTEXT while the wrong text stayed in
|
|
24
|
+
* refine's CONSTRAINTS, and CONSTRAINTS is what the implementer is told is
|
|
25
|
+
* authoritative.
|
|
26
|
+
*
|
|
27
|
+
* THE LEVER. Deterministic detection, then a scoped removal, BEFORE compose sees
|
|
28
|
+
* the refined task. Never a model rewrite: every model-rewrite lever at this seam
|
|
29
|
+
* has resolved contradictions by deleting the AUTHORITATIVE side
|
|
30
|
+
* (`memory/owned-freeze-critique-lever-refuted.md`, 11/20). This pass can only
|
|
31
|
+
* delete a refine-invented token, and can never touch an owned line.
|
|
32
|
+
*
|
|
33
|
+
* The match is lexical, never semantic: a research CONTEXT bullet refutes a
|
|
34
|
+
* refine constraint when it carries one of a closed set of negation-of-need
|
|
35
|
+
* shapes AROUND a backticked token, and that same token appears backticked in a
|
|
36
|
+
* refine CONSTRAINTS line. Both sides are already separate strings in the task
|
|
37
|
+
* file.
|
|
38
|
+
*
|
|
39
|
+
* STEP 0 (`scripts/refuted-constraint-baserate.ts`) measured the closed set over
|
|
40
|
+
* every recorded task file in the corpus before any of it was wired.
|
|
41
|
+
*/
|
|
42
|
+
export type Refutation = {
|
|
43
|
+
/** The token as it appears inside the backticks, e.g. `argon2`. */
|
|
44
|
+
token: string;
|
|
45
|
+
/** Index into the refined prompt's lines. */
|
|
46
|
+
line: number;
|
|
47
|
+
/** The refine CONSTRAINTS line, verbatim, before the drop. */
|
|
48
|
+
constraint: string;
|
|
49
|
+
/** The research CONTEXT bullet that refutes it, verbatim. */
|
|
50
|
+
research: string;
|
|
51
|
+
/** Which member of the closed negation set fired. */
|
|
52
|
+
pattern: string;
|
|
53
|
+
};
|
|
54
|
+
/** Section body between a bare ALL-CAPS header and the next one (or EOF). */
|
|
55
|
+
export declare function extractCapsSection(text: string, heading: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Find every (refine constraint line, research refutation bullet, shared token)
|
|
58
|
+
* triple. `refined` is the refined prompt, `research` the research output; both
|
|
59
|
+
* are the exact strings compose is handed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function detectRefutations(refined: string, research: string): Refutation[];
|
|
62
|
+
/**
|
|
63
|
+
* Delete one token from a constraint line: the backticked span, any word-suffix
|
|
64
|
+
* glued to it (`` `bun-sql` ``-equivalent), and ONE adjacent list separator —
|
|
65
|
+
* the preceding one where there is one, so the list keeps its shape.
|
|
66
|
+
*
|
|
67
|
+
* Returns null when nothing but boilerplate would be left, which the caller
|
|
68
|
+
* turns into a whole-line drop. Never rephrases, never adds a character.
|
|
69
|
+
*/
|
|
70
|
+
export declare function dropToken(line: string, token: string): string | null;
|
|
71
|
+
export type RefutationResult = {
|
|
72
|
+
/** The refined prompt with every refuted token deleted. */
|
|
73
|
+
refined: string;
|
|
74
|
+
/** One trail line per drop, both source lines quoted. */
|
|
75
|
+
trail: string[];
|
|
76
|
+
refutations: Refutation[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Apply every detected refutation to the refined prompt. Purely subtractive: the
|
|
80
|
+
* result is always a character subsequence of the input (`inv-no-line-invention`).
|
|
81
|
+
*/
|
|
82
|
+
export declare function applyRefutations(refined: string, research: string): RefutationResult;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A refutation is a DELETION, not an addition.
|
|
3
|
+
*
|
|
4
|
+
* THE LEAD (mx5 run 19, `~/hub/mx5 @ dfbdd6f`, TASK_0001). Refine invented a
|
|
5
|
+
* dependency the design explicitly rules out; the run's own research said so, in
|
|
6
|
+
* writing, in the same file; the composed spec then turned the invention into a
|
|
7
|
+
* capitalised prohibition against the design's own API:
|
|
8
|
+
*
|
|
9
|
+
* :21 refine CONSTRAINTS "Add only new entries the task requires (e.g.,
|
|
10
|
+
* `hono`, `bun-sql`-equivalent, …, `argon2`, …)"
|
|
11
|
+
* :92 research CONTEXT "Password hashing uses `Bun.password` (built-in
|
|
12
|
+
* argon2id) — no external `argon2` or
|
|
13
|
+
* `@node-rs/argon2` dependency needed despite the
|
|
14
|
+
* task's mention of it."
|
|
15
|
+
* :132 spec CONSTRAINTS "Do NOT use built-in `Bun.password` for hashing —
|
|
16
|
+
* the refined task explicitly requires `argon2`."
|
|
17
|
+
*
|
|
18
|
+
* `argon2@^0.41.0` shipped as a runtime dependency of a repo that never imports
|
|
19
|
+
* it, locked in by a VERIFY assertion, next to a spec telling the next
|
|
20
|
+
* implementer not to use `Bun.sql` — the API the whole data layer is built on.
|
|
21
|
+
*
|
|
22
|
+
* This is `memory/phantom-correction-additive-not-subtractive.md` at spec scale:
|
|
23
|
+
* the correction was APPENDED to research CONTEXT while the wrong text stayed in
|
|
24
|
+
* refine's CONSTRAINTS, and CONSTRAINTS is what the implementer is told is
|
|
25
|
+
* authoritative.
|
|
26
|
+
*
|
|
27
|
+
* THE LEVER. Deterministic detection, then a scoped removal, BEFORE compose sees
|
|
28
|
+
* the refined task. Never a model rewrite: every model-rewrite lever at this seam
|
|
29
|
+
* has resolved contradictions by deleting the AUTHORITATIVE side
|
|
30
|
+
* (`memory/owned-freeze-critique-lever-refuted.md`, 11/20). This pass can only
|
|
31
|
+
* delete a refine-invented token, and can never touch an owned line.
|
|
32
|
+
*
|
|
33
|
+
* The match is lexical, never semantic: a research CONTEXT bullet refutes a
|
|
34
|
+
* refine constraint when it carries one of a closed set of negation-of-need
|
|
35
|
+
* shapes AROUND a backticked token, and that same token appears backticked in a
|
|
36
|
+
* refine CONSTRAINTS line. Both sides are already separate strings in the task
|
|
37
|
+
* file.
|
|
38
|
+
*
|
|
39
|
+
* STEP 0 (`scripts/refuted-constraint-baserate.ts`) measured the closed set over
|
|
40
|
+
* every recorded task file in the corpus before any of it was wired.
|
|
41
|
+
*/
|
|
42
|
+
/** Bare ALL-CAPS section header, the boundary convention used across the
|
|
43
|
+
* refined prompt and every research section. */
|
|
44
|
+
const HEADER = /^[A-Z][A-Z -]*$/;
|
|
45
|
+
/** A backticked run with no whitespace inside — the only thing this pass will
|
|
46
|
+
* ever treat as a token. */
|
|
47
|
+
const TOKEN = '`[^`\\s]+`';
|
|
48
|
+
/** One token, or a list of them joined by `,` / `or` / `and`. */
|
|
49
|
+
const TOKEN_LIST = `${TOKEN}(?:(?:\\s*,\\s*|\\s+or\\s+|\\s+and\\s+)${TOKEN})*`;
|
|
50
|
+
/**
|
|
51
|
+
* The closed negation set. Each pattern anchors the token list INSIDE the
|
|
52
|
+
* negation, so a bullet that names one package negatively and another positively
|
|
53
|
+
* ("no `@node-rs/argon2` dependency — we use `argon2`") can only ever refute the
|
|
54
|
+
* one inside the phrase.
|
|
55
|
+
*
|
|
56
|
+
* Kept deliberately small. `inv-precision` is a FAIL of the whole task on one
|
|
57
|
+
* false drop, so a shape earns its place here only by surviving a hand-read of
|
|
58
|
+
* every corpus hit it produces.
|
|
59
|
+
*/
|
|
60
|
+
const NEGATIONS = [
|
|
61
|
+
{
|
|
62
|
+
// "no external `argon2` or `@node-rs/argon2` dependency needed" — THE LEAD
|
|
63
|
+
name: 'no-external-dep-needed',
|
|
64
|
+
re: new RegExp(`\\bno\\s+external\\s+(${TOKEN_LIST})\\s+(?:\\w+\\s+){0,2}dependenc(?:y|ies)\\b[^.;]{0,40}?\\b(?:needed|required)\\b`, 'i')
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
// "no `bun-sql` npm package exists"
|
|
68
|
+
name: 'no-package-exists',
|
|
69
|
+
re: new RegExp(`\\bno\\s+(${TOKEN_LIST})\\s+(?:npm\\s+)?(?:package|module)\\s+exists\\b`, 'i')
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
// "no `argon2` dependency is needed".
|
|
73
|
+
//
|
|
74
|
+
// The trailing need-negation is NOT optional, and STEP 0 is why. The
|
|
75
|
+
// first cut of this pattern was the bare "no `X` dependency", and it
|
|
76
|
+
// fired 300 times in 306 corpus hits — every one of them on a
|
|
77
|
+
// MANIFEST-STATE bullet that says the exact opposite of a refutation:
|
|
78
|
+
//
|
|
79
|
+
// "`package.json` currently lists no `hono` or `@hono/zod-validator`
|
|
80
|
+
// dependencies; they MUST BE ADDED at versions 4.12.27 and 0.8.0"
|
|
81
|
+
// "`package.json` has … and no `sharp` dependency — MUST ADD `sharp`
|
|
82
|
+
// as devDependency"
|
|
83
|
+
//
|
|
84
|
+
// Dropping on that signal mutilated real constraints ("use with the
|
|
85
|
+
// shared `loginSchema`"). "The repo does not have X yet" is a fact about
|
|
86
|
+
// the tree; "X is not needed" is a claim about the task. Only the second
|
|
87
|
+
// one refutes anything.
|
|
88
|
+
name: 'no-dep-needed',
|
|
89
|
+
re: new RegExp(`\\bno\\s+(${TOKEN_LIST})\\s+dependenc(?:y|ies)\\s+(?:is\\s+|are\\s+)?(?:needed|required|necessary)\\b`, 'i')
|
|
90
|
+
},
|
|
91
|
+
// The two shapes below name no dependency noun of their own, so they carry a
|
|
92
|
+
// DEP_WORD guard. STEP 0's near-miss census is the reason: "does not require"
|
|
93
|
+
// appears in 412 corpus CONTEXT bullets, and the shape is overwhelmingly
|
|
94
|
+
// prose about behaviour, not dependencies — "the `GET /api/listings`
|
|
95
|
+
// endpoint does NOT require authentication for the `mine` filter". Token
|
|
96
|
+
// adjacency alone would eventually reach a backticked API expression there.
|
|
97
|
+
{
|
|
98
|
+
// "`argon2` is not needed" / "`argon2` and `sharp` are not required"
|
|
99
|
+
name: 'not-needed',
|
|
100
|
+
re: new RegExp(`(${TOKEN_LIST})\\s+(?:is|are)\\s+not\\s+(?:needed|required)\\b`, 'i'),
|
|
101
|
+
needsDepWord: true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
// "does not require `argon2`"
|
|
105
|
+
name: 'does-not-require',
|
|
106
|
+
re: new RegExp(`\\b(?:does|do)\\s+not\\s+require\\s+(${TOKEN_LIST})`, 'i'),
|
|
107
|
+
needsDepWord: true
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
/** The claim has to be ABOUT a dependency for a dependency to be dropped. */
|
|
111
|
+
const DEP_WORD = /\bdependenc(?:y|ies)\b|\bnpm\b|\bpackages?\b|\bdevDependenc(?:y|ies)\b/i;
|
|
112
|
+
/** The owned-requirement stamp (requirements.ts:813). A line carrying it is a
|
|
113
|
+
* design-sourced obligation and is NEVER refutable by research — this is the
|
|
114
|
+
* guard that keeps this pass out of nexttask 2's failure mode. */
|
|
115
|
+
const OWNED_MARKER = 'owned requirement from the source design';
|
|
116
|
+
/** Section body between a bare ALL-CAPS header and the next one (or EOF). */
|
|
117
|
+
export function extractCapsSection(text, heading) {
|
|
118
|
+
const lines = text.split('\n');
|
|
119
|
+
const start = lines.findIndex(l => l.trim() === heading);
|
|
120
|
+
if (start === -1)
|
|
121
|
+
return null;
|
|
122
|
+
const rest = lines.slice(start + 1);
|
|
123
|
+
const end = rest.findIndex(l => HEADER.test(l.trim()) && l.trim().length > 1);
|
|
124
|
+
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
|
|
125
|
+
}
|
|
126
|
+
function tokensIn(span) {
|
|
127
|
+
return [...span.matchAll(/`([^`\s]+)`/g)].map(m => m[1]);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Does this look like a package/module name at all? Refutations are about
|
|
131
|
+
* DEPENDENCIES; a backticked prose fragment, a path, or an API expression is not
|
|
132
|
+
* one, and dropping it from a constraint would be a semantic edit.
|
|
133
|
+
*/
|
|
134
|
+
function isPackageToken(tok) {
|
|
135
|
+
if (tok.length > 64)
|
|
136
|
+
return false;
|
|
137
|
+
if (!/^@?[a-z0-9][a-z0-9._/-]*$/i.test(tok))
|
|
138
|
+
return false;
|
|
139
|
+
// `Bun.password`, `p.dependencies` — dotted API expressions, not packages.
|
|
140
|
+
if (/^[A-Z]/.test(tok))
|
|
141
|
+
return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/** Every research CONTEXT bullet, one entry per bullet (continuations joined). */
|
|
145
|
+
function bullets(section) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const raw of section.split('\n')) {
|
|
148
|
+
const line = raw.trimEnd();
|
|
149
|
+
if (!line.trim())
|
|
150
|
+
continue;
|
|
151
|
+
if (/^\s*[-*]\s+/.test(line))
|
|
152
|
+
out.push(line.trim());
|
|
153
|
+
else if (out.length > 0)
|
|
154
|
+
out[out.length - 1] += ` ${line.trim()}`;
|
|
155
|
+
}
|
|
156
|
+
return out.length > 0 ? out : section.split('\n').filter(l => l.trim());
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Find every (refine constraint line, research refutation bullet, shared token)
|
|
160
|
+
* triple. `refined` is the refined prompt, `research` the research output; both
|
|
161
|
+
* are the exact strings compose is handed.
|
|
162
|
+
*/
|
|
163
|
+
export function detectRefutations(refined, research) {
|
|
164
|
+
const constraintsBody = extractCapsSection(refined, 'CONSTRAINTS');
|
|
165
|
+
const contextBody = extractCapsSection(research, 'CONTEXT');
|
|
166
|
+
if (constraintsBody === null || contextBody === null)
|
|
167
|
+
return [];
|
|
168
|
+
// Refuted token → the bullet that refuted it, and the pattern that fired.
|
|
169
|
+
const refuted = new Map();
|
|
170
|
+
for (const bullet of bullets(contextBody)) {
|
|
171
|
+
for (const { name, re, needsDepWord } of NEGATIONS) {
|
|
172
|
+
const m = re.exec(bullet);
|
|
173
|
+
if (!m)
|
|
174
|
+
continue;
|
|
175
|
+
if (needsDepWord && !DEP_WORD.test(bullet))
|
|
176
|
+
continue;
|
|
177
|
+
for (const tok of tokensIn(m[1])) {
|
|
178
|
+
if (!isPackageToken(tok))
|
|
179
|
+
continue;
|
|
180
|
+
if (!refuted.has(tok))
|
|
181
|
+
refuted.set(tok, { research: bullet, pattern: name });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (refuted.size === 0)
|
|
186
|
+
return [];
|
|
187
|
+
const lines = refined.split('\n');
|
|
188
|
+
const constraintStart = lines.findIndex(l => l.trim() === 'CONSTRAINTS');
|
|
189
|
+
const out = [];
|
|
190
|
+
for (let i = constraintStart + 1; i < lines.length; i++) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
if (HEADER.test(line.trim()) && line.trim().length > 1)
|
|
193
|
+
break;
|
|
194
|
+
if (line.includes(OWNED_MARKER))
|
|
195
|
+
continue;
|
|
196
|
+
for (const [tok, src] of refuted) {
|
|
197
|
+
if (!new RegExp(`\`${escapeRe(tok)}\``).test(line))
|
|
198
|
+
continue;
|
|
199
|
+
out.push({
|
|
200
|
+
token: tok,
|
|
201
|
+
line: i,
|
|
202
|
+
constraint: line,
|
|
203
|
+
research: src.research,
|
|
204
|
+
pattern: src.pattern
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function escapeRe(s) {
|
|
211
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Delete one token from a constraint line: the backticked span, any word-suffix
|
|
215
|
+
* glued to it (`` `bun-sql` ``-equivalent), and ONE adjacent list separator —
|
|
216
|
+
* the preceding one where there is one, so the list keeps its shape.
|
|
217
|
+
*
|
|
218
|
+
* Returns null when nothing but boilerplate would be left, which the caller
|
|
219
|
+
* turns into a whole-line drop. Never rephrases, never adds a character.
|
|
220
|
+
*/
|
|
221
|
+
export function dropToken(line, token) {
|
|
222
|
+
const tokRe = new RegExp(`\`${escapeRe(token)}\`[A-Za-z0-9-]*`, 'g');
|
|
223
|
+
let next = line;
|
|
224
|
+
for (;;) {
|
|
225
|
+
const m = tokRe.exec(next);
|
|
226
|
+
if (!m)
|
|
227
|
+
break;
|
|
228
|
+
const start = m.index;
|
|
229
|
+
const end = start + m[0].length;
|
|
230
|
+
// Prefer eating the separator BEFORE the token; fall back to the one
|
|
231
|
+
// after it (first element of a list).
|
|
232
|
+
const before = next.slice(0, start);
|
|
233
|
+
const sepBefore = /(?:,\s*|\s+or\s+|\s+and\s+)$/.exec(before);
|
|
234
|
+
if (sepBefore) {
|
|
235
|
+
next = next.slice(0, start - sepBefore[0].length) + next.slice(end);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const sepAfter = /^(?:\s*,\s*|\s+or\s+|\s+and\s+)/.exec(next.slice(end));
|
|
239
|
+
next = before + next.slice(end + (sepAfter ? sepAfter[0].length : 0));
|
|
240
|
+
}
|
|
241
|
+
tokRe.lastIndex = 0;
|
|
242
|
+
}
|
|
243
|
+
if (next === line)
|
|
244
|
+
return line;
|
|
245
|
+
// An emptied list ("(e.g., )") or an emptied bullet is a whole-line drop.
|
|
246
|
+
const carcass = next
|
|
247
|
+
.replace(/^\s*[-*]\s*/, '')
|
|
248
|
+
.replace(/\(\s*e\.g\.,?\s*\)/gi, '')
|
|
249
|
+
.replace(/[\s,.;:()]/g, '');
|
|
250
|
+
if (carcass.length === 0)
|
|
251
|
+
return null;
|
|
252
|
+
return next;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Apply every detected refutation to the refined prompt. Purely subtractive: the
|
|
256
|
+
* result is always a character subsequence of the input (`inv-no-line-invention`).
|
|
257
|
+
*/
|
|
258
|
+
export function applyRefutations(refined, research) {
|
|
259
|
+
const refutations = detectRefutations(refined, research);
|
|
260
|
+
if (refutations.length === 0)
|
|
261
|
+
return { refined, trail: [], refutations };
|
|
262
|
+
const lines = refined.split('\n');
|
|
263
|
+
const dropped = new Set();
|
|
264
|
+
const trail = [];
|
|
265
|
+
for (const r of refutations) {
|
|
266
|
+
const current = lines[r.line];
|
|
267
|
+
const next = dropToken(current, r.token);
|
|
268
|
+
if (next === null) {
|
|
269
|
+
dropped.add(r.line);
|
|
270
|
+
trail.push(`constraint refuted by research — dropped the whole CONSTRAINTS line for '${r.token}'`
|
|
271
|
+
+ ` | constraint: "${r.constraint.trim()}" | research: "${r.research.trim()}"`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
lines[r.line] = next;
|
|
275
|
+
trail.push(`constraint refuted by research — dropped '${r.token}' from CONSTRAINTS`
|
|
276
|
+
+ ` | constraint: "${r.constraint.trim()}" | research: "${r.research.trim()}"`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const kept = lines.filter((_l, i) => !dropped.has(i));
|
|
280
|
+
return { refined: kept.join('\n'), trail, refutations };
|
|
281
|
+
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nexttask 5B — the two candidate bounds on worker:apis's project-source fan-out.
|
|
3
3
|
*
|
|
4
|
-
* ⚠
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* ⚠ ONE of the levers in this file is wired: the RESCUE progress deadline
|
|
5
|
+
* (`workerProgressCeilingMs`) SHIPPED ON in nexttask 9, on a PASS measured over 42
|
|
6
|
+
* trials per arm against an instrument whose own false-break rate is on record at
|
|
7
|
+
* 1.5%. CAP, SCALE and RESCUE-CARRY remain OFF unless their env var is set — CAP
|
|
8
|
+
* and SCALE were rejected on argument (see below), carry-forward was measured
|
|
9
|
+
* HARMFUL on its own.
|
|
10
|
+
*
|
|
11
|
+
* The OFF levers exist so `scripts/live-research-fanout-budget-ab.ts` can run them
|
|
12
|
+
* against the shipped baseline in the SAME build — the alternative (dist surgery)
|
|
13
|
+
* measures a patched copy of the code and not the code. Nothing may read them
|
|
14
|
+
* outside that harness until it reports PASS; a lever wired on argument rather
|
|
15
|
+
* than measurement is the failure mode nexttasks exists to prevent.
|
|
11
16
|
*
|
|
12
17
|
* THE FAULT THEY TARGET (mx5 run 18, measured — scripts/research-restart-baserate.ts):
|
|
13
18
|
* `worker:apis` fans out `pi-worker-docs(module: ".")` project-source lookups, each
|
|
@@ -102,9 +107,40 @@ export declare function fanoutTimeoutPolicy(env?: Env): {
|
|
|
102
107
|
*/
|
|
103
108
|
export declare function workerCarryForward(env?: Env): boolean;
|
|
104
109
|
/**
|
|
105
|
-
* The absolute backstop for the progress-based deadline
|
|
106
|
-
*
|
|
107
|
-
*
|
|
110
|
+
* The absolute backstop for the progress-based deadline.
|
|
111
|
+
*
|
|
112
|
+
* WHY THIS NUMBER. It is not a budget and it does not decide how long a worker
|
|
113
|
+
* may take — the no-progress deadline does that, and it resets on every tool call.
|
|
114
|
+
* This is the last-resort bound on a worker that never stops moving (an infinite
|
|
115
|
+
* tool-call loop the loop detector somehow misses), so its only requirement is to
|
|
116
|
+
* sit clear of the real workload. Measured on 42 progress-arm trials
|
|
117
|
+
* (`~/tmp/research-fanout-ab-v3`): median 275s, p90 523s, **max 730s**. 20 minutes
|
|
118
|
+
* is 1.6x the observed worst case, and 1.7x the 720s the SHIPPED path already
|
|
119
|
+
* spends on a worker that burns all three attempts and returns nothing.
|
|
120
|
+
*
|
|
121
|
+
* A ceiling that never fires in production is the correct behaviour for a
|
|
122
|
+
* backstop, not evidence it is untested: it fires under test
|
|
123
|
+
* (`pi-worker-core.test.ts` — 'the absolute ceiling still bounds a worker that
|
|
124
|
+
* never stops moving'), and a worker that goes QUIET is killed long before it, at
|
|
125
|
+
* `timeoutMs` without progress and by the stall probe.
|
|
126
|
+
*/
|
|
127
|
+
export declare const DEFAULT_WORKER_PROGRESS_CEILING_MS = 1200000;
|
|
128
|
+
/**
|
|
129
|
+
* The progress-based deadline's ceiling, or null when the lever is OFF.
|
|
130
|
+
*
|
|
131
|
+
* SHIPPED ON as of nexttask 9 — the env var is now the OFF switch, not the on
|
|
132
|
+
* switch. Measured baseline vs progress over 42 trials/arm on a calibrated
|
|
133
|
+
* instrument (A/A false-break 1.5%): worker-timeout restarts 22/24 → 0/24,
|
|
134
|
+
* degrades 8/24 → 0/24, entries up on all four high-fan-out fixtures (TASK_0021
|
|
135
|
+
* 11.0 → 25.5), quality invariants HOLD, every treatment-arm ungrounded flag
|
|
136
|
+
* hand-verified as an instrument artifact rather than a fabrication.
|
|
137
|
+
*
|
|
138
|
+
* unset ON at DEFAULT_WORKER_PROGRESS_CEILING_MS
|
|
139
|
+
* "0" | "off" OFF — the fixed elapsed-time cap, exactly as before
|
|
140
|
+
* positive int ON at that ceiling, in ms
|
|
141
|
+
*
|
|
142
|
+
* A garbage value keeps the SHIPPED behaviour rather than silently disabling the
|
|
143
|
+
* lever: turning it off is a decision and has to be spelled.
|
|
108
144
|
*/
|
|
109
145
|
export declare function workerProgressCeilingMs(env?: Env): number | null;
|
|
110
146
|
/**
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nexttask 5B — the two candidate bounds on worker:apis's project-source fan-out.
|
|
3
3
|
*
|
|
4
|
-
* ⚠
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* ⚠ ONE of the levers in this file is wired: the RESCUE progress deadline
|
|
5
|
+
* (`workerProgressCeilingMs`) SHIPPED ON in nexttask 9, on a PASS measured over 42
|
|
6
|
+
* trials per arm against an instrument whose own false-break rate is on record at
|
|
7
|
+
* 1.5%. CAP, SCALE and RESCUE-CARRY remain OFF unless their env var is set — CAP
|
|
8
|
+
* and SCALE were rejected on argument (see below), carry-forward was measured
|
|
9
|
+
* HARMFUL on its own.
|
|
10
|
+
*
|
|
11
|
+
* The OFF levers exist so `scripts/live-research-fanout-budget-ab.ts` can run them
|
|
12
|
+
* against the shipped baseline in the SAME build — the alternative (dist surgery)
|
|
13
|
+
* measures a patched copy of the code and not the code. Nothing may read them
|
|
14
|
+
* outside that harness until it reports PASS; a lever wired on argument rather
|
|
15
|
+
* than measurement is the failure mode nexttasks exists to prevent.
|
|
11
16
|
*
|
|
12
17
|
* THE FAULT THEY TARGET (mx5 run 18, measured — scripts/research-restart-baserate.ts):
|
|
13
18
|
* `worker:apis` fans out `pi-worker-docs(module: ".")` project-source lookups, each
|
|
@@ -113,12 +118,48 @@ export function workerCarryForward(env = defaultEnv) {
|
|
|
113
118
|
return env(WORKER_CARRY_FORWARD_ENV) === '1';
|
|
114
119
|
}
|
|
115
120
|
/**
|
|
116
|
-
* The absolute backstop for the progress-based deadline
|
|
117
|
-
*
|
|
118
|
-
*
|
|
121
|
+
* The absolute backstop for the progress-based deadline.
|
|
122
|
+
*
|
|
123
|
+
* WHY THIS NUMBER. It is not a budget and it does not decide how long a worker
|
|
124
|
+
* may take — the no-progress deadline does that, and it resets on every tool call.
|
|
125
|
+
* This is the last-resort bound on a worker that never stops moving (an infinite
|
|
126
|
+
* tool-call loop the loop detector somehow misses), so its only requirement is to
|
|
127
|
+
* sit clear of the real workload. Measured on 42 progress-arm trials
|
|
128
|
+
* (`~/tmp/research-fanout-ab-v3`): median 275s, p90 523s, **max 730s**. 20 minutes
|
|
129
|
+
* is 1.6x the observed worst case, and 1.7x the 720s the SHIPPED path already
|
|
130
|
+
* spends on a worker that burns all three attempts and returns nothing.
|
|
131
|
+
*
|
|
132
|
+
* A ceiling that never fires in production is the correct behaviour for a
|
|
133
|
+
* backstop, not evidence it is untested: it fires under test
|
|
134
|
+
* (`pi-worker-core.test.ts` — 'the absolute ceiling still bounds a worker that
|
|
135
|
+
* never stops moving'), and a worker that goes QUIET is killed long before it, at
|
|
136
|
+
* `timeoutMs` without progress and by the stall probe.
|
|
137
|
+
*/
|
|
138
|
+
export const DEFAULT_WORKER_PROGRESS_CEILING_MS = 1_200_000;
|
|
139
|
+
/**
|
|
140
|
+
* The progress-based deadline's ceiling, or null when the lever is OFF.
|
|
141
|
+
*
|
|
142
|
+
* SHIPPED ON as of nexttask 9 — the env var is now the OFF switch, not the on
|
|
143
|
+
* switch. Measured baseline vs progress over 42 trials/arm on a calibrated
|
|
144
|
+
* instrument (A/A false-break 1.5%): worker-timeout restarts 22/24 → 0/24,
|
|
145
|
+
* degrades 8/24 → 0/24, entries up on all four high-fan-out fixtures (TASK_0021
|
|
146
|
+
* 11.0 → 25.5), quality invariants HOLD, every treatment-arm ungrounded flag
|
|
147
|
+
* hand-verified as an instrument artifact rather than a fabrication.
|
|
148
|
+
*
|
|
149
|
+
* unset ON at DEFAULT_WORKER_PROGRESS_CEILING_MS
|
|
150
|
+
* "0" | "off" OFF — the fixed elapsed-time cap, exactly as before
|
|
151
|
+
* positive int ON at that ceiling, in ms
|
|
152
|
+
*
|
|
153
|
+
* A garbage value keeps the SHIPPED behaviour rather than silently disabling the
|
|
154
|
+
* lever: turning it off is a decision and has to be spelled.
|
|
119
155
|
*/
|
|
120
156
|
export function workerProgressCeilingMs(env = defaultEnv) {
|
|
121
|
-
|
|
157
|
+
const raw = env(WORKER_PROGRESS_CEILING_ENV);
|
|
158
|
+
if (raw === undefined || raw.trim() === '')
|
|
159
|
+
return DEFAULT_WORKER_PROGRESS_CEILING_MS;
|
|
160
|
+
if (raw.trim() === '0' || raw.trim().toLowerCase() === 'off')
|
|
161
|
+
return null;
|
|
162
|
+
return positiveInt(raw) ?? DEFAULT_WORKER_PROGRESS_CEILING_MS;
|
|
122
163
|
}
|
|
123
164
|
/**
|
|
124
165
|
* The upfront half of the CAP arm, appended to the APIS worker's prompt.
|
|
@@ -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
|
package/dist/task/task-gates.js
CHANGED
|
@@ -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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.34.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",
|