@mjasnikovs/pi-task 0.18.20 → 0.18.21
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/gate-deps.js +26 -10
- package/dist/task/lint-fix.d.ts +11 -33
- package/dist/task/lint-fix.js +79 -4
- package/dist/task/task-gates.d.ts +1 -1
- package/dist/task/task-gates.js +1 -1
- 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>
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -492,17 +492,33 @@ export function buildGateDeps(params) {
|
|
|
492
492
|
contracts: () => readContracts(cwd2)
|
|
493
493
|
});
|
|
494
494
|
},
|
|
495
|
-
lintFix: (fixCtx, cwd2, taskTitle, failReason) =>
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
495
|
+
lintFix: async (fixCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
496
|
+
// Same frozen extraction the enforce guard and the verify rule-4b
|
|
497
|
+
// probe consume (mx5 run 12: the lint-fix child edited spec-frozen
|
|
498
|
+
// tsconfig.json because ESLint's own error text instructed it, then
|
|
499
|
+
// verify failed the TASK for that edit — the child must know the
|
|
500
|
+
// spec's do-not-touch list AND be mechanically denied it).
|
|
501
|
+
let frozenPaths = [];
|
|
502
|
+
try {
|
|
503
|
+
const { body } = await readTaskFile(cwd2, taskId);
|
|
504
|
+
frozenPaths = frozenPathsFromSpec(extractSpecForVerification(body));
|
|
504
505
|
}
|
|
505
|
-
|
|
506
|
+
catch {
|
|
507
|
+
// spec unreadable → no frozen paths; the guard degrades to a no-op
|
|
508
|
+
}
|
|
509
|
+
return runBoundedLintFix({
|
|
510
|
+
cwd: cwd2,
|
|
511
|
+
signal,
|
|
512
|
+
failReason,
|
|
513
|
+
runChild: makeGateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
|
|
514
|
+
repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
515
|
+
git: async (args) => {
|
|
516
|
+
const r = await git(cwd2, args, signal);
|
|
517
|
+
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
518
|
+
},
|
|
519
|
+
frozenPaths
|
|
520
|
+
});
|
|
521
|
+
},
|
|
506
522
|
// Deterministic static check + tree helpers for the enforce pre-commit gate.
|
|
507
523
|
repoHealth: cwd2 => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
508
524
|
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;
|
|
@@ -51,6 +21,13 @@ export interface LintFixDeps {
|
|
|
51
21
|
exitCode: number;
|
|
52
22
|
stdout: string;
|
|
53
23
|
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Paths the task's spec forbids modifying (frozenPathsFromSpec over the same
|
|
26
|
+
* composed spec verify judges). Injected into the child's prompt AND enforced
|
|
27
|
+
* deterministically post-child: a frozen path the child changed is reverted
|
|
28
|
+
* and the fix reported not-applied. Absent/empty → no-op, prior behavior.
|
|
29
|
+
*/
|
|
30
|
+
frozenPaths?: string[];
|
|
54
31
|
}
|
|
55
32
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
56
33
|
export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
|
@@ -60,14 +37,15 @@ export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
|
|
60
37
|
* an explicit ban on discarding work (both validation runs reached green via
|
|
61
38
|
* `git checkout` of the work file until the guard existed).
|
|
62
39
|
*/
|
|
63
|
-
export declare function buildLintFixPrompt(failReason: string): string;
|
|
40
|
+
export declare function buildLintFixPrompt(failReason: string, frozenPaths?: string[]): string;
|
|
64
41
|
/**
|
|
65
42
|
* Pure guard core: which pre-existing work files did the fix pass revert to HEAD?
|
|
66
43
|
* `preDirty` = files differing from HEAD before the fix; `stillDirty` = after.
|
|
67
44
|
*/
|
|
68
45
|
export declare function revertGuardViolations(preDirty: string[], stillDirty: Set<string>): string[];
|
|
69
46
|
/**
|
|
70
|
-
* Run the bounded fix: snapshot → child → revert-guard →
|
|
71
|
-
* throws for an outcome; only a child-level user cancel
|
|
47
|
+
* Run the bounded fix: snapshot → child → revert-guard → frozen-path guard →
|
|
48
|
+
* converge check. Never throws for an outcome; only a child-level user cancel
|
|
49
|
+
* propagates from runChild.
|
|
72
50
|
*/
|
|
73
51
|
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, 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,6 +224,31 @@ 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) {
|
|
179
254
|
return { ok: false, reason: `did not converge: ${health.reason}` };
|
|
@@ -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
|
}>;
|
package/dist/task/task-gates.js
CHANGED
|
@@ -98,7 +98,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
98
98
|
if (!lintFixAttempted && deps.lintFix && failReason.startsWith('repo health:')) {
|
|
99
99
|
lintFixAttempted = true;
|
|
100
100
|
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);
|
|
101
|
+
const fix = await deps.lintFix(active, p.cwd, p.title, p.taskId, failReason);
|
|
102
102
|
await rec(`lint-fix: ${fix.ok ?
|
|
103
103
|
`applied${fix.reason ? ` (${fix.reason})` : ''} — re-verifying`
|
|
104
104
|
: `not applied (${fix.reason ?? 'failed'})`}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.21",
|
|
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
|
}
|