@mjasnikovs/pi-task 0.13.30 → 0.13.32
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.
|
@@ -21,8 +21,12 @@ export interface AutoDeps {
|
|
|
21
21
|
* CLAUDE.md guidelines (and auto-fix violations) BEFORE it is committed.
|
|
22
22
|
* Optional: when absent (tests, or the feature disabled), the loop treats it
|
|
23
23
|
* as a pass. ok === false blocks the commit and fails the task.
|
|
24
|
+
*
|
|
25
|
+
* Takes the loop's CURRENT ctx (not the one captured at defaultDeps time):
|
|
26
|
+
* each task runs in a fresh session, so the captured ctx is stale by the time
|
|
27
|
+
* enforcement runs and using it for the loader widget throws "stale ctx".
|
|
24
28
|
*/
|
|
25
|
-
enforce?: (cwd: string, taskTitle: string) => Promise<EnforceOutcome>;
|
|
29
|
+
enforce?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string) => Promise<EnforceOutcome>;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -15,7 +15,7 @@ import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './quest
|
|
|
15
15
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
16
16
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
17
17
|
import { gitCommitAll } from './auto-commit.js';
|
|
18
|
-
import { runGuidelineEnforcement, ENFORCE_TIMEOUT_MS } from './enforce-guidelines.js';
|
|
18
|
+
import { runGuidelineEnforcement, classifyEnforceChildFailure, ENFORCE_TIMEOUT_MS } from './enforce-guidelines.js';
|
|
19
19
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
20
20
|
import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
21
21
|
import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
|
|
@@ -306,7 +306,7 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
306
306
|
commit: (cwd2, message) => getConfig().autoCommit ?
|
|
307
307
|
gitCommitAll(cwd2, message, signal)
|
|
308
308
|
: Promise.resolve({ committed: false, reason: 'auto-commit disabled' }),
|
|
309
|
-
enforce: (cwd2,
|
|
309
|
+
enforce: (enforceCtx, cwd2, taskTitle) => {
|
|
310
310
|
if (!getConfig().enforceGuidelines) {
|
|
311
311
|
return Promise.resolve({ ok: true, reason: 'disabled' });
|
|
312
312
|
}
|
|
@@ -315,30 +315,49 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
315
315
|
signal,
|
|
316
316
|
// Reuse the timeout- and loop-guarded worker child. It runs the
|
|
317
317
|
// same local model with edit tools so it can fix violations in
|
|
318
|
-
// place.
|
|
319
|
-
//
|
|
320
|
-
//
|
|
318
|
+
// place. classifyEnforceChildFailure turns any non-clean exit
|
|
319
|
+
// (loop kill, timeout, leaked tool call, non-zero) into a thrown
|
|
320
|
+
// error so the pass becomes a blocking outcome rather than
|
|
321
|
+
// trusting a partial transcript.
|
|
321
322
|
runChild: async (tools, prompt, sig) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
323
|
+
// The enforcement child is a slow local-model pass with edit
|
|
324
|
+
// tools; show the same /task-auto status block as planning so
|
|
325
|
+
// it isn't silent — head · enforcing guidelines/elapsed · ↳
|
|
326
|
+
// last line. (runWorker surfaces no context usage, so that
|
|
327
|
+
// line is just omitted.)
|
|
328
|
+
lastLine = undefined;
|
|
329
|
+
contextUsage = undefined;
|
|
330
|
+
const startedAt = Date.now();
|
|
331
|
+
const stopLoader = startAutoLoader(enforceCtx, () => ({
|
|
332
|
+
title: taskTitle,
|
|
333
|
+
kind: 'enforce',
|
|
334
|
+
step: 'guidelines',
|
|
335
|
+
stepNum: 1,
|
|
336
|
+
stepTotal: 1,
|
|
337
|
+
startedAt,
|
|
338
|
+
lastLine,
|
|
339
|
+
contextUsage
|
|
340
|
+
}));
|
|
341
|
+
try {
|
|
342
|
+
const r = await runWorker({
|
|
343
|
+
prompt,
|
|
344
|
+
cwd: cwd2,
|
|
345
|
+
signal: sig,
|
|
346
|
+
tools,
|
|
347
|
+
timeoutMs: ENFORCE_TIMEOUT_MS,
|
|
348
|
+
onLine: line => {
|
|
349
|
+
lastLine = line;
|
|
350
|
+
phaseDeps.logDebug?.(`enforce: ${line}`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const failure = classifyEnforceChildFailure(r);
|
|
354
|
+
if (failure)
|
|
355
|
+
throw new Error(failure);
|
|
356
|
+
return r.text;
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
stopLoader();
|
|
340
360
|
}
|
|
341
|
-
return r.text;
|
|
342
361
|
}
|
|
343
362
|
});
|
|
344
363
|
}
|
|
@@ -451,7 +470,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
451
470
|
// dep.) Fixes it makes are folded into this task's snapshot below.
|
|
452
471
|
if (deps.enforce) {
|
|
453
472
|
active.ui.notify(`${id}: enforcing AGENTS.md/CLAUDE.md on "${next.title}"…`, 'info');
|
|
454
|
-
const verdict = await deps.enforce(cwd, next.title);
|
|
473
|
+
const verdict = await deps.enforce(active, cwd, next.title);
|
|
455
474
|
if (!verdict.ok) {
|
|
456
475
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
457
476
|
announceDone(active, `${id} stopped at "${next.title}" — ${verdict.reason ?? 'guideline check failed'} — fix and run /task-auto-resume.`, 'error');
|
|
@@ -46,6 +46,26 @@ export declare function parseEnforceVerdict(text: string): {
|
|
|
46
46
|
clean: boolean;
|
|
47
47
|
detail: string;
|
|
48
48
|
};
|
|
49
|
+
/** The subset of a runWorker result the enforcement-child mapping reads. */
|
|
50
|
+
export interface EnforceChildResult {
|
|
51
|
+
text: string;
|
|
52
|
+
exitCode: number;
|
|
53
|
+
aborted: boolean;
|
|
54
|
+
timedOut?: boolean;
|
|
55
|
+
loopHit?: unknown;
|
|
56
|
+
leakedToolCall?: unknown;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Map the enforcement child's runWorker result to a fatal error message, or null
|
|
60
|
+
* when it finished cleanly enough to parse a verdict from its text.
|
|
61
|
+
*
|
|
62
|
+
* The order is load-bearing. A loop-kill AND a wall-clock timeout BOTH also set
|
|
63
|
+
* `aborted` — killProc flips it on every kill path — so the specific causes
|
|
64
|
+
* (timeout, loop, leaked tool call) must be named BEFORE the generic
|
|
65
|
+
* `aborted → user-cancel` mapping. Checking `aborted` first (as the original
|
|
66
|
+
* inline code did) mislabels a loop-killed enforcement child as a user cancel.
|
|
67
|
+
*/
|
|
68
|
+
export declare function classifyEnforceChildFailure(r: EnforceChildResult): string | null;
|
|
49
69
|
/**
|
|
50
70
|
* Capture the work to verify as text: the tracked diff against HEAD plus the
|
|
51
71
|
* names of any new (untracked) files. Non-destructive — it does not touch the
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import * as fsp from 'node:fs/promises';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { runChildDefault } from '../shared/child-process.js';
|
|
22
|
+
import { USER_CANCELLED } from './child-runner.js';
|
|
22
23
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
23
24
|
export const GUIDELINE_FILENAMES = ['AGENTS.md', 'CLAUDE.md'];
|
|
24
25
|
/** Tools the fix pass is allowed: read/search to inspect, edit/write to fix. */
|
|
@@ -99,6 +100,29 @@ export function parseEnforceVerdict(text) {
|
|
|
99
100
|
const clean = last[1].toUpperCase() === 'CLEAN';
|
|
100
101
|
return { clean, detail: clean ? '' : last[2].trim() || 'unspecified violation' };
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Map the enforcement child's runWorker result to a fatal error message, or null
|
|
105
|
+
* when it finished cleanly enough to parse a verdict from its text.
|
|
106
|
+
*
|
|
107
|
+
* The order is load-bearing. A loop-kill AND a wall-clock timeout BOTH also set
|
|
108
|
+
* `aborted` — killProc flips it on every kill path — so the specific causes
|
|
109
|
+
* (timeout, loop, leaked tool call) must be named BEFORE the generic
|
|
110
|
+
* `aborted → user-cancel` mapping. Checking `aborted` first (as the original
|
|
111
|
+
* inline code did) mislabels a loop-killed enforcement child as a user cancel.
|
|
112
|
+
*/
|
|
113
|
+
export function classifyEnforceChildFailure(r) {
|
|
114
|
+
if (r.timedOut)
|
|
115
|
+
return 'enforcement child timed out';
|
|
116
|
+
if (r.loopHit)
|
|
117
|
+
return 'enforcement child looped';
|
|
118
|
+
if (r.leakedToolCall)
|
|
119
|
+
return 'enforcement child leaked a tool call';
|
|
120
|
+
if (r.aborted)
|
|
121
|
+
return USER_CANCELLED;
|
|
122
|
+
if (r.exitCode !== 0)
|
|
123
|
+
return `enforcement child exited ${r.exitCode}`;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
102
126
|
/**
|
|
103
127
|
* Capture the work to verify as text: the tracked diff against HEAD plus the
|
|
104
128
|
* names of any new (untracked) files. Non-destructive — it does not touch the
|
|
@@ -135,6 +159,11 @@ export async function runGuidelineEnforcement(deps) {
|
|
|
135
159
|
text = await deps.runChild(ENFORCE_TOOLS, buildEnforcePrompt(doc.text, diff), deps.signal);
|
|
136
160
|
}
|
|
137
161
|
catch (err) {
|
|
162
|
+
// A user cancellation is not an enforcement failure — re-throw it so the
|
|
163
|
+
// /task-auto loop's USER_CANCELLED handler reports a clean "cancelled —
|
|
164
|
+
// resume" instead of wrapping it as "enforcement pass could not run".
|
|
165
|
+
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
166
|
+
throw err;
|
|
138
167
|
const msg = err instanceof Error ? err.message : String(err);
|
|
139
168
|
return { ok: false, reason: `enforcement pass could not run: ${msg}` };
|
|
140
169
|
}
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export interface AutoLoaderState {
|
|
|
49
49
|
startedAt: number;
|
|
50
50
|
lastLine?: string;
|
|
51
51
|
contextUsage?: ContextSnapshot;
|
|
52
|
+
/** Which /task-auto stage this loader is for. Defaults to 'planning' (the
|
|
53
|
+
* numbered clarify/decompose steps); 'enforce' is the per-task guideline
|
|
54
|
+
* pass, which has no step numbering. */
|
|
55
|
+
kind?: 'planning' | 'enforce';
|
|
52
56
|
}
|
|
53
57
|
export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetTheme): string[];
|
|
54
58
|
/**
|
package/dist/task/widget.js
CHANGED
|
@@ -121,7 +121,9 @@ export function startWidget(ctx, getState) {
|
|
|
121
121
|
export function buildAutoLoaderLines(s, theme) {
|
|
122
122
|
const elapsed = formatDuration(Date.now() - s.startedAt);
|
|
123
123
|
const head = `/task-auto · ${s.title}`;
|
|
124
|
-
let detail =
|
|
124
|
+
let detail = s.kind === 'enforce' ?
|
|
125
|
+
`enforcing guidelines · ${elapsed}`
|
|
126
|
+
: `planning ${s.stepNum}/${s.stepTotal} ${s.step} · ${elapsed}`;
|
|
125
127
|
if (s.contextUsage) {
|
|
126
128
|
const ctxDetail = formatContextDetail(s.contextUsage, theme);
|
|
127
129
|
if (ctxDetail)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.32",
|
|
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",
|