@mjasnikovs/pi-task 0.13.30 → 0.13.31

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.
@@ -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, _taskTitle) => {
309
+ enforce: (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. Any non-clean exit (loop kill, timeout, leaked tool call,
319
- // non-zero) throws so the pass becomes a blocking outcome rather
320
- // than trusting a partial transcript.
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
- const r = await runWorker({
323
- prompt,
324
- cwd: cwd2,
325
- signal: sig,
326
- tools,
327
- timeoutMs: ENFORCE_TIMEOUT_MS,
328
- onLine: line => phaseDeps.logDebug?.(`enforce: ${line}`)
329
- });
330
- if (r.aborted && !r.timedOut)
331
- throw new Error(USER_CANCELLED);
332
- if (r.timedOut)
333
- throw new Error('enforcement child timed out');
334
- if (r.loopHit)
335
- throw new Error('enforcement child looped');
336
- if (r.leakedToolCall)
337
- throw new Error('enforcement child leaked a tool call');
338
- if (r.exitCode !== 0) {
339
- throw new Error(`enforcement child exited ${r.exitCode}`);
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(ctx, () => ({
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
  }
@@ -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
@@ -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
  /**
@@ -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 = `planning ${s.stepNum}/${s.stepTotal} ${s.step} · ${elapsed}`;
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.30",
3
+ "version": "0.13.31",
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",