@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-gates — the post-implementation GATE sequence shared by /task-auto's per-task
|
|
3
|
+
* loop and the single /task command.
|
|
4
|
+
*
|
|
5
|
+
* After a task's implementation turn settles, the same two gates run against the
|
|
6
|
+
* just-finished work:
|
|
7
|
+
*
|
|
8
|
+
* 1. VERIFY — RUN the composed spec's VERIFY block in the real workspace and
|
|
9
|
+
* judge a PASS/FAIL. A FAIL offers the user a boxed AUTOFIX / ACCEPT / dismiss
|
|
10
|
+
* picker (the user always decides; AUTOFIX re-runs the implementation turn and
|
|
11
|
+
* loops back to the gate uncapped).
|
|
12
|
+
* 2. ENFORCE — hold the committed work to the project's AGENTS.md / CLAUDE.md
|
|
13
|
+
* rules. Runs in `edit` mode (fix in place) only when the verify gate produced
|
|
14
|
+
* a genuine clean pass to guard the edits against; otherwise `flag` mode
|
|
15
|
+
* (read-only, report don't fix). An edit pass that regresses the verify signal
|
|
16
|
+
* is reverted.
|
|
17
|
+
*
|
|
18
|
+
* Both gates are no-ops when their config flag is off (the injected deps return a
|
|
19
|
+
* disabled pass), so wiring this into a command is inert until the user enables
|
|
20
|
+
* `verify work` / `enforce guidelines` in /task-config.
|
|
21
|
+
*
|
|
22
|
+
* The sequence is parameterised so each caller supplies the parent-specific glue
|
|
23
|
+
* (notify prefix, the autofix scope fence, and the "mark verified" step — a parent
|
|
24
|
+
* task-list check-off for /task-auto, a no-op for /task). Terminal outcomes are
|
|
25
|
+
* returned as a discriminated GateResult; the caller turns them into its own
|
|
26
|
+
* announce + state changes (the resume command differs: /task-auto-resume vs
|
|
27
|
+
* /task-resume).
|
|
28
|
+
*/
|
|
29
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
30
|
+
import type { RunSingleTaskResult } from './orchestrator.js';
|
|
31
|
+
import type { CommitResult } from './auto-commit.js';
|
|
32
|
+
import type { VerifyOutcome } from './verify-work.js';
|
|
33
|
+
import type { EnforceOutcome } from './enforce-guidelines.js';
|
|
34
|
+
import { type ResolutionOutcome, type ResolutionChoice } from './verify-resolution.js';
|
|
35
|
+
/**
|
|
36
|
+
* The deps the gate sequence drives. A superset of these is built once per command
|
|
37
|
+
* by buildGateDeps; AutoDeps extends this with the planning-only `runChild`. Every
|
|
38
|
+
* gate dep is injected so the sequence is testable without spawning pi, and so the
|
|
39
|
+
* optional gates (verify/enforce/recommend/revert) can be absent in tests or when
|
|
40
|
+
* their config flag is off — the sequence then treats them as a pass / no-op.
|
|
41
|
+
*/
|
|
42
|
+
export interface GateDeps {
|
|
43
|
+
/**
|
|
44
|
+
* Re-run a task's implementation turn (AUTOFIX after a verify FAIL). Resumes
|
|
45
|
+
* the same inner task id, optionally fenced by a plan scope and led by a
|
|
46
|
+
* RE-ATTEMPT banner naming the verification failure.
|
|
47
|
+
*/
|
|
48
|
+
runTask: (ctx: ExtensionCommandContext, cwd: string, title: string, opts?: {
|
|
49
|
+
resumeId?: string;
|
|
50
|
+
onStart?: (taskId: string) => void | Promise<void>;
|
|
51
|
+
planContext?: string;
|
|
52
|
+
fixInstruction?: string;
|
|
53
|
+
}) => Promise<RunSingleTaskResult>;
|
|
54
|
+
/** Snapshot the working tree into one commit after a task passes. */
|
|
55
|
+
commit: (cwd: string, message: string) => Promise<CommitResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Verify the just-finished task's work by RUNNING its composed spec's VERIFY
|
|
58
|
+
* block in the real workspace, then reporting a PASS/FAIL verdict. Absent in
|
|
59
|
+
* tests or when `verify work` is off → the sequence treats it as a pass.
|
|
60
|
+
*/
|
|
61
|
+
verify?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string) => Promise<VerifyOutcome>;
|
|
62
|
+
/**
|
|
63
|
+
* Hold the committed work to AGENTS.md / CLAUDE.md. `edit` (fix in place) only
|
|
64
|
+
* with a clean verify signal to guard against; otherwise `flag` (report only).
|
|
65
|
+
* Absent in tests or when `enforce guidelines` is off → skipped.
|
|
66
|
+
*/
|
|
67
|
+
enforce?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, mode: 'edit' | 'flag') => Promise<EnforceOutcome>;
|
|
68
|
+
/**
|
|
69
|
+
* After a verify FAIL, research whether to recommend AUTOFIX or ACCEPT — only
|
|
70
|
+
* sets which card the picker tints RECOMMENDED; the user always decides. Absent
|
|
71
|
+
* → the picker defaults the recommendation to AUTOFIX.
|
|
72
|
+
*/
|
|
73
|
+
recommend?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string, failReason: string) => Promise<ResolutionOutcome>;
|
|
74
|
+
/**
|
|
75
|
+
* Discard the working-tree edits an `edit` enforcement pass made, restoring the
|
|
76
|
+
* verified task commit (the differential guard's revert). Absent → the guard
|
|
77
|
+
* skips the revert and warns.
|
|
78
|
+
*/
|
|
79
|
+
revert?: (cwd: string) => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/** Inputs the sequence needs that vary per caller. */
|
|
82
|
+
export interface GateParams {
|
|
83
|
+
cwd: string;
|
|
84
|
+
/** The inner task id whose spec is verified and whose work is committed. */
|
|
85
|
+
taskId: string;
|
|
86
|
+
/** The task title — used in commit messages and user-facing notifies. */
|
|
87
|
+
title: string;
|
|
88
|
+
/** Prefix for in-progress notifies (the parent /task-auto id, or the /task id). */
|
|
89
|
+
tag: string;
|
|
90
|
+
/** Scope fence forwarded to an AUTOFIX re-run's refine (siblings for /task-auto;
|
|
91
|
+
* undefined for a bare /task). */
|
|
92
|
+
planContext?: string;
|
|
93
|
+
/** Runs after the verify gate passes/accepts and BEFORE the work is committed —
|
|
94
|
+
* the parent task-list check-off for /task-auto, omitted for /task. */
|
|
95
|
+
onVerified?: () => void | Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Why the gate sequence stopped. `done` means the work verified (or was accepted),
|
|
99
|
+
* was checked off + committed, and enforcement ran — the caller proceeds. The other
|
|
100
|
+
* kinds are terminal: the caller announces them (with its own resume command) and
|
|
101
|
+
* stops. `ctx` is always the live (possibly session-replaced) context the caller
|
|
102
|
+
* must adopt.
|
|
103
|
+
*/
|
|
104
|
+
export type GateResult = {
|
|
105
|
+
kind: 'done';
|
|
106
|
+
ctx: ExtensionCommandContext;
|
|
107
|
+
}
|
|
108
|
+
/** User dismissed the verify-FAIL picker — pause, leave the work unblessed. */
|
|
109
|
+
| {
|
|
110
|
+
kind: 'paused';
|
|
111
|
+
ctx: ExtensionCommandContext;
|
|
112
|
+
reason: string;
|
|
113
|
+
}
|
|
114
|
+
/** An AUTOFIX re-run could not start a fresh session. */
|
|
115
|
+
| {
|
|
116
|
+
kind: 'session-cancelled';
|
|
117
|
+
ctx: ExtensionCommandContext;
|
|
118
|
+
}
|
|
119
|
+
/** An AUTOFIX re-run was interrupted (ESC) and the user declined to steer. */
|
|
120
|
+
| {
|
|
121
|
+
kind: 'interrupted';
|
|
122
|
+
ctx: ExtensionCommandContext;
|
|
123
|
+
}
|
|
124
|
+
/** An AUTOFIX re-run's implementation itself failed. */
|
|
125
|
+
| {
|
|
126
|
+
kind: 'failed';
|
|
127
|
+
ctx: ExtensionCommandContext;
|
|
128
|
+
reason?: string;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Show the boxed two-choice picker after a verify FAIL and return what the user
|
|
132
|
+
* decided. The model-recommended card is placed first so the renderer tints it
|
|
133
|
+
* green; the user ALWAYS makes the final call (there is no auto-pick). Mirrors the
|
|
134
|
+
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker against
|
|
135
|
+
* a remote answer, with the two actions also surfaced as remote buttons.
|
|
136
|
+
*/
|
|
137
|
+
export declare function askVerifyResolution(ctx: ExtensionCommandContext, title: string, failReason: string, rec: ResolutionOutcome): Promise<ResolutionChoice>;
|
|
138
|
+
/**
|
|
139
|
+
* Run the verify + enforce gates against a task's just-finished implementation.
|
|
140
|
+
*
|
|
141
|
+
* Lifted verbatim from /task-auto's per-task loop so the two commands gate
|
|
142
|
+
* identically. Returns a GateResult; `done` means the caller should proceed (the
|
|
143
|
+
* work is verified-or-accepted, checked off, committed, and enforced), every other
|
|
144
|
+
* kind is a terminal stop the caller announces. Never throws for a gate outcome —
|
|
145
|
+
* only a user cancel inside a gate child propagates (handled by the caller's
|
|
146
|
+
* USER_CANCELLED path).
|
|
147
|
+
*/
|
|
148
|
+
export declare function runGatesForTask(ctxIn: ExtensionCommandContext, deps: GateDeps, p: GateParams): Promise<GateResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
|
|
2
|
+
import { SessionUI } from '../remote/bridge.js';
|
|
3
|
+
/**
|
|
4
|
+
* Show the boxed two-choice picker after a verify FAIL and return what the user
|
|
5
|
+
* decided. The model-recommended card is placed first so the renderer tints it
|
|
6
|
+
* green; the user ALWAYS makes the final call (there is no auto-pick). Mirrors the
|
|
7
|
+
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker against
|
|
8
|
+
* a remote answer, with the two actions also surfaced as remote buttons.
|
|
9
|
+
*/
|
|
10
|
+
export async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
11
|
+
const options = resolutionOptions(rec.recommend);
|
|
12
|
+
const question = `Verification FAILED for "${title}".\n\n${failReason}\n\n`
|
|
13
|
+
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}`;
|
|
14
|
+
const answer = await new SessionUI(ctx).ask({
|
|
15
|
+
localTitle: 'Verification failed — how should pi proceed?',
|
|
16
|
+
displayQuestion: question,
|
|
17
|
+
question,
|
|
18
|
+
recommended: options[0].label,
|
|
19
|
+
recommended2: options[1].label,
|
|
20
|
+
allowSkip: false,
|
|
21
|
+
options
|
|
22
|
+
});
|
|
23
|
+
return classifyResolutionAnswer(answer);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run the verify + enforce gates against a task's just-finished implementation.
|
|
27
|
+
*
|
|
28
|
+
* Lifted verbatim from /task-auto's per-task loop so the two commands gate
|
|
29
|
+
* identically. Returns a GateResult; `done` means the caller should proceed (the
|
|
30
|
+
* work is verified-or-accepted, checked off, committed, and enforced), every other
|
|
31
|
+
* kind is a terminal stop the caller announces. Never throws for a gate outcome —
|
|
32
|
+
* only a user cancel inside a gate child propagates (handled by the caller's
|
|
33
|
+
* USER_CANCELLED path).
|
|
34
|
+
*/
|
|
35
|
+
export async function runGatesForTask(ctxIn, deps, p) {
|
|
36
|
+
let active = ctxIn;
|
|
37
|
+
// GATE: actually RUN the task's verification against the just-finished work
|
|
38
|
+
// BEFORE it is checked off or committed. Whether this produced a GENUINE clean
|
|
39
|
+
// pass (a real signal ran and the work met it) also decides how the enforce pass
|
|
40
|
+
// below may behave: only a genuine pass gives a signal to revert against, so only
|
|
41
|
+
// then may enforce edit in place. A no-op pass (no spec), a disabled gate, or an
|
|
42
|
+
// accept-override leaves this false → enforce runs flag-only.
|
|
43
|
+
let verifyCleanPass = false;
|
|
44
|
+
if (deps.verify) {
|
|
45
|
+
active.ui.notify(`${p.tag}: verifying "${p.title}"…`, 'info');
|
|
46
|
+
let verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
47
|
+
// A FAIL no longer dead-stops: offer the boxed AUTOFIX / ACCEPT / dismiss
|
|
48
|
+
// picker. The USER always decides; AUTOFIX loops straight back to the gate
|
|
49
|
+
// as many times as they keep choosing it (no attempt cap).
|
|
50
|
+
while (!verified.ok) {
|
|
51
|
+
const failReason = verified.reason ?? 'did not verify';
|
|
52
|
+
const rec = deps.recommend ?
|
|
53
|
+
await deps.recommend(active, p.cwd, p.title, p.taskId, failReason)
|
|
54
|
+
: { recommend: 'autofix', rationale: failReason };
|
|
55
|
+
const choice = await askVerifyResolution(active, p.title, failReason, rec);
|
|
56
|
+
if (choice.action === 'cancel') {
|
|
57
|
+
return { kind: 'paused', ctx: active, reason: failReason };
|
|
58
|
+
}
|
|
59
|
+
if (choice.action === 'accept') {
|
|
60
|
+
active.ui.notify(`${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
// AUTOFIX: re-run the implementation turn with the failure (and any typed
|
|
64
|
+
// guidance) prepended as a RE-ATTEMPT banner, then re-verify.
|
|
65
|
+
active.ui.notify(`${p.tag}: autofixing "${p.title}"…`, 'info');
|
|
66
|
+
const fixInstruction = choice.guidance ? `${failReason}\n\nUser guidance: ${choice.guidance}` : failReason;
|
|
67
|
+
const fixRes = await deps.runTask(active, p.cwd, p.title, {
|
|
68
|
+
resumeId: p.taskId,
|
|
69
|
+
planContext: p.planContext,
|
|
70
|
+
fixInstruction
|
|
71
|
+
});
|
|
72
|
+
active = fixRes.ctx ?? active;
|
|
73
|
+
if (fixRes.sessionCancelled)
|
|
74
|
+
return { kind: 'session-cancelled', ctx: active };
|
|
75
|
+
if (fixRes.interrupted)
|
|
76
|
+
return { kind: 'interrupted', ctx: active };
|
|
77
|
+
if (!fixRes.ok)
|
|
78
|
+
return { kind: 'failed', ctx: active, reason: fixRes.reason };
|
|
79
|
+
// Resume reuses the same inner task id, so p.taskId is stable.
|
|
80
|
+
verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
81
|
+
}
|
|
82
|
+
// Loop exited because the work verified OR the user accepted the artifact. A
|
|
83
|
+
// genuine clean pass is ok===true with NO reason; a no-op pass or an
|
|
84
|
+
// accept-override (verified.ok still false at break) is NOT a guardable signal.
|
|
85
|
+
verifyCleanPass = verified.ok && !verified.reason;
|
|
86
|
+
}
|
|
87
|
+
// Mark the work verified (parent task-list check-off for /task-auto; no-op for
|
|
88
|
+
// /task) BEFORE committing, so the commit captures the check-off too.
|
|
89
|
+
await p.onVerified?.();
|
|
90
|
+
// Commit the task's work as one snapshot FIRST — before guideline enforcement —
|
|
91
|
+
// so a passing task is durably recorded no matter what enforcement later finds.
|
|
92
|
+
const commit = await deps.commit(p.cwd, `task: ${p.title} (${p.taskId})`);
|
|
93
|
+
if (commit.committed) {
|
|
94
|
+
active.ui.notify(`${p.tag}: committed "${p.title}".`, 'info');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
active.ui.notify(`${p.tag}: not committed (${commit.reason ?? 'unknown'}) — continuing.`, 'warning');
|
|
98
|
+
}
|
|
99
|
+
// With the task committed, hold its work to AGENTS.md / CLAUDE.md — but as a step
|
|
100
|
+
// INSIDE the validation gate, gated by the verify signal (see GateDeps.enforce).
|
|
101
|
+
// Skipped when nothing was committed this round, when enforce is off, or in tests
|
|
102
|
+
// with no enforce dep.
|
|
103
|
+
if (deps.enforce && commit.committed) {
|
|
104
|
+
const mode = verifyCleanPass ? 'edit' : 'flag';
|
|
105
|
+
active.ui.notify(mode === 'edit' ?
|
|
106
|
+
`${p.tag}: enforcing AGENTS.md/CLAUDE.md on "${p.title}"…`
|
|
107
|
+
: `${p.tag}: reviewing "${p.title}" against AGENTS.md/CLAUDE.md (no verify signal — report only)…`, 'info');
|
|
108
|
+
const verdict = await deps.enforce(active, p.cwd, p.title, mode);
|
|
109
|
+
if (!verdict.ok) {
|
|
110
|
+
active.ui.notify(`${p.tag}: guideline ${mode === 'edit' ? 'enforcement' : 'review'} on "${p.title}" — ${verdict.reason ?? 'not clean'} — continuing.`, 'warning');
|
|
111
|
+
}
|
|
112
|
+
if (mode === 'edit') {
|
|
113
|
+
// Commit whatever the pass fixed as its own snapshot. A no-op when it made
|
|
114
|
+
// no edits (nothing to commit) — then there is nothing to re-verify/revert.
|
|
115
|
+
const enforceCommit = await deps.commit(p.cwd, `ENFORCE GUIDELINES: ${p.title} (${p.taskId})`);
|
|
116
|
+
if (enforceCommit.committed) {
|
|
117
|
+
// Differential guard: re-run the verify signal against the enforced
|
|
118
|
+
// tree. A regression ⇒ drop the enforce commit, keep the verified work.
|
|
119
|
+
const after = deps.verify ?
|
|
120
|
+
await deps.verify(active, p.cwd, p.title, p.taskId)
|
|
121
|
+
: { ok: true };
|
|
122
|
+
if (!after.ok) {
|
|
123
|
+
if (deps.revert)
|
|
124
|
+
await deps.revert(p.cwd);
|
|
125
|
+
active.ui.notify(`${p.tag}: guideline fixes regressed verification on "${p.title}" (${(after.reason ?? 'now fails').slice(0, 120)}) — ${deps.revert ? 'reverted them, kept the verified work' : 'left in place (no revert available)'}.`, 'warning');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
active.ui.notify(`${p.tag}: committed guideline fixes for "${p.title}".`, 'info');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 'flag' mode makes no edits — nothing to commit or revert.
|
|
133
|
+
}
|
|
134
|
+
return { kind: 'done', ctx: active };
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
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",
|