@mjasnikovs/pi-task 0.18.21 → 0.18.22
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/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +27 -6
- package/dist/task/frozen-conflict.d.ts +36 -0
- package/dist/task/frozen-conflict.js +206 -0
- package/dist/task/frozen-path-guard.d.ts +9 -0
- package/dist/task/frozen-path-guard.js +15 -0
- package/dist/task/gate-deps.js +5 -1
- package/dist/task/lint-fix.d.ts +4 -1
- package/dist/task/lint-fix.js +20 -1
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +31 -9
- package/dist/task/prohibition-probe.d.ts +7 -0
- package/dist/task/prohibition-probe.js +1 -1
- package/dist/task/task-gates.d.ts +8 -0
- package/dist/task/task-gates.js +60 -9
- package/package.json +1 -1
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
* server entry point … the Hono server cannot be started"), so the terminal defect
|
|
7
7
|
* was FOUND and then erased by the very mechanism that found it. Persisted here so
|
|
8
8
|
* the final gate re-checks and surfaces it instead of letting it die with the revert.
|
|
9
|
+
* - 'frozen-blocked' — a repo-health verify-FAIL whose only fix is an edit to a path
|
|
10
|
+
* THIS task's spec froze (mx5 run 12: `bun run lint` permanently red because the
|
|
11
|
+
* created files need a tsconfig registration every spec forbids). Cross-task
|
|
12
|
+
* contradiction: no unattended re-run can converge, so the gate loop records the
|
|
13
|
+
* defect and routes to the human picker instead of burning AUTOFIX rounds.
|
|
9
14
|
*/
|
|
10
|
-
export type DebtOrigin = 'accepted' | 'enforce-revert';
|
|
15
|
+
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked';
|
|
11
16
|
/** One recorded defect: the task, why its VERIFY failed, and how it was recorded. */
|
|
12
17
|
export interface AcceptDebt {
|
|
13
18
|
taskId: string;
|
|
@@ -46,6 +51,15 @@ export declare function recordAcceptDebt(cwd: string, taskId: string, reason: st
|
|
|
46
51
|
* rather than letting it die with the revert.
|
|
47
52
|
*/
|
|
48
53
|
export declare function recordEnforceRevertDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Record a FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a repo-health FAIL
|
|
56
|
+
* whose static findings can only be fixed by editing a path this task's spec froze —
|
|
57
|
+
* a cross-task contradiction no unattended re-run may resolve. Recorded when the gate
|
|
58
|
+
* loop routes to the picker (regardless of what the human then picks), so the final
|
|
59
|
+
* gate re-checks it at run end. Static-class by reason prefix (`repo health: …`), so
|
|
60
|
+
* it auto-closes iff the final gate's own static check passes.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordFrozenBlockedDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
49
63
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
50
64
|
export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Promise<void>;
|
|
51
65
|
/**
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -74,7 +74,9 @@ export function parseAcceptDebts(raw) {
|
|
|
74
74
|
out.push({
|
|
75
75
|
taskId: parts[0].trim(),
|
|
76
76
|
reason: parts[1].trim(),
|
|
77
|
-
...(origin === 'enforce-revert'
|
|
77
|
+
...(origin === 'enforce-revert' || origin === 'frozen-blocked' ?
|
|
78
|
+
{ origin: origin }
|
|
79
|
+
: {})
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
return out;
|
|
@@ -92,8 +94,8 @@ function normaliseReason(reason) {
|
|
|
92
94
|
}
|
|
93
95
|
function serialize(d) {
|
|
94
96
|
// Legacy 2-field shape for 'accepted' (backward compatible); a 3rd origin field
|
|
95
|
-
// only for the
|
|
96
|
-
return d.origin
|
|
97
|
+
// only for the non-accepted classes, so old readers/files round-trip unchanged.
|
|
98
|
+
return d.origin && d.origin !== 'accepted' ?
|
|
97
99
|
`${d.taskId}${FIELD_SEP}${d.reason}${FIELD_SEP}${d.origin}`
|
|
98
100
|
: `${d.taskId}${FIELD_SEP}${d.reason}`;
|
|
99
101
|
}
|
|
@@ -140,6 +142,21 @@ export async function recordEnforceRevertDebt(cwd, taskId, reason) {
|
|
|
140
142
|
origin: 'enforce-revert'
|
|
141
143
|
});
|
|
142
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Record a FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a repo-health FAIL
|
|
147
|
+
* whose static findings can only be fixed by editing a path this task's spec froze —
|
|
148
|
+
* a cross-task contradiction no unattended re-run may resolve. Recorded when the gate
|
|
149
|
+
* loop routes to the picker (regardless of what the human then picks), so the final
|
|
150
|
+
* gate re-checks it at run end. Static-class by reason prefix (`repo health: …`), so
|
|
151
|
+
* it auto-closes iff the final gate's own static check passes.
|
|
152
|
+
*/
|
|
153
|
+
export async function recordFrozenBlockedDebt(cwd, taskId, reason) {
|
|
154
|
+
await appendDebt(cwd, {
|
|
155
|
+
taskId: taskId.trim(),
|
|
156
|
+
reason: normaliseReason(reason),
|
|
157
|
+
origin: 'frozen-blocked'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
143
160
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
144
161
|
export async function writeAcceptDebts(cwd, debts) {
|
|
145
162
|
try {
|
|
@@ -255,7 +272,11 @@ export function buildAcceptDebtNote(open) {
|
|
|
255
272
|
}
|
|
256
273
|
/** One-line provenance label for a debt, for the surfaced report. */
|
|
257
274
|
export function describeDebt(d) {
|
|
258
|
-
|
|
259
|
-
'enforce re-verify FAILED then the edits were reverted (defect indicts the ORIGINAL work, still shipped)'
|
|
260
|
-
|
|
275
|
+
if (d.origin === 'enforce-revert') {
|
|
276
|
+
return 'enforce re-verify FAILED then the edits were reverted (defect indicts the ORIGINAL work, still shipped)';
|
|
277
|
+
}
|
|
278
|
+
if (d.origin === 'frozen-blocked') {
|
|
279
|
+
return 'repo health blocked by a spec-frozen path (cross-task contradiction — no task may perform the fixing edit)';
|
|
280
|
+
}
|
|
281
|
+
return 'accepted despite verify-FAIL';
|
|
261
282
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** One unsatisfiable freeze/requires-edit pair found in a composed spec. */
|
|
2
|
+
export interface FrozenPathConflict {
|
|
3
|
+
/** The frozen path (normalized: no leading ./, no trailing /). */
|
|
4
|
+
path: string;
|
|
5
|
+
/** The freeze line, verbatim (the `Do NOT modify` constraint). */
|
|
6
|
+
constraint: string;
|
|
7
|
+
/** The sentence stating the deliverable requires changing that path
|
|
8
|
+
* (or surrendering to the consequences of not being allowed to). */
|
|
9
|
+
statement: string;
|
|
10
|
+
/** Where the statement was found: the spec's own body, or the task's
|
|
11
|
+
* research (the compose INPUT — live drafts sometimes drop the research
|
|
12
|
+
* nuance from the spec text while the contradiction remains real). */
|
|
13
|
+
source: 'spec' | 'research';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Find every unsatisfiable pair in the composed spec: a BLANKET frozen path
|
|
17
|
+
* whose registration/edit is declared necessary by the spec's own body (or
|
|
18
|
+
* explicitly surrendered to), or - when `research` is given - by the task's
|
|
19
|
+
* research the spec was composed FROM. The frozen paths always come from the
|
|
20
|
+
* SPEC alone; research contributes only the statement side (live compose
|
|
21
|
+
* sometimes drops the research's "must also be included" nuance from the spec
|
|
22
|
+
* text while shipping the freeze and the file creation - the contradiction is
|
|
23
|
+
* then visible only across the compose boundary). Deterministic, pure text -
|
|
24
|
+
* the same extraction the live A/B grounds its measurements in, so the model
|
|
25
|
+
* cannot self-report its way past it. Empty on a null spec or one that froze
|
|
26
|
+
* nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findFrozenPathConflicts(spec: string | null | undefined, research?: string | null): FrozenPathConflict[];
|
|
29
|
+
/**
|
|
30
|
+
* The forced critique-rewrite defect text (skip-escape pattern: MANDATORY,
|
|
31
|
+
* self-contained, names the exact resolution options). The two permitted
|
|
32
|
+
* resolutions come straight from the incident analysis: scoped ownership or
|
|
33
|
+
* dropping the creation — prose acknowledging the gap is called out as a
|
|
34
|
+
* non-resolution because that is precisely what the live model shipped.
|
|
35
|
+
*/
|
|
36
|
+
export declare function frozenConflictProbeText(conflicts: FrozenPathConflict[]): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frozen-conflict — deterministic detection of an UNSATISFIABLE spec pair at
|
|
3
|
+
* compose time (mx5 run 12 root cause, PROMPT 1 layer A).
|
|
4
|
+
*
|
|
5
|
+
* The failure this closes: compose authored TASK_0020 with a blanket freeze
|
|
6
|
+
* ("Do not modify `tsconfig.json`, `eslint.config.js`, or `.prettierrc.cjs`;
|
|
7
|
+
* those are handled in steps 1–2") while the deliverable — new
|
|
8
|
+
* `playwright-ct.config.ts` + `playwright/index.ts` — needs exactly that
|
|
9
|
+
* registration edit, and the spec "resolved" the contradiction with prose
|
|
10
|
+
* surrender ("Accept that `playwright-ct.config.ts` will not be covered by
|
|
11
|
+
* `tsc --noEmit` since `tsconfig.json` is not modified in this step"). Typed
|
|
12
|
+
* ESLint does not "accept" anything: the moment the created files land,
|
|
13
|
+
* `bun run lint` hard-errors REPO-WIDE and permanently — and the "owning"
|
|
14
|
+
* steps 1–2 completed long ago, so NO task may ever perform the edit (every
|
|
15
|
+
* spec carries the same freeze). Every subsequent task then burned its
|
|
16
|
+
* unattended AUTOFIX rounds on a repo-health FAIL none of them was allowed to
|
|
17
|
+
* fix, and the eventual "escape" was a child DELETING the deliverables.
|
|
18
|
+
*
|
|
19
|
+
* The contradiction is visible IN THE COMPOSED TEXT ITSELF, so it must die at
|
|
20
|
+
* spec time. Like the skip-escape / synthesized-wiring / plan-contradiction
|
|
21
|
+
* probes (prompt-only rules are A/B-proven ~0–1/5 on the weak model), the
|
|
22
|
+
* detector is deterministic and its finding is FORCED into the critique
|
|
23
|
+
* rewrite: the rewrite must either grant scoped ownership ("MAY edit `X` ONLY
|
|
24
|
+
* to register the files this task creates") or drop the file creation.
|
|
25
|
+
*
|
|
26
|
+
* High-precision by construction (FP-swept over the 26 real mx5 run-12 specs):
|
|
27
|
+
* - the freeze side must be BLANKET — a `Do NOT modify` line with no
|
|
28
|
+
* exception clause; a scoped freeze ("MAY edit … ONLY to register",
|
|
29
|
+
* "Only edit `X` …", "… except to add …") is already the resolution shape
|
|
30
|
+
* and must never re-fire;
|
|
31
|
+
* - the statement side is judged per SENTENCE, not per line (real GOALs are
|
|
32
|
+
* one giant line — line scoping false-fired on `must include` in a
|
|
33
|
+
* response-shape sentence three paths away from the frozen one);
|
|
34
|
+
* - a sentence that is itself prohibition-shaped ("Preserve … do not
|
|
35
|
+
* remove or modify …") is the freeze side restated, never a statement;
|
|
36
|
+
* - the sentence must NAME the frozen path (pathNamedIn) AND match one of
|
|
37
|
+
* the measured phrasing families: passive registration ("must also be
|
|
38
|
+
* included"), unless-added ("won't be type-checked unless added"),
|
|
39
|
+
* directional active ("requires adding … to" / "must add … to"), or the
|
|
40
|
+
* prose SURRENDER itself ("will not be covered … since `X` is not
|
|
41
|
+
* modified" — the exact line the live TASK_0020 spec shipped).
|
|
42
|
+
* Mere co-mention never fires.
|
|
43
|
+
*/
|
|
44
|
+
import { extractProhibitions, PROHIBITION_RE } from './prohibition-probe.js';
|
|
45
|
+
import { pathNamedIn } from './frozen-path-guard.js';
|
|
46
|
+
/**
|
|
47
|
+
* A freeze that carves out its own exception is SCOPED, not blanket — it is
|
|
48
|
+
* exactly the resolution shape the probe demands ("MAY edit `X` ONLY to
|
|
49
|
+
* register…", "Only edit `X` (add the search route)…", "Do not modify `X`
|
|
50
|
+
* except to add…"), so it never counts as the freeze side of a conflict, or
|
|
51
|
+
* the recomposed spec would re-fire forever.
|
|
52
|
+
*/
|
|
53
|
+
const EXCEPTION_RE = /\b(?:except|unless|beyond|other\s+than|apart\s+from|only\s+(?:to|for|if|when|where|edit|modify|change|touch|update)|may\s+(?:edit|modify|change|update|add))\b/i;
|
|
54
|
+
/** Passive registration: "must (also) be included/added/registered/…". */
|
|
55
|
+
const PASSIVE_REG_RE = /\b(?:must|needs?\s+to|has\s+to|should)\s+(?:also\s+)?be\s+(?:includ|add|regist|list|referenc|declar|updat)\w*/i;
|
|
56
|
+
/** "unless (it is) added/included/registered/updated". */
|
|
57
|
+
const UNLESS_ADDED_RE = /\bunless\s+(?:it\s+is\s+|it'?s\s+|they\s+are\s+|first\s+)?(?:add|includ|regist|updat)\w*/i;
|
|
58
|
+
/** "won't be type-checked/compiled/linted/covered … unless …". */
|
|
59
|
+
const NOT_CHECKED_UNLESS_RE = /\b(?:won'?t|will\s+not|cannot|can'?t|does\s+not|doesn'?t|is\s+not|isn'?t)\s+(?:be\s+)?[\w\s-]{0,40}?(?:type-?check|compil|lint|cover|resolv|recogni[sz]|includ|pick)\w*[^;]{0,80}?\bunless\b/i;
|
|
60
|
+
/** Directional active: "requires adding … to …" / "must add … to/into/in …".
|
|
61
|
+
* The preposition requirement is what keeps a response-shape "must include
|
|
62
|
+
* `field`" sentence from counting as a registration edit. */
|
|
63
|
+
const REQUIRES_DIRECTIONAL_RE = /\brequires?\s+(?:add|includ|regist|updat|edit|modify|chang)\w*[^;]{0,80}?\b(?:to|into|in)\b/i;
|
|
64
|
+
const MUST_ADD_DIRECTIONAL_RE = /\bmust\s+(?:also\s+)?(?:add|includ|regist|updat)\w*[^;]{0,80}?\b(?:to|into|in)\b/i;
|
|
65
|
+
/** The prose-surrender pair: an artifact "will not be covered/type-checked/…"
|
|
66
|
+
* BECAUSE the (frozen) path "is not modified". Both halves must be present —
|
|
67
|
+
* this is the exact contradiction shape the live TASK_0020 spec shipped. */
|
|
68
|
+
const NOT_COVERED_RE = /\b(?:will\s+not|won'?t|cannot|can'?t|is\s+not|isn'?t)\s+(?:be\s+)?(?:covered|type-?checked|checked|compiled|linted|validated|included)\b/i;
|
|
69
|
+
const BECAUSE_NOT_MODIFIED_RE = /\b(?:since|because|as)\b[^;]{0,80}?\b(?:is\s+|are\s+|was\s+|were\s+)?not\s+(?:be(?:ing)?\s+)?(?:modif|edit|chang|updat|touch)\w*/i;
|
|
70
|
+
function isRequiresEditStatement(sentence) {
|
|
71
|
+
return (PASSIVE_REG_RE.test(sentence)
|
|
72
|
+
|| UNLESS_ADDED_RE.test(sentence)
|
|
73
|
+
|| NOT_CHECKED_UNLESS_RE.test(sentence)
|
|
74
|
+
|| REQUIRES_DIRECTIONAL_RE.test(sentence)
|
|
75
|
+
|| MUST_ADD_DIRECTIONAL_RE.test(sentence)
|
|
76
|
+
|| (NOT_COVERED_RE.test(sentence) && BECAUSE_NOT_MODIFIED_RE.test(sentence)));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Split a spec line into sentences: a `.` or `;` followed by whitespace and a
|
|
80
|
+
* capital/backtick/bracket opener ends a sentence. The opener requirement
|
|
81
|
+
* keeps `e.g. foo` and mid-path dots intact; backtick paths carry no `. ` so
|
|
82
|
+
* they never split. Judging per sentence is what makes one-giant-line GOALs
|
|
83
|
+
* scannable without cross-sentence false fires.
|
|
84
|
+
*/
|
|
85
|
+
function splitSentences(line) {
|
|
86
|
+
return line.split(/[.;]\s+(?=[A-Z`([-])/);
|
|
87
|
+
}
|
|
88
|
+
/** Backtick-quoted path-shaped tokens in a sentence (the same shape rule the
|
|
89
|
+
* prohibition extractor applies), minus surrounding quotes. */
|
|
90
|
+
function pathTokensIn(sentence) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const m of sentence.matchAll(/`([^`]+)`/g)) {
|
|
93
|
+
const token = m[1].trim().replace(/^["']|["']$/g, '');
|
|
94
|
+
if (!/^[\w.@~/-]+$/.test(token))
|
|
95
|
+
continue;
|
|
96
|
+
if (token.includes('/') || /\.[A-Za-z0-9]+$/.test(token) || token.startsWith('.')) {
|
|
97
|
+
out.push(token);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** Scan one text's sentences for requires-edit statements naming a blanket
|
|
103
|
+
* frozen path, appending each new pair to `out`.
|
|
104
|
+
*
|
|
105
|
+
* `anchorSpec` (research scans only): a research statement counts ONLY when,
|
|
106
|
+
* besides the frozen path, it names at least one other path that still
|
|
107
|
+
* appears in the SPEC. That anchor is what makes resolution (b) terminal:
|
|
108
|
+
* when the rewrite DROPS the file creation, the created file vanishes from
|
|
109
|
+
* the spec, the research statement about it loses its anchor, and the
|
|
110
|
+
* detector goes quiet instead of re-firing forever on stale research. */
|
|
111
|
+
function scanForStatements(text, blanket, source, out, seen, anchorSpec) {
|
|
112
|
+
for (const raw of text.split('\n')) {
|
|
113
|
+
for (const fragment of splitSentences(raw)) {
|
|
114
|
+
const sentence = fragment.trim();
|
|
115
|
+
if (sentence.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
// A prohibition-shaped sentence IS the freeze side (the constraint
|
|
118
|
+
// itself, or a "Preserve/do not remove or modify" restatement) -
|
|
119
|
+
// never the requires-edit side.
|
|
120
|
+
if (PROHIBITION_RE.test(sentence))
|
|
121
|
+
continue;
|
|
122
|
+
if (!isRequiresEditStatement(sentence))
|
|
123
|
+
continue;
|
|
124
|
+
for (const p of blanket) {
|
|
125
|
+
if (!pathNamedIn(sentence, p.path))
|
|
126
|
+
continue;
|
|
127
|
+
if (anchorSpec !== undefined) {
|
|
128
|
+
const anchors = pathTokensIn(sentence).filter(t => t !== p.path && !pathNamedIn(p.path, t));
|
|
129
|
+
if (!anchors.some(a => pathNamedIn(anchorSpec, a)))
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const key = `${p.path} ${sentence}`;
|
|
133
|
+
if (seen.has(key))
|
|
134
|
+
continue;
|
|
135
|
+
seen.add(key);
|
|
136
|
+
out.push({ path: p.path, constraint: p.constraint, statement: sentence, source });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Find every unsatisfiable pair in the composed spec: a BLANKET frozen path
|
|
143
|
+
* whose registration/edit is declared necessary by the spec's own body (or
|
|
144
|
+
* explicitly surrendered to), or - when `research` is given - by the task's
|
|
145
|
+
* research the spec was composed FROM. The frozen paths always come from the
|
|
146
|
+
* SPEC alone; research contributes only the statement side (live compose
|
|
147
|
+
* sometimes drops the research's "must also be included" nuance from the spec
|
|
148
|
+
* text while shipping the freeze and the file creation - the contradiction is
|
|
149
|
+
* then visible only across the compose boundary). Deterministic, pure text -
|
|
150
|
+
* the same extraction the live A/B grounds its measurements in, so the model
|
|
151
|
+
* cannot self-report its way past it. Empty on a null spec or one that froze
|
|
152
|
+
* nothing.
|
|
153
|
+
*/
|
|
154
|
+
export function findFrozenPathConflicts(spec, research) {
|
|
155
|
+
if (!spec)
|
|
156
|
+
return [];
|
|
157
|
+
const blanket = extractProhibitions(spec)
|
|
158
|
+
.filter(p => !EXCEPTION_RE.test(p.constraint))
|
|
159
|
+
.map(p => ({
|
|
160
|
+
path: p.path.replace(/^\.\//, '').replace(/\/+$/, ''),
|
|
161
|
+
constraint: p.constraint
|
|
162
|
+
}))
|
|
163
|
+
.filter(p => p.path.length > 0);
|
|
164
|
+
if (blanket.length === 0)
|
|
165
|
+
return [];
|
|
166
|
+
const out = [];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
scanForStatements(spec, blanket, 'spec', out, seen);
|
|
169
|
+
if (research)
|
|
170
|
+
scanForStatements(research, blanket, 'research', out, seen, spec);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The forced critique-rewrite defect text (skip-escape pattern: MANDATORY,
|
|
175
|
+
* self-contained, names the exact resolution options). The two permitted
|
|
176
|
+
* resolutions come straight from the incident analysis: scoped ownership or
|
|
177
|
+
* dropping the creation — prose acknowledging the gap is called out as a
|
|
178
|
+
* non-resolution because that is precisely what the live model shipped.
|
|
179
|
+
*/
|
|
180
|
+
export function frozenConflictProbeText(conflicts) {
|
|
181
|
+
const items = conflicts.map(c => `- the spec FREEZES \`${c.path}\` ("${c.constraint.slice(0, 160)}") yet `
|
|
182
|
+
+ (c.source === 'research' ?
|
|
183
|
+
`the task's own RESEARCH (the input this spec was composed from) states the `
|
|
184
|
+
+ `deliverable REQUIRES changing it ("${c.statement.slice(0, 160)}") — the spec `
|
|
185
|
+
+ `omitting this fact does not remove the requirement`
|
|
186
|
+
: `its own body states the deliverable REQUIRES changing it `
|
|
187
|
+
+ `("${c.statement.slice(0, 160)}")`));
|
|
188
|
+
return [
|
|
189
|
+
'UNSATISFIABLE-CONSTRAINT FINDING (deterministic; MUST be resolved, it overrides a CLEAN triage):',
|
|
190
|
+
...items,
|
|
191
|
+
'This pair is self-contradictory: the files this task creates need a registration edit',
|
|
192
|
+
'that the spec itself forbids, and the step that "owns" the frozen file has already',
|
|
193
|
+
'completed — no task will ever be allowed to perform the edit. The moment the created',
|
|
194
|
+
'files land, the repo-wide static check fails PERMANENTLY and every later task burns',
|
|
195
|
+
'its autofix rounds on a defect none of them may touch. Prose acknowledging the gap',
|
|
196
|
+
'("accept that it will not be covered…") is NOT a resolution.',
|
|
197
|
+
'REWRITE the spec to resolve it in exactly ONE of these two ways:',
|
|
198
|
+
' (a) SCOPED OWNERSHIP — replace the blanket freeze on the conflicting path with:',
|
|
199
|
+
' "You MAY edit `<path>` ONLY to register the files this task creates (e.g. add',
|
|
200
|
+
' them to its include list); any other change to `<path>` is forbidden." Keep the',
|
|
201
|
+
' blanket freeze for the other frozen paths.',
|
|
202
|
+
' (b) DROP the creation of the files that would require the frozen edit (and remove',
|
|
203
|
+
' the acceptance/verify steps that depend on them), stating why.',
|
|
204
|
+
'Never ship both the blanket freeze and the requires-edit statement.'
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
@@ -11,6 +11,15 @@ export type FrozenGit = (args: string[]) => Promise<{
|
|
|
11
11
|
* no-op by construction.
|
|
12
12
|
*/
|
|
13
13
|
export declare function frozenPathsFromSpec(spec: string | null | undefined): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Does this prose/tool-output text NAME the given path? Word-bounded on both
|
|
16
|
+
* sides so `tsconfig.json` matches `` `tsconfig.json` ``, `(tsconfig.json:18)`
|
|
17
|
+
* and a bare mention, but never `foo.tsconfig.json`, `config/tsconfig.json`
|
|
18
|
+
* (a different file) or `tsconfig.json5`/`tsconfig.json.bak`. Shared by the
|
|
19
|
+
* compose-time unsatisfiable-pair detector (frozen-conflict.ts) and lint-fix's
|
|
20
|
+
* non-convergence trace, so "the text names a frozen path" means one thing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pathNamedIn(text: string, path: string): boolean;
|
|
14
23
|
/**
|
|
15
24
|
* Parse `git status --porcelain` output (already scoped to the frozen pathspec)
|
|
16
25
|
* into the list of changed files, for the gate-trail record and to decide whether
|
|
@@ -48,6 +48,21 @@ export function frozenPathsFromSpec(spec) {
|
|
|
48
48
|
}
|
|
49
49
|
return [...seen];
|
|
50
50
|
}
|
|
51
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
/**
|
|
53
|
+
* Does this prose/tool-output text NAME the given path? Word-bounded on both
|
|
54
|
+
* sides so `tsconfig.json` matches `` `tsconfig.json` ``, `(tsconfig.json:18)`
|
|
55
|
+
* and a bare mention, but never `foo.tsconfig.json`, `config/tsconfig.json`
|
|
56
|
+
* (a different file) or `tsconfig.json5`/`tsconfig.json.bak`. Shared by the
|
|
57
|
+
* compose-time unsatisfiable-pair detector (frozen-conflict.ts) and lint-fix's
|
|
58
|
+
* non-convergence trace, so "the text names a frozen path" means one thing.
|
|
59
|
+
*/
|
|
60
|
+
export function pathNamedIn(text, path) {
|
|
61
|
+
const p = path.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
62
|
+
if (p.length === 0)
|
|
63
|
+
return false;
|
|
64
|
+
return new RegExp(`(?:^|[^\\w./-])${escapeRe(p)}(?!\\.?[\\w-])`, 'im').test(text);
|
|
65
|
+
}
|
|
51
66
|
/**
|
|
52
67
|
* Parse `git status --porcelain` output (already scoped to the frozen pathspec)
|
|
53
68
|
* into the list of changed files, for the gate-trail record and to decide whether
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -21,7 +21,7 @@ import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-
|
|
|
21
21
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
22
22
|
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
23
23
|
import { readContracts } from './contracts.js';
|
|
24
|
-
import { recordAcceptDebt, recordEnforceRevertDebt } from './accept-debt.js';
|
|
24
|
+
import { recordAcceptDebt, recordEnforceRevertDebt, recordFrozenBlockedDebt } from './accept-debt.js';
|
|
25
25
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
26
26
|
import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate.js';
|
|
27
27
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
@@ -314,6 +314,10 @@ export function buildGateDeps(params) {
|
|
|
314
314
|
// discardEdits): the final integration gate re-checks each debt at run end.
|
|
315
315
|
recordAcceptDebt: (cwd2, taskId, reason) => recordAcceptDebt(cwd2, taskId, reason),
|
|
316
316
|
recordEnforceRevertDebt: (cwd2, taskId, reason) => recordEnforceRevertDebt(cwd2, taskId, reason),
|
|
317
|
+
// Durable cross-task-contradiction ledger (PROMPT 1 layer B): a repo-health
|
|
318
|
+
// FAIL whose only fix is an edit to a path this task's spec froze — recorded
|
|
319
|
+
// when the gate loop routes it to the picker, re-checked by the final gate.
|
|
320
|
+
recordFrozenBlockedDebt: (cwd2, taskId, reason) => recordFrozenBlockedDebt(cwd2, taskId, reason),
|
|
317
321
|
// Frozen-path write-deny (see frozen-path-guard.ts): the concrete paths this
|
|
318
322
|
// task's spec forbids modifying, so the gate sequence can UNDO any edit the
|
|
319
323
|
// enforce EDIT pass makes to them before those edits are committed. Reads the
|
package/dist/task/lint-fix.d.ts
CHANGED
|
@@ -11,10 +11,13 @@ export interface LintFixDeps {
|
|
|
11
11
|
failReason: string;
|
|
12
12
|
/** Run the fix child; same closure shape the other gate children use. */
|
|
13
13
|
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
14
|
-
/** The deterministic whole-repo static check to converge against.
|
|
14
|
+
/** The deterministic whole-repo static check to converge against. `output`
|
|
15
|
+
* (the failing command's captured text, when the impl provides it) lets the
|
|
16
|
+
* non-convergence path trace the findings to a spec-frozen path. */
|
|
15
17
|
repoHealth: () => Promise<{
|
|
16
18
|
ok: boolean;
|
|
17
19
|
reason: string;
|
|
20
|
+
output?: string;
|
|
18
21
|
}>;
|
|
19
22
|
/** Run git in cwd; injected so the guard logic is unit-testable. */
|
|
20
23
|
git: (args: string[]) => Promise<{
|
package/dist/task/lint-fix.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* dirty with (possibly task) work is left alone, in the guard's safe direction:
|
|
45
45
|
* cost time, never work.
|
|
46
46
|
*/
|
|
47
|
-
import { parseChangedFrozenFiles, revertFrozenPaths } from './frozen-path-guard.js';
|
|
47
|
+
import { parseChangedFrozenFiles, pathNamedIn, revertFrozenPaths } from './frozen-path-guard.js';
|
|
48
48
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
49
49
|
export const LINT_FIX_TOOLS = 'read,edit,bash';
|
|
50
50
|
/**
|
|
@@ -251,6 +251,25 @@ export async function runBoundedLintFix(deps) {
|
|
|
251
251
|
}
|
|
252
252
|
const health = await deps.repoHealth();
|
|
253
253
|
if (!health.ok) {
|
|
254
|
+
// FROZEN-PATH TRACE on non-convergence (PROMPT 1 layer B): when the child
|
|
255
|
+
// was honest — it did NOT touch the frozen path, so the guard above never
|
|
256
|
+
// tripped — but the check is still red and its own output NAMES a frozen
|
|
257
|
+
// path (typed ESLint: "playwright/index.ts was not found by the project …
|
|
258
|
+
// consider including it in the tsconfig.json"), the findings can only be
|
|
259
|
+
// fixed by an edit this task's spec forbids. Report it under the same
|
|
260
|
+
// `frozen-path:` prefix as the guard trip, so the gate loop can route
|
|
261
|
+
// straight to the human picker instead of burning unattended AUTOFIX
|
|
262
|
+
// rounds an impl re-run under the same freeze cannot converge out of.
|
|
263
|
+
const implicated = frozen.filter(p => pathNamedIn(`${health.reason}\n${health.output ?? ''}`, p));
|
|
264
|
+
if (implicated.length > 0) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
reason: `frozen-path: static findings implicate spec-frozen path(s) `
|
|
268
|
+
+ `(${implicated.slice(0, 3).join(', ')}`
|
|
269
|
+
+ `${implicated.length > 3 ? ', …' : ''}) — did not converge `
|
|
270
|
+
+ `(${health.reason}); a fix under this task's constraints cannot converge`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
254
273
|
return { ok: false, reason: `did not converge: ${health.reason}` };
|
|
255
274
|
}
|
|
256
275
|
return { ok: true, reason: guardNote };
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export interface PhaseAutoAnswerDeps {
|
|
|
117
117
|
export declare function phaseAutoAnswer(deps: PhaseDeps, refined: string, research: string, question: string, autoDeps?: PhaseAutoAnswerDeps): Promise<AutoAnswer>;
|
|
118
118
|
export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext, widgetState: WidgetState, refined: string, research: string): Promise<string>;
|
|
119
119
|
export declare function phaseCompose(deps: PhaseDeps, refined: string, research: string, qa: string): Promise<string>;
|
|
120
|
-
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string, planContext?: string): Promise<string>;
|
|
120
|
+
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string, planContext?: string, research?: string): Promise<string>;
|
|
121
121
|
export declare function critiqueWithFallback(d: PhaseDeps, p: PhaseContext): Promise<string>;
|
|
122
122
|
export declare const PHASES: PhaseConfig[];
|
|
123
123
|
export declare function postCommitPhase(phase: PhaseConfig, deps: PhaseDeps, pc: PhaseContext, out: string): Promise<void>;
|
package/dist/task/phases.js
CHANGED
|
@@ -29,6 +29,7 @@ import { parseVerifyBlock, validateSpecShape, stripSpecPreamble, isCritiqueClean
|
|
|
29
29
|
import { findSkipEscapes, skipEscapeDefectText } from './skip-escape.js';
|
|
30
30
|
import { findSynthesizedWiring, wiringProbeText, readReferencedDocs } from './wiring-claims.js';
|
|
31
31
|
import { findAbsenceConflicts, absenceProbeText, siblingTitlesFromPlanContext } from './verify-reconcile.js';
|
|
32
|
+
import { findFrozenPathConflicts, frozenConflictProbeText } from './frozen-conflict.js';
|
|
32
33
|
import { existsSync } from 'node:fs';
|
|
33
34
|
import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from './contracts.js';
|
|
34
35
|
import { readRequirements, buildRequirementsBlock } from './requirements.js';
|
|
@@ -791,7 +792,7 @@ export async function phaseCompose(deps, refined, research, qa) {
|
|
|
791
792
|
return { ok: true, value: stripped };
|
|
792
793
|
}, problem => new Error(`compose_invalid: ${problem}`));
|
|
793
794
|
}
|
|
794
|
-
export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
795
|
+
export async function phaseCritique(deps, spec, refined, qa, planContext, research) {
|
|
795
796
|
// Fast triage before the expensive full rewrite. The rewrite regenerates
|
|
796
797
|
// the entire spec from scratch and is the costliest tail of the pipeline
|
|
797
798
|
// (observed up to ~240s). Most compose drafts are already good, so we first
|
|
@@ -850,6 +851,21 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
850
851
|
deps.logDebug?.('plan-contradiction flagged in VERIFY: '
|
|
851
852
|
+ absenceConflicts.map(c => `${c.assertion.target} (${c.against})`).join(' | '));
|
|
852
853
|
}
|
|
854
|
+
// DETERMINISTIC unsatisfiable-pair probe (mx5 run 12 root cause): a blanket
|
|
855
|
+
// frozen path ("Do NOT modify `tsconfig.json` … handled in steps 1–2") whose
|
|
856
|
+
// registration edit the spec's OWN body — or the task's RESEARCH the spec was
|
|
857
|
+
// composed from (live drafts sometimes drop the nuance while shipping the
|
|
858
|
+
// freeze and the creation) — says the deliverable requires ("must also be
|
|
859
|
+
// included …"). Shipped as-is, the created files turn the repo-wide static
|
|
860
|
+
// check permanently red and no task is allowed to fix it — every later task
|
|
861
|
+
// burns its AUTOFIX rounds on it. Forced into the rewrite like the other
|
|
862
|
+
// probes: the rewrite must grant scoped ownership or drop the creation.
|
|
863
|
+
const frozenConflicts = findFrozenPathConflicts(spec, research);
|
|
864
|
+
const frozenProbe = frozenConflicts.length > 0 ? frozenConflictProbeText(frozenConflicts) : null;
|
|
865
|
+
if (frozenProbe) {
|
|
866
|
+
deps.logDebug?.('unsatisfiable freeze/requires-edit pair flagged in spec: '
|
|
867
|
+
+ frozenConflicts.map(c => c.path).join(' | '));
|
|
868
|
+
}
|
|
853
869
|
let triageDefects = null;
|
|
854
870
|
if (parseVerifyBlock(spec) !== null) {
|
|
855
871
|
const tTriage = Date.now();
|
|
@@ -866,12 +882,15 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
866
882
|
}
|
|
867
883
|
deps.recordSubStep?.('triage', Date.now() - tTriage);
|
|
868
884
|
if (verdict !== null) {
|
|
869
|
-
// A deterministic skip-escape, synthesized-wiring,
|
|
870
|
-
// finding overrides a CLEAN triage: the draft must
|
|
871
|
-
// it even if the model judged the rest clean
|
|
872
|
-
// self-discover any of them reliably).
|
|
885
|
+
// A deterministic skip-escape, synthesized-wiring, plan-contradiction,
|
|
886
|
+
// or unsatisfiable-pair finding overrides a CLEAN triage: the draft must
|
|
887
|
+
// be rewritten to resolve it even if the model judged the rest clean
|
|
888
|
+
// (the model does not self-discover any of them reliably).
|
|
873
889
|
if (isCritiqueClean(verdict)) {
|
|
874
|
-
if (skipDefects === null
|
|
890
|
+
if (skipDefects === null
|
|
891
|
+
&& wiringProbe === null
|
|
892
|
+
&& absenceProbe === null
|
|
893
|
+
&& frozenProbe === null) {
|
|
875
894
|
return spec;
|
|
876
895
|
}
|
|
877
896
|
}
|
|
@@ -881,8 +900,11 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
881
900
|
}
|
|
882
901
|
}
|
|
883
902
|
// Merge the deterministic skip-escape + synthesized-wiring + plan-contradiction
|
|
884
|
-
// defects with any triage defects for the rewrite (all are
|
|
885
|
-
|
|
903
|
+
// + unsatisfiable-pair defects with any triage defects for the rewrite (all are
|
|
904
|
+
// forced FOCUS items).
|
|
905
|
+
const rewriteDefects = [skipDefects, wiringProbe, absenceProbe, frozenProbe, triageDefects]
|
|
906
|
+
.filter(Boolean)
|
|
907
|
+
.join('\n\n') || null;
|
|
886
908
|
const tRewrite = Date.now();
|
|
887
909
|
try {
|
|
888
910
|
return await runWithEmphasisRetry(deps, 'critique', 'read', problem => CRITIQUE_PROMPT(spec, refined, qa, problem !== null, rewriteDefects, contractsBlock), text => {
|
|
@@ -902,7 +924,7 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
902
924
|
// ─── Critique with fallback ──────────────────────────────────────────────────
|
|
903
925
|
export async function critiqueWithFallback(d, p) {
|
|
904
926
|
try {
|
|
905
|
-
return await phaseCritique(d, p.spec, p.refined, p.qa, p.planContext);
|
|
927
|
+
return await phaseCritique(d, p.spec, p.refined, p.qa, p.planContext, p.research);
|
|
906
928
|
}
|
|
907
929
|
catch (err) {
|
|
908
930
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -36,6 +36,13 @@ export interface Prohibition {
|
|
|
36
36
|
* against the EXACT wording — including any exception clause it states. */
|
|
37
37
|
constraint: string;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Does this line express a modification ban? Matches the active forms ("do not
|
|
41
|
+
* modify", "must not touch", "never edit", "don't change") and the passive form
|
|
42
|
+
* ("must not be modified"). Deliberately verb-scoped to modification — a "do not
|
|
43
|
+
* add a dependency" style rule names no path and is the prompt rule's job.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PROHIBITION_RE: RegExp;
|
|
39
46
|
/**
|
|
40
47
|
* Extract the concrete paths the spec explicitly forbids modifying: every
|
|
41
48
|
* backtick-quoted path-like token on a line that expresses a modification ban.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ("must not be modified"). Deliberately verb-scoped to modification — a "do not
|
|
5
5
|
* add a dependency" style rule names no path and is the prompt rule's job.
|
|
6
6
|
*/
|
|
7
|
-
const PROHIBITION_RE = /\b(?:do\s+not|don'?t|must\s+not|never)\s+(?:be\s+)?(?:modify|modified|touch|touched|edit|edited|change|changed|alter|altered|rewrite|rewritten|overwrite|overwritten|delete|deleted|remove|removed)\b/i;
|
|
7
|
+
export const PROHIBITION_RE = /\b(?:do\s+not|don'?t|must\s+not|never)\s+(?:be\s+)?(?:modify|modified|touch|touched|edit|edited|change|changed|alter|altered|rewrite|rewritten|overwrite|overwritten|delete|deleted|remove|removed)\b/i;
|
|
8
8
|
/**
|
|
9
9
|
* A backtick token counts as a path only when it is whitespace-free, uses path
|
|
10
10
|
* characters, and either contains a directory separator, has a file extension,
|
|
@@ -135,6 +135,14 @@ export interface GateDeps {
|
|
|
135
135
|
* gate re-checks and surfaces it like an accept-debt. Best-effort; absent in tests.
|
|
136
136
|
*/
|
|
137
137
|
recordEnforceRevertDebt?: (cwd: string, taskId: string, reason: string) => Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Record a durable FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a
|
|
140
|
+
* repo-health FAIL whose only fix is an edit to a path THIS task's spec froze —
|
|
141
|
+
* a cross-task contradiction. Recorded when the loop routes such a FAIL to the
|
|
142
|
+
* picker (whatever the human then picks, the defect is real and no task may fix
|
|
143
|
+
* it), so the final gate re-checks it at run end. Best-effort; absent in tests.
|
|
144
|
+
*/
|
|
145
|
+
recordFrozenBlockedDebt?: (cwd: string, taskId: string, reason: string) => Promise<void>;
|
|
138
146
|
/**
|
|
139
147
|
* The concrete paths this task's spec forbids modifying (its `Do NOT modify`
|
|
140
148
|
* CONSTRAINTS — see frozen-path-guard.ts / prohibition-probe.ts). Used to
|
package/dist/task/task-gates.js
CHANGED
|
@@ -89,6 +89,15 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
89
89
|
// auto attempts still FAIL, the picker returns so a person can break the loop.
|
|
90
90
|
let lintFixAttempted = false;
|
|
91
91
|
let autoFixCount = 0;
|
|
92
|
+
// Set when the bounded lint-fix reports the `frozen-path:` rejection: the
|
|
93
|
+
// repo-health FAIL can only be fixed by editing a path THIS task's spec
|
|
94
|
+
// froze (mx5 run 12's unsatisfiable registration pair). An impl re-run is
|
|
95
|
+
// under the same freeze — rule 4b fails the task if it complies with the
|
|
96
|
+
// linter — so unattended AUTOFIX rounds CANNOT converge and are skipped;
|
|
97
|
+
// the picker is forced with the cross-task contradiction named, and the
|
|
98
|
+
// defect is recorded as a durable debt for the final gate.
|
|
99
|
+
let frozenContradiction = null;
|
|
100
|
+
let frozenDebtRecorded = false;
|
|
92
101
|
while (!verified.ok) {
|
|
93
102
|
const failReason = verified.reason ?? 'did not verify';
|
|
94
103
|
// GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
|
|
@@ -107,6 +116,9 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
107
116
|
await rec(verdictLine(verified));
|
|
108
117
|
continue;
|
|
109
118
|
}
|
|
119
|
+
if ((fix.reason ?? '').startsWith('frozen-path:')) {
|
|
120
|
+
frozenContradiction = fix.reason ?? null;
|
|
121
|
+
}
|
|
110
122
|
}
|
|
111
123
|
// UNOBSERVED (rule 5c): a spec-required behavioral check could not run because
|
|
112
124
|
// its observation tooling is absent. An unattended AUTOFIX re-run cannot install
|
|
@@ -114,14 +126,48 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
114
126
|
// decision (provision the tool, or accept the unproven behavior) is the human's.
|
|
115
127
|
// Skip the (moot) recommendation research and force the picker.
|
|
116
128
|
const isUnobserved = verified.unobserved === true;
|
|
129
|
+
// FROZEN-BLOCKED (cross-task contradiction): skip the recommendation
|
|
130
|
+
// research too — it would only re-derive what the deterministic lint-fix
|
|
131
|
+
// rejection already proved. The picker shows the contradiction; ACCEPT
|
|
132
|
+
// records the (already-recorded) defect as the human's call. Applies
|
|
133
|
+
// only while the FAIL is still the repo-health one the contradiction
|
|
134
|
+
// explains — a later, different FAIL gets the ordinary resolution path.
|
|
135
|
+
const isFrozenBlocked = frozenContradiction !== null && failReason.startsWith('repo health:');
|
|
117
136
|
const recOutcome = isUnobserved ? { recommend: 'autofix', rationale: failReason }
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
: isFrozenBlocked ?
|
|
138
|
+
{
|
|
139
|
+
recommend: 'accept',
|
|
140
|
+
rationale: `${frozenContradiction} — the failing static check can only be fixed by `
|
|
141
|
+
+ `editing a path this task's spec freezes (a cross-task contradiction: `
|
|
142
|
+
+ `the spec forbids the very edit the repo needs; the "owning" earlier `
|
|
143
|
+
+ `step already completed). An implementation re-run under the same `
|
|
144
|
+
+ `freeze cannot converge. ACCEPT records it as durable debt the final `
|
|
145
|
+
+ `gate re-checks; fixing it needs a plan-level change, not a re-run.`
|
|
146
|
+
}
|
|
147
|
+
: deps.recommend ?
|
|
148
|
+
await deps.recommend(active, p.cwd, p.title, p.taskId, failReason)
|
|
149
|
+
: { recommend: 'autofix', rationale: failReason };
|
|
121
150
|
await rec(isUnobserved ?
|
|
122
151
|
'resolution: verify UNOBSERVED — spec-required check could not run (tooling absent); '
|
|
123
152
|
+ 'forcing the human picker, an unattended re-run cannot provision it'
|
|
124
|
-
:
|
|
153
|
+
: isFrozenBlocked ?
|
|
154
|
+
'resolution: repo-health FAIL is blocked by spec-frozen path(s) — cross-task '
|
|
155
|
+
+ 'contradiction; unattended AUTOFIX skipped (an impl re-run under the same '
|
|
156
|
+
+ 'freeze cannot converge), forcing the human picker'
|
|
157
|
+
: `resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
158
|
+
if (isFrozenBlocked && !frozenDebtRecorded) {
|
|
159
|
+
frozenDebtRecorded = true;
|
|
160
|
+
// Durable regardless of what the human picks next: the contradiction
|
|
161
|
+
// is real, cross-task, and outside this task's power to fix — the
|
|
162
|
+
// final gate must surface it at run end (static-class: it auto-closes
|
|
163
|
+
// iff the run-end static check passes).
|
|
164
|
+
try {
|
|
165
|
+
await deps.recordFrozenBlockedDebt?.(p.cwd, p.taskId, `${failReason} — ${frozenContradiction}`);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// recording must never break the gate sequence
|
|
169
|
+
}
|
|
170
|
+
}
|
|
125
171
|
// AUTO-RESOLVE the AUTOFIX path: when the research says the work is
|
|
126
172
|
// genuinely wrong, re-run the fix WITHOUT prompting the user. The picker is
|
|
127
173
|
// reserved for the ACCEPT recommendation (the human decides whether to bless
|
|
@@ -129,6 +175,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
129
175
|
// MAX_AUTO_AUTOFIX consecutive unattended attempts that still FAIL, hand
|
|
130
176
|
// control back so a person can break a non-converging loop.
|
|
131
177
|
const autoFixNow = !isUnobserved
|
|
178
|
+
&& !isFrozenBlocked
|
|
132
179
|
&& recOutcome.recommend === 'autofix'
|
|
133
180
|
&& autoFixCount < MAX_AUTO_AUTOFIX;
|
|
134
181
|
let choice;
|
|
@@ -151,11 +198,15 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
151
198
|
// defect ships and nothing else revisits it (mx5 run 4 B3 / run 8
|
|
152
199
|
// TASK_0012). Record it to the run ledger; the final integration gate
|
|
153
200
|
// re-checks it at run end and surfaces it if still open. Best-effort.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
201
|
+
// The frozen-blocked routing above already recorded this defect (with
|
|
202
|
+
// the contradiction named) — don't double-enter it in the ledger.
|
|
203
|
+
if (!frozenDebtRecorded) {
|
|
204
|
+
try {
|
|
205
|
+
await deps.recordAcceptDebt?.(p.cwd, p.taskId, failReason);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// recording must never break the gate sequence
|
|
209
|
+
}
|
|
159
210
|
}
|
|
160
211
|
active.ui.notify(`${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
161
212
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.22",
|
|
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",
|