@mjasnikovs/pi-task 0.14.2 → 0.14.3
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.
|
@@ -315,9 +315,9 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
315
315
|
signal,
|
|
316
316
|
// Run the worker child UNGUARDED: no loop detector, no wall-clock
|
|
317
317
|
// timeout. This pass's job is to rework files in place until every
|
|
318
|
-
// violation is fixed, and it legitimately reads
|
|
319
|
-
//
|
|
320
|
-
//
|
|
318
|
+
// violation is fixed, and it legitimately reads and edits the same
|
|
319
|
+
// file many times — the research-worker guards mislabel that as a
|
|
320
|
+
// runaway and kill good work (proven on mx5 TASK_0002). Let it
|
|
321
321
|
// run until the model finishes; classifyEnforceChildFailure still
|
|
322
322
|
// blocks the commit on a real failure (non-zero exit, leaked tool
|
|
323
323
|
// call) or a user cancel.
|
|
@@ -366,7 +366,7 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
366
366
|
// disables the path-revisit heuristic, so revisiting one
|
|
367
367
|
// file (which IS this pass's job) never trips — only a
|
|
368
368
|
// literally-identical call repeated past threshold does
|
|
369
|
-
// (e.g. the same
|
|
369
|
+
// (e.g. the same read or edit repeated hundreds of times).
|
|
370
370
|
// A hit nudges via the normal restart-with-hint; a loop
|
|
371
371
|
// that survives the nudges is WARNED, not blocked (see
|
|
372
372
|
// r.loopHit handling below and classifyEnforceChildFailure).
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
2
2
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
3
3
|
export declare const GUIDELINE_FILENAMES: readonly ["AGENTS.md", "CLAUDE.md"];
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The fix pass gets exactly two tools: `read` and `edit`. Deliberately no
|
|
6
|
+
* `write`, `grep`, `find`, or `ls`.
|
|
7
|
+
*
|
|
8
|
+
* - No `write` (and `edit` ENOENTs on a missing path) → the pass cannot create
|
|
9
|
+
* new files. Without this the local model, unable to run lint/tsc/tests, wrote
|
|
10
|
+
* hundreds of scratch "runner" scripts hoping to execute them — 864 junk files
|
|
11
|
+
* in one real run (see enforce-debug.log analysis). This is the load-bearing
|
|
12
|
+
* removal: dropping `write` is what stops the runaway file creation.
|
|
13
|
+
* - No `grep`/`find`/`ls` → the pass cannot enumerate or roam the tree; it is
|
|
14
|
+
* handed the explicit, closed list of changed files (see captureDiff) and is
|
|
15
|
+
* told to edit only those.
|
|
16
|
+
* - `read` IS allowed: the model genuinely needs it — to re-read a file after
|
|
17
|
+
* editing to confirm the fix landed, and to read a neighbouring file for the
|
|
18
|
+
* correct import/type when bringing the change into compliance (without it the
|
|
19
|
+
* model fabricates imports — validated against the local model). `read` has no
|
|
20
|
+
* per-path sandbox in pi, so "only the changed files" is enforced for EDITS
|
|
21
|
+
* (the prompt scopes them) but is a soft instruction for READS.
|
|
22
|
+
*/
|
|
23
|
+
declare const ENFORCE_TOOLS = "read,edit";
|
|
6
24
|
export interface GuidelineDoc {
|
|
7
25
|
/** Filenames found, in discovery order (e.g. ['AGENTS.md', 'CLAUDE.md']). */
|
|
8
26
|
files: string[];
|
|
@@ -70,17 +88,26 @@ export interface EnforceChildResult {
|
|
|
70
88
|
*/
|
|
71
89
|
export declare function classifyEnforceChildFailure(r: EnforceChildResult): string | null;
|
|
72
90
|
/**
|
|
73
|
-
* Capture the work to verify as text: the tracked diff against HEAD
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
91
|
+
* Capture the work to verify as text: the tracked diff against HEAD followed by
|
|
92
|
+
* the COMPLETE list of changed files — tracked-modified ∪ new (untracked) — that
|
|
93
|
+
* the child should read and is allowed to edit.
|
|
94
|
+
*
|
|
95
|
+
* The child has a `read` tool, so file contents are NOT inlined here: it reads
|
|
96
|
+
* each listed file itself (and re-reads after editing to confirm). The list is the
|
|
97
|
+
* scoping signal — it tells the model the closed set of files to touch so it edits
|
|
98
|
+
* only the change set rather than roaming. Validated against the local model:
|
|
99
|
+
* inlining full bodies bought nothing (it read the files by name regardless).
|
|
100
|
+
*
|
|
101
|
+
* Non-destructive — it does not touch the index, so the later `git add -A` in
|
|
102
|
+
* gitCommitAll still stages everything (including fixes the enforcement child
|
|
103
|
+
* makes).
|
|
77
104
|
*
|
|
78
|
-
* The `.pi-tasks/` directory is excluded from
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
105
|
+
* The `.pi-tasks/` directory is excluded from every git command. Those task files
|
|
106
|
+
* are committable (tracked) by design, so they show up in `git diff HEAD` and
|
|
107
|
+
* `git ls-files --others` — git does not honor the fd/ripgrep `.ignore` that keeps
|
|
108
|
+
* them out of the worker's find/grep discovery. Without this pathspec the enforce
|
|
109
|
+
* child is handed its own TASK_*.md / TASK_AUTO_*.md bookkeeping as "changes to
|
|
110
|
+
* verify" and edits them, corrupting the front matter mid-run.
|
|
84
111
|
*/
|
|
85
112
|
export declare function captureDiff(cwd: string, signal?: AbortSignal, spawnFn?: SpawnFn): Promise<string>;
|
|
86
113
|
export interface EnforcementDeps {
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* when those files sit right next to the code. After a /task-auto sub-task's
|
|
6
6
|
* implementation turn settles — but BEFORE its per-task commit — this runs a
|
|
7
7
|
* fresh child pi (the same local model that did the work) that is handed the
|
|
8
|
-
* project's guideline files
|
|
9
|
-
*
|
|
8
|
+
* project's guideline files, the task's diff, and the list of changed files. With
|
|
9
|
+
* read + edit tools (and nothing else) it reads those files, fixes any violations
|
|
10
|
+
* in place, and reports a CLEAN / VIOLATION verdict. It cannot create files or run
|
|
11
|
+
* anything: no `write` (so it can't scaffold junk) and no `grep`/`find`/`ls` (so it
|
|
12
|
+
* can't roam the tree) — see ENFORCE_TOOLS.
|
|
10
13
|
*
|
|
11
14
|
* A VIOLATION (or an enforcement pass that cannot confirm CLEAN) blocks the
|
|
12
15
|
* commit and fails the task, so non-compliant work is never snapshotted — the
|
|
@@ -23,8 +26,26 @@ import { USER_CANCELLED } from './child-runner.js';
|
|
|
23
26
|
import { TASKS_DIR_NAME } from './task-types.js';
|
|
24
27
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
25
28
|
export const GUIDELINE_FILENAMES = ['AGENTS.md', 'CLAUDE.md'];
|
|
26
|
-
/**
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The fix pass gets exactly two tools: `read` and `edit`. Deliberately no
|
|
31
|
+
* `write`, `grep`, `find`, or `ls`.
|
|
32
|
+
*
|
|
33
|
+
* - No `write` (and `edit` ENOENTs on a missing path) → the pass cannot create
|
|
34
|
+
* new files. Without this the local model, unable to run lint/tsc/tests, wrote
|
|
35
|
+
* hundreds of scratch "runner" scripts hoping to execute them — 864 junk files
|
|
36
|
+
* in one real run (see enforce-debug.log analysis). This is the load-bearing
|
|
37
|
+
* removal: dropping `write` is what stops the runaway file creation.
|
|
38
|
+
* - No `grep`/`find`/`ls` → the pass cannot enumerate or roam the tree; it is
|
|
39
|
+
* handed the explicit, closed list of changed files (see captureDiff) and is
|
|
40
|
+
* told to edit only those.
|
|
41
|
+
* - `read` IS allowed: the model genuinely needs it — to re-read a file after
|
|
42
|
+
* editing to confirm the fix landed, and to read a neighbouring file for the
|
|
43
|
+
* correct import/type when bringing the change into compliance (without it the
|
|
44
|
+
* model fabricates imports — validated against the local model). `read` has no
|
|
45
|
+
* per-path sandbox in pi, so "only the changed files" is enforced for EDITS
|
|
46
|
+
* (the prompt scopes them) but is a soft instruction for READS.
|
|
47
|
+
*/
|
|
48
|
+
const ENFORCE_TOOLS = 'read,edit';
|
|
28
49
|
/**
|
|
29
50
|
* Read the guideline files that live directly in `cwd` (AGENTS.md, CLAUDE.md).
|
|
30
51
|
* Returns null when none exist or all are empty — the caller treats that as a
|
|
@@ -59,18 +80,25 @@ export function buildEnforcePrompt(rulesText, diff) {
|
|
|
59
80
|
return [
|
|
60
81
|
'You are a strict guideline-enforcement pass running after an AI coding agent',
|
|
61
82
|
'finished a task but before its work is committed. The agent is known to skip',
|
|
62
|
-
'project rules, so do not trust that it followed them — verify against the
|
|
83
|
+
'project rules, so do not trust that it followed them — verify against the changes.',
|
|
84
|
+
'',
|
|
85
|
+
'You have a `read` tool and an `edit` tool — nothing else. You CANNOT run',
|
|
86
|
+
'commands, run lint/tsc/tests, or create files. Read the changed files listed',
|
|
87
|
+
'below to inspect them; you may also read a neighbouring file when you need the',
|
|
88
|
+
'correct import or type. But EDIT ONLY the changed files listed below — do not',
|
|
89
|
+
'create new files and do not modify anything outside that set.',
|
|
63
90
|
'',
|
|
64
91
|
'PROJECT GUIDELINES (from this repository):',
|
|
65
92
|
rulesText,
|
|
66
93
|
'',
|
|
67
|
-
'CHANGES JUST MADE (verify these specifically
|
|
68
|
-
diff.trim().length > 0 ? diff : '(no textual diff captured —
|
|
94
|
+
'CHANGES JUST MADE (verify these specifically against the rules):',
|
|
95
|
+
diff.trim().length > 0 ? diff : '(no textual diff captured — nothing to verify)',
|
|
69
96
|
'',
|
|
70
97
|
'Your job:',
|
|
71
|
-
'1.
|
|
72
|
-
'2. For each violation you find, FIX it directly
|
|
73
|
-
'
|
|
98
|
+
'1. Read each changed file and check it against EVERY rule above.',
|
|
99
|
+
'2. For each violation you find, FIX it directly with your `edit` tool, then',
|
|
100
|
+
' re-read the file to confirm the fix landed. Make the minimal change that',
|
|
101
|
+
' satisfies the rule; do not rewrite unrelated code.',
|
|
74
102
|
'3. Do not introduce new behavior or features — only bring the work into compliance.',
|
|
75
103
|
'',
|
|
76
104
|
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
@@ -128,17 +156,26 @@ export function classifyEnforceChildFailure(r) {
|
|
|
128
156
|
return null;
|
|
129
157
|
}
|
|
130
158
|
/**
|
|
131
|
-
* Capture the work to verify as text: the tracked diff against HEAD
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
159
|
+
* Capture the work to verify as text: the tracked diff against HEAD followed by
|
|
160
|
+
* the COMPLETE list of changed files — tracked-modified ∪ new (untracked) — that
|
|
161
|
+
* the child should read and is allowed to edit.
|
|
162
|
+
*
|
|
163
|
+
* The child has a `read` tool, so file contents are NOT inlined here: it reads
|
|
164
|
+
* each listed file itself (and re-reads after editing to confirm). The list is the
|
|
165
|
+
* scoping signal — it tells the model the closed set of files to touch so it edits
|
|
166
|
+
* only the change set rather than roaming. Validated against the local model:
|
|
167
|
+
* inlining full bodies bought nothing (it read the files by name regardless).
|
|
168
|
+
*
|
|
169
|
+
* Non-destructive — it does not touch the index, so the later `git add -A` in
|
|
170
|
+
* gitCommitAll still stages everything (including fixes the enforcement child
|
|
171
|
+
* makes).
|
|
135
172
|
*
|
|
136
|
-
* The `.pi-tasks/` directory is excluded from
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
173
|
+
* The `.pi-tasks/` directory is excluded from every git command. Those task files
|
|
174
|
+
* are committable (tracked) by design, so they show up in `git diff HEAD` and
|
|
175
|
+
* `git ls-files --others` — git does not honor the fd/ripgrep `.ignore` that keeps
|
|
176
|
+
* them out of the worker's find/grep discovery. Without this pathspec the enforce
|
|
177
|
+
* child is handed its own TASK_*.md / TASK_AUTO_*.md bookkeeping as "changes to
|
|
178
|
+
* verify" and edits them, corrupting the front matter mid-run.
|
|
142
179
|
*/
|
|
143
180
|
export async function captureDiff(cwd, signal, spawnFn) {
|
|
144
181
|
// `:(exclude)<dir>` is a git pathspec that drops everything under the tasks
|
|
@@ -146,6 +183,7 @@ export async function captureDiff(cwd, signal, spawnFn) {
|
|
|
146
183
|
const excludeTasks = `:(exclude)${TASKS_DIR_NAME}`;
|
|
147
184
|
const run = (args) => runChildDefault({ command: 'git', args }, cwd, signal, { mode: 'text' }, spawnFn);
|
|
148
185
|
const tracked = await run(['diff', 'HEAD', '--', '.', excludeTasks]);
|
|
186
|
+
const trackedNames = await run(['diff', 'HEAD', '--name-only', '--', '.', excludeTasks]);
|
|
149
187
|
const untracked = await run([
|
|
150
188
|
'ls-files',
|
|
151
189
|
'--others',
|
|
@@ -156,10 +194,28 @@ export async function captureDiff(cwd, signal, spawnFn) {
|
|
|
156
194
|
]);
|
|
157
195
|
const parts = [];
|
|
158
196
|
if (tracked.exitCode === 0 && tracked.stdout.trim().length > 0)
|
|
159
|
-
parts.push(tracked.stdout.trim());
|
|
160
|
-
const
|
|
197
|
+
parts.push(`UNIFIED DIFF (modified files):\n${tracked.stdout.trim()}`);
|
|
198
|
+
const lines = (out) => out.exitCode === 0 ?
|
|
199
|
+
out.stdout
|
|
200
|
+
.split('\n')
|
|
201
|
+
.map(s => s.trim())
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
: [];
|
|
204
|
+
const modified = lines(trackedNames).sort();
|
|
205
|
+
// Untracked-NEW files do NOT appear in the diff above, so they are listed in
|
|
206
|
+
// their own emphatic section: in a flat list the model overlooks them and then
|
|
207
|
+
// emits a false CLEAN while a new file still violates the rules (observed on the
|
|
208
|
+
// local model). A diff-only file (already in `modified`) is not repeated here.
|
|
209
|
+
const newFiles = lines(untracked)
|
|
210
|
+
.filter(n => !modified.includes(n))
|
|
211
|
+
.sort();
|
|
212
|
+
if (modified.length > 0)
|
|
213
|
+
parts.push('MODIFIED FILES — read each in full and check it against the rules:\n'
|
|
214
|
+
+ modified.map(n => `- ${n}`).join('\n'));
|
|
161
215
|
if (newFiles.length > 0)
|
|
162
|
-
parts.push(
|
|
216
|
+
parts.push('NEW FILES — created by this task and NOT shown in the diff above. You MUST'
|
|
217
|
+
+ ' read and check each one against EVERY rule too:\n'
|
|
218
|
+
+ newFiles.map(n => `- ${n}`).join('\n'));
|
|
163
219
|
return parts.join('\n\n');
|
|
164
220
|
}
|
|
165
221
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.3",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|