@mjasnikovs/pi-task 0.14.12 → 0.14.14
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/config.d.ts +1 -0
- package/dist/config/config.js +2 -1
- package/dist/config/register.js +5 -0
- package/dist/task/auto-orchestrator.d.ts +11 -0
- package/dist/task/auto-orchestrator.js +99 -0
- package/dist/task/question-box.js +10 -5
- package/dist/task/verify-work.d.ts +68 -0
- package/dist/task/verify-work.js +161 -0
- package/dist/task/widget.d.ts +3 -2
- package/dist/task/widget.js +3 -3
- package/package.json +1 -1
package/dist/config/config.d.ts
CHANGED
package/dist/config/config.js
CHANGED
|
@@ -7,7 +7,8 @@ const DEFAULTS = {
|
|
|
7
7
|
compressReasoning: true,
|
|
8
8
|
autoCommit: true,
|
|
9
9
|
orientation: true,
|
|
10
|
-
enforceGuidelines: false
|
|
10
|
+
enforceGuidelines: false,
|
|
11
|
+
verifyWork: false
|
|
11
12
|
};
|
|
12
13
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
13
14
|
const _g = globalThis;
|
package/dist/config/register.js
CHANGED
|
@@ -22,6 +22,11 @@ const ITEMS = [
|
|
|
22
22
|
id: 'enforceGuidelines',
|
|
23
23
|
label: 'enforce guidelines',
|
|
24
24
|
description: 'Before each /task-auto commit, re-check the work against AGENTS.md/CLAUDE.md and fix drift'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'verifyWork',
|
|
28
|
+
label: 'verify work',
|
|
29
|
+
description: "After each /task-auto task, RUN its spec's VERIFY block in the workspace and report a PASS/FAIL verdict"
|
|
25
30
|
}
|
|
26
31
|
];
|
|
27
32
|
function makeTheme(theme) {
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-c
|
|
|
2
2
|
import type { RunSingleTaskResult } from './orchestrator.js';
|
|
3
3
|
import { type CommitResult } from './auto-commit.js';
|
|
4
4
|
import { type EnforceOutcome } from './enforce-guidelines.js';
|
|
5
|
+
import { type VerifyOutcome } from './verify-work.js';
|
|
5
6
|
/**
|
|
6
7
|
* Injectable seams so the planner and loop are testable without spawning pi.
|
|
7
8
|
* `runChild` is used by planAuto; `runTask` is used by runAutoLoop.
|
|
@@ -29,6 +30,16 @@ export interface AutoDeps {
|
|
|
29
30
|
* enforcement runs and using it for the loader widget throws "stale ctx".
|
|
30
31
|
*/
|
|
31
32
|
enforce?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string) => Promise<EnforceOutcome>;
|
|
33
|
+
/**
|
|
34
|
+
* Verify the just-finished task's work by RUNNING its composed spec's VERIFY
|
|
35
|
+
* block in the real workspace (a fresh child of the same local model, with a
|
|
36
|
+
* `bash` tool), then reporting a PASS/FAIL verdict. Optional: absent in tests
|
|
37
|
+
* or when the `verifyWork` flag is off, in which case the loop treats it as a
|
|
38
|
+
* pass. Runs BEFORE the task is checked off/committed and is a hard GATE: ok
|
|
39
|
+
* === false stops the run and fails the task (left unchecked so resume re-runs
|
|
40
|
+
* it). Needs the inner taskId to read that task's spec.
|
|
41
|
+
*/
|
|
42
|
+
verify?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, taskId: string) => Promise<VerifyOutcome>;
|
|
32
43
|
}
|
|
33
44
|
/**
|
|
34
45
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -16,6 +16,7 @@ import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, check
|
|
|
16
16
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
17
17
|
import { gitCommitAll } from './auto-commit.js';
|
|
18
18
|
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
19
|
+
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
19
20
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
20
21
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
21
22
|
import { runPhaseChild, prependHint, formatLoopHint, USER_CANCELLED } from './child-runner.js';
|
|
@@ -481,6 +482,86 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
484
|
});
|
|
485
|
+
},
|
|
486
|
+
verify: async (verifyCtx, cwd2, taskTitle, taskId) => {
|
|
487
|
+
if (!getConfig().verifyWork) {
|
|
488
|
+
return { ok: true, reason: 'disabled' };
|
|
489
|
+
}
|
|
490
|
+
// The spec to verify against is the composed spec committed in the
|
|
491
|
+
// task file. A task that never reached compose has no spec section —
|
|
492
|
+
// runWorkVerification treats a null spec as a no-op pass.
|
|
493
|
+
let spec;
|
|
494
|
+
try {
|
|
495
|
+
const { body } = await readTaskFile(cwd2, taskId);
|
|
496
|
+
spec = extractSpecForVerification(body);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
spec = null;
|
|
500
|
+
}
|
|
501
|
+
return runWorkVerification({
|
|
502
|
+
cwd: cwd2,
|
|
503
|
+
signal,
|
|
504
|
+
spec,
|
|
505
|
+
// Same unguarded child as enforce: no wall-clock timeout (a build
|
|
506
|
+
// or test suite legitimately takes minutes) and exact-match loop
|
|
507
|
+
// guard only. Re-running the same VERIFY command is the job, so the
|
|
508
|
+
// path-revisit heuristic is disabled; only a literally-identical
|
|
509
|
+
// call repeated past threshold trips, and that warns rather than
|
|
510
|
+
// blocks.
|
|
511
|
+
runChild: async (tools, prompt, sig) => {
|
|
512
|
+
lastLine = undefined;
|
|
513
|
+
contextUsage = undefined;
|
|
514
|
+
const startedAt = Date.now();
|
|
515
|
+
const verifyLogPath = path.join(tasksDir(cwd2), 'verify-debug.log');
|
|
516
|
+
const logVerify = (msg) => {
|
|
517
|
+
void fsp
|
|
518
|
+
.appendFile(verifyLogPath, `${new Date().toISOString()} ${msg}\n`)
|
|
519
|
+
.catch(() => { });
|
|
520
|
+
};
|
|
521
|
+
logVerify(`=== verify start: ${taskTitle} ===`);
|
|
522
|
+
const stopLoader = startAutoLoader(verifyCtx, () => ({
|
|
523
|
+
title: taskTitle,
|
|
524
|
+
kind: 'verify',
|
|
525
|
+
step: 'verify',
|
|
526
|
+
stepNum: 1,
|
|
527
|
+
stepTotal: 1,
|
|
528
|
+
startedAt,
|
|
529
|
+
lastLine,
|
|
530
|
+
contextUsage
|
|
531
|
+
}));
|
|
532
|
+
try {
|
|
533
|
+
const r = await runWorker({
|
|
534
|
+
prompt,
|
|
535
|
+
cwd: cwd2,
|
|
536
|
+
signal: sig,
|
|
537
|
+
tools,
|
|
538
|
+
timeoutMs: 0,
|
|
539
|
+
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
540
|
+
onLine: line => {
|
|
541
|
+
lastLine = line;
|
|
542
|
+
logVerify(line);
|
|
543
|
+
},
|
|
544
|
+
onContextUsage: snapshot => {
|
|
545
|
+
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
if (r.loopHit) {
|
|
549
|
+
logVerify(`=== verify LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
550
|
+
verifyCtx.ui.notify(`${taskTitle}: verify worker looped past the nudges — continuing (not blocked).`, 'warning');
|
|
551
|
+
}
|
|
552
|
+
const failure = classifyEnforceChildFailure(r);
|
|
553
|
+
logVerify(failure ?
|
|
554
|
+
`=== verify end: FAIL — ${failure} ===`
|
|
555
|
+
: '=== verify end: verdict captured ===');
|
|
556
|
+
if (failure)
|
|
557
|
+
throw new Error(failure);
|
|
558
|
+
return r.text;
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
stopLoader();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
484
565
|
}
|
|
485
566
|
};
|
|
486
567
|
}
|
|
@@ -586,6 +667,24 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
586
667
|
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
587
668
|
return;
|
|
588
669
|
}
|
|
670
|
+
// GATE: actually RUN the task's verification against the just-finished
|
|
671
|
+
// work BEFORE it is checked off or committed. The composed spec ships a
|
|
672
|
+
// VERIFY block, but nothing in the pipeline ever executed it — a task
|
|
673
|
+
// that did not work was checked off identically to one that did. Run it
|
|
674
|
+
// now in the real workspace; a FAIL stops the whole run exactly like an
|
|
675
|
+
// implementation failure — the task is left UNCHECKED and UNCOMMITTED so
|
|
676
|
+
// /task-auto-resume re-runs it (rather than blessing broken work). Off
|
|
677
|
+
// by default (deps.verify returns a disabled no-op) and a no-op when the
|
|
678
|
+
// task has no composed spec to verify against.
|
|
679
|
+
if (deps.verify) {
|
|
680
|
+
active.ui.notify(`${id}: verifying "${next.title}"…`, 'info');
|
|
681
|
+
const verified = await deps.verify(active, cwd, next.title, res.taskId);
|
|
682
|
+
if (!verified.ok) {
|
|
683
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
684
|
+
announceDone(active, `${id} stopped at "${next.title}" — ${verified.reason ?? 'did not verify'} — fix and run /task-auto-resume.`, 'error');
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
589
688
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
590
689
|
// allocated TASK_NNNN id (never empty here). checkOffTask tolerates an
|
|
591
690
|
// empty id by writing a plain checked line, but that path is unreachable.
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { getKeybindings, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
19
19
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
20
|
-
/** Left gutter: two columns reserved for the
|
|
20
|
+
/** Left gutter: two columns reserved for the cursor bar so boxes stay aligned. */
|
|
21
21
|
const GUTTER = 2;
|
|
22
|
+
/** Cap the dialog at a readable column count. Without this the boxes stretch the
|
|
23
|
+
* full terminal width on wide/ultrawide screens — long question lines become
|
|
24
|
+
* hard to scan and short answers leave a huge empty right margin inside the box. */
|
|
25
|
+
const MAX_CONTENT_WIDTH = 96;
|
|
22
26
|
const RECOMMENDED_TAG = 'RECOMMENDED';
|
|
23
27
|
/** Smallest box that still fits "┌─ RECOMMENDED ─┐". Below this we drop the tag. */
|
|
24
28
|
const TAG_MIN_BOX_WIDTH = visibleWidth(`┌─ ${RECOMMENDED_TAG} ─┐`);
|
|
@@ -49,9 +53,10 @@ function renderCard(card, selected, boxWidth, c) {
|
|
|
49
53
|
const bodyLines = wrapTextWithAnsi(card.label, innerWidth);
|
|
50
54
|
const body = (bodyLines.length > 0 ? bodyLines : ['']).map(line => borderFn('│ ') + padTo(line, innerWidth) + borderFn(' │'));
|
|
51
55
|
const bottom = borderFn(`└${dash(boxWidth - 2)}┘`);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
// A full-height accent bar in the gutter marks the selected card — far more
|
|
57
|
+
// visible than a single "▸" glyph on the top border alone.
|
|
58
|
+
const cursor = selected ? c.arrow('▌') + ' ' : ' ';
|
|
59
|
+
return [cursor + top, ...body.map(b => cursor + b), cursor + bottom];
|
|
55
60
|
}
|
|
56
61
|
/**
|
|
57
62
|
* Lay the whole dialog out for `width` columns: a QUESTION label + wrapped
|
|
@@ -59,7 +64,7 @@ function renderCard(card, selected, boxWidth, c) {
|
|
|
59
64
|
* highlighted with an accent border and "▸" arrow), and a key-hint footer.
|
|
60
65
|
*/
|
|
61
66
|
export function renderQuestionBox(width, question, cards, selected, c, hintText = HINT_TEXT) {
|
|
62
|
-
const W = Math.max(width, 12);
|
|
67
|
+
const W = Math.min(Math.max(width, 12), MAX_CONTENT_WIDTH);
|
|
63
68
|
const boxWidth = Math.max(6, W - GUTTER);
|
|
64
69
|
const out = [''];
|
|
65
70
|
out.push(' ' + c.questionLabel(QUESTION_LABEL));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verification child gets exactly two tools: `read` and `bash`.
|
|
3
|
+
*
|
|
4
|
+
* - `bash` IS the point: it lets the child actually execute the spec's VERIFY
|
|
5
|
+
* commands (or whatever check the ACCEPTANCE criteria imply) and see the real
|
|
6
|
+
* exit codes and output, rather than reasoning about whether the code "looks"
|
|
7
|
+
* correct.
|
|
8
|
+
* - `read` lets it inspect a file the VERIFY output points at (a build error's
|
|
9
|
+
* source line, a config it just validated) to characterise a failure precisely.
|
|
10
|
+
* - No `edit`/`write`: this pass reports a verdict, it does not change the tree.
|
|
11
|
+
* Keeping it read-only means a verify run can never itself introduce a change
|
|
12
|
+
* that needs re-verifying, and never scaffolds the junk-file runaway that the
|
|
13
|
+
* enforce pass had to drop `write` to stop.
|
|
14
|
+
*/
|
|
15
|
+
declare const VERIFY_TOOLS = "read,bash";
|
|
16
|
+
export interface VerifyOutcome {
|
|
17
|
+
/** true → the work verified (or verification disabled / nothing to verify).
|
|
18
|
+
* false → the child reported the work does NOT satisfy its spec, or the pass
|
|
19
|
+
* could not run / produced no verdict. */
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** Short, human-readable reason. Always set when ok === false; on the pass
|
|
22
|
+
* path set to the no-op cause ('disabled', 'no spec to verify'). */
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Slice the delivered spec (GOAL / CONSTRAINTS / ACCEPTANCE / VERIFY) out of a
|
|
27
|
+
* task file body. The composed spec lives under a `## spec` header and runs until
|
|
28
|
+
* the next top-level `## ` section (`## phase timings`). Returns null when no spec
|
|
29
|
+
* section is present (e.g. a task that never reached compose), which the caller
|
|
30
|
+
* treats as a pass — there is nothing to verify against.
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractSpecForVerification(taskBody: string): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
35
|
+
* without spawning pi. The contract: run the spec's own verification in the real
|
|
36
|
+
* workspace, judge against ACCEPTANCE, and end on exactly one verdict line.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildVerifyPrompt(spec: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
|
|
41
|
+
* (the model discusses before concluding, and bash output may echo the word
|
|
42
|
+
* "VERIFY", so a distinct token and last-match win matter).
|
|
43
|
+
*
|
|
44
|
+
* No marker at all is NOT a pass: a verification that cannot state a verdict is a
|
|
45
|
+
* gray area, and the contract is that unverified work is reported as such.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseVerifyVerdict(text: string): {
|
|
48
|
+
pass: boolean;
|
|
49
|
+
detail: string;
|
|
50
|
+
};
|
|
51
|
+
export interface VerificationDeps {
|
|
52
|
+
cwd: string;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
/** The composed spec (GOAL/ACCEPTANCE/VERIFY) of the task just committed. When
|
|
55
|
+
* null/empty the pass is a no-op (ok: true). */
|
|
56
|
+
spec: string | null;
|
|
57
|
+
/** Runs the verification child and returns its assistant text. Injected so
|
|
58
|
+
* the orchestrator wires the real child runner and tests use a fake. */
|
|
59
|
+
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run the verification pass for one task. A missing spec is a pass. Otherwise run
|
|
63
|
+
* the child against the real workspace and turn its verdict into an ok/blocked
|
|
64
|
+
* outcome. Never throws (except a user cancel, which propagates so the loop's
|
|
65
|
+
* USER_CANCELLED handler reports a clean "cancelled — resume").
|
|
66
|
+
*/
|
|
67
|
+
export declare function runWorkVerification(deps: VerificationDeps): Promise<VerifyOutcome>;
|
|
68
|
+
export { VERIFY_TOOLS };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work verification for /task-auto.
|
|
3
|
+
*
|
|
4
|
+
* The root failure this addresses: pi-task never *runs* any verification. Each
|
|
5
|
+
* task's composed spec carries a VERIFY block (and ACCEPTANCE criteria), but that
|
|
6
|
+
* block is only authored and presence-checked — never executed. The task is then
|
|
7
|
+
* marked `completed` at handoff. So a task whose implementation does not actually
|
|
8
|
+
* work (a SPA that won't build, a route wired to a dead stub, a CI file nobody
|
|
9
|
+
* runs) is indistinguishable from one that does.
|
|
10
|
+
*
|
|
11
|
+
* This pass closes that gap WITHOUT assuming any particular shape of project. It
|
|
12
|
+
* does NOT hardcode `build`, an HTTP probe, a server boot, or a test command —
|
|
13
|
+
* many tasks have none of those. Instead it hands the just-committed spec (GOAL /
|
|
14
|
+
* ACCEPTANCE / VERIFY) to a fresh child of the SAME local model, gives it a `read`
|
|
15
|
+
* and a `bash` tool in the real workspace, and lets the model do its job: run the
|
|
16
|
+
* verification the spec already declares, observe the REAL output, and report a
|
|
17
|
+
* PASS / FAIL verdict. If the spec's VERIFY is legitimately a no-op (config-only
|
|
18
|
+
* change, re-read of a file), the model says so and that is a PASS.
|
|
19
|
+
*
|
|
20
|
+
* It runs as a GATE right after the implementation turn, BEFORE the task is
|
|
21
|
+
* checked off or committed. A FAIL stops the /task-auto run exactly like an
|
|
22
|
+
* implementation failure: the task is left unchecked and uncommitted so
|
|
23
|
+
* /task-auto-resume re-runs it, rather than blessing work that does not run.
|
|
24
|
+
*
|
|
25
|
+
* Tools: `read` + `bash` only. No `edit`/`write` — verification observes, it does
|
|
26
|
+
* not fix (fixing committed work is the enforcement pass's job, and a verify pass
|
|
27
|
+
* that also edits would blur "did it work" with "make it work"). `bash` is what
|
|
28
|
+
* makes this real: it is the difference between reading the VERIFY block and
|
|
29
|
+
* RUNNING it.
|
|
30
|
+
*
|
|
31
|
+
* Gated by the `verifyWork` config flag. With the flag off, or with no spec to
|
|
32
|
+
* verify, this is a pass (ok: true).
|
|
33
|
+
*/
|
|
34
|
+
import { USER_CANCELLED } from './child-runner.js';
|
|
35
|
+
/**
|
|
36
|
+
* The verification child gets exactly two tools: `read` and `bash`.
|
|
37
|
+
*
|
|
38
|
+
* - `bash` IS the point: it lets the child actually execute the spec's VERIFY
|
|
39
|
+
* commands (or whatever check the ACCEPTANCE criteria imply) and see the real
|
|
40
|
+
* exit codes and output, rather than reasoning about whether the code "looks"
|
|
41
|
+
* correct.
|
|
42
|
+
* - `read` lets it inspect a file the VERIFY output points at (a build error's
|
|
43
|
+
* source line, a config it just validated) to characterise a failure precisely.
|
|
44
|
+
* - No `edit`/`write`: this pass reports a verdict, it does not change the tree.
|
|
45
|
+
* Keeping it read-only means a verify run can never itself introduce a change
|
|
46
|
+
* that needs re-verifying, and never scaffolds the junk-file runaway that the
|
|
47
|
+
* enforce pass had to drop `write` to stop.
|
|
48
|
+
*/
|
|
49
|
+
const VERIFY_TOOLS = 'read,bash';
|
|
50
|
+
/**
|
|
51
|
+
* Slice the delivered spec (GOAL / CONSTRAINTS / ACCEPTANCE / VERIFY) out of a
|
|
52
|
+
* task file body. The composed spec lives under a `## spec` header and runs until
|
|
53
|
+
* the next top-level `## ` section (`## phase timings`). Returns null when no spec
|
|
54
|
+
* section is present (e.g. a task that never reached compose), which the caller
|
|
55
|
+
* treats as a pass — there is nothing to verify against.
|
|
56
|
+
*/
|
|
57
|
+
export function extractSpecForVerification(taskBody) {
|
|
58
|
+
const lines = taskBody.split('\n');
|
|
59
|
+
let start = -1;
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
if (/^##\s+spec\s*$/i.test(lines[i])) {
|
|
62
|
+
start = i + 1;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (start === -1)
|
|
67
|
+
return null;
|
|
68
|
+
let end = lines.length;
|
|
69
|
+
for (let i = start; i < lines.length; i++) {
|
|
70
|
+
if (/^##\s+\S/.test(lines[i])) {
|
|
71
|
+
end = i;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const spec = lines.slice(start, end).join('\n').trim();
|
|
76
|
+
return spec.length > 0 ? spec : null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
80
|
+
* without spawning pi. The contract: run the spec's own verification in the real
|
|
81
|
+
* workspace, judge against ACCEPTANCE, and end on exactly one verdict line.
|
|
82
|
+
*/
|
|
83
|
+
export function buildVerifyPrompt(spec) {
|
|
84
|
+
return [
|
|
85
|
+
'You are a strict verification pass running right after an AI coding agent',
|
|
86
|
+
'finished a task and committed it. The agent is known to mark work "done"',
|
|
87
|
+
'without ever running it, so DO NOT trust that the implementation works —',
|
|
88
|
+
'prove it by running the verification this task ships with.',
|
|
89
|
+
'',
|
|
90
|
+
'You have a `read` tool and a `bash` tool — nothing else. You can run any',
|
|
91
|
+
'command and read any file, but you CANNOT edit or create files. Your job is',
|
|
92
|
+
'to VERIFY, not to fix.',
|
|
93
|
+
'',
|
|
94
|
+
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block are the contract):',
|
|
95
|
+
spec.trim(),
|
|
96
|
+
'',
|
|
97
|
+
'How to verify:',
|
|
98
|
+
"1. Run the commands in the spec's VERIFY block, in order, with your `bash`",
|
|
99
|
+
' tool, from the repository root. Observe the REAL exit codes and output —',
|
|
100
|
+
' do not assume success.',
|
|
101
|
+
'2. Treat the ACCEPTANCE criteria as the bar. If a VERIFY command fails, or',
|
|
102
|
+
' its output contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
103
|
+
'3. If a VERIFY command depends on something this environment genuinely lacks',
|
|
104
|
+
' (a service, a network resource) and that is clearly an environment gap',
|
|
105
|
+
' rather than a defect in the code, note it and judge the rest. Do not fail',
|
|
106
|
+
' the task for a missing external service it cannot control.',
|
|
107
|
+
'4. If the spec legitimately has no runnable verification (a config-only or',
|
|
108
|
+
' docs-only change whose VERIFY just re-reads or validates a file), running',
|
|
109
|
+
' that re-read/validation cleanly is a PASS.',
|
|
110
|
+
'5. Do NOT edit anything to make a check pass. Report what you actually saw.',
|
|
111
|
+
'',
|
|
112
|
+
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
113
|
+
' WORK-VERIFIED: PASS (every check ran and the work meets its spec)',
|
|
114
|
+
' WORK-VERIFIED: FAIL <text> (a check failed or the work does not meet its spec; say what failed)',
|
|
115
|
+
'Output the verdict line verbatim — it is parsed mechanically.'
|
|
116
|
+
].join('\n');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
|
|
120
|
+
* (the model discusses before concluding, and bash output may echo the word
|
|
121
|
+
* "VERIFY", so a distinct token and last-match win matter).
|
|
122
|
+
*
|
|
123
|
+
* No marker at all is NOT a pass: a verification that cannot state a verdict is a
|
|
124
|
+
* gray area, and the contract is that unverified work is reported as such.
|
|
125
|
+
*/
|
|
126
|
+
export function parseVerifyVerdict(text) {
|
|
127
|
+
const re = /WORK-VERIFIED:\s*(PASS|FAIL)\b[ \t]*(.*)/gi;
|
|
128
|
+
let last = null;
|
|
129
|
+
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
130
|
+
last = m;
|
|
131
|
+
if (!last)
|
|
132
|
+
return { pass: false, detail: 'no verdict emitted' };
|
|
133
|
+
const pass = last[1].toUpperCase() === 'PASS';
|
|
134
|
+
return { pass, detail: pass ? '' : last[2].trim() || 'unspecified failure' };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run the verification pass for one task. A missing spec is a pass. Otherwise run
|
|
138
|
+
* the child against the real workspace and turn its verdict into an ok/blocked
|
|
139
|
+
* outcome. Never throws (except a user cancel, which propagates so the loop's
|
|
140
|
+
* USER_CANCELLED handler reports a clean "cancelled — resume").
|
|
141
|
+
*/
|
|
142
|
+
export async function runWorkVerification(deps) {
|
|
143
|
+
if (!deps.spec || deps.spec.trim().length === 0) {
|
|
144
|
+
return { ok: true, reason: 'no spec to verify' };
|
|
145
|
+
}
|
|
146
|
+
let text;
|
|
147
|
+
try {
|
|
148
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec), deps.signal);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
152
|
+
throw err;
|
|
153
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
154
|
+
return { ok: false, reason: `verification pass could not run: ${msg}` };
|
|
155
|
+
}
|
|
156
|
+
const verdict = parseVerifyVerdict(text);
|
|
157
|
+
if (verdict.pass)
|
|
158
|
+
return { ok: true };
|
|
159
|
+
return { ok: false, reason: `work did not verify: ${verdict.detail}` };
|
|
160
|
+
}
|
|
161
|
+
export { VERIFY_TOOLS };
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -51,8 +51,9 @@ export interface AutoLoaderState {
|
|
|
51
51
|
contextUsage?: ContextSnapshot;
|
|
52
52
|
/** Which /task-auto stage this loader is for. Defaults to 'planning' (the
|
|
53
53
|
* numbered clarify/decompose steps); 'enforce' is the per-task guideline
|
|
54
|
-
* pass
|
|
55
|
-
|
|
54
|
+
* pass and 'verify' is the per-task work-verification pass, neither of which
|
|
55
|
+
* has step numbering. */
|
|
56
|
+
kind?: 'planning' | 'enforce' | 'verify';
|
|
56
57
|
}
|
|
57
58
|
export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetTheme): string[];
|
|
58
59
|
/**
|
package/dist/task/widget.js
CHANGED
|
@@ -121,9 +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 = s.kind === 'enforce' ?
|
|
125
|
-
`
|
|
126
|
-
|
|
124
|
+
let detail = s.kind === 'enforce' ? `enforcing guidelines · ${elapsed}`
|
|
125
|
+
: s.kind === 'verify' ? `verifying work · ${elapsed}`
|
|
126
|
+
: `planning ${s.stepNum}/${s.stepTotal} ${s.step} · ${elapsed}`;
|
|
127
127
|
if (s.contextUsage) {
|
|
128
128
|
const ctxDetail = formatContextDetail(s.contextUsage, theme);
|
|
129
129
|
if (ctxDetail)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.14",
|
|
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",
|