@mjasnikovs/pi-task 0.17.13 → 0.17.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/task/auto-commit.d.ts +6 -0
- package/dist/task/auto-commit.js +1 -1
- package/dist/task/gate-deps.d.ts +10 -0
- package/dist/task/gate-deps.js +73 -2
- package/dist/task/lint-fix.d.ts +73 -0
- package/dist/task/lint-fix.js +146 -0
- package/dist/task/substitution-probe.d.ts +51 -0
- package/dist/task/substitution-probe.js +63 -0
- package/dist/task/task-gates.d.ts +29 -0
- package/dist/task/task-gates.js +43 -2
- package/dist/task/verify-work.d.ts +14 -1
- package/dist/task/verify-work.js +55 -2
- package/dist/task/widget.d.ts +3 -2
- package/dist/task/widget.js +2 -1
- package/package.json +1 -1
|
@@ -12,6 +12,12 @@ export interface CommitResult {
|
|
|
12
12
|
/** Short, human-readable reason when committed === false. */
|
|
13
13
|
reason?: string;
|
|
14
14
|
}
|
|
15
|
+
export declare function git(cwd: string, args: string[], signal: AbortSignal | undefined, spawnFn?: SpawnFn): Promise<{
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
aborted: boolean;
|
|
20
|
+
}>;
|
|
15
21
|
/**
|
|
16
22
|
* Stage everything (`git add -A`) and commit it with `message`. Honors
|
|
17
23
|
* .gitignore via git itself. Never throws — failures surface as
|
package/dist/task/auto-commit.js
CHANGED
|
@@ -11,7 +11,7 @@ function firstLine(s) {
|
|
|
11
11
|
const line = s.split('\n').find(l => l.trim().length > 0);
|
|
12
12
|
return (line ?? s).trim();
|
|
13
13
|
}
|
|
14
|
-
async function git(cwd, args, signal, spawnFn) {
|
|
14
|
+
export async function git(cwd, args, signal, spawnFn) {
|
|
15
15
|
const r = await runChildDefault({ command: 'git', args }, cwd, signal, { mode: 'text' }, spawnFn);
|
|
16
16
|
return { stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode, aborted: r.aborted };
|
|
17
17
|
}
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { GateDeps } from './task-gates.js';
|
|
2
|
+
import { type ChangedFile } from './substitution-probe.js';
|
|
2
3
|
/** A function that re-runs a task's implementation turn (AUTOFIX). Injected by the
|
|
3
4
|
* command so this module stays free of the orchestrators (avoids an import cycle). */
|
|
4
5
|
export type RunTaskFn = GateDeps['runTask'];
|
|
6
|
+
/**
|
|
7
|
+
* Collect the task's changed files as pure GIT SHAPE — path + added-line count,
|
|
8
|
+
* no content, no language parsing — for the self-verification probe. Before the
|
|
9
|
+
* task commit the work is the tree-vs-HEAD diff (+ untracked files, counted from
|
|
10
|
+
* disk); on the post-enforce re-verify the tree is clean, so fall back to the last
|
|
11
|
+
* commit. Failures degrade to an empty list — the probe is a sharpener, never a
|
|
12
|
+
* blocker.
|
|
13
|
+
*/
|
|
14
|
+
export declare function collectChangedFiles(cwd: string, signal?: AbortSignal): Promise<ChangedFile[]>;
|
|
5
15
|
/**
|
|
6
16
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
7
17
|
* implementation re-runner, injected by the caller. The returned object also drives
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -16,16 +16,60 @@
|
|
|
16
16
|
import * as fsp from 'node:fs/promises';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { tasksDir, readTaskFile, appendGateRecord } from './task-io.js';
|
|
19
|
-
import { gitCommitAll, gitDropLastCommit } from './auto-commit.js';
|
|
19
|
+
import { gitCommitAll, gitDropLastCommit, git } from './auto-commit.js';
|
|
20
20
|
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
21
21
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
22
22
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
23
23
|
import { researchResolution } from './verify-resolution.js';
|
|
24
|
+
import { findSubstitutionSuspects } from './substitution-probe.js';
|
|
25
|
+
import { runBoundedLintFix } from './lint-fix.js';
|
|
24
26
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
25
27
|
import { formatLoopHint } from './child-runner.js';
|
|
26
28
|
import { getConfig } from '../config/config.js';
|
|
27
29
|
import { startAutoLoader } from './widget.js';
|
|
28
30
|
import { resolveContextUsage } from './context-usage.js';
|
|
31
|
+
/** Keep the gate machinery's own artifacts out of every git pathspec below. */
|
|
32
|
+
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
33
|
+
const splitLines = (s) => s
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map(l => l.trim())
|
|
36
|
+
.filter(l => l.length > 0);
|
|
37
|
+
/** Parse `git diff --numstat` output into {path, addedLines} (binary files → 0). */
|
|
38
|
+
const parseNumstat = (stdout) => splitLines(stdout).flatMap(line => {
|
|
39
|
+
const [added, , ...rest] = line.split('\t');
|
|
40
|
+
const p = rest.join('\t');
|
|
41
|
+
if (!p)
|
|
42
|
+
return [];
|
|
43
|
+
const n = Number.parseInt(added, 10);
|
|
44
|
+
return [{ path: p, addedLines: Number.isNaN(n) ? 0 : n }];
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Collect the task's changed files as pure GIT SHAPE — path + added-line count,
|
|
48
|
+
* no content, no language parsing — for the self-verification probe. Before the
|
|
49
|
+
* task commit the work is the tree-vs-HEAD diff (+ untracked files, counted from
|
|
50
|
+
* disk); on the post-enforce re-verify the tree is clean, so fall back to the last
|
|
51
|
+
* commit. Failures degrade to an empty list — the probe is a sharpener, never a
|
|
52
|
+
* blocker.
|
|
53
|
+
*/
|
|
54
|
+
export async function collectChangedFiles(cwd, signal) {
|
|
55
|
+
const tracked = await git(cwd, ['diff', '--numstat', 'HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
56
|
+
const files = tracked.exitCode === 0 ? parseNumstat(tracked.stdout) : [];
|
|
57
|
+
const untracked = await git(cwd, ['ls-files', '--others', '--exclude-standard', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
58
|
+
for (const name of splitLines(untracked.exitCode === 0 ? untracked.stdout : '')) {
|
|
59
|
+
try {
|
|
60
|
+
const content = await fsp.readFile(path.join(cwd, name), 'utf8');
|
|
61
|
+
files.push({ path: name, addedLines: content.split('\n').length });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// unreadable/binary — nothing to report
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
const last = await git(cwd, ['diff', '--numstat', 'HEAD~1..HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
69
|
+
return last.exitCode === 0 ? parseNumstat(last.stdout) : [];
|
|
70
|
+
}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
29
73
|
/**
|
|
30
74
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
31
75
|
* implementation re-runner, injected by the caller. The returned object also drives
|
|
@@ -203,9 +247,36 @@ export function buildGateDeps(params) {
|
|
|
203
247
|
// Deterministic whole-repo static-analysis gate — runs the project's
|
|
204
248
|
// own lint/typecheck and fails on a real non-zero exit, independent of
|
|
205
249
|
// the model-authored VERIFY block (which may not lint at all).
|
|
206
|
-
repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2))
|
|
250
|
+
repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
251
|
+
// Deterministic self-verification probe: test files the task itself
|
|
252
|
+
// authored/changed become prompt-level findings mandating the child
|
|
253
|
+
// to drive the real artifact before trusting their green result.
|
|
254
|
+
probe: () => collectChangedFiles(cwd2, signal).then(findSubstitutionSuspects)
|
|
207
255
|
});
|
|
208
256
|
},
|
|
257
|
+
lintFix: (fixCtx, cwd2, taskTitle, failReason) => runBoundedLintFix({
|
|
258
|
+
cwd: cwd2,
|
|
259
|
+
signal,
|
|
260
|
+
failReason,
|
|
261
|
+
runChild: makeGateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
|
|
262
|
+
repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
263
|
+
git: async (args) => {
|
|
264
|
+
const r = await git(cwd2, args, signal);
|
|
265
|
+
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
266
|
+
}
|
|
267
|
+
}),
|
|
268
|
+
// Deterministic static check + tree helpers for the enforce pre-commit gate.
|
|
269
|
+
repoHealth: cwd2 => Promise.resolve(runRepoHealthCheck(cwd2)),
|
|
270
|
+
dirty: async (cwd2) => {
|
|
271
|
+
const r = await git(cwd2, ['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
272
|
+
return r.exitCode === 0 && r.stdout.trim().length > 0;
|
|
273
|
+
},
|
|
274
|
+
discardEdits: async (cwd2) => {
|
|
275
|
+
// Restore tracked files to HEAD and drop files the pass created; the
|
|
276
|
+
// .pi-tasks trail/log writes made during the pass survive both.
|
|
277
|
+
await git(cwd2, ['checkout', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
278
|
+
await git(cwd2, ['clean', '-fd', '-e', '.pi-tasks'], signal);
|
|
279
|
+
},
|
|
209
280
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
210
281
|
// Read the same composed spec the verify gate judged against, so the
|
|
211
282
|
// recommendation reasons over the real contract (degrade to the bare title).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint-fix — the bounded, graduated resolution for a repo-health verify FAIL.
|
|
3
|
+
*
|
|
4
|
+
* The failure this closes (mx5 run 3, TASK_0017): AUTOFIX's only hammer is a FULL
|
|
5
|
+
* implementation re-run. For a repo-health FAIL of 10 trivial lint findings the live
|
|
6
|
+
* run burned two 36–56-minute impl turns, each REGENERATING a fresh 900-line rewrite
|
|
7
|
+
* that failed lint differently — the loop cannot converge because the tool is bigger
|
|
8
|
+
* than the defect. Validated live on the real TASK_0017 tree: a bounded fix child
|
|
9
|
+
* (read,edit,bash) reached lint-clean in 64s and 106s, 2/2.
|
|
10
|
+
*
|
|
11
|
+
* The same validation caught the design's failure mode: BOTH runs cheated, running
|
|
12
|
+
* `git checkout -- src/test/request.ts` — REVERTING the task's uncommitted work to
|
|
13
|
+
* make the findings vanish. So this pass ships with a deterministic REVERT-GUARD,
|
|
14
|
+
* outcome-based (command filtering can't catch every path to the same effect):
|
|
15
|
+
*
|
|
16
|
+
* - Before the child runs: snapshot the full working state (`git add -A` +
|
|
17
|
+
* `git write-tree`, then unstage) and record which files differ from HEAD.
|
|
18
|
+
* - After: any pre-existing work file now byte-identical to HEAD means the child
|
|
19
|
+
* discarded work instead of fixing it → restore the snapshot, report not-applied.
|
|
20
|
+
* - Converge check: the injected repoHealth must pass; otherwise not-applied.
|
|
21
|
+
*
|
|
22
|
+
* A file whose ENTIRE pre-fix diff was the lint finding (fixing it legitimately
|
|
23
|
+
* restores HEAD) trips the guard conservatively — the fix is discarded and the
|
|
24
|
+
* ordinary AUTOFIX picker takes over. Safe direction: the guard may only cost time,
|
|
25
|
+
* never work.
|
|
26
|
+
*
|
|
27
|
+
* Not-applied is never terminal: the caller falls through to the existing
|
|
28
|
+
* recommend → AUTOFIX/ACCEPT/dismiss picker, so this pass can only make the loop
|
|
29
|
+
* faster, never change what it can decide.
|
|
30
|
+
*/
|
|
31
|
+
export interface LintFixResult {
|
|
32
|
+
/** true → findings fixed, repo health passes, work preserved. */
|
|
33
|
+
ok: boolean;
|
|
34
|
+
/** why the fix was not applied (guard trip, no convergence, child error). */
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface LintFixDeps {
|
|
38
|
+
cwd: string;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
/** The verify FAIL reason (names the failing command, e.g. "repo health: `bun run lint` exited 1"). */
|
|
41
|
+
failReason: string;
|
|
42
|
+
/** Run the fix child; same closure shape the other gate children use. */
|
|
43
|
+
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
44
|
+
/** The deterministic whole-repo static check to converge against. */
|
|
45
|
+
repoHealth: () => Promise<{
|
|
46
|
+
ok: boolean;
|
|
47
|
+
reason: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** Run git in cwd; injected so the guard logic is unit-testable. */
|
|
50
|
+
git: (args: string[]) => Promise<{
|
|
51
|
+
exitCode: number;
|
|
52
|
+
stdout: string;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
56
|
+
export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
|
57
|
+
/**
|
|
58
|
+
* Build the fix child's prompt. The constraints encode both live findings: smallest
|
|
59
|
+
* edits only (the impl re-run's rewrite habit is the defect this pass replaces), and
|
|
60
|
+
* an explicit ban on discarding work (both validation runs reached green via
|
|
61
|
+
* `git checkout` of the work file until the guard existed).
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildLintFixPrompt(failReason: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Pure guard core: which pre-existing work files did the fix pass revert to HEAD?
|
|
66
|
+
* `preDirty` = files differing from HEAD before the fix; `stillDirty` = after.
|
|
67
|
+
*/
|
|
68
|
+
export declare function revertGuardViolations(preDirty: string[], stillDirty: Set<string>): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Run the bounded fix: snapshot → child → revert-guard → converge check. Never
|
|
71
|
+
* throws for an outcome; only a child-level user cancel propagates from runChild.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runBoundedLintFix(deps: LintFixDeps): Promise<LintFixResult>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint-fix — the bounded, graduated resolution for a repo-health verify FAIL.
|
|
3
|
+
*
|
|
4
|
+
* The failure this closes (mx5 run 3, TASK_0017): AUTOFIX's only hammer is a FULL
|
|
5
|
+
* implementation re-run. For a repo-health FAIL of 10 trivial lint findings the live
|
|
6
|
+
* run burned two 36–56-minute impl turns, each REGENERATING a fresh 900-line rewrite
|
|
7
|
+
* that failed lint differently — the loop cannot converge because the tool is bigger
|
|
8
|
+
* than the defect. Validated live on the real TASK_0017 tree: a bounded fix child
|
|
9
|
+
* (read,edit,bash) reached lint-clean in 64s and 106s, 2/2.
|
|
10
|
+
*
|
|
11
|
+
* The same validation caught the design's failure mode: BOTH runs cheated, running
|
|
12
|
+
* `git checkout -- src/test/request.ts` — REVERTING the task's uncommitted work to
|
|
13
|
+
* make the findings vanish. So this pass ships with a deterministic REVERT-GUARD,
|
|
14
|
+
* outcome-based (command filtering can't catch every path to the same effect):
|
|
15
|
+
*
|
|
16
|
+
* - Before the child runs: snapshot the full working state (`git add -A` +
|
|
17
|
+
* `git write-tree`, then unstage) and record which files differ from HEAD.
|
|
18
|
+
* - After: any pre-existing work file now byte-identical to HEAD means the child
|
|
19
|
+
* discarded work instead of fixing it → restore the snapshot, report not-applied.
|
|
20
|
+
* - Converge check: the injected repoHealth must pass; otherwise not-applied.
|
|
21
|
+
*
|
|
22
|
+
* A file whose ENTIRE pre-fix diff was the lint finding (fixing it legitimately
|
|
23
|
+
* restores HEAD) trips the guard conservatively — the fix is discarded and the
|
|
24
|
+
* ordinary AUTOFIX picker takes over. Safe direction: the guard may only cost time,
|
|
25
|
+
* never work.
|
|
26
|
+
*
|
|
27
|
+
* Not-applied is never terminal: the caller falls through to the existing
|
|
28
|
+
* recommend → AUTOFIX/ACCEPT/dismiss picker, so this pass can only make the loop
|
|
29
|
+
* faster, never change what it can decide.
|
|
30
|
+
*/
|
|
31
|
+
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
32
|
+
export const LINT_FIX_TOOLS = 'read,edit,bash';
|
|
33
|
+
/**
|
|
34
|
+
* Build the fix child's prompt. The constraints encode both live findings: smallest
|
|
35
|
+
* edits only (the impl re-run's rewrite habit is the defect this pass replaces), and
|
|
36
|
+
* an explicit ban on discarding work (both validation runs reached green via
|
|
37
|
+
* `git checkout` of the work file until the guard existed).
|
|
38
|
+
*/
|
|
39
|
+
export function buildLintFixPrompt(failReason) {
|
|
40
|
+
return [
|
|
41
|
+
'You are a bounded static-analysis fix pass. A verification gate just failed',
|
|
42
|
+
`with: ${failReason}`,
|
|
43
|
+
'',
|
|
44
|
+
'The working tree contains UNCOMMITTED task work. Your ONLY job is to make the',
|
|
45
|
+
"project's own static check pass again while preserving that work untouched.",
|
|
46
|
+
'',
|
|
47
|
+
'1. Run the failing check first to see the exact findings (the command is named',
|
|
48
|
+
" above; it is one of the project's own package.json scripts / make targets).",
|
|
49
|
+
'',
|
|
50
|
+
'2. Fix EXACTLY those findings with the smallest possible edits.',
|
|
51
|
+
' - Do NOT rewrite, restructure, or "improve" anything else.',
|
|
52
|
+
' - Do NOT touch files that have no findings.',
|
|
53
|
+
' - An unused variable/import introduced by the current work: delete just',
|
|
54
|
+
' those lines (or prefix an intentionally-unused parameter with _).',
|
|
55
|
+
'',
|
|
56
|
+
'3. HARD CONSTRAINT — never discard work: you must NOT run git checkout,',
|
|
57
|
+
' git restore, git stash, git reset, git clean, or any git command that',
|
|
58
|
+
' mutates state; you must NOT delete or revert whole files or functions.',
|
|
59
|
+
' Reverting the work would make the findings vanish — that is destroying the',
|
|
60
|
+
' task, not fixing it, and it is detected and rejected.',
|
|
61
|
+
'',
|
|
62
|
+
'4. Re-run the check after editing and confirm it exits 0.',
|
|
63
|
+
'',
|
|
64
|
+
'End with exactly one line:',
|
|
65
|
+
' LINT-FIX: DONE',
|
|
66
|
+
' LINT-FIX: BLOCKED <why>'
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pure guard core: which pre-existing work files did the fix pass revert to HEAD?
|
|
71
|
+
* `preDirty` = files differing from HEAD before the fix; `stillDirty` = after.
|
|
72
|
+
*/
|
|
73
|
+
export function revertGuardViolations(preDirty, stillDirty) {
|
|
74
|
+
return preDirty.filter(f => !stillDirty.has(f));
|
|
75
|
+
}
|
|
76
|
+
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
77
|
+
async function dirtyFiles(deps) {
|
|
78
|
+
const r = await deps.git(['diff', '--name-only', 'HEAD', '--', '.', EXCLUDE_TASKS_DIR]);
|
|
79
|
+
if (r.exitCode !== 0)
|
|
80
|
+
return [];
|
|
81
|
+
return r.stdout
|
|
82
|
+
.split('\n')
|
|
83
|
+
.map(l => l.trim())
|
|
84
|
+
.filter(l => l.length > 0);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run the bounded fix: snapshot → child → revert-guard → converge check. Never
|
|
88
|
+
* throws for an outcome; only a child-level user cancel propagates from runChild.
|
|
89
|
+
*/
|
|
90
|
+
export async function runBoundedLintFix(deps) {
|
|
91
|
+
// Snapshot the full working state as a tree object (includes untracked files),
|
|
92
|
+
// then unstage so the child sees the tree exactly as it was.
|
|
93
|
+
const preDirty = await dirtyFiles(deps);
|
|
94
|
+
const preUntracked = (await deps.git(['ls-files', '--others', '--exclude-standard', '--', '.', EXCLUDE_TASKS_DIR])).stdout
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map(l => l.trim())
|
|
97
|
+
.filter(l => l.length > 0);
|
|
98
|
+
let snapshot = null;
|
|
99
|
+
const add = await deps.git(['add', '-A']);
|
|
100
|
+
if (add.exitCode === 0) {
|
|
101
|
+
const wt = await deps.git(['write-tree']);
|
|
102
|
+
if (wt.exitCode === 0)
|
|
103
|
+
snapshot = wt.stdout.trim();
|
|
104
|
+
await deps.git(['reset']);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await deps.runChild(LINT_FIX_TOOLS, buildLintFixPrompt(deps.failReason), deps.signal);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
111
|
+
// A cancel must propagate so the caller's USER_CANCELLED path runs.
|
|
112
|
+
if (msg === '__user_cancelled__')
|
|
113
|
+
throw err;
|
|
114
|
+
return { ok: false, reason: `fix child failed: ${msg}` };
|
|
115
|
+
}
|
|
116
|
+
// REVERT-GUARD: every pre-existing work file must still differ from HEAD, and
|
|
117
|
+
// every pre-existing untracked file must still exist. Trip → restore snapshot.
|
|
118
|
+
const stillDirty = new Set(await dirtyFiles(deps));
|
|
119
|
+
const violations = revertGuardViolations(preDirty, stillDirty);
|
|
120
|
+
for (const f of preUntracked) {
|
|
121
|
+
const probe = await deps.git(['ls-files', '--others', '--exclude-standard', '--', f]);
|
|
122
|
+
const gone = probe.stdout.trim().length === 0
|
|
123
|
+
// ...unless the child legitimately made it tracked-dirty (it edited it).
|
|
124
|
+
&& !stillDirty.has(f);
|
|
125
|
+
if (gone)
|
|
126
|
+
violations.push(f);
|
|
127
|
+
}
|
|
128
|
+
if (violations.length > 0) {
|
|
129
|
+
if (snapshot) {
|
|
130
|
+
// Restore the pre-fix state: tree paths back, then unstage. .pi-tasks is
|
|
131
|
+
// excluded so gate logs appended during the fix survive.
|
|
132
|
+
await deps.git(['checkout', snapshot, '--', '.', EXCLUDE_TASKS_DIR]);
|
|
133
|
+
await deps.git(['reset']);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
reason: `revert-guard: fix pass discarded work (${violations.slice(0, 3).join(', ')}`
|
|
138
|
+
+ `${violations.length > 3 ? ', …' : ''}) — fix ${snapshot ? 'rolled back' : 'REJECTED but no snapshot to restore'}`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const health = await deps.repoHealth();
|
|
142
|
+
if (!health.ok) {
|
|
143
|
+
return { ok: false, reason: `did not converge: ${health.reason}` };
|
|
144
|
+
}
|
|
145
|
+
return { ok: true };
|
|
146
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* substitution-probe — deterministic detection of SELF-VERIFIED work, feeding the
|
|
3
|
+
* verify gate's prompt.
|
|
4
|
+
*
|
|
5
|
+
* The failure class (mx5 run 3, TASK_0016/0017): the implementation hit a real bug
|
|
6
|
+
* in the shipped app ("Context is not finalized" from a mis-signatured middleware),
|
|
7
|
+
* blamed the framework, and shipped "integration tests" that re-implement the API
|
|
8
|
+
* inline — an own server intercepting protected routes, and a 943-line hand-rolled
|
|
9
|
+
* duplicate in a test-support file that imports the real app and never calls it.
|
|
10
|
+
* The suite runs 26/26 green while every protected route of the real server 500s.
|
|
11
|
+
* The verify child judged "do the tests pass", saw green, and PASSed.
|
|
12
|
+
*
|
|
13
|
+
* A/B on the live local model (5 runs/arm, the real mx5 tree as fixture) proved the
|
|
14
|
+
* fix must be a DETERMINISTIC, CONCRETE finding in the prompt, not prompt language
|
|
15
|
+
* alone: the bare substitution rule caught 2/5 (right verdicts, 40% attention); the
|
|
16
|
+
* rule PLUS a probe finding naming the suspect file caught 5/5, each verdict naming
|
|
17
|
+
* the exact inline handlers and confirming the real app crashes.
|
|
18
|
+
*
|
|
19
|
+
* WHAT THE PROBE ASSERTS is deliberately the class INVARIANT, not any framework
|
|
20
|
+
* shape: when a task's own diff includes the very tests whose green result would
|
|
21
|
+
* bless it, the work is grading itself — so the verify child must independently
|
|
22
|
+
* confirm those tests exercise the real shipped artifact before trusting them.
|
|
23
|
+
* That is computable from pure git diff shape (which changed files live in test
|
|
24
|
+
* paths, and how many lines the task added there): no language parsing, no server-
|
|
25
|
+
* constructor lists, no import syntax — a Python, Go, or Rust project produces the
|
|
26
|
+
* same finding the same way. (An earlier draft pattern-matched JS server
|
|
27
|
+
* constructors; that only re-encoded the one incident. A behavioral canary — rerun
|
|
28
|
+
* the suite with shipped sources removed, still-green ⇒ copy — was also rejected:
|
|
29
|
+
* the real mx5 copy still imported the shipped db module, so poisoning sources
|
|
30
|
+
* breaks the copy's tests too and the conviction never fires.)
|
|
31
|
+
*
|
|
32
|
+
* Findings are advisory: they mandate the spot-check, they never auto-FAIL. Honest
|
|
33
|
+
* self-authored tests survive the spot-check (control fixtures PASS with the
|
|
34
|
+
* mandate present); only tests that bypass the artifact get named in a FAIL.
|
|
35
|
+
*/
|
|
36
|
+
/** One changed file as the git-shape collector reports it. */
|
|
37
|
+
export interface ChangedFile {
|
|
38
|
+
/** Path relative to the repo root (used verbatim in the finding text). */
|
|
39
|
+
path: string;
|
|
40
|
+
/** Lines the task's diff ADDED to this file (0 for pure deletions). */
|
|
41
|
+
addedLines: number;
|
|
42
|
+
}
|
|
43
|
+
/** Is this a file whose job is testing — by suffix or by living in a test dir? */
|
|
44
|
+
export declare function isTestFile(p: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Analyse the task's changed files and return self-verification findings — one
|
|
47
|
+
* human-readable line per test file the task itself authored or changed, ready to
|
|
48
|
+
* inject into the verify prompt. Empty array → the task ships no tests of its own,
|
|
49
|
+
* the prompt gets no probe block.
|
|
50
|
+
*/
|
|
51
|
+
export declare function findSubstitutionSuspects(files: ChangedFile[]): string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* substitution-probe — deterministic detection of SELF-VERIFIED work, feeding the
|
|
3
|
+
* verify gate's prompt.
|
|
4
|
+
*
|
|
5
|
+
* The failure class (mx5 run 3, TASK_0016/0017): the implementation hit a real bug
|
|
6
|
+
* in the shipped app ("Context is not finalized" from a mis-signatured middleware),
|
|
7
|
+
* blamed the framework, and shipped "integration tests" that re-implement the API
|
|
8
|
+
* inline — an own server intercepting protected routes, and a 943-line hand-rolled
|
|
9
|
+
* duplicate in a test-support file that imports the real app and never calls it.
|
|
10
|
+
* The suite runs 26/26 green while every protected route of the real server 500s.
|
|
11
|
+
* The verify child judged "do the tests pass", saw green, and PASSed.
|
|
12
|
+
*
|
|
13
|
+
* A/B on the live local model (5 runs/arm, the real mx5 tree as fixture) proved the
|
|
14
|
+
* fix must be a DETERMINISTIC, CONCRETE finding in the prompt, not prompt language
|
|
15
|
+
* alone: the bare substitution rule caught 2/5 (right verdicts, 40% attention); the
|
|
16
|
+
* rule PLUS a probe finding naming the suspect file caught 5/5, each verdict naming
|
|
17
|
+
* the exact inline handlers and confirming the real app crashes.
|
|
18
|
+
*
|
|
19
|
+
* WHAT THE PROBE ASSERTS is deliberately the class INVARIANT, not any framework
|
|
20
|
+
* shape: when a task's own diff includes the very tests whose green result would
|
|
21
|
+
* bless it, the work is grading itself — so the verify child must independently
|
|
22
|
+
* confirm those tests exercise the real shipped artifact before trusting them.
|
|
23
|
+
* That is computable from pure git diff shape (which changed files live in test
|
|
24
|
+
* paths, and how many lines the task added there): no language parsing, no server-
|
|
25
|
+
* constructor lists, no import syntax — a Python, Go, or Rust project produces the
|
|
26
|
+
* same finding the same way. (An earlier draft pattern-matched JS server
|
|
27
|
+
* constructors; that only re-encoded the one incident. A behavioral canary — rerun
|
|
28
|
+
* the suite with shipped sources removed, still-green ⇒ copy — was also rejected:
|
|
29
|
+
* the real mx5 copy still imported the shipped db module, so poisoning sources
|
|
30
|
+
* breaks the copy's tests too and the conviction never fires.)
|
|
31
|
+
*
|
|
32
|
+
* Findings are advisory: they mandate the spot-check, they never auto-FAIL. Honest
|
|
33
|
+
* self-authored tests survive the spot-check (control fixtures PASS with the
|
|
34
|
+
* mandate present); only tests that bypass the artifact get named in a FAIL.
|
|
35
|
+
*/
|
|
36
|
+
/** Is this a file whose job is testing — by suffix or by living in a test dir? */
|
|
37
|
+
export function isTestFile(p) {
|
|
38
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(p))
|
|
39
|
+
return true;
|
|
40
|
+
if (/(^|[._-])(test|spec)s?\.[a-z]+$/.test(p))
|
|
41
|
+
return true; // test_x.py, x_test.go, x.spec.rb
|
|
42
|
+
if (/(^|\/)test_[^/]+$/.test(p))
|
|
43
|
+
return true;
|
|
44
|
+
return /(^|\/)(test|tests|__tests__|spec|specs)\//.test(p);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Analyse the task's changed files and return self-verification findings — one
|
|
48
|
+
* human-readable line per test file the task itself authored or changed, ready to
|
|
49
|
+
* inject into the verify prompt. Empty array → the task ships no tests of its own,
|
|
50
|
+
* the prompt gets no probe block.
|
|
51
|
+
*/
|
|
52
|
+
export function findSubstitutionSuspects(files) {
|
|
53
|
+
const findings = [];
|
|
54
|
+
for (const f of files) {
|
|
55
|
+
if (!isTestFile(f.path))
|
|
56
|
+
continue;
|
|
57
|
+
if (f.addedLines <= 0)
|
|
58
|
+
continue;
|
|
59
|
+
findings.push(`${f.path} (+${f.addedLines} lines) — a test file this task authored or changed itself; `
|
|
60
|
+
+ 'its green result blesses the very work that wrote it');
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
@@ -77,6 +77,35 @@ export interface GateDeps {
|
|
|
77
77
|
* skips the revert and warns.
|
|
78
78
|
*/
|
|
79
79
|
revert?: (cwd: string) => Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* BOUNDED fix for a repo-health verify FAIL: a small read,edit,bash child fixes
|
|
82
|
+
* exactly the static findings (revert-guarded — see lint-fix.ts), instead of the
|
|
83
|
+
* full implementation re-run AUTOFIX reaches for. Validated live: 64s/106s to
|
|
84
|
+
* lint-clean where the impl re-run burned 36–56 min and did not converge. Runs
|
|
85
|
+
* at most once per gate sequence; not-applied falls through to the picker.
|
|
86
|
+
* Absent → the loop goes straight to recommend/picker as before.
|
|
87
|
+
*/
|
|
88
|
+
lintFix?: (ctx: ExtensionCommandContext, cwd: string, taskTitle: string, failReason: string) => Promise<{
|
|
89
|
+
ok: boolean;
|
|
90
|
+
reason?: string;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Deterministic whole-repo static check (repo-health), used as the PRE-COMMIT
|
|
94
|
+
* gate on an edit-mode enforce pass: live data shows enforce edits broke the
|
|
95
|
+
* repo's own lint in 11 of 16 tasks, each costing a commit + model re-verify +
|
|
96
|
+
* revert cycle. Checking before committing skips that cycle. Absent → the old
|
|
97
|
+
* commit-then-differential path runs unchanged.
|
|
98
|
+
*/
|
|
99
|
+
repoHealth?: (cwd: string) => Promise<{
|
|
100
|
+
ok: boolean;
|
|
101
|
+
reason: string;
|
|
102
|
+
}>;
|
|
103
|
+
/** Does the working tree hold changes (excluding .pi-tasks)? Lets the pre-commit
|
|
104
|
+
* health check run only when the enforce pass actually edited something. */
|
|
105
|
+
dirty?: (cwd: string) => Promise<boolean>;
|
|
106
|
+
/** Restore the working tree to HEAD (excluding .pi-tasks) — discards enforce
|
|
107
|
+
* edits that failed the pre-commit health check, before they are committed. */
|
|
108
|
+
discardEdits?: (cwd: string) => Promise<void>;
|
|
80
109
|
/**
|
|
81
110
|
* Append one line to the task's durable gate trail (`## gates` in the task
|
|
82
111
|
* file). Every gate outcome — each verify verdict, the user's FAIL resolution,
|
package/dist/task/task-gates.js
CHANGED
|
@@ -63,8 +63,24 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
63
63
|
// A FAIL no longer dead-stops: offer the boxed AUTOFIX / ACCEPT / dismiss
|
|
64
64
|
// picker. The USER always decides; AUTOFIX loops straight back to the gate
|
|
65
65
|
// as many times as they keep choosing it (no attempt cap).
|
|
66
|
+
let lintFixAttempted = false;
|
|
66
67
|
while (!verified.ok) {
|
|
67
68
|
const failReason = verified.reason ?? 'did not verify';
|
|
69
|
+
// GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
|
|
70
|
+
// bounded fix attempt before the picker — smallest tool first. Applied →
|
|
71
|
+
// re-verify and re-enter the loop on the fresh verdict; not applied (guard
|
|
72
|
+
// trip, no convergence) → fall through to the ordinary picker unchanged.
|
|
73
|
+
if (!lintFixAttempted && deps.lintFix && failReason.startsWith('repo health:')) {
|
|
74
|
+
lintFixAttempted = true;
|
|
75
|
+
active.ui.notify(`${p.tag}: static findings on "${p.title}" — attempting bounded lint fix…`, 'info');
|
|
76
|
+
const fix = await deps.lintFix(active, p.cwd, p.title, failReason);
|
|
77
|
+
await rec(`lint-fix: ${fix.ok ? 'applied — re-verifying' : `not applied (${fix.reason ?? 'failed'})`}`);
|
|
78
|
+
if (fix.ok) {
|
|
79
|
+
verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
80
|
+
await rec(verdictLine(verified));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
68
84
|
const recOutcome = deps.recommend ?
|
|
69
85
|
await deps.recommend(active, p.cwd, p.title, p.taskId, failReason)
|
|
70
86
|
: { recommend: 'autofix', rationale: failReason };
|
|
@@ -129,11 +145,36 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
129
145
|
`${p.tag}: enforcing AGENTS.md/CLAUDE.md on "${p.title}"…`
|
|
130
146
|
: `${p.tag}: reviewing "${p.title}" against AGENTS.md/CLAUDE.md (no verify signal — report only)…`, 'info');
|
|
131
147
|
const verdict = await deps.enforce(active, p.cwd, p.title, mode);
|
|
132
|
-
|
|
148
|
+
// The child's verdict and its edits are independent facts: the pass has been
|
|
149
|
+
// observed declaring "clean" while having edited files (which then get
|
|
150
|
+
// committed as fixes) — record both so the trail cannot contradict itself.
|
|
151
|
+
const editsMade = mode === 'edit' && deps.dirty ? await deps.dirty(p.cwd) : undefined;
|
|
152
|
+
await rec(`enforce(${mode}): ${verdict.ok ? `clean${verdict.reason ? ` (${verdict.reason})` : ''}` : (verdict.reason ?? 'not clean')}${editsMade ? ' — edits in tree' : ''}`);
|
|
133
153
|
if (!verdict.ok) {
|
|
134
154
|
active.ui.notify(`${p.tag}: guideline ${mode === 'edit' ? 'enforcement' : 'review'} on "${p.title}" — ${verdict.reason ?? 'not clean'} — continuing.`, 'warning');
|
|
135
155
|
}
|
|
136
|
-
|
|
156
|
+
// PRE-COMMIT health gate on enforce edits: live data shows the edit pass broke
|
|
157
|
+
// the repo's own lint in 11/16 tasks, each burning a commit + model re-verify +
|
|
158
|
+
// revert cycle. A deterministic static check BEFORE committing skips the cycle
|
|
159
|
+
// and discards the bad edits outright. Only runs when the tree is actually
|
|
160
|
+
// dirty (or dirtiness is unknowable); the differential guard below still
|
|
161
|
+
// catches behavioral regressions the static check cannot see.
|
|
162
|
+
let enforceEditsBlocked = false;
|
|
163
|
+
if (mode === 'edit' && deps.repoHealth && editsMade !== false) {
|
|
164
|
+
const h = await deps.repoHealth(p.cwd);
|
|
165
|
+
if (!h.ok) {
|
|
166
|
+
enforceEditsBlocked = true;
|
|
167
|
+
if (deps.discardEdits) {
|
|
168
|
+
await deps.discardEdits(p.cwd);
|
|
169
|
+
await rec(`enforce: edits discarded pre-commit (repo health: ${h.reason})`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await rec(`enforce: edits FAILED repo health pre-commit (${h.reason}) — no discard available, left uncommitted`);
|
|
173
|
+
}
|
|
174
|
+
active.ui.notify(`${p.tag}: guideline edits on "${p.title}" failed repo health (${h.reason.slice(0, 120)}) — discarded before commit.`, 'warning');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (mode === 'edit' && !enforceEditsBlocked) {
|
|
137
178
|
// Commit whatever the pass fixed as its own snapshot. A no-op when it made
|
|
138
179
|
// no edits (nothing to commit) — then there is nothing to re-verify/revert.
|
|
139
180
|
const enforceCommit = await deps.commit(p.cwd, `ENFORCE GUIDELINES: ${p.title} (${p.taskId})`);
|
|
@@ -34,8 +34,15 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
34
34
|
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
35
35
|
* without spawning pi. The contract: run the spec's own verification in the real
|
|
36
36
|
* workspace, judge against ACCEPTANCE, and end on exactly one verdict line.
|
|
37
|
+
*
|
|
38
|
+
* `probeFindings` are the deterministic self-verification probe results (see
|
|
39
|
+
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
40
|
+
* prompt carries both the rule (3b) AND a concrete finding naming the suspect
|
|
41
|
+
* file — the rule alone got 2/5 attention on the live model. The findings are
|
|
42
|
+
* pure git shape (test files the task itself changed), so the mandate is
|
|
43
|
+
* language- and framework-agnostic.
|
|
37
44
|
*/
|
|
38
|
-
export declare function buildVerifyPrompt(spec: string): string;
|
|
45
|
+
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[]): string;
|
|
39
46
|
/**
|
|
40
47
|
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
|
|
41
48
|
* (the model discusses before concluding, and bash output may echo the word
|
|
@@ -69,6 +76,12 @@ export interface VerificationDeps {
|
|
|
69
76
|
ok: boolean;
|
|
70
77
|
reason: string;
|
|
71
78
|
}>;
|
|
79
|
+
/**
|
|
80
|
+
* DETERMINISTIC substitution probe (see substitution-probe.ts): scans the task's
|
|
81
|
+
* changed test files for test-the-copy shapes and returns finding lines to inject
|
|
82
|
+
* into the child's prompt. A/B-proven load-bearing: the prompt rule alone caught
|
|
83
|
+
* the class 2/5, rule + probe finding 5/5. ABSENT or empty → no probe block. */
|
|
84
|
+
probe?: () => Promise<string[]>;
|
|
72
85
|
}
|
|
73
86
|
/**
|
|
74
87
|
* Run the verification pass for one task. A missing spec is a pass. Otherwise run
|
package/dist/task/verify-work.js
CHANGED
|
@@ -115,8 +115,28 @@ export function extractSpecForVerification(taskBody) {
|
|
|
115
115
|
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
116
116
|
* without spawning pi. The contract: run the spec's own verification in the real
|
|
117
117
|
* workspace, judge against ACCEPTANCE, and end on exactly one verdict line.
|
|
118
|
+
*
|
|
119
|
+
* `probeFindings` are the deterministic self-verification probe results (see
|
|
120
|
+
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
121
|
+
* prompt carries both the rule (3b) AND a concrete finding naming the suspect
|
|
122
|
+
* file — the rule alone got 2/5 attention on the live model. The findings are
|
|
123
|
+
* pure git shape (test files the task itself changed), so the mandate is
|
|
124
|
+
* language- and framework-agnostic.
|
|
118
125
|
*/
|
|
119
|
-
export function buildVerifyPrompt(spec) {
|
|
126
|
+
export function buildVerifyPrompt(spec, probeFindings) {
|
|
127
|
+
const probeBlock = probeFindings && probeFindings.length > 0 ?
|
|
128
|
+
[
|
|
129
|
+
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
130
|
+
'this task wrote or changed the very tests whose green result would bless it:',
|
|
131
|
+
...probeFindings.map(f => `- ${f}`),
|
|
132
|
+
'A green run of self-authored tests is NOT sufficient verification. Before any PASS',
|
|
133
|
+
'you MUST confirm these tests exercise the REAL shipped artifact: drive 1-2 tested',
|
|
134
|
+
'behaviors directly against the real app / module / entry point and judge what IT',
|
|
135
|
+
'returns. Tests that intercept, re-implement, or stub the artifact prove only the',
|
|
136
|
+
'copy (rule 3b below).',
|
|
137
|
+
''
|
|
138
|
+
]
|
|
139
|
+
: [];
|
|
120
140
|
return [
|
|
121
141
|
'You are a strict verification pass running right after an AI coding agent',
|
|
122
142
|
'finished a task and committed it. The agent is known to mark work "done"',
|
|
@@ -130,6 +150,7 @@ export function buildVerifyPrompt(spec) {
|
|
|
130
150
|
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block are the contract):',
|
|
131
151
|
spec.trim(),
|
|
132
152
|
'',
|
|
153
|
+
...probeBlock,
|
|
133
154
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
134
155
|
'checkout (or CI run) would experience it:',
|
|
135
156
|
'',
|
|
@@ -159,6 +180,14 @@ export function buildVerifyPrompt(spec) {
|
|
|
159
180
|
' never executed is NOT a PASS — a static match cannot prove an artifact works, only',
|
|
160
181
|
' running it can.',
|
|
161
182
|
'',
|
|
183
|
+
'3b. SUBSTITUTION: a test suite (or any check) shipped by the work counts as verification',
|
|
184
|
+
" ONLY if it exercises the real shipped code paths. If the work's own tests re-implement",
|
|
185
|
+
' logic inline, intercept or stub the artifact behind their own server or handlers, or',
|
|
186
|
+
' import the real module and then never call it, a green suite proves only the copy —',
|
|
187
|
+
' NOT the deliverable. Spot-check this: pick 1-2 tested behaviors and drive the REAL',
|
|
188
|
+
' shipped artifact directly (the real app / module / entry point). If the real artifact',
|
|
189
|
+
' fails where the tests pass, report FAIL and name the bypass.',
|
|
190
|
+
'',
|
|
162
191
|
'4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
|
|
163
192
|
' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
164
193
|
'',
|
|
@@ -178,11 +207,24 @@ export function buildVerifyPrompt(spec) {
|
|
|
178
207
|
' not load, a wrong default, a command that cannot run unaided) is a defect, not an',
|
|
179
208
|
' environment gap.',
|
|
180
209
|
'',
|
|
210
|
+
'5b. EXTERNAL SERVICE STATE IS PART OF "AS SHIPPED": you may probe and read an external',
|
|
211
|
+
' service, but you must NOT create, alter, or repair its schema or data to make the',
|
|
212
|
+
' artifact work — no ALTER TABLE, CREATE TABLE, manual INSERT/UPDATE fixes, or ad-hoc',
|
|
213
|
+
" DDL. You may apply only the project's OWN migration/schema files, exactly as shipped.",
|
|
214
|
+
" If the shipped code needs schema or data that the project's own files do not create,",
|
|
215
|
+
' that is a FAIL naming the gap. Any schema surgery you performed to reach green IS the',
|
|
216
|
+
' defect.',
|
|
217
|
+
'',
|
|
181
218
|
'6. If the spec legitimately has no runnable verification (a pure docs / config change',
|
|
182
219
|
' with nothing to build or run), validating it cleanly is a PASS.',
|
|
183
220
|
'',
|
|
184
221
|
'7. Do NOT edit anything to make a check pass. Report what you actually saw.',
|
|
185
222
|
'',
|
|
223
|
+
'VERDICT DISCIPLINE: the verdict must follow mechanically from your findings. If ANY',
|
|
224
|
+
'acceptance criterion is unmet — a required function absent, required data not persisted,',
|
|
225
|
+
'a required behavior missing — the verdict is FAIL, even if typecheck and lint are green',
|
|
226
|
+
'and even if the gap seems minor. Never downgrade an unmet criterion to a warning note.',
|
|
227
|
+
'',
|
|
186
228
|
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
187
229
|
" WORK-VERIFIED: PASS (the project's own command, run unaided, met the spec)",
|
|
188
230
|
' WORK-VERIFIED: FAIL <text> (the shipped command failed or did not meet the spec; say what failed)',
|
|
@@ -228,9 +270,20 @@ export async function runWorkVerification(deps) {
|
|
|
228
270
|
if (!deps.spec || deps.spec.trim().length === 0) {
|
|
229
271
|
return { ok: true, reason: 'no spec to verify' };
|
|
230
272
|
}
|
|
273
|
+
// Substitution probe findings feed the prompt; a probe failure must never block
|
|
274
|
+
// verification (it is an optional sharpener, the gate still runs without it).
|
|
275
|
+
let findings = [];
|
|
276
|
+
if (deps.probe) {
|
|
277
|
+
try {
|
|
278
|
+
findings = await deps.probe();
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
findings = [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
231
284
|
let text;
|
|
232
285
|
try {
|
|
233
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec), deps.signal);
|
|
286
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings), deps.signal);
|
|
234
287
|
}
|
|
235
288
|
catch (err) {
|
|
236
289
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -53,8 +53,9 @@ export interface AutoLoaderState {
|
|
|
53
53
|
* numbered clarify/decompose steps); 'enforce' is the per-task guideline
|
|
54
54
|
* pass and 'verify' is the per-task work-verification pass, neither of which
|
|
55
55
|
* has step numbering. 'recommend' is the read-only research that picks the
|
|
56
|
-
* recommended action after a verify FAIL.
|
|
57
|
-
|
|
56
|
+
* recommended action after a verify FAIL. 'lint-fix' is the bounded fix pass
|
|
57
|
+
* for a repo-health verify FAIL. */
|
|
58
|
+
kind?: 'planning' | 'enforce' | 'verify' | 'recommend' | 'lint-fix';
|
|
58
59
|
}
|
|
59
60
|
export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetTheme): string[];
|
|
60
61
|
/**
|
package/dist/task/widget.js
CHANGED
|
@@ -124,7 +124,8 @@ export function buildAutoLoaderLines(s, theme) {
|
|
|
124
124
|
let detail = s.kind === 'enforce' ? `enforcing guidelines · ${elapsed}`
|
|
125
125
|
: s.kind === 'verify' ? `verifying work · ${elapsed}`
|
|
126
126
|
: s.kind === 'recommend' ? `assessing the failure · ${elapsed}`
|
|
127
|
-
:
|
|
127
|
+
: s.kind === 'lint-fix' ? `fixing static findings · ${elapsed}`
|
|
128
|
+
: `planning ${s.stepNum}/${s.stepTotal} ${s.step} · ${elapsed}`;
|
|
128
129
|
if (s.contextUsage) {
|
|
129
130
|
const ctxDetail = formatContextDetail(s.contextUsage, theme);
|
|
130
131
|
if (ctxDetail)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.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",
|