@mjasnikovs/pi-task 0.17.0 → 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.
@@ -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.17.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",