@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- ![pi-task pipeline: a /task request runs through refine, research, grill, compose and critique, then the final spec is delivered to your main pi session in the same chat. Every phase boundary is persisted to .pi-tasks/TASK_NNNN.md, so the task is crash-safe and resumable.](https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/pipeline.svg)
3
+ ![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.](https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/hero.svg)
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
+ ![pi-task pipeline: a /task request runs through refine, research, grill, compose and critique, then the final spec is delivered to your main pi session in the same chat. Every phase boundary is persisted to .pi-tasks/TASK_NNNN.md, so the task is crash-safe and resumable.](https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/pipeline.svg)
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. |
Binary file
@@ -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&#39;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>
@@ -492,17 +492,33 @@ export function buildGateDeps(params) {
492
492
  contracts: () => readContracts(cwd2)
493
493
  });
494
494
  },
495
- lintFix: (fixCtx, cwd2, taskTitle, failReason) => runBoundedLintFix({
496
- cwd: cwd2,
497
- signal,
498
- failReason,
499
- runChild: makeGateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
500
- repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2)),
501
- git: async (args) => {
502
- const r = await git(cwd2, args, signal);
503
- return { exitCode: r.exitCode, stdout: r.stdout };
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) => {
@@ -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 → converge check. Never
71
- * throws for an outcome; only a child-level user cancel propagates from runChild.
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>;
@@ -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
- * Run the bounded fix: snapshot → child → revert-guard → converge check. Never
95
- * throws for an outcome; only a child-level user cancel propagates from runChild.
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
  }>;
@@ -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.20",
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/pipeline.png"
88
+ "image": "https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/hero.png"
89
89
  }
90
90
  }