@mjasnikovs/pi-task 0.17.0 → 0.17.2

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
+ }
@@ -17,17 +17,27 @@
17
17
  * PASS / FAIL verdict. If the spec's VERIFY is legitimately a no-op (config-only
18
18
  * change, re-read of a file), the model says so and that is a PASS.
19
19
  *
20
- * It must verify the REAL deliverable, not a stand-in. The spec's VERIFY block is
21
- * authored by the same weak model that did the work, so it frequently grades a
22
- * SIMULATION it controls: it reconstructs the build in a scratch dir, compiles with
23
- * options the project does not ship, or only greps the SOURCE for a string — all of
24
- * which pass while the project's own build ships broken output. The prompt therefore
25
- * anchors the child to the project's OWN build/run command and the artifact it SHIPS,
26
- * treats source-text presence as insufficient (a build artifact that still contains an
27
- * un-processed source directive means the build never ran), and treats "I had to add a
28
- * dependency/flag/config the project lacks to make it pass" as the defect itself.
29
- * A/B-proven on the live local model: the old prompt false-passed 3/3 on a
30
- * broken-pipeline fixture; the anchored prompt caught it 3/3, naming the real defect.
20
+ * It must verify the REAL deliverable AS SHIPPED, not a run the verifier itself
21
+ * prepared into passing. The failure class is broader than a bad VERIFY block: the
22
+ * child has `bash`, so it can quietly make almost anything go green export an env
23
+ * var, source a config file, run a different command, rebuild in a scratch dir,
24
+ * fabricate the artifact by hand and then report PASS, masking a defect a fresh
25
+ * checkout or CI run would hit. (This is exactly what sank an mx5 run: the verify
26
+ * child `export`ed the test DB URL its own shell, watched the suite go green, and
27
+ * passed a project whose documented command failed unaided.) The prompt therefore
28
+ * anchors the child to ONE principle: run the project's own commands verbatim in the
29
+ * tree as found, and treat any preparation/repair/substitution it had to perform to
30
+ * reach green as ITSELF the defect while still distinguishing a genuinely-absent
31
+ * EXTERNAL service (an environment gap, not a code fault) from the project mis-wiring
32
+ * how it connects. This generalises across languages and toolchains and assumes no
33
+ * tests, build, or particular runtime.
34
+ *
35
+ * A/B-proven on the live local model (Qwen3.6-35B), 5 runs/arm on a work-around-to-pass
36
+ * fixture (documented command fails unaided; greppable env file makes it pass): the old
37
+ * prompt false-passed 2/5; the new prompt caught it 5/5, each time naming the unwired
38
+ * config. Guards (3 runs/arm): a healthy project still PASSes 3/3 (no false-fail), a
39
+ * genuinely-broken shipped build FAILs 3/3, and a genuine external-service gap — which
40
+ * the OLD prompt wrongly blamed on the code 3/3 — now correctly PASSes 3/3.
31
41
  *
32
42
  * It runs as a GATE right after the implementation turn, BEFORE the task is
33
43
  * checked off or committed. A FAIL stops the /task-auto run exactly like an
@@ -106,37 +116,49 @@ export function buildVerifyPrompt(spec) {
106
116
  'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block are the contract):',
107
117
  spec.trim(),
108
118
  '',
109
- 'How to verify — verify the REAL deliverable, not a stand-in:',
110
- "1. Run the project's OWN build/run/serve command the one it actually ships",
111
- ' (the scripts in package.json, the command the spec names). Run it exactly as',
112
- ' the project defines it, then inspect the artifact it PRODUCES. Also run the',
113
- " spec's VERIFY block but the project's real build/output is the bar.",
114
- '2. NEVER substitute your own build, compile step, plugin, flag, scratch transform,',
115
- ' or config to make a check pass. If the VERIFY block reconstructs the output in',
116
- ' a temp/scratch dir, builds with options the project does not use, or only greps',
117
- ' the SOURCE for a string, that is INSUFFICIENT — it proves the code *could* work,',
118
- " not that the shipping pipeline *does*. You must still run the project's real",
119
- ' build and judge its real output.',
120
- '3. If, to make any check pass, you find you must add an import, plugin, flag, or',
121
- ' config the project itself does not have, STOP: that missing piece IS the defect.',
122
- ' Report FAIL and name exactly what the project is missing.',
123
- '4. Presence of a token/directive/string in a SOURCE file is NOT verification. Judge',
124
- ' the produced/shipped artifact and the actual runtime behavior. (A build directive',
125
- ' belongs in the source; if that same raw directive SURVIVES into the built output,',
126
- ' the build did not run that is a FAIL, not a PASS.)',
127
- '5. Treat the ACCEPTANCE criteria as the bar. If a VERIFY command fails, or its',
128
- ' output (or the real build output) contradicts an ACCEPTANCE criterion, the work',
129
- ' has NOT verified.',
130
- '6. If a check depends on something this environment genuinely lacks (a service, a',
131
- ' network resource) and that is clearly an environment gap rather than a defect in',
132
- ' the code, note it and judge the rest. Do not fail for a missing external service.',
133
- '7. If the spec legitimately has no runnable verification (a pure docs/config change',
119
+ 'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
120
+ 'checkout (or CI run) would experience it:',
121
+ '',
122
+ "1. Run the project's OWN commands the verbatim scripts / targets / binaries it",
123
+ ' ships (package.json scripts, Makefile targets, the command the spec names) —',
124
+ ' exactly as written, in the workspace as you found it. Judge the artifact or',
125
+ " output they actually PRODUCE. The project's own command and its real output are",
126
+ " the bar; also run the spec's VERIFY block, but it does not override that bar.",
127
+ '',
128
+ '2. Do NOT prepare, repair, reconfigure, or stand in for the run to make a check',
129
+ ' pass. Concretely, to reach a green result you must NOT: set or export an',
130
+ ' environment variable, source an env file, add or change a flag, edit or replace',
131
+ ' the command, install or add a dependency / plugin / import / config the project',
132
+ ' lacks, create or pre-populate files or state by hand, or rebuild / compile in a',
133
+ ' scratch dir or with options the project does not itself use. If a check only goes',
134
+ ' green AFTER you intervene like that, then the very thing you had to do IS the',
135
+ ' defect: the project does not work as shipped. Report FAIL and name the missing or',
136
+ ' broken wiring exactly (e.g. "shipped command `<cmd>` fails unaided because <why>").',
137
+ '',
138
+ '3. Presence of a token / directive / string in a SOURCE file is NOT verification.',
139
+ ' Judge the produced artifact and the real runtime behavior. A raw build directive',
140
+ ' that SURVIVES into the built output means the build never ran that is a FAIL.',
141
+ '',
142
+ '4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
143
+ ' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
144
+ '',
145
+ '5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
146
+ ' service or network resource (a database server, an API host) that the project',
147
+ ' documents as a prerequisite. If a command fails purely because such an external',
148
+ ' service is ABSENT from this machine — and NOT because the project misconfigures',
149
+ ' how it connects — note that as an environment gap and judge the rest; do not fail',
150
+ ' the code for it. But a command that fails because of how the PROJECT ITSELF is',
151
+ ' wired (config it owns but does not load, a wrong default, a command that cannot',
152
+ ' run unaided) is a defect, not an environment gap.',
153
+ '',
154
+ '6. If the spec legitimately has no runnable verification (a pure docs / config change',
134
155
  ' with nothing to build or run), validating it cleanly is a PASS.',
135
- '8. Do NOT edit anything to make a check pass. Report what you actually saw.',
156
+ '',
157
+ '7. Do NOT edit anything to make a check pass. Report what you actually saw.',
136
158
  '',
137
159
  'When you are done, output EXACTLY ONE of these as the final line:',
138
- ' WORK-VERIFIED: PASS (the real build/run produced an artifact that meets the spec)',
139
- ' WORK-VERIFIED: FAIL <text> (the real build/output failed or does not meet the spec; say what failed)',
160
+ " WORK-VERIFIED: PASS (the project's own command, run unaided, met the spec)",
161
+ ' WORK-VERIFIED: FAIL <text> (the shipped command failed or did not meet the spec; say what failed)',
140
162
  'Output the verdict line verbatim — it is parsed mechanically.'
141
163
  ].join('\n');
142
164
  }
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.2",
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",