@mjasnikovs/pi-task 0.18.20 → 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/README.md +23 -1
- package/assets/hero.png +0 -0
- package/assets/hero.svg +92 -0
- 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 +31 -11
- package/dist/task/lint-fix.d.ts +15 -34
- package/dist/task/lint-fix.js +98 -4
- 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 +9 -1
- package/dist/task/task-gates.js +61 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
# pi-task
|
|
6
6
|
|
|
@@ -38,6 +38,22 @@ pi install npm:@mjasnikovs/pi-task
|
|
|
38
38
|
|
|
39
39
|
> Requires [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) (the Earendil coding agent) ≥ 0.80.
|
|
40
40
|
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
One change — `/task` runs it through the full pipeline and hands the finished spec back in the same chat:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
/task add rate limiting to the /api/upload endpoint
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A whole plan — `/task-auto` splits it into an ordered task list and runs each one through `/task`:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
/task-auto Implement @MY_DETAILED_AND_LARGE_PLAN.md
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`@`-mentioning a file inlines its **contents**, so point it at the design doc you already wrote — no copy-paste.
|
|
56
|
+
|
|
41
57
|
## Slash commands
|
|
42
58
|
|
|
43
59
|
| Command | What it does |
|
|
@@ -54,6 +70,12 @@ pi install npm:@mjasnikovs/pi-task
|
|
|
54
70
|
|
|
55
71
|
## The pipeline
|
|
56
72
|
|
|
73
|
+
<div align="center">
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
</div>
|
|
78
|
+
|
|
57
79
|
| Phase | Output section | What happens |
|
|
58
80
|
| --- | --- | --- |
|
|
59
81
|
| **refine** | `refined prompt` | Sharpens your raw ask into an unambiguous, self-contained statement. |
|
package/assets/hero.png
ADDED
|
Binary file
|
package/assets/hero.svg
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" role="img" aria-label="pi-task. Local models drift. pi-task doesn't trust one prompt. Your ask runs through five fixed phases — refine, research, grill, compose, critique — and what comes out is a spec.">
|
|
2
|
+
<defs>
|
|
3
|
+
<clipPath id="frame"><rect x="6" y="6" width="948" height="588" rx="18"/></clipPath>
|
|
4
|
+
<style>
|
|
5
|
+
.code { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
6
|
+
.ic { fill:none; stroke-width:2.25; stroke-linecap:round; stroke-linejoin:round; }
|
|
7
|
+
.icf { stroke:none; }
|
|
8
|
+
.ph { font-size:18px; fill:#c9d1d9; }
|
|
9
|
+
.arw { fill:none; stroke:#6e7681; stroke-width:2.07; stroke-linecap:round; stroke-linejoin:round; }
|
|
10
|
+
</style>
|
|
11
|
+
</defs>
|
|
12
|
+
|
|
13
|
+
<rect x="6" y="6" width="948" height="588" rx="18" fill="#0d1117" stroke="#30363d" stroke-width="1.5"/>
|
|
14
|
+
|
|
15
|
+
<!-- ── watermark ── -->
|
|
16
|
+
<defs>
|
|
17
|
+
<linearGradient id="mtFade" gradientUnits="userSpaceOnUse" x1="6" y1="0" x2="688" y2="0">
|
|
18
|
+
<stop offset="0" stop-color="#fff" stop-opacity="1"/>
|
|
19
|
+
<stop offset="0.55" stop-color="#fff" stop-opacity="0.9"/>
|
|
20
|
+
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
|
21
|
+
</linearGradient>
|
|
22
|
+
<mask id="mtMask"><rect x="0" y="0" width="960" height="600" fill="url(#mtFade)"/></mask>
|
|
23
|
+
</defs>
|
|
24
|
+
<g clip-path="url(#frame)" mask="url(#mtMask)" fill="none" stroke="#a371f7" stroke-linecap="round" stroke-linejoin="round">
|
|
25
|
+
<path d="M6.0 594.0L75.6 530.2L139.4 550.5L203.2 486.7L275.7 518.6L342.4 449.0L412.0 501.2L484.5 460.6L559.9 521.5L629.5 486.7L687.5 594.0" stroke-width="1.5" opacity="0.12"/>
|
|
26
|
+
<path d="M264.1 330.1L6.0 594.0M264.1 330.1L130.7 594.0M264.1 330.1L255.4 594.0M264.1 330.1L380.1 594.0M264.1 330.1L504.8 594.0M264.1 330.1L629.5 594.0" stroke-width="1" opacity="0.08"/>
|
|
27
|
+
<path d="M56.8 538.9L56.8 594.0M95.9 556.3L95.9 594.0M148.1 475.1L148.1 594.0M191.6 495.4L191.6 594.0M226.4 405.5L226.4 594.0M264.1 330.1L264.1 594.0M290.2 382.3L290.2 594.0M316.3 364.9L316.3 594.0M351.1 443.2L351.1 594.0M385.9 422.9L385.9 594.0M423.6 489.6L423.6 594.0M458.4 469.3L458.4 594.0M499.0 530.2L499.0 594.0M539.6 515.7L539.6 594.0M586.0 567.9L586.0 594.0" stroke-width="1" opacity="0.07"/>
|
|
28
|
+
<path d="M6.0 594.0L56.8 538.9L95.9 556.3L148.1 475.1L191.6 495.4L226.4 405.5L264.1 330.1L290.2 382.3L316.3 364.9L351.1 443.2L385.9 422.9L423.6 489.6L458.4 469.3L499.0 530.2L539.6 515.7L586.0 567.9L629.5 594.0" stroke-width="2" opacity="0.30"/>
|
|
29
|
+
<circle cx="6.0" cy="594.0" r="2.6" fill="#a371f7" stroke="none" opacity="0.34"/>
|
|
30
|
+
<circle cx="148.1" cy="475.1" r="2.6" fill="#a371f7" stroke="none" opacity="0.34"/>
|
|
31
|
+
<circle cx="264.1" cy="330.1" r="2.6" fill="#a371f7" stroke="none" opacity="0.34"/>
|
|
32
|
+
<circle cx="351.1" cy="443.2" r="2.6" fill="#a371f7" stroke="none" opacity="0.34"/>
|
|
33
|
+
<circle cx="458.4" cy="469.3" r="2.6" fill="#a371f7" stroke="none" opacity="0.34"/>
|
|
34
|
+
<circle cx="586.0" cy="567.9" r="2.6" fill="#a371f7" stroke="none" opacity="0.34"/>
|
|
35
|
+
</g>
|
|
36
|
+
|
|
37
|
+
<!-- ── logo lockup · π in a broken circle, drawn as paths (no font dependency) ── -->
|
|
38
|
+
<g transform="translate(371,59) scale(1.2500)" fill="none" stroke="#a371f7" stroke-width="2.40" stroke-linecap="round" stroke-linejoin="round">
|
|
39
|
+
<path d="M30.18 4.98A20 20 0 1 0 41.66 14.61"/>
|
|
40
|
+
<path d="M13.5 17H34.5M19.5 17V32M28.5 17V32"/>
|
|
41
|
+
</g>
|
|
42
|
+
<text x="449" y="99" font-size="44" font-weight="700" fill="#e6edf3">pi-task</text>
|
|
43
|
+
|
|
44
|
+
<!-- ── headline ── -->
|
|
45
|
+
<text x="480" y="224" text-anchor="middle" font-size="82" font-weight="700" fill="#6e7681">Local models drift.</text>
|
|
46
|
+
<text x="480" y="319" text-anchor="middle" font-size="82" font-weight="700" fill="#e6edf3">pi-task doesn't trust</text>
|
|
47
|
+
<text x="480" y="414" text-anchor="middle" font-size="82" font-weight="700" fill="#e6edf3">one prompt.</text>
|
|
48
|
+
|
|
49
|
+
<!-- ── phase strip ── -->
|
|
50
|
+
<rect x="65" y="473" width="831" height="61" rx="30.5" fill="#161b22" fill-opacity="0.92" stroke="#30363d" stroke-width="1.5"/>
|
|
51
|
+
<g transform="translate(108,492.5) scale(0.9167)" stroke="#58a6ff">
|
|
52
|
+
<circle class="ic" cx="12" cy="12" r="10"/>
|
|
53
|
+
<path class="ic" d="M22 12h-4M6 12H2M12 6V2M12 22v-4"/>
|
|
54
|
+
<circle class="icf" cx="12" cy="12" r="2.4" fill="#58a6ff"/>
|
|
55
|
+
</g>
|
|
56
|
+
<text x="139" y="509.8" class="ph">refine</text>
|
|
57
|
+
<path class="arw" d="M195 503.5h17.2M206.6 496.8l6.7 6.7-6.7 6.7"/>
|
|
58
|
+
<g transform="translate(229,492.5) scale(0.9167)" stroke="#39c5cf">
|
|
59
|
+
<path class="ic" d="M9.56 9.83 15.44 6.67M10.1 12h4.79M9.56 14.17 15.44 17.33"/>
|
|
60
|
+
<circle class="icf" cx="5.5" cy="12" r="2.4" fill="#39c5cf"/>
|
|
61
|
+
<circle class="icf" cx="19.5" cy="4.5" r="2.1" fill="#39c5cf"/>
|
|
62
|
+
<circle class="icf" cx="19.5" cy="12" r="2.1" fill="#39c5cf"/>
|
|
63
|
+
<circle class="icf" cx="19.5" cy="19.5" r="2.1" fill="#39c5cf"/>
|
|
64
|
+
</g>
|
|
65
|
+
<text x="260" y="509.8" class="ph">research</text>
|
|
66
|
+
<path class="arw" d="M341 503.5h17.2M352.6 496.8l6.7 6.7-6.7 6.7"/>
|
|
67
|
+
<g transform="translate(375,492.5) scale(0.9167)" stroke="#d29922">
|
|
68
|
+
<path class="ic" d="M7.6 8.4a4.5 4.5 0 1 1 5.9 4.3c-.9.35-1.5 1.2-1.5 2.2v.6"/>
|
|
69
|
+
<circle class="icf" cx="12" cy="19.8" r="1.5" fill="#d29922"/>
|
|
70
|
+
</g>
|
|
71
|
+
<text x="406" y="509.8" class="ph">grill</text>
|
|
72
|
+
<path class="arw" d="M446 503.5h17.2M457.6 496.8l6.7 6.7-6.7 6.7"/>
|
|
73
|
+
<g transform="translate(480,492.5) scale(0.9167)" stroke="#3fb950">
|
|
74
|
+
<path class="ic" d="M12 2.5 22 7.5 12 12.5 2 7.5 12 2.5z"/>
|
|
75
|
+
<path class="ic" d="M2 16.5 12 21.5 22 16.5"/>
|
|
76
|
+
</g>
|
|
77
|
+
<text x="511" y="509.8" class="ph">compose</text>
|
|
78
|
+
<path class="arw" d="M597 503.5h17.2M608.5 496.8l6.7 6.7-6.7 6.7"/>
|
|
79
|
+
<g transform="translate(631,492.5) scale(0.9167)" stroke="#a371f7">
|
|
80
|
+
<path class="ic" d="M20.5 15a9 9 0 1 1-2.1-9.4L23 10"/>
|
|
81
|
+
<path class="ic" d="M23 4v6h-6"/>
|
|
82
|
+
</g>
|
|
83
|
+
<text x="662" y="509.8" class="ph">critique</text>
|
|
84
|
+
<path class="arw" d="M732 503.5h17.2M743.5 496.8l6.7 6.7-6.7 6.7"/>
|
|
85
|
+
<g transform="translate(766,492.5) scale(0.9167)" stroke="#3fb950">
|
|
86
|
+
<path class="ic" d="M14 2.5H6.5a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8z"/>
|
|
87
|
+
<path class="ic" d="M14 2.5V8h5.5"/>
|
|
88
|
+
<path class="ic" d="M8.5 13.5h7M8.5 17.5h7"/>
|
|
89
|
+
</g>
|
|
90
|
+
<text x="797" y="509.8" class="ph" font-weight="600" fill="#3fb950">a spec</text>
|
|
91
|
+
|
|
92
|
+
</svg>
|
|
@@ -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
|
|
@@ -492,17 +496,33 @@ export function buildGateDeps(params) {
|
|
|
492
496
|
contracts: () => readContracts(cwd2)
|
|
493
497
|
});
|
|
494
498
|
},
|
|
495
|
-
lintFix: (fixCtx, cwd2, taskTitle, failReason) =>
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
499
|
+
lintFix: async (fixCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
500
|
+
// Same frozen extraction the enforce guard and the verify rule-4b
|
|
501
|
+
// probe consume (mx5 run 12: the lint-fix child edited spec-frozen
|
|
502
|
+
// tsconfig.json because ESLint's own error text instructed it, then
|
|
503
|
+
// verify failed the TASK for that edit — the child must know the
|
|
504
|
+
// spec's do-not-touch list AND be mechanically denied it).
|
|
505
|
+
let frozenPaths = [];
|
|
506
|
+
try {
|
|
507
|
+
const { body } = await readTaskFile(cwd2, taskId);
|
|
508
|
+
frozenPaths = frozenPathsFromSpec(extractSpecForVerification(body));
|
|
504
509
|
}
|
|
505
|
-
|
|
510
|
+
catch {
|
|
511
|
+
// spec unreadable → no frozen paths; the guard degrades to a no-op
|
|
512
|
+
}
|
|
513
|
+
return runBoundedLintFix({
|
|
514
|
+
cwd: cwd2,
|
|
515
|
+
signal,
|
|
516
|
+
failReason,
|
|
517
|
+
runChild: makeGateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
|
|
518
|
+
repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
519
|
+
git: async (args) => {
|
|
520
|
+
const r = await git(cwd2, args, signal);
|
|
521
|
+
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
522
|
+
},
|
|
523
|
+
frozenPaths
|
|
524
|
+
});
|
|
525
|
+
},
|
|
506
526
|
// Deterministic static check + tree helpers for the enforce pre-commit gate.
|
|
507
527
|
repoHealth: cwd2 => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
508
528
|
dirty: async (cwd2) => {
|
package/dist/task/lint-fix.d.ts
CHANGED
|
@@ -1,33 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lint-fix — the bounded, graduated resolution for a repo-health verify FAIL.
|
|
3
|
-
*
|
|
4
|
-
* The failure this closes (mx5 run 3, TASK_0017): AUTOFIX's only hammer is a FULL
|
|
5
|
-
* implementation re-run. For a repo-health FAIL of 10 trivial lint findings the live
|
|
6
|
-
* run burned two 36–56-minute impl turns, each REGENERATING a fresh 900-line rewrite
|
|
7
|
-
* that failed lint differently — the loop cannot converge because the tool is bigger
|
|
8
|
-
* than the defect. Validated live on the real TASK_0017 tree: a bounded fix child
|
|
9
|
-
* (read,edit,bash) reached lint-clean in 64s and 106s, 2/2.
|
|
10
|
-
*
|
|
11
|
-
* The same validation caught the design's failure mode: BOTH runs cheated, running
|
|
12
|
-
* `git checkout -- src/test/request.ts` — REVERTING the task's uncommitted work to
|
|
13
|
-
* make the findings vanish. So this pass ships with a deterministic REVERT-GUARD,
|
|
14
|
-
* outcome-based (command filtering can't catch every path to the same effect):
|
|
15
|
-
*
|
|
16
|
-
* - Before the child runs: snapshot the full working state (`git add -A` +
|
|
17
|
-
* `git write-tree`, then unstage) and record which files differ from HEAD.
|
|
18
|
-
* - After: any pre-existing work file now byte-identical to HEAD means the child
|
|
19
|
-
* discarded work instead of fixing it → restore the snapshot, report not-applied.
|
|
20
|
-
* - Converge check: the injected repoHealth must pass; otherwise not-applied.
|
|
21
|
-
*
|
|
22
|
-
* A file whose ENTIRE pre-fix diff was the lint finding (fixing it legitimately
|
|
23
|
-
* restores HEAD) trips the guard conservatively — the fix is discarded and the
|
|
24
|
-
* ordinary AUTOFIX picker takes over. Safe direction: the guard may only cost time,
|
|
25
|
-
* never work.
|
|
26
|
-
*
|
|
27
|
-
* Not-applied is never terminal: the caller falls through to the existing
|
|
28
|
-
* recommend → AUTOFIX/ACCEPT/dismiss picker, so this pass can only make the loop
|
|
29
|
-
* faster, never change what it can decide.
|
|
30
|
-
*/
|
|
31
1
|
export interface LintFixResult {
|
|
32
2
|
/** true → findings fixed, repo health passes, work preserved. */
|
|
33
3
|
ok: boolean;
|
|
@@ -41,16 +11,26 @@ export interface LintFixDeps {
|
|
|
41
11
|
failReason: string;
|
|
42
12
|
/** Run the fix child; same closure shape the other gate children use. */
|
|
43
13
|
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
44
|
-
/** 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. */
|
|
45
17
|
repoHealth: () => Promise<{
|
|
46
18
|
ok: boolean;
|
|
47
19
|
reason: string;
|
|
20
|
+
output?: string;
|
|
48
21
|
}>;
|
|
49
22
|
/** Run git in cwd; injected so the guard logic is unit-testable. */
|
|
50
23
|
git: (args: string[]) => Promise<{
|
|
51
24
|
exitCode: number;
|
|
52
25
|
stdout: string;
|
|
53
26
|
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Paths the task's spec forbids modifying (frozenPathsFromSpec over the same
|
|
29
|
+
* composed spec verify judges). Injected into the child's prompt AND enforced
|
|
30
|
+
* deterministically post-child: a frozen path the child changed is reverted
|
|
31
|
+
* and the fix reported not-applied. Absent/empty → no-op, prior behavior.
|
|
32
|
+
*/
|
|
33
|
+
frozenPaths?: string[];
|
|
54
34
|
}
|
|
55
35
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
56
36
|
export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
|
@@ -60,14 +40,15 @@ export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
|
|
60
40
|
* an explicit ban on discarding work (both validation runs reached green via
|
|
61
41
|
* `git checkout` of the work file until the guard existed).
|
|
62
42
|
*/
|
|
63
|
-
export declare function buildLintFixPrompt(failReason: string): string;
|
|
43
|
+
export declare function buildLintFixPrompt(failReason: string, frozenPaths?: string[]): string;
|
|
64
44
|
/**
|
|
65
45
|
* Pure guard core: which pre-existing work files did the fix pass revert to HEAD?
|
|
66
46
|
* `preDirty` = files differing from HEAD before the fix; `stillDirty` = after.
|
|
67
47
|
*/
|
|
68
48
|
export declare function revertGuardViolations(preDirty: string[], stillDirty: Set<string>): string[];
|
|
69
49
|
/**
|
|
70
|
-
* Run the bounded fix: snapshot → child → revert-guard →
|
|
71
|
-
* throws for an outcome; only a child-level user cancel
|
|
50
|
+
* Run the bounded fix: snapshot → child → revert-guard → frozen-path guard →
|
|
51
|
+
* converge check. Never throws for an outcome; only a child-level user cancel
|
|
52
|
+
* propagates from runChild.
|
|
72
53
|
*/
|
|
73
54
|
export declare function runBoundedLintFix(deps: LintFixDeps): Promise<LintFixResult>;
|
package/dist/task/lint-fix.js
CHANGED
|
@@ -27,7 +27,24 @@
|
|
|
27
27
|
* Not-applied is never terminal: the caller falls through to the existing
|
|
28
28
|
* recommend → AUTOFIX/ACCEPT/dismiss picker, so this pass can only make the loop
|
|
29
29
|
* faster, never change what it can decide.
|
|
30
|
+
*
|
|
31
|
+
* FROZEN-PATH GUARD (mx5 run 12, TASK_0021/0022): the checker's own error text
|
|
32
|
+
* can INSTRUCT an edit to a spec-frozen file (typed ESLint: "playwright/index.ts
|
|
33
|
+
* was not found by the project … Consider either including it in the
|
|
34
|
+
* tsconfig.json") and this child complies — while the task's spec froze
|
|
35
|
+
* `tsconfig.json` and verify's rule-4b prohibition probe then fails the TASK for
|
|
36
|
+
* the gate child's edit. Two gates, contradictory rules, same file; the loop
|
|
37
|
+
* never converges (live: TASK_0021 burned all three unattended AUTOFIX rounds).
|
|
38
|
+
* Prompt framing alone is A/B-proven ~0–1/5 on the weak model (see
|
|
39
|
+
* frozen-path-guard.ts), so the deny is mechanical: the spec's frozen paths are
|
|
40
|
+
* threaded in via `frozenPaths`, injected into the prompt as a do-not-touch list
|
|
41
|
+
* (belt), and any frozen path the child still changed is deterministically
|
|
42
|
+
* reverted post-child and the fix reported not-applied (suspenders). Only paths
|
|
43
|
+
* that were CLEAN before the child ran are reverted — a frozen path already
|
|
44
|
+
* dirty with (possibly task) work is left alone, in the guard's safe direction:
|
|
45
|
+
* cost time, never work.
|
|
30
46
|
*/
|
|
47
|
+
import { parseChangedFrozenFiles, pathNamedIn, revertFrozenPaths } from './frozen-path-guard.js';
|
|
31
48
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
32
49
|
export const LINT_FIX_TOOLS = 'read,edit,bash';
|
|
33
50
|
/**
|
|
@@ -36,7 +53,20 @@ export const LINT_FIX_TOOLS = 'read,edit,bash';
|
|
|
36
53
|
* an explicit ban on discarding work (both validation runs reached green via
|
|
37
54
|
* `git checkout` of the work file until the guard existed).
|
|
38
55
|
*/
|
|
39
|
-
export function buildLintFixPrompt(failReason) {
|
|
56
|
+
export function buildLintFixPrompt(failReason, frozenPaths = []) {
|
|
57
|
+
const frozenBlock = frozenPaths.length === 0 ?
|
|
58
|
+
[]
|
|
59
|
+
: [
|
|
60
|
+
'3b. HARD CONSTRAINT — SPEC-FROZEN PATHS. The task spec forbids modifying:',
|
|
61
|
+
...frozenPaths.map(p => ` - ${p}`),
|
|
62
|
+
' You must NOT edit, create, or delete anything at or under these paths,',
|
|
63
|
+
" EVEN IF the checker's own error message suggests exactly that fix",
|
|
64
|
+
' (e.g. "consider including it in the tsconfig.json"). Any change you make',
|
|
65
|
+
' to a frozen path is detected and reverted, and the whole fix is rejected.',
|
|
66
|
+
' If the check cannot pass without touching a frozen path, STOP and report',
|
|
67
|
+
' LINT-FIX: BLOCKED with the reason.',
|
|
68
|
+
''
|
|
69
|
+
];
|
|
40
70
|
return [
|
|
41
71
|
'You are a bounded static-analysis fix pass. A verification gate just failed',
|
|
42
72
|
`with: ${failReason}`,
|
|
@@ -59,6 +89,7 @@ export function buildLintFixPrompt(failReason) {
|
|
|
59
89
|
' Reverting the work would make the findings vanish — that is destroying the',
|
|
60
90
|
' task, not fixing it, and it is detected and rejected.',
|
|
61
91
|
'',
|
|
92
|
+
...frozenBlock,
|
|
62
93
|
'4. Re-run the check after editing and confirm it exits 0.',
|
|
63
94
|
'',
|
|
64
95
|
'End with exactly one line:',
|
|
@@ -91,8 +122,21 @@ async function dirtyFiles(deps) {
|
|
|
91
122
|
.filter(l => l.length > 0);
|
|
92
123
|
}
|
|
93
124
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
125
|
+
* Which spec-frozen paths currently show a change in `git status`? Null when git
|
|
126
|
+
* itself failed — inconclusive, not evidence (same discipline as dirtyFiles).
|
|
127
|
+
*/
|
|
128
|
+
async function frozenDirtySet(deps, frozen) {
|
|
129
|
+
if (frozen.length === 0)
|
|
130
|
+
return new Set();
|
|
131
|
+
const r = await deps.git(['status', '--porcelain', '--', ...frozen]);
|
|
132
|
+
if (r.exitCode !== 0)
|
|
133
|
+
return null;
|
|
134
|
+
return new Set(parseChangedFrozenFiles(r.stdout));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run the bounded fix: snapshot → child → revert-guard → frozen-path guard →
|
|
138
|
+
* converge check. Never throws for an outcome; only a child-level user cancel
|
|
139
|
+
* propagates from runChild.
|
|
96
140
|
*/
|
|
97
141
|
export async function runBoundedLintFix(deps) {
|
|
98
142
|
// Snapshot the full working state as a tree object (includes untracked files),
|
|
@@ -122,8 +166,14 @@ export async function runBoundedLintFix(deps) {
|
|
|
122
166
|
snapshot = wt.stdout.trim();
|
|
123
167
|
await deps.git(['reset']);
|
|
124
168
|
}
|
|
169
|
+
// FROZEN-PATH baseline: frozen paths already dirty BEFORE the child ran carry
|
|
170
|
+
// (possibly the task's own) work — the guard must never revert those, only
|
|
171
|
+
// changes the CHILD introduces on top of a clean frozen path. A git failure
|
|
172
|
+
// here disables the guard for this run (inconclusive ≠ license to revert).
|
|
173
|
+
const frozen = deps.frozenPaths ?? [];
|
|
174
|
+
const preFrozenDirty = await frozenDirtySet(deps, frozen);
|
|
125
175
|
try {
|
|
126
|
-
await deps.runChild(LINT_FIX_TOOLS, buildLintFixPrompt(deps.failReason), deps.signal);
|
|
176
|
+
await deps.runChild(LINT_FIX_TOOLS, buildLintFixPrompt(deps.failReason, frozen), deps.signal);
|
|
127
177
|
}
|
|
128
178
|
catch (err) {
|
|
129
179
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -174,8 +224,52 @@ export async function runBoundedLintFix(deps) {
|
|
|
174
224
|
+ `${violations.length > 3 ? ', …' : ''}) — fix ${snapshot ? 'rolled back' : 'REJECTED but no snapshot to restore'}`
|
|
175
225
|
};
|
|
176
226
|
}
|
|
227
|
+
// FROZEN-PATH GUARD: a frozen path that was clean pre-child and is changed
|
|
228
|
+
// now is the child's edit — the exact write verify's rule-4b prohibition
|
|
229
|
+
// probe is guaranteed to fail the TASK for (mx5 run 12: ESLint's own error
|
|
230
|
+
// text instructed the tsconfig.json edit and the child complied, 5/5 in the
|
|
231
|
+
// live A/B). Revert JUST those paths (they were clean, so HEAD == pre-state:
|
|
232
|
+
// no work can be lost) and report not-applied so the caller falls through to
|
|
233
|
+
// the ordinary recommend → AUTOFIX/ACCEPT picker. Inconclusive git (either
|
|
234
|
+
// side) → guard steps aside; the verify probe still catches what survives.
|
|
235
|
+
if (preFrozenDirty !== null) {
|
|
236
|
+
const postFrozenDirty = await frozenDirtySet(deps, frozen);
|
|
237
|
+
if (postFrozenDirty !== null) {
|
|
238
|
+
const frozenViolations = [...postFrozenDirty].filter(f => !preFrozenDirty.has(f));
|
|
239
|
+
if (frozenViolations.length > 0) {
|
|
240
|
+
const reverted = await revertFrozenPaths(frozenViolations, deps.git);
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: `frozen-path: fix child modified spec-frozen path(s) `
|
|
244
|
+
+ `(${frozenViolations.slice(0, 3).join(', ')}`
|
|
245
|
+
+ `${frozenViolations.length > 3 ? ', …' : ''}) — `
|
|
246
|
+
+ `${reverted.length > 0 ? 'reverted' : 'REJECTED (revert found nothing to undo)'}; `
|
|
247
|
+
+ `the static findings need a fix that respects the spec's constraints`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
177
252
|
const health = await deps.repoHealth();
|
|
178
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
|
+
}
|
|
179
273
|
return { ok: false, reason: `did not converge: ${health.reason}` };
|
|
180
274
|
}
|
|
181
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,
|
|
@@ -87,7 +87,7 @@ export interface GateDeps {
|
|
|
87
87
|
* at most once per gate sequence; not-applied falls through to the picker.
|
|
88
88
|
* Absent → the loop goes straight to recommend/picker as before.
|
|
89
89
|
*/
|
|
90
|
-
lintFix?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, failReason: string) => Promise<{
|
|
90
|
+
lintFix?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string, failReason: string) => Promise<{
|
|
91
91
|
ok: boolean;
|
|
92
92
|
reason?: string;
|
|
93
93
|
}>;
|
|
@@ -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
|
|
@@ -98,7 +107,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
98
107
|
if (!lintFixAttempted && deps.lintFix && failReason.startsWith('repo health:')) {
|
|
99
108
|
lintFixAttempted = true;
|
|
100
109
|
active.ui.notify(`${p.tag}: static findings on "${p.title}" — attempting bounded lint fix…`, 'info');
|
|
101
|
-
const fix = await deps.lintFix(active, p.cwd, p.title, failReason);
|
|
110
|
+
const fix = await deps.lintFix(active, p.cwd, p.title, p.taskId, failReason);
|
|
102
111
|
await rec(`lint-fix: ${fix.ok ?
|
|
103
112
|
`applied${fix.reason ? ` (${fix.reason})` : ''} — re-verifying`
|
|
104
113
|
: `not applied (${fix.reason ?? 'failed'})`}`);
|
|
@@ -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",
|
|
@@ -85,6 +85,6 @@
|
|
|
85
85
|
"extensions": [
|
|
86
86
|
"dist/index.js"
|
|
87
87
|
],
|
|
88
|
-
"image": "https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/
|
|
88
|
+
"image": "https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/hero.png"
|
|
89
89
|
}
|
|
90
90
|
}
|