@mjasnikovs/pi-task 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/task/gate-deps.js
CHANGED
|
@@ -24,6 +24,7 @@ import { runRepoHealthCheck } from './repo-health-check.js';
|
|
|
24
24
|
import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate.js';
|
|
25
25
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
26
26
|
import { researchResolution } from './verify-resolution.js';
|
|
27
|
+
import { extractProhibitions, findProhibitionViolations } from './prohibition-probe.js';
|
|
27
28
|
import { findSubstitutionSuspects } from './substitution-probe.js';
|
|
28
29
|
import { runBoundedLintFix } from './lint-fix.js';
|
|
29
30
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
@@ -290,6 +291,16 @@ export function buildGateDeps(params) {
|
|
|
290
291
|
// authored/changed become prompt-level findings mandating the child
|
|
291
292
|
// to drive the real artifact before trusting their green result.
|
|
292
293
|
probe: () => collectChangedFiles(cwd2, signal).then(findSubstitutionSuspects),
|
|
294
|
+
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
295
|
+
// that the task's diff modified anyway become prompt-level findings
|
|
296
|
+
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
297
|
+
// diff` and cannot even see the violation.
|
|
298
|
+
prohibitionProbe: () => {
|
|
299
|
+
const banned = spec ? extractProhibitions(spec) : [];
|
|
300
|
+
if (banned.length === 0)
|
|
301
|
+
return Promise.resolve([]);
|
|
302
|
+
return collectChangedFiles(cwd2, signal).then(files => findProhibitionViolations(banned, files));
|
|
303
|
+
},
|
|
293
304
|
// Git-state guard result of the most recent child run: a verdict
|
|
294
305
|
// computed on a tree the child itself mutated is discarded (the
|
|
295
306
|
// guard already restored the state — see git-state-guard.ts).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prohibition-probe — deterministic detection of VIOLATED SPEC PROHIBITIONS,
|
|
3
|
+
* feeding the verify gate's prompt.
|
|
4
|
+
*
|
|
5
|
+
* The failure class (mx5 run 7, "New Listing page" task): the spec's CONSTRAINTS
|
|
6
|
+
* said "**Do NOT modify** any server-side code: `src/server/index.ts`, …"; the
|
|
7
|
+
* implementation modified `src/server/index.ts` anyway; the verify child SAW it
|
|
8
|
+
* ("VIOLATES 'Do NOT modify server-side code'"), waived it ("BUT: this is
|
|
9
|
+
* additive, tests pass with it"), and PASSed. Worse, the reproduction fixture
|
|
10
|
+
* showed the baseline child usually never LOOKS: 5/5 baseline runs consulted no
|
|
11
|
+
* diff at all and several affirmatively claimed the forbidden file was untouched.
|
|
12
|
+
*
|
|
13
|
+
* So — like the substitution probe (see substitution-probe.ts, whose A/B proved
|
|
14
|
+
* prompt language alone gets ~40% attention while a concrete deterministic
|
|
15
|
+
* finding gets 100%) — the fix is a deterministic pre-check whose finding is
|
|
16
|
+
* injected into the prompt: extract the concrete paths the spec forbids
|
|
17
|
+
* modifying, intersect with the task's changed files (pure git shape, already
|
|
18
|
+
* collected for the substitution probe), and hand the child each hit with the
|
|
19
|
+
* exact constraint wording.
|
|
20
|
+
*
|
|
21
|
+
* The finding is advisory, not an auto-FAIL, for one reason: prohibitions in
|
|
22
|
+
* real specs are prose and can be CONDITIONAL ("Do NOT modify `api.ts` beyond
|
|
23
|
+
* what is needed for the new endpoint") — a hard gate on prose extraction would
|
|
24
|
+
* false-FAIL legitimate work. The finding therefore carries the constraint line
|
|
25
|
+
* verbatim and the prompt's no-waiver rule (4b in verify-work.ts) forbids
|
|
26
|
+
* excusing an ABSOLUTE prohibition while directing conditional ones to be judged
|
|
27
|
+
* against their own stated exception. A violation that was fully reverted before
|
|
28
|
+
* verify produces no diff entry, so it never fires — reverted = not violated.
|
|
29
|
+
*/
|
|
30
|
+
import type { ChangedFile } from './substitution-probe.js';
|
|
31
|
+
/** One "do not modify X" constraint extracted from the spec text. */
|
|
32
|
+
export interface Prohibition {
|
|
33
|
+
/** The forbidden path exactly as the spec spells it (file or directory). */
|
|
34
|
+
path: string;
|
|
35
|
+
/** The full spec line carrying the prohibition, so the verify child judges
|
|
36
|
+
* against the EXACT wording — including any exception clause it states. */
|
|
37
|
+
constraint: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract the concrete paths the spec explicitly forbids modifying: every
|
|
41
|
+
* backtick-quoted path-like token on a line that expresses a modification ban.
|
|
42
|
+
* Prose-only prohibitions ("do not modify server-side code" with no path named)
|
|
43
|
+
* extract nothing — the prompt-level rule still covers them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractProhibitions(spec: string): Prohibition[];
|
|
46
|
+
/**
|
|
47
|
+
* Intersect the spec's prohibitions with the task's changed files (git shape —
|
|
48
|
+
* the same collector the substitution probe uses). A prohibition matches a
|
|
49
|
+
* changed file exactly, or as a directory prefix (`src/server` covers
|
|
50
|
+
* `src/server/index.ts`). One finding line per violated file, carrying the
|
|
51
|
+
* constraint verbatim; empty array → no block in the prompt.
|
|
52
|
+
*/
|
|
53
|
+
export declare function findProhibitionViolations(prohibitions: Prohibition[], files: ChangedFile[]): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this line express a modification ban? Matches the active forms ("do not
|
|
3
|
+
* modify", "must not touch", "never edit", "don't change") and the passive form
|
|
4
|
+
* ("must not be modified"). Deliberately verb-scoped to modification — a "do not
|
|
5
|
+
* add a dependency" style rule names no path and is the prompt rule's job.
|
|
6
|
+
*/
|
|
7
|
+
const PROHIBITION_RE = /\b(?:do\s+not|don'?t|must\s+not|never)\s+(?:be\s+)?(?:modify|modified|touch|touched|edit|edited|change|changed|alter|altered|rewrite|rewritten|overwrite|overwritten|delete|deleted|remove|removed)\b/i;
|
|
8
|
+
/**
|
|
9
|
+
* A backtick token counts as a path only when it is whitespace-free, uses path
|
|
10
|
+
* characters, and either contains a directory separator, has a file extension,
|
|
11
|
+
* or is a dotfile. Bare identifiers (`getOrder`), API routes with spaces
|
|
12
|
+
* (`POST /api/listings`), and code snippets are rejected — the probe would
|
|
13
|
+
* rather miss a `Makefile` than fire on prose.
|
|
14
|
+
*/
|
|
15
|
+
function looksLikePath(token) {
|
|
16
|
+
if (!/^[\w.@~/-]+$/.test(token))
|
|
17
|
+
return false;
|
|
18
|
+
return token.includes('/') || /\.[A-Za-z0-9]+$/.test(token) || token.startsWith('.');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract the concrete paths the spec explicitly forbids modifying: every
|
|
22
|
+
* backtick-quoted path-like token on a line that expresses a modification ban.
|
|
23
|
+
* Prose-only prohibitions ("do not modify server-side code" with no path named)
|
|
24
|
+
* extract nothing — the prompt-level rule still covers them.
|
|
25
|
+
*/
|
|
26
|
+
export function extractProhibitions(spec) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const line of spec.split('\n')) {
|
|
30
|
+
if (!PROHIBITION_RE.test(line))
|
|
31
|
+
continue;
|
|
32
|
+
for (const m of line.matchAll(/`([^`]+)`/g)) {
|
|
33
|
+
const token = m[1].trim();
|
|
34
|
+
if (!looksLikePath(token) || seen.has(token))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(token);
|
|
37
|
+
out.push({ path: token, constraint: line.trim() });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Normalise a path for comparison: strip leading ./ and trailing /. */
|
|
43
|
+
const norm = (p) => p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
44
|
+
/**
|
|
45
|
+
* Intersect the spec's prohibitions with the task's changed files (git shape —
|
|
46
|
+
* the same collector the substitution probe uses). A prohibition matches a
|
|
47
|
+
* changed file exactly, or as a directory prefix (`src/server` covers
|
|
48
|
+
* `src/server/index.ts`). One finding line per violated file, carrying the
|
|
49
|
+
* constraint verbatim; empty array → no block in the prompt.
|
|
50
|
+
*/
|
|
51
|
+
export function findProhibitionViolations(prohibitions, files) {
|
|
52
|
+
const findings = [];
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
const fp = norm(f.path);
|
|
55
|
+
const hit = prohibitions.find(p => {
|
|
56
|
+
const pp = norm(p.path);
|
|
57
|
+
return fp === pp || fp.startsWith(`${pp}/`);
|
|
58
|
+
});
|
|
59
|
+
if (!hit)
|
|
60
|
+
continue;
|
|
61
|
+
findings.push(`${f.path} — modified by this task, but the spec forbids it: "${hit.constraint}"`);
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
@@ -38,11 +38,24 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
38
38
|
* `probeFindings` are the deterministic self-verification probe results (see
|
|
39
39
|
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
40
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
|
|
41
|
+
* file — the rule alone got 2/5 attention on the local model. The findings are
|
|
42
42
|
* pure git shape (test files the task itself changed), so the mandate is
|
|
43
43
|
* language- and framework-agnostic.
|
|
44
|
+
*
|
|
45
|
+
* `prohibitionFindings` are the deterministic prohibition probe results (see
|
|
46
|
+
* prohibition-probe.ts): spec-forbidden paths the task's diff modified anyway.
|
|
47
|
+
* Same probe+rule design, same reason: the VIOLATION-EXCUSAL class (mx5 run 7:
|
|
48
|
+
* child saw "Do NOT modify server-side code" violated, waived it as "additive,
|
|
49
|
+
* tests pass", PASSed) needs both the no-waiver rule (4b) AND the concrete diff
|
|
50
|
+
* fact — the baseline child usually never runs `git diff` at all, so without the
|
|
51
|
+
* finding it cannot even SEE the violation. A/B on the live local model
|
|
52
|
+
* (violated-but-working fixture, everything green, forbidden file modified
|
|
53
|
+
* additively): old prompt 5/5 false-PASS (several runs affirmatively claimed the
|
|
54
|
+
* forbidden file was untouched); rule+finding 5/5 FAIL naming the constraint.
|
|
55
|
+
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
56
|
+
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
44
57
|
*/
|
|
45
|
-
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string): string;
|
|
58
|
+
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string, prohibitionFindings?: string[]): string;
|
|
46
59
|
/**
|
|
47
60
|
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
|
|
48
61
|
* (the model discusses before concluding, and bash output may echo the word
|
|
@@ -82,6 +95,12 @@ export interface VerificationDeps {
|
|
|
82
95
|
* into the child's prompt. A/B-proven load-bearing: the prompt rule alone caught
|
|
83
96
|
* the class 2/5, rule + probe finding 5/5. ABSENT or empty → no probe block. */
|
|
84
97
|
probe?: () => Promise<string[]>;
|
|
98
|
+
/**
|
|
99
|
+
* DETERMINISTIC prohibition probe (see prohibition-probe.ts): spec-forbidden
|
|
100
|
+
* paths the task's diff modified anyway, injected as prompt findings under the
|
|
101
|
+
* no-waiver rule (4b). Advisory, never auto-FAIL — real prohibitions can be
|
|
102
|
+
* conditional prose. ABSENT or empty → no prohibition block. */
|
|
103
|
+
prohibitionProbe?: () => Promise<string[]>;
|
|
85
104
|
/**
|
|
86
105
|
* Result of the git-state guard for the MOST RECENT runChild call (see
|
|
87
106
|
* git-state-guard.ts): did the child mutate repo state (stash/checkout/file
|
package/dist/task/verify-work.js
CHANGED
|
@@ -120,11 +120,24 @@ export function extractSpecForVerification(taskBody) {
|
|
|
120
120
|
* `probeFindings` are the deterministic self-verification probe results (see
|
|
121
121
|
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
122
122
|
* prompt carries both the rule (3b) AND a concrete finding naming the suspect
|
|
123
|
-
* file — the rule alone got 2/5 attention on the
|
|
123
|
+
* file — the rule alone got 2/5 attention on the local model. The findings are
|
|
124
124
|
* pure git shape (test files the task itself changed), so the mandate is
|
|
125
125
|
* language- and framework-agnostic.
|
|
126
|
+
*
|
|
127
|
+
* `prohibitionFindings` are the deterministic prohibition probe results (see
|
|
128
|
+
* prohibition-probe.ts): spec-forbidden paths the task's diff modified anyway.
|
|
129
|
+
* Same probe+rule design, same reason: the VIOLATION-EXCUSAL class (mx5 run 7:
|
|
130
|
+
* child saw "Do NOT modify server-side code" violated, waived it as "additive,
|
|
131
|
+
* tests pass", PASSed) needs both the no-waiver rule (4b) AND the concrete diff
|
|
132
|
+
* fact — the baseline child usually never runs `git diff` at all, so without the
|
|
133
|
+
* finding it cannot even SEE the violation. A/B on the live local model
|
|
134
|
+
* (violated-but-working fixture, everything green, forbidden file modified
|
|
135
|
+
* additively): old prompt 5/5 false-PASS (several runs affirmatively claimed the
|
|
136
|
+
* forbidden file was untouched); rule+finding 5/5 FAIL naming the constraint.
|
|
137
|
+
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
138
|
+
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
126
139
|
*/
|
|
127
|
-
export function buildVerifyPrompt(spec, probeFindings, envNotes) {
|
|
140
|
+
export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings) {
|
|
128
141
|
const probeBlock = probeFindings && probeFindings.length > 0 ?
|
|
129
142
|
[
|
|
130
143
|
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
@@ -138,6 +151,19 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes) {
|
|
|
138
151
|
''
|
|
139
152
|
]
|
|
140
153
|
: [];
|
|
154
|
+
const prohibitionBlock = prohibitionFindings && prohibitionFindings.length > 0 ?
|
|
155
|
+
[
|
|
156
|
+
'PROHIBITION NOTICE (deterministic, computed by the orchestrator from the',
|
|
157
|
+
"spec's own constraint lines and the task's diff): this task MODIFIED paths",
|
|
158
|
+
'the spec explicitly forbids modifying:',
|
|
159
|
+
...prohibitionFindings.map(f => `- ${f}`),
|
|
160
|
+
'Read the exact constraint wording in the spec. Unless that wording itself',
|
|
161
|
+
'states an exception that covers this change, this is a violated prohibition:',
|
|
162
|
+
'rule 4b applies and the verdict is FAIL naming the forbidden path — even if',
|
|
163
|
+
'every test passes and the change looks harmless.',
|
|
164
|
+
''
|
|
165
|
+
]
|
|
166
|
+
: [];
|
|
141
167
|
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
|
|
142
168
|
return [
|
|
143
169
|
'You are a strict verification pass running right after an AI coding agent',
|
|
@@ -154,6 +180,7 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes) {
|
|
|
154
180
|
'',
|
|
155
181
|
...envBlock,
|
|
156
182
|
...probeBlock,
|
|
183
|
+
...prohibitionBlock,
|
|
157
184
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
158
185
|
'checkout (or CI run) would experience it:',
|
|
159
186
|
'',
|
|
@@ -218,6 +245,19 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes) {
|
|
|
218
245
|
'4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
|
|
219
246
|
' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
220
247
|
'',
|
|
248
|
+
'4b. SPEC PROHIBITIONS ARE PART OF THE BAR — YOU HAVE NO WAIVER AUTHORITY: when the',
|
|
249
|
+
' spec explicitly forbids something ("Do NOT modify X", "MUST NOT touch Y") and the',
|
|
250
|
+
' shipped work does it anyway, that is a FAIL naming the violated constraint. You',
|
|
251
|
+
' may not excuse a violation because it is additive, small, harmless, an improvement,',
|
|
252
|
+
' or because every test still passes — "it works anyway" is exactly the waiver you do',
|
|
253
|
+
" not have; relaxing a constraint is the spec owner's call, not yours. Check the",
|
|
254
|
+
" task's own diff (git) against the spec's prohibitions — a forbidden file can be",
|
|
255
|
+
' modified without any test noticing. Only two outcomes are not a FAIL: the',
|
|
256
|
+
' violation was fully REVERTED (the shipped tree no longer violates), or the',
|
|
257
|
+
' prohibition\'s own wording states an exception ("except…", "beyond what is needed',
|
|
258
|
+
' for…") that covers the change — judged against that stated exception, not against',
|
|
259
|
+
' your view of harmlessness.',
|
|
260
|
+
'',
|
|
221
261
|
'5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
|
|
222
262
|
' service or network resource (a database server, an API host) that the project',
|
|
223
263
|
' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
|
|
@@ -310,6 +350,15 @@ export async function runWorkVerification(deps) {
|
|
|
310
350
|
findings = [];
|
|
311
351
|
}
|
|
312
352
|
}
|
|
353
|
+
let prohibitions = [];
|
|
354
|
+
if (deps.prohibitionProbe) {
|
|
355
|
+
try {
|
|
356
|
+
prohibitions = await deps.prohibitionProbe();
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
prohibitions = [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
313
362
|
// Environment facts from earlier gate children (best-effort; a cache failure
|
|
314
363
|
// must never block verification).
|
|
315
364
|
let envNotes = '';
|
|
@@ -328,7 +377,7 @@ export async function runWorkVerification(deps) {
|
|
|
328
377
|
for (let attempt = 1;; attempt++) {
|
|
329
378
|
let text;
|
|
330
379
|
try {
|
|
331
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes), deps.signal);
|
|
380
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions), deps.signal);
|
|
332
381
|
}
|
|
333
382
|
catch (err) {
|
|
334
383
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|