@mjasnikovs/pi-task 0.16.5 → 0.17.1
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/dist/config/register.js +2 -2
- package/dist/task/auto-orchestrator.d.ts +5 -66
- package/dist/task/auto-orchestrator.js +48 -396
- package/dist/task/auto-prompts.d.ts +1 -1
- package/dist/task/auto-prompts.js +3 -6
- package/dist/task/file-inventory.d.ts +0 -16
- package/dist/task/file-inventory.js +0 -37
- package/dist/task/gate-deps.d.ts +16 -0
- package/dist/task/gate-deps.js +221 -0
- package/dist/task/orchestrator.d.ts +21 -0
- package/dist/task/orchestrator.js +134 -1
- package/dist/task/phases.d.ts +8 -0
- package/dist/task/phases.js +62 -10
- package/dist/task/prompts.d.ts +1 -1
- package/dist/task/prompts.js +2 -2
- package/dist/task/task-gates.d.ts +148 -0
- package/dist/task/task-gates.js +135 -0
- package/package.json +1 -1
package/dist/config/register.js
CHANGED
|
@@ -21,12 +21,12 @@ const ITEMS = [
|
|
|
21
21
|
{
|
|
22
22
|
id: 'verifyWork',
|
|
23
23
|
label: 'verify work',
|
|
24
|
-
description: "After each /task-auto task, RUN its spec's VERIFY block in the workspace and report a PASS/FAIL verdict (also the signal that lets 'enforce guidelines' fix safely)"
|
|
24
|
+
description: "After each /task (and /task-auto task), RUN its spec's VERIFY block in the workspace and report a PASS/FAIL verdict (also the signal that lets 'enforce guidelines' fix safely). Enabling it makes /task wait for the implementation"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
id: 'enforceGuidelines',
|
|
28
28
|
label: 'enforce guidelines',
|
|
29
|
-
description: "Check each /task-auto commit against AGENTS.md/CLAUDE.md. Needs 'verify work' to FIX drift (fixes are reverted if they regress verification); without it, only reports violations"
|
|
29
|
+
description: "Check each /task and /task-auto commit against AGENTS.md/CLAUDE.md. Needs 'verify work' to FIX drift (fixes are reverted if they regress verification); without it, only reports violations. Enabling it makes /task wait for the implementation"
|
|
30
30
|
}
|
|
31
31
|
];
|
|
32
32
|
function makeTheme(theme) {
|
|
@@ -1,74 +1,13 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import type
|
|
3
|
-
import { type CommitResult } from './auto-commit.js';
|
|
4
|
-
import { type EnforceOutcome } from './enforce-guidelines.js';
|
|
5
|
-
import { type VerifyOutcome } from './verify-work.js';
|
|
6
|
-
import { type ResolutionOutcome } from './verify-resolution.js';
|
|
2
|
+
import { type GateDeps } from './task-gates.js';
|
|
7
3
|
/**
|
|
8
4
|
* Injectable seams so the planner and loop are testable without spawning pi.
|
|
9
|
-
* `runChild` is used by planAuto;
|
|
5
|
+
* `runChild` is the planning-only seam used by planAuto; everything else (runTask,
|
|
6
|
+
* commit, verify, enforce, recommend, revert) is the shared post-implementation
|
|
7
|
+
* gate surface defined by {@link GateDeps} and built by {@link buildGateDeps}.
|
|
10
8
|
*/
|
|
11
|
-
export interface AutoDeps {
|
|
9
|
+
export interface AutoDeps extends GateDeps {
|
|
12
10
|
runChild: (name: string, tools: string, prompt: string) => Promise<string>;
|
|
13
|
-
runTask: (ctx: ExtensionCommandContext, cwd: string, title: string, opts?: {
|
|
14
|
-
/** Resume this inner task id instead of allocating a fresh one. */
|
|
15
|
-
resumeId?: string;
|
|
16
|
-
/** Called with the inner task id once its file exists, before phases. */
|
|
17
|
-
onStart?: (taskId: string) => void | Promise<void>;
|
|
18
|
-
/** Scope fence naming the sibling steps, forwarded into refine. */
|
|
19
|
-
planContext?: string;
|
|
20
|
-
/** Verify-FAIL autofix re-attempt: prepended to the delivered spec as
|
|
21
|
-
* a RE-ATTEMPT banner so the re-run fixes that specific failure. */
|
|
22
|
-
fixInstruction?: string;
|
|
23
|
-
}) => Promise<RunSingleTaskResult>;
|
|
24
|
-
/** Snapshot the working tree into one commit after a task passes. */
|
|
25
|
-
commit: (cwd: string, message: string) => Promise<CommitResult>;
|
|
26
|
-
/**
|
|
27
|
-
* Verify the just-finished task's work against the project's AGENTS.md /
|
|
28
|
-
* CLAUDE.md guidelines (and auto-fix violations) BEFORE it is committed.
|
|
29
|
-
* Optional: when absent (tests, or the feature disabled), the loop treats it
|
|
30
|
-
* as a pass. ok === false blocks the commit and fails the task.
|
|
31
|
-
*
|
|
32
|
-
* Takes the loop's CURRENT ctx (not the one captured at defaultDeps time):
|
|
33
|
-
* each task runs in a fresh session, so the captured ctx is stale by the time
|
|
34
|
-
* enforcement runs and using it for the loader widget throws "stale ctx".
|
|
35
|
-
*/
|
|
36
|
-
enforce?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string,
|
|
37
|
-
/**
|
|
38
|
-
* `'edit'` (read,edit — fix in place) only when there is a clean
|
|
39
|
-
* verification signal to guard the edits against; otherwise `'flag'`
|
|
40
|
-
* (read-only, report don't fix). The loop picks the mode from whether the
|
|
41
|
-
* verify gate produced a genuine clean pass.
|
|
42
|
-
*/
|
|
43
|
-
mode: 'edit' | 'flag') => Promise<EnforceOutcome>;
|
|
44
|
-
/**
|
|
45
|
-
* Verify the just-finished task's work by RUNNING its composed spec's VERIFY
|
|
46
|
-
* block in the real workspace (a fresh child of the same local model, with a
|
|
47
|
-
* `bash` tool), then reporting a PASS/FAIL verdict. Optional: absent in tests
|
|
48
|
-
* or when the `verifyWork` flag is off, in which case the loop treats it as a
|
|
49
|
-
* pass. Runs BEFORE the task is checked off/committed and is a hard GATE: ok
|
|
50
|
-
* === false stops the run and fails the task (left unchecked so resume re-runs
|
|
51
|
-
* it). Needs the inner taskId to read that task's spec.
|
|
52
|
-
*/
|
|
53
|
-
verify?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string) => Promise<VerifyOutcome>;
|
|
54
|
-
/**
|
|
55
|
-
* After a verify FAIL, research whether to recommend AUTOFIX (re-do the work)
|
|
56
|
-
* or ACCEPT (the gate misjudged a good artifact) — read-only, same local
|
|
57
|
-
* model. Only sets which card the picker tints RECOMMENDED; the user always
|
|
58
|
-
* makes the final call. Optional: absent in tests or when verify is off, in
|
|
59
|
-
* which case the loop defaults the recommendation to AUTOFIX. Needs the inner
|
|
60
|
-
* taskId (to read the spec) and the gate's failure reason.
|
|
61
|
-
*/
|
|
62
|
-
recommend?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string, failReason: string) => Promise<ResolutionOutcome>;
|
|
63
|
-
/**
|
|
64
|
-
* Discard the working-tree edits an `'edit'` enforcement pass made, restoring
|
|
65
|
-
* the verified task commit. The differential guard calls this when re-running
|
|
66
|
-
* verification AFTER enforcement reports a regression: the guideline fixes are
|
|
67
|
-
* thrown away and the verified implementation is kept. Optional: absent in
|
|
68
|
-
* tests; the loop then skips the revert (and warns). Scoped to exclude the
|
|
69
|
-
* .pi-tasks bookkeeping so task front matter is not rolled back.
|
|
70
|
-
*/
|
|
71
|
-
revert?: (cwd: string) => Promise<void>;
|
|
72
11
|
}
|
|
73
12
|
/**
|
|
74
13
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -7,26 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fsp from 'node:fs/promises';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { gateRunTask } from './orchestrator.js';
|
|
11
11
|
import { parseClarifyList, deriveTitle } from './parsers.js';
|
|
12
12
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
13
13
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT } from './auto-prompts.js';
|
|
14
|
-
import { getProjectSnapshot } from './file-inventory.js';
|
|
15
14
|
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
16
15
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
17
16
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
18
|
-
import { gitCommitAll, gitDropLastCommit } from './auto-commit.js';
|
|
19
|
-
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
20
|
-
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
21
|
-
import { researchResolution, resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
|
|
22
|
-
import { runWorker } from '../workers/pi-worker-core.js';
|
|
23
17
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
24
|
-
import { runPhaseChild, prependHint,
|
|
18
|
+
import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
25
19
|
import { SessionUI, registerBridgeCommand, publishLifecycleNotice } from '../remote/bridge.js';
|
|
26
20
|
import { pushNotify } from '../remote/push.js';
|
|
27
|
-
import { getConfig } from '../config/config.js';
|
|
28
21
|
import { startAutoLoader } from './widget.js';
|
|
29
22
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
23
|
+
import { buildGateDeps } from './gate-deps.js';
|
|
24
|
+
import { runGatesForTask } from './task-gates.js';
|
|
30
25
|
// Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
|
|
31
26
|
// when the model emits NONE), but a model that never says NONE would otherwise
|
|
32
27
|
// barrage the user — the real mx5 run asked 10, several of them redundant.
|
|
@@ -213,13 +208,6 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
213
208
|
if (planPhantoms.length > 0) {
|
|
214
209
|
logPlanDebug(cwd, `phantom specifiers rewritten in plan spec: ${planPhantoms.map(x => x.spec).join(', ')}`);
|
|
215
210
|
}
|
|
216
|
-
// Factual snapshot of the target directory (root files + top-level dirs).
|
|
217
|
-
// Clarify only "may" read the repo; a model that skips the read assumes a
|
|
218
|
-
// greenfield project and recommends bootstrapping config/manifest files that
|
|
219
|
-
// already exist (or invents a path like /workspace). Handing it the facts
|
|
220
|
-
// up front removes the guess. '' for a non-git/empty tree ⇒ prompt omits the
|
|
221
|
-
// block and the model correctly treats the target as greenfield.
|
|
222
|
-
const projectState = await getProjectSnapshot(cwd);
|
|
223
211
|
const answers = [];
|
|
224
212
|
// Plain text of every question already shown, for the duplicate backstop.
|
|
225
213
|
const askedQuestions = [];
|
|
@@ -232,7 +220,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
232
220
|
// Open-ended: keep asking until the model emits NONE or the user dismisses —
|
|
233
221
|
// but never past MAX_CLARIFY_QUESTIONS distinct questions.
|
|
234
222
|
while (askedQuestions.length < MAX_CLARIFY_QUESTIONS) {
|
|
235
|
-
const qRaw = await deps.runChild('auto-clarify', 'read', prependHint(dupHint, AUTO_CLARIFY_PROMPT(featureForModel, answers.join('\n')
|
|
223
|
+
const qRaw = await deps.runChild('auto-clarify', 'read', prependHint(dupHint, AUTO_CLARIFY_PROMPT(featureForModel, answers.join('\n'))));
|
|
236
224
|
const parsed = parseClarifyList(qRaw);
|
|
237
225
|
if (parsed.length === 0)
|
|
238
226
|
break; // NONE / nothing left to ask
|
|
@@ -348,66 +336,12 @@ const AUTO_PLAN_STEPS = {
|
|
|
348
336
|
};
|
|
349
337
|
const AUTO_PLAN_STEP_TOTAL = 2;
|
|
350
338
|
function defaultDeps(ctx, cwd, signal, title) {
|
|
351
|
-
// Captured by the loader's getState so the widget mirrors the child's
|
|
352
|
-
// output line and context usage, exactly like the single-task phase
|
|
339
|
+
// Captured by the planning loader's getState so the widget mirrors the child's
|
|
340
|
+
// latest output line and context usage, exactly like the single-task phase
|
|
341
|
+
// widget. (The gate children manage their own loaders inside buildGateDeps.)
|
|
353
342
|
let lastLine;
|
|
354
343
|
let contextUsage;
|
|
355
344
|
const parentContextWindow = getParentContextWindow(ctx);
|
|
356
|
-
// Shared runner for the per-task GATE children (verify + post-FAIL recommend).
|
|
357
|
-
// Both are read-only passes of the same local model that must run to
|
|
358
|
-
// completion: unguarded (no wall-clock timeout, exact-match loop guard only,
|
|
359
|
-
// path-revisit disabled because re-running the same check is the job), with a
|
|
360
|
-
// status widget and a per-gate debug log. Returns the closure
|
|
361
|
-
// runWorkVerification / researchResolution expect as `runChild`.
|
|
362
|
-
const makeGateChild = (gateCtx, cwd2, taskTitle, kind, logFile) => async (tools, prompt, sig) => {
|
|
363
|
-
lastLine = undefined;
|
|
364
|
-
contextUsage = undefined;
|
|
365
|
-
const startedAt = Date.now();
|
|
366
|
-
const logPath = path.join(tasksDir(cwd2), logFile);
|
|
367
|
-
const log = (msg) => {
|
|
368
|
-
void fsp.appendFile(logPath, `${new Date().toISOString()} ${msg}\n`).catch(() => { });
|
|
369
|
-
};
|
|
370
|
-
log(`=== ${kind} start: ${taskTitle} ===`);
|
|
371
|
-
const stopLoader = startAutoLoader(gateCtx, () => ({
|
|
372
|
-
title: taskTitle,
|
|
373
|
-
kind,
|
|
374
|
-
step: kind,
|
|
375
|
-
stepNum: 1,
|
|
376
|
-
stepTotal: 1,
|
|
377
|
-
startedAt,
|
|
378
|
-
lastLine,
|
|
379
|
-
contextUsage
|
|
380
|
-
}));
|
|
381
|
-
try {
|
|
382
|
-
const r = await runWorker({
|
|
383
|
-
prompt,
|
|
384
|
-
cwd: cwd2,
|
|
385
|
-
signal: sig,
|
|
386
|
-
tools,
|
|
387
|
-
timeoutMs: 0,
|
|
388
|
-
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
389
|
-
onLine: line => {
|
|
390
|
-
lastLine = line;
|
|
391
|
-
log(line);
|
|
392
|
-
},
|
|
393
|
-
onContextUsage: snapshot => {
|
|
394
|
-
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
if (r.loopHit) {
|
|
398
|
-
log(`=== ${kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
399
|
-
gateCtx.ui.notify(`${taskTitle}: ${kind} worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
400
|
-
}
|
|
401
|
-
const failure = classifyEnforceChildFailure(r);
|
|
402
|
-
log(failure ? `=== ${kind} end: FAIL — ${failure} ===` : `=== ${kind} end: ok ===`);
|
|
403
|
-
if (failure)
|
|
404
|
-
throw new Error(failure);
|
|
405
|
-
return r.text;
|
|
406
|
-
}
|
|
407
|
-
finally {
|
|
408
|
-
stopLoader();
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
345
|
const phaseDeps = {
|
|
412
346
|
cwd,
|
|
413
347
|
taskId: '',
|
|
@@ -420,6 +354,9 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
420
354
|
}
|
|
421
355
|
};
|
|
422
356
|
return {
|
|
357
|
+
// Planning-only seam. The shared gate surface (runTask/commit/verify/
|
|
358
|
+
// enforce/recommend/revert) comes from buildGateDeps below — identical to
|
|
359
|
+
// what /task builds, so both commands gate the same way.
|
|
423
360
|
runChild: async (name, tools, prompt) => {
|
|
424
361
|
// Planning children are slow LLM calls with no UI of their own; show
|
|
425
362
|
// the same status block as /task so this never goes silent until the
|
|
@@ -444,179 +381,10 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
444
381
|
stopLoader();
|
|
445
382
|
}
|
|
446
383
|
},
|
|
447
|
-
|
|
448
|
-
waitForImplementation: true,
|
|
449
|
-
resumeId: opts?.resumeId,
|
|
450
|
-
onStart: opts?.onStart,
|
|
451
|
-
planContext: opts?.planContext,
|
|
452
|
-
fixInstruction: opts?.fixInstruction
|
|
453
|
-
}),
|
|
454
|
-
commit: (cwd2, message) => getConfig().autoCommit ?
|
|
455
|
-
gitCommitAll(cwd2, message, signal)
|
|
456
|
-
: Promise.resolve({ committed: false, reason: 'auto-commit disabled' }),
|
|
457
|
-
revert: cwd2 => gitDropLastCommit(cwd2, signal),
|
|
458
|
-
enforce: (enforceCtx, cwd2, taskTitle, mode) => {
|
|
459
|
-
if (!getConfig().enforceGuidelines) {
|
|
460
|
-
return Promise.resolve({ ok: true, reason: 'disabled' });
|
|
461
|
-
}
|
|
462
|
-
return runGuidelineEnforcement({
|
|
463
|
-
cwd: cwd2,
|
|
464
|
-
signal,
|
|
465
|
-
mode,
|
|
466
|
-
// Run the worker child UNGUARDED: no loop detector, no wall-clock
|
|
467
|
-
// timeout. This pass's job is to rework files in place until every
|
|
468
|
-
// violation is fixed, and it legitimately reads and edits the same
|
|
469
|
-
// file many times — the research-worker guards mislabel that as a
|
|
470
|
-
// runaway and kill good work (proven on mx5 TASK_0002). Let it
|
|
471
|
-
// run until the model finishes; classifyEnforceChildFailure still
|
|
472
|
-
// blocks the commit on a real failure (non-zero exit, leaked tool
|
|
473
|
-
// call) or a user cancel.
|
|
474
|
-
runChild: async (tools, prompt, sig) => {
|
|
475
|
-
// The enforcement child is a slow local-model pass with edit
|
|
476
|
-
// tools; show the same /task-auto status block as planning so
|
|
477
|
-
// it isn't silent — head · enforcing guidelines/elapsed ·
|
|
478
|
-
// tokens/window [bar] · ↳ last line. The worker runs the same
|
|
479
|
-
// `--mode json` stream as the phase children, so it emits
|
|
480
|
-
// context_usage; forward it (onContextUsage below) so the bar
|
|
481
|
-
// renders here exactly like every other phase widget.
|
|
482
|
-
lastLine = undefined;
|
|
483
|
-
contextUsage = undefined;
|
|
484
|
-
const startedAt = Date.now();
|
|
485
|
-
// Per-pass debug log. The enforce child is otherwise
|
|
486
|
-
// unobservable (it has no per-task file like the impl runner).
|
|
487
|
-
// One rolling file under .pi-tasks/; each pass brackets its
|
|
488
|
-
// tool/text lines with start/end markers naming the task and
|
|
489
|
-
// the terminal outcome, so a stop (a real failure or a found
|
|
490
|
-
// violation) is diagnosable. Fire-and-forget.
|
|
491
|
-
const enforceLogPath = path.join(tasksDir(cwd2), 'enforce-debug.log');
|
|
492
|
-
const logEnforce = (msg) => {
|
|
493
|
-
void fsp
|
|
494
|
-
.appendFile(enforceLogPath, `${new Date().toISOString()} ${msg}\n`)
|
|
495
|
-
.catch(() => { });
|
|
496
|
-
};
|
|
497
|
-
logEnforce(`=== enforce start: ${taskTitle} ===`);
|
|
498
|
-
const stopLoader = startAutoLoader(enforceCtx, () => ({
|
|
499
|
-
title: taskTitle,
|
|
500
|
-
kind: 'enforce',
|
|
501
|
-
step: 'guidelines',
|
|
502
|
-
stepNum: 1,
|
|
503
|
-
stepTotal: 1,
|
|
504
|
-
startedAt,
|
|
505
|
-
lastLine,
|
|
506
|
-
contextUsage
|
|
507
|
-
}));
|
|
508
|
-
try {
|
|
509
|
-
const r = await runWorker({
|
|
510
|
-
prompt,
|
|
511
|
-
cwd: cwd2,
|
|
512
|
-
signal: sig,
|
|
513
|
-
tools,
|
|
514
|
-
timeoutMs: 0, // no wall-clock timeout — run to completion
|
|
515
|
-
// Exact-match loop guard only: pathThreshold Infinity
|
|
516
|
-
// disables the path-revisit heuristic, so revisiting one
|
|
517
|
-
// file (which IS this pass's job) never trips — only a
|
|
518
|
-
// literally-identical call repeated past threshold does
|
|
519
|
-
// (e.g. the same read or edit repeated hundreds of times).
|
|
520
|
-
// A hit nudges via the normal restart-with-hint; a loop
|
|
521
|
-
// that survives the nudges is WARNED, not blocked (see
|
|
522
|
-
// r.loopHit handling below and classifyEnforceChildFailure).
|
|
523
|
-
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
524
|
-
onLine: line => {
|
|
525
|
-
lastLine = line;
|
|
526
|
-
logEnforce(line);
|
|
527
|
-
},
|
|
528
|
-
onContextUsage: snapshot => {
|
|
529
|
-
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
// A loop that survived the restart-with-hint nudges is a
|
|
533
|
-
// warning, not a failure: log it and tell the user, but let
|
|
534
|
-
// the verdict gate (below) be the only thing that can block.
|
|
535
|
-
if (r.loopHit) {
|
|
536
|
-
logEnforce(`=== enforce LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
537
|
-
enforceCtx.ui.notify(`${taskTitle}: enforce worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
538
|
-
}
|
|
539
|
-
const failure = classifyEnforceChildFailure(r);
|
|
540
|
-
logEnforce(failure ?
|
|
541
|
-
`=== enforce end: FAIL — ${failure} ===`
|
|
542
|
-
: '=== enforce end: verdict captured ===');
|
|
543
|
-
if (failure)
|
|
544
|
-
throw new Error(failure);
|
|
545
|
-
return r.text;
|
|
546
|
-
}
|
|
547
|
-
finally {
|
|
548
|
-
stopLoader();
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
},
|
|
553
|
-
verify: async (verifyCtx, cwd2, taskTitle, taskId) => {
|
|
554
|
-
if (!getConfig().verifyWork) {
|
|
555
|
-
return { ok: true, reason: 'disabled' };
|
|
556
|
-
}
|
|
557
|
-
// The spec to verify against is the composed spec committed in the
|
|
558
|
-
// task file. A task that never reached compose has no spec section —
|
|
559
|
-
// runWorkVerification treats a null spec as a no-op pass.
|
|
560
|
-
let spec;
|
|
561
|
-
try {
|
|
562
|
-
const { body } = await readTaskFile(cwd2, taskId);
|
|
563
|
-
spec = extractSpecForVerification(body);
|
|
564
|
-
}
|
|
565
|
-
catch {
|
|
566
|
-
spec = null;
|
|
567
|
-
}
|
|
568
|
-
return runWorkVerification({
|
|
569
|
-
cwd: cwd2,
|
|
570
|
-
signal,
|
|
571
|
-
spec,
|
|
572
|
-
runChild: makeGateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log')
|
|
573
|
-
});
|
|
574
|
-
},
|
|
575
|
-
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
576
|
-
// Read the same composed spec the verify gate judged against, so the
|
|
577
|
-
// recommendation reasons over the real contract (a missing spec is
|
|
578
|
-
// unusual here — verify already ran — but degrade to the bare title).
|
|
579
|
-
let spec;
|
|
580
|
-
try {
|
|
581
|
-
const { body } = await readTaskFile(cwd2, taskId);
|
|
582
|
-
spec = extractSpecForVerification(body) ?? taskTitle;
|
|
583
|
-
}
|
|
584
|
-
catch {
|
|
585
|
-
spec = taskTitle;
|
|
586
|
-
}
|
|
587
|
-
return researchResolution({
|
|
588
|
-
cwd: cwd2,
|
|
589
|
-
signal,
|
|
590
|
-
spec,
|
|
591
|
-
failReason,
|
|
592
|
-
runChild: makeGateChild(recCtx, cwd2, taskTitle, 'recommend', 'verify-debug.log')
|
|
593
|
-
});
|
|
594
|
-
}
|
|
384
|
+
...buildGateDeps({ signal, parentContextWindow, runTask: gateRunTask })
|
|
595
385
|
};
|
|
596
386
|
}
|
|
597
387
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
598
|
-
/**
|
|
599
|
-
* Show the boxed two-choice picker after a verify FAIL and return what the user
|
|
600
|
-
* decided. The model-recommended card is placed first so the renderer tints it
|
|
601
|
-
* green; the user ALWAYS makes the final call (there is no auto-pick). Mirrors the
|
|
602
|
-
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker
|
|
603
|
-
* against a remote answer, with the two actions also surfaced as remote buttons.
|
|
604
|
-
*/
|
|
605
|
-
async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
606
|
-
const options = resolutionOptions(rec.recommend);
|
|
607
|
-
const question = `Verification FAILED for "${title}".\n\n${failReason}\n\n`
|
|
608
|
-
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}`;
|
|
609
|
-
const answer = await new SessionUI(ctx).ask({
|
|
610
|
-
localTitle: 'Verification failed — how should pi proceed?',
|
|
611
|
-
displayQuestion: question,
|
|
612
|
-
question,
|
|
613
|
-
recommended: options[0].label,
|
|
614
|
-
recommended2: options[1].label,
|
|
615
|
-
allowSkip: false,
|
|
616
|
-
options
|
|
617
|
-
});
|
|
618
|
-
return classifyResolutionAnswer(answer);
|
|
619
|
-
}
|
|
620
388
|
let cancelRequested = false;
|
|
621
389
|
let autoRunning = false;
|
|
622
390
|
export function requestAutoCancel() {
|
|
@@ -723,162 +491,46 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
723
491
|
return;
|
|
724
492
|
}
|
|
725
493
|
// GATE: actually RUN the task's verification against the just-finished
|
|
726
|
-
// work
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// no attempt cap: AUTOFIX loops straight back to the gate as many
|
|
751
|
-
// times as the user keeps choosing it. The run only leaves the loop
|
|
752
|
-
// when the work verifies, the user ACCEPTs, or the user dismisses
|
|
753
|
-
// the picker (which pauses the run, resumable).
|
|
754
|
-
while (!verified.ok) {
|
|
755
|
-
const failReason = verified.reason ?? 'did not verify';
|
|
756
|
-
const rec = deps.recommend ?
|
|
757
|
-
await deps.recommend(active, cwd, next.title, res.taskId, failReason)
|
|
758
|
-
: { recommend: 'autofix', rationale: failReason };
|
|
759
|
-
const choice = await askVerifyResolution(active, next.title, failReason, rec);
|
|
760
|
-
if (choice.action === 'cancel') {
|
|
761
|
-
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
762
|
-
announceDone(active, `${id} paused at "${next.title}" — verification failed and you dismissed the choice; resume with /task-auto-resume.`, 'warning');
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
if (choice.action === 'accept') {
|
|
766
|
-
active.ui.notify(`${id}: accepted "${next.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
767
|
-
break;
|
|
768
|
-
}
|
|
769
|
-
// AUTOFIX: re-run the implementation turn with the failure (and
|
|
770
|
-
// any typed guidance) prepended as a RE-ATTEMPT banner, then
|
|
771
|
-
// re-verify — looping back to the gate. No cap: the user decides
|
|
772
|
-
// when to stop (by accepting or dismissing the next picker).
|
|
773
|
-
active.ui.notify(`${id}: autofixing "${next.title}"…`, 'info');
|
|
774
|
-
const fixInstruction = choice.guidance ?
|
|
775
|
-
`${failReason}\n\nUser guidance: ${choice.guidance}`
|
|
776
|
-
: failReason;
|
|
777
|
-
const fixRes = await deps.runTask(active, cwd, next.title, {
|
|
778
|
-
resumeId: res.taskId,
|
|
779
|
-
planContext: buildScopeFence(entries.map(e => e.title), next.index),
|
|
780
|
-
fixInstruction
|
|
781
|
-
});
|
|
782
|
-
active = fixRes.ctx ?? active;
|
|
783
|
-
if (fixRes.sessionCancelled) {
|
|
784
|
-
announceDone(active, `${id} paused — could not start a session for autofix. Run /task-auto-resume to retry.`, 'warning');
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
if (fixRes.interrupted) {
|
|
788
|
-
announceDone(active, `${id} paused at "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
if (!fixRes.ok) {
|
|
792
|
-
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
793
|
-
const why = fixRes.reason ? ` — ${fixRes.reason.slice(0, 160)}` : '';
|
|
794
|
-
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
// Resume reuses the same inner task id, so res.taskId is stable.
|
|
798
|
-
verified = await deps.verify(active, cwd, next.title, res.taskId);
|
|
799
|
-
}
|
|
800
|
-
// Loop exited because the work verified OR the user accepted the
|
|
801
|
-
// artifact — either way fall through to check-off/commit/enforce.
|
|
802
|
-
// A genuine clean pass is ok===true with NO reason; a no-op pass
|
|
803
|
-
// ('no spec to verify') or an accept-override (verified.ok still
|
|
804
|
-
// false at break) does NOT count as a guardable signal.
|
|
805
|
-
verifyCleanPass = verified.ok && !verified.reason;
|
|
494
|
+
// work, then hold the committed result to AGENTS.md / CLAUDE.md. Shared
|
|
495
|
+
// verbatim with /task so both commands gate identically — see
|
|
496
|
+
// runGatesForTask. `done` means the work verified (or was accepted),
|
|
497
|
+
// was checked off + committed, and enforcement ran; every other kind is
|
|
498
|
+
// a terminal stop this loop announces with its own /task-auto-resume
|
|
499
|
+
// wording (the shared gate is command-agnostic).
|
|
500
|
+
const gate = await runGatesForTask(active, deps, {
|
|
501
|
+
cwd,
|
|
502
|
+
taskId: res.taskId,
|
|
503
|
+
title: next.title,
|
|
504
|
+
tag: id,
|
|
505
|
+
// Fence an AUTOFIX re-run against re-expanding the whole spec.
|
|
506
|
+
planContext: buildScopeFence(entries.map(e => e.title), next.index),
|
|
507
|
+
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
508
|
+
// allocated TASK_NNNN id (never empty here). The parent task-list
|
|
509
|
+
// check-off runs after verify passes/accepts and before the commit,
|
|
510
|
+
// so the commit captures the checked box too.
|
|
511
|
+
onVerified: () => checkOffTask(cwd, id, next.index, res.taskId, next.title)
|
|
512
|
+
});
|
|
513
|
+
active = gate.ctx;
|
|
514
|
+
if (gate.kind === 'paused') {
|
|
515
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
516
|
+
announceDone(active, `${id} paused at "${next.title}" — verification failed and you dismissed the choice; resume with /task-auto-resume.`, 'warning');
|
|
517
|
+
return;
|
|
806
518
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
await checkOffTask(cwd, id, next.index, res.taskId, next.title);
|
|
811
|
-
// Commit the task's work (and the just-written check-off) as one
|
|
812
|
-
// snapshot FIRST — before guideline enforcement — so a passing task is
|
|
813
|
-
// durably recorded no matter what enforcement later finds. Best-effort:
|
|
814
|
-
// a failed/empty commit only warns; the task already passed.
|
|
815
|
-
const message = `task: ${next.title} (${res.taskId})`;
|
|
816
|
-
const commit = await deps.commit(cwd, message);
|
|
817
|
-
if (commit.committed) {
|
|
818
|
-
active.ui.notify(`${id}: committed "${next.title}".`, 'info');
|
|
519
|
+
if (gate.kind === 'session-cancelled') {
|
|
520
|
+
announceDone(active, `${id} paused — could not start a session for autofix. Run /task-auto-resume to retry.`, 'warning');
|
|
521
|
+
return;
|
|
819
522
|
}
|
|
820
|
-
|
|
821
|
-
active
|
|
523
|
+
if (gate.kind === 'interrupted') {
|
|
524
|
+
announceDone(active, `${id} paused at "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
525
|
+
return;
|
|
822
526
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
// verify signal:
|
|
829
|
-
//
|
|
830
|
-
// - 'edit' mode (read,edit — fix in place) runs ONLY when the verify
|
|
831
|
-
// gate just produced a genuine clean pass. That pass is the
|
|
832
|
-
// differential SIGNAL: enforce commits its fixes, then the SAME
|
|
833
|
-
// signal is re-run against the enforced tree. A regression means the
|
|
834
|
-
// pass made the artifact worse than it found it — its commit is
|
|
835
|
-
// dropped and the verified implementation is kept (A/B: 5/5 clean).
|
|
836
|
-
// - 'flag' mode (read-only — report don't fix) runs when there is no
|
|
837
|
-
// such signal (verify off, no spec, or an accept-override): with
|
|
838
|
-
// nothing to revert against, the pass may not rewrite logic, only
|
|
839
|
-
// surface violations as a warning ("no signal ⇒ no license"; A/B:
|
|
840
|
-
// 5/5 clean, 5/5 caught the real violation).
|
|
841
|
-
//
|
|
842
|
-
// Skipped when nothing was committed this round (autoCommit off or an
|
|
843
|
-
// empty commit), when the feature is off, when there are no guideline
|
|
844
|
-
// files, or in tests with no enforce dep.
|
|
845
|
-
if (deps.enforce && commit.committed) {
|
|
846
|
-
const mode = verifyCleanPass ? 'edit' : 'flag';
|
|
847
|
-
active.ui.notify(mode === 'edit' ?
|
|
848
|
-
`${id}: enforcing AGENTS.md/CLAUDE.md on "${next.title}"…`
|
|
849
|
-
: `${id}: reviewing "${next.title}" against AGENTS.md/CLAUDE.md (no verify signal — report only)…`, 'info');
|
|
850
|
-
const verdict = await deps.enforce(active, cwd, next.title, mode);
|
|
851
|
-
if (!verdict.ok) {
|
|
852
|
-
active.ui.notify(`${id}: guideline ${mode === 'edit' ? 'enforcement' : 'review'} on "${next.title}" — ${verdict.reason ?? 'not clean'} — continuing.`, 'warning');
|
|
853
|
-
}
|
|
854
|
-
if (mode === 'edit') {
|
|
855
|
-
// Commit whatever the pass fixed as its own snapshot. A no-op
|
|
856
|
-
// when it made no edits (nothing to commit) — and then there is
|
|
857
|
-
// nothing to re-verify or revert, so a clean task skips the
|
|
858
|
-
// second verify entirely.
|
|
859
|
-
const enforceCommit = await deps.commit(cwd, `ENFORCE GUIDELINES: ${next.title} (${res.taskId})`);
|
|
860
|
-
if (enforceCommit.committed) {
|
|
861
|
-
// Differential guard: re-run the verify signal against the
|
|
862
|
-
// enforced tree. verifyCleanPass implies deps.verify exists,
|
|
863
|
-
// so this is a real check. A regression ⇒ drop the enforce
|
|
864
|
-
// commit, keep the verified task commit underneath it.
|
|
865
|
-
const after = deps.verify ?
|
|
866
|
-
await deps.verify(active, cwd, next.title, res.taskId)
|
|
867
|
-
: { ok: true };
|
|
868
|
-
if (!after.ok) {
|
|
869
|
-
// Regression: drop the enforce commit if we can, and
|
|
870
|
-
// never report it as a kept fix.
|
|
871
|
-
if (deps.revert)
|
|
872
|
-
await deps.revert(cwd);
|
|
873
|
-
active.ui.notify(`${id}: guideline fixes regressed verification on "${next.title}" (${(after.reason ?? 'now fails').slice(0, 120)}) — ${deps.revert ? 'reverted them, kept the verified work' : 'left in place (no revert available)'}.`, 'warning');
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
active.ui.notify(`${id}: committed guideline fixes for "${next.title}".`, 'info');
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
// 'flag' mode makes no edits — nothing to commit or revert.
|
|
527
|
+
if (gate.kind === 'failed') {
|
|
528
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
529
|
+
const why = gate.reason ? ` — ${gate.reason.slice(0, 160)}` : '';
|
|
530
|
+
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
531
|
+
return;
|
|
881
532
|
}
|
|
533
|
+
// gate.kind === 'done' → fall through to the next task.
|
|
882
534
|
}
|
|
883
535
|
}
|
|
884
536
|
catch (err) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* when no clarification remains. priorQA carries the questions already answered so
|
|
10
10
|
* each next question adapts to them.
|
|
11
11
|
*/
|
|
12
|
-
export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string
|
|
12
|
+
export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) => string;
|
|
13
13
|
/**
|
|
14
14
|
* Decompose: output a markdown checkbox list of task titles (one line each).
|
|
15
15
|
*/
|