@mjasnikovs/pi-task 0.18.3 → 0.18.4
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/enforce-guidelines.d.ts +2 -2
- package/dist/task/enforce-guidelines.js +36 -3
- package/dist/task/env-notes.d.ts +24 -8
- package/dist/task/env-notes.js +124 -24
- package/dist/task/gate-deps.d.ts +10 -0
- package/dist/task/gate-deps.js +97 -3
- package/dist/task/probe-gaming.d.ts +60 -0
- package/dist/task/probe-gaming.js +0 -0
- package/dist/task/test-assembly.d.ts +87 -0
- package/dist/task/test-assembly.js +163 -0
- package/dist/task/verify-work.d.ts +17 -1
- package/dist/task/verify-work.js +87 -2
- package/package.json +2 -2
|
@@ -61,14 +61,14 @@ export declare function discoverGuidelines(cwd: string, readFile?: (p: string) =
|
|
|
61
61
|
* done, and the contract for the final verdict line. Kept pure so the wording
|
|
62
62
|
* is unit-tested without spawning pi.
|
|
63
63
|
*/
|
|
64
|
-
export declare function buildEnforcePrompt(rulesText: string, diff: string): string;
|
|
64
|
+
export declare function buildEnforcePrompt(rulesText: string, diff: string, probeGamingFindings?: string[]): string;
|
|
65
65
|
/**
|
|
66
66
|
* Build the FLAG-ONLY enforcement prompt: same rules + diff, but the child has a
|
|
67
67
|
* `read` tool only and is told to REPORT violations, not fix them. Kept pure so
|
|
68
68
|
* the wording is unit-tested without spawning pi. Used when there is no
|
|
69
69
|
* verification signal to guard a destructive edit (see ENFORCE_FLAG_TOOLS).
|
|
70
70
|
*/
|
|
71
|
-
export declare function buildEnforceFlagPrompt(rulesText: string, diff: string): string;
|
|
71
|
+
export declare function buildEnforceFlagPrompt(rulesText: string, diff: string, probeGamingFindings?: string[]): string;
|
|
72
72
|
/**
|
|
73
73
|
* Parse the child's final verdict. Scans for the LAST `ENFORCE: CLEAN` /
|
|
74
74
|
* `ENFORCE: VIOLATION` marker (the model may discuss before concluding).
|
|
@@ -26,6 +26,7 @@ import * as path from 'node:path';
|
|
|
26
26
|
import { runChildDefault } from '../shared/child-process.js';
|
|
27
27
|
import { USER_CANCELLED } from './child-runner.js';
|
|
28
28
|
import { TASKS_DIR_NAME } from './task-types.js';
|
|
29
|
+
import { findProbeGamingInDiff } from './probe-gaming.js';
|
|
29
30
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
30
31
|
export const GUIDELINE_FILENAMES = ['AGENTS.md', 'CLAUDE.md'];
|
|
31
32
|
/**
|
|
@@ -87,12 +88,33 @@ export async function discoverGuidelines(cwd, readFile = p => fsp.readFile(p, 'u
|
|
|
87
88
|
return null;
|
|
88
89
|
return { files, text: sections.join('\n\n') };
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Render the deterministic probe-gaming findings (run-8 F6) as a prompt block, or
|
|
93
|
+
* an empty array when there are none. Shared by the edit and flag prompts: the
|
|
94
|
+
* finding is a concrete diff line whose stated purpose is to make a check pass
|
|
95
|
+
* rather than meet the requirement — the reliable lever the prompt rule leans on.
|
|
96
|
+
* The `action` line differs (fix vs report) between the two capability modes.
|
|
97
|
+
*/
|
|
98
|
+
function probeGamingEnforceBlock(findings, action) {
|
|
99
|
+
if (findings.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
return [
|
|
102
|
+
'CHECK-GAMING NOTICE (deterministic, computed from the diff): these added lines',
|
|
103
|
+
'state their own purpose is to make a CHECK pass (a test / verification / lint /',
|
|
104
|
+
'gate), not to meet the requirement the check stands for:',
|
|
105
|
+
...findings.map(f => `- ${f}`),
|
|
106
|
+
'A check is a MESSENGER for a requirement; code written only to quiet the messenger',
|
|
107
|
+
'is a defect even when the check is green (run-8 F6: a handler returned 401 "so the',
|
|
108
|
+
`verification test passes" while the real route stayed dead). ${action}`,
|
|
109
|
+
''
|
|
110
|
+
];
|
|
111
|
+
}
|
|
90
112
|
/**
|
|
91
113
|
* Build the enforcement child's prompt: the rules, the diff of the work just
|
|
92
114
|
* done, and the contract for the final verdict line. Kept pure so the wording
|
|
93
115
|
* is unit-tested without spawning pi.
|
|
94
116
|
*/
|
|
95
|
-
export function buildEnforcePrompt(rulesText, diff) {
|
|
117
|
+
export function buildEnforcePrompt(rulesText, diff, probeGamingFindings = []) {
|
|
96
118
|
return [
|
|
97
119
|
'You are a strict guideline-enforcement pass running right after an AI coding',
|
|
98
120
|
'agent finished a task and committed it. The agent is known to skip project',
|
|
@@ -111,6 +133,9 @@ export function buildEnforcePrompt(rulesText, diff) {
|
|
|
111
133
|
'CHANGES IN THE LAST COMMIT (verify these specifically against the rules):',
|
|
112
134
|
diff.trim().length > 0 ? diff : '(no textual diff captured — nothing to verify)',
|
|
113
135
|
'',
|
|
136
|
+
...probeGamingEnforceBlock(probeGamingFindings, 'Treat this as a violation: replace the check-gaming code with a real'
|
|
137
|
+
+ ' implementation of the requirement, or if you cannot, report it as a'
|
|
138
|
+
+ ' VIOLATION naming the gamed check.'),
|
|
114
139
|
'Your job:',
|
|
115
140
|
'1. Read each changed file and check it against EVERY rule above.',
|
|
116
141
|
'2. For each violation you find, FIX it directly with your `edit` tool, then',
|
|
@@ -130,7 +155,7 @@ export function buildEnforcePrompt(rulesText, diff) {
|
|
|
130
155
|
* the wording is unit-tested without spawning pi. Used when there is no
|
|
131
156
|
* verification signal to guard a destructive edit (see ENFORCE_FLAG_TOOLS).
|
|
132
157
|
*/
|
|
133
|
-
export function buildEnforceFlagPrompt(rulesText, diff) {
|
|
158
|
+
export function buildEnforceFlagPrompt(rulesText, diff, probeGamingFindings = []) {
|
|
134
159
|
return [
|
|
135
160
|
'You are a strict guideline-enforcement REVIEW pass running right after an AI',
|
|
136
161
|
'coding agent finished a task and committed it. The agent is known to skip',
|
|
@@ -146,6 +171,8 @@ export function buildEnforceFlagPrompt(rulesText, diff) {
|
|
|
146
171
|
'CHANGES IN THE LAST COMMIT (review these specifically against the rules):',
|
|
147
172
|
diff.trim().length > 0 ? diff : '(no textual diff captured — nothing to verify)',
|
|
148
173
|
'',
|
|
174
|
+
...probeGamingEnforceBlock(probeGamingFindings, 'Treat this as a violation and REPORT it (do not fix it): name the gamed check'
|
|
175
|
+
+ ' and the requirement left unmet.'),
|
|
149
176
|
'Read each changed file and check it against EVERY rule above. Do NOT attempt',
|
|
150
177
|
'to fix anything — only report what you find.',
|
|
151
178
|
'',
|
|
@@ -279,9 +306,15 @@ export async function runGuidelineEnforcement(deps) {
|
|
|
279
306
|
if (!doc)
|
|
280
307
|
return { ok: true, reason: 'no guideline files' };
|
|
281
308
|
const diff = await getDiff(deps.cwd, deps.signal);
|
|
309
|
+
// Deterministic probe-gaming findings (F6) straight from the captured diff — no
|
|
310
|
+
// extra git call, the diff is already in hand. Injected under the CHECK-GAMING
|
|
311
|
+
// rule so the child acts on a concrete line, not on self-discovered intent.
|
|
312
|
+
const probeGaming = findProbeGamingInDiff(diff);
|
|
282
313
|
const flagOnly = deps.mode === 'flag';
|
|
283
314
|
const tools = flagOnly ? ENFORCE_FLAG_TOOLS : ENFORCE_TOOLS;
|
|
284
|
-
const prompt = flagOnly ?
|
|
315
|
+
const prompt = flagOnly ?
|
|
316
|
+
buildEnforceFlagPrompt(doc.text, diff, probeGaming)
|
|
317
|
+
: buildEnforcePrompt(doc.text, diff, probeGaming);
|
|
285
318
|
let text;
|
|
286
319
|
try {
|
|
287
320
|
text = await deps.runChild(tools, prompt, deps.signal);
|
package/dist/task/env-notes.d.ts
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
export declare function envNotesFile(cwd: string): string;
|
|
2
|
-
/** The
|
|
2
|
+
/** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
|
|
3
3
|
export declare function readEnvNotes(cwd: string): Promise<string>;
|
|
4
|
+
/** One recorded fact plus the origin task that established it (may be ''). */
|
|
5
|
+
export interface EnvNote {
|
|
6
|
+
fact: string;
|
|
7
|
+
origin: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse the stored file into fact+origin records. Legacy lines written before
|
|
11
|
+
* provenance (no separator) parse with an empty origin, so old caches still read.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseEnvNotes(raw: string): EnvNote[];
|
|
4
14
|
/**
|
|
5
15
|
* Pull `ENV-NOTE: <fact>` lines out of a child's answer text. Deduplicated,
|
|
6
16
|
* length-capped; verdict markers can never match (different prefix).
|
|
7
17
|
*/
|
|
8
18
|
export declare function extractEnvNotes(text: string): string[];
|
|
19
|
+
/** True when a fact reads like a standing excuse (see EXCUSE_PATTERNS). */
|
|
20
|
+
export declare function isExcuseNote(fact: string): boolean;
|
|
9
21
|
/**
|
|
10
22
|
* Append newly discovered facts to the cache, deduplicated against what is
|
|
11
|
-
* already there (case-insensitive
|
|
12
|
-
*
|
|
23
|
+
* already there (case-insensitive fact match), keeping the newest MAX_NOTES.
|
|
24
|
+
* Each new fact is stamped with the `origin` task that recorded it; a fact
|
|
25
|
+
* already present keeps its ORIGINAL origin (provenance traces to who first
|
|
26
|
+
* established it). Failures are swallowed — the cache is a sharpener, never a
|
|
13
27
|
* blocker.
|
|
14
28
|
*/
|
|
15
|
-
export declare function appendEnvNotes(cwd: string, notes: string[]): Promise<void>;
|
|
29
|
+
export declare function appendEnvNotes(cwd: string, notes: string[], origin?: string): Promise<void>;
|
|
16
30
|
/**
|
|
17
|
-
* The prompt block a gate child receives when notes exist.
|
|
18
|
-
* load-bearing: facts save re-discovery time but grant no
|
|
19
|
-
*
|
|
31
|
+
* The prompt block a gate child receives when notes exist. Two things are
|
|
32
|
+
* load-bearing: the no-waiver caveat (facts save re-discovery time but grant no
|
|
33
|
+
* license to prepare/repair) and the trust discipline (a note is second-hand
|
|
34
|
+
* until re-validated; an EXCUSE-CLASS note may not wave off a failure without a
|
|
35
|
+
* live re-check; a grep of a generated artifact is not evidence of absence).
|
|
20
36
|
*/
|
|
21
|
-
export declare function buildEnvNotesBlock(
|
|
37
|
+
export declare function buildEnvNotesBlock(raw: string): string;
|
|
22
38
|
/** The emit instruction appended to bash-capable gate-child prompts. */
|
|
23
39
|
export declare const ENV_NOTE_EMIT_INSTRUCTION: string;
|
package/dist/task/env-notes.js
CHANGED
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
* project setup: the verify-as-shipped rule ("any prep you needed IS the
|
|
19
19
|
* defect") still governs every verdict — the block injected into prompts says
|
|
20
20
|
* so explicitly. The cache only kills re-discovery time.
|
|
21
|
+
*
|
|
22
|
+
* PROVENANCE + RE-VALIDATION (run 8, F7): a verify child once grepped component
|
|
23
|
+
* names in a MINIFIED bundle (identifiers mangled ⇒ 0 hits by construction),
|
|
24
|
+
* wrote "build tree-shakes ALL route components — pre-existing issue" to the
|
|
25
|
+
* cache, and ten later tasks inherited it verbatim as a standing "pre-existing,
|
|
26
|
+
* unrelated" excuse to wave off a genuinely broken deliverable — nobody
|
|
27
|
+
* re-checked. Two guards close that class: (a) each note is stamped host-side
|
|
28
|
+
* with the ORIGIN task that recorded it (a note is second-hand hearsay, not the
|
|
29
|
+
* reader's own observation); (b) the injected block demands the reader
|
|
30
|
+
* RE-VALIDATE a note in the CURRENT tree before citing it to excuse a failure,
|
|
31
|
+
* flags EXCUSE-CLASS notes ("pre-existing", "unrelated", "tree-shaken") for
|
|
32
|
+
* exactly that scrutiny, and forbids treating a grep of a generated artifact as
|
|
33
|
+
* evidence of absence. Provenance is mechanical; re-validation is prompt-level.
|
|
21
34
|
*/
|
|
22
35
|
import * as fsp from 'node:fs/promises';
|
|
23
36
|
import * as path from 'node:path';
|
|
@@ -27,10 +40,16 @@ const ENV_NOTES_FILE = 'env-notes.md';
|
|
|
27
40
|
const MAX_NOTES = 40;
|
|
28
41
|
/** A single fact is one line; anything longer is prose, not a fact. */
|
|
29
42
|
const MAX_NOTE_LENGTH = 240;
|
|
43
|
+
/**
|
|
44
|
+
* Field separator between a fact and its origin in the stored file. A tab never
|
|
45
|
+
* occurs in a one-line fact (facts are prose), so it round-trips cleanly and any
|
|
46
|
+
* stray tab in an emitted fact is normalised to a space before storage.
|
|
47
|
+
*/
|
|
48
|
+
const ORIGIN_SEP = '\t';
|
|
30
49
|
export function envNotesFile(cwd) {
|
|
31
50
|
return path.join(tasksDir(cwd), ENV_NOTES_FILE);
|
|
32
51
|
}
|
|
33
|
-
/** The
|
|
52
|
+
/** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
|
|
34
53
|
export async function readEnvNotes(cwd) {
|
|
35
54
|
try {
|
|
36
55
|
return (await fsp.readFile(envNotesFile(cwd), 'utf8')).trim();
|
|
@@ -39,6 +58,27 @@ export async function readEnvNotes(cwd) {
|
|
|
39
58
|
return '';
|
|
40
59
|
}
|
|
41
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse the stored file into fact+origin records. Legacy lines written before
|
|
63
|
+
* provenance (no separator) parse with an empty origin, so old caches still read.
|
|
64
|
+
*/
|
|
65
|
+
export function parseEnvNotes(raw) {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const line of raw.split('\n')) {
|
|
68
|
+
const t = line.trim();
|
|
69
|
+
if (t.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const i = t.indexOf(ORIGIN_SEP);
|
|
72
|
+
if (i === -1)
|
|
73
|
+
out.push({ fact: t, origin: '' });
|
|
74
|
+
else
|
|
75
|
+
out.push({ fact: t.slice(0, i).trim(), origin: t.slice(i + 1).trim() });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function serializeNote(n) {
|
|
80
|
+
return n.origin ? `${n.fact}${ORIGIN_SEP}${n.origin}` : n.fact;
|
|
81
|
+
}
|
|
42
82
|
/**
|
|
43
83
|
* Pull `ENV-NOTE: <fact>` lines out of a child's answer text. Deduplicated,
|
|
44
84
|
* length-capped; verdict markers can never match (different prefix).
|
|
@@ -58,54 +98,110 @@ export function extractEnvNotes(text) {
|
|
|
58
98
|
}
|
|
59
99
|
return notes;
|
|
60
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* EXCUSE-CLASS wording: a note that waves a problem off as someone else's or a
|
|
103
|
+
* prior condition ("pre-existing … mismatch", "unrelated to this task",
|
|
104
|
+
* "tree-shaken", "not applicable"). These are the notes that propagate across
|
|
105
|
+
* slices as standing excuses (mx5 run-8 F7) — every one of the run-8 cache's
|
|
106
|
+
* dozen such notes was either the false tree-shake fact or the schema mismatch
|
|
107
|
+
* the final gate later proved was a REAL defect. Pure text, stack-agnostic; the
|
|
108
|
+
* flag never drops or fails a note, it only marks it as needing live
|
|
109
|
+
* re-validation before it may EXCUSE a failure. FP is harmless by construction:
|
|
110
|
+
* a benign fact re-validates and is used, the marker only bites a citation-to-
|
|
111
|
+
* wave-off. Benign status facts ("5 pre-existing warnings") do not match — a
|
|
112
|
+
* "pre-existing" match requires a co-located problem word.
|
|
113
|
+
*/
|
|
114
|
+
const EXCUSE_PATTERNS = [
|
|
115
|
+
/\bunrelated\b/i,
|
|
116
|
+
/\bnot (?:my|our|this) (?:task|concern|deliverable|slice|problem)\b/i,
|
|
117
|
+
/\boutside (?:the|this) deliverable\b/i,
|
|
118
|
+
/\bnot applicable\b/i,
|
|
119
|
+
/\bnot specific to\b/i,
|
|
120
|
+
/\btree[-\s]?shak/i,
|
|
121
|
+
/\bpre-?existing\b[^.\n]*\b(?:fail|issue|mismatch|bug|error|broken|problem|affect)/i,
|
|
122
|
+
/\baffect(?:ing|s)? all\b/i
|
|
123
|
+
];
|
|
124
|
+
/** True when a fact reads like a standing excuse (see EXCUSE_PATTERNS). */
|
|
125
|
+
export function isExcuseNote(fact) {
|
|
126
|
+
return EXCUSE_PATTERNS.some(re => re.test(fact));
|
|
127
|
+
}
|
|
61
128
|
/**
|
|
62
129
|
* Append newly discovered facts to the cache, deduplicated against what is
|
|
63
|
-
* already there (case-insensitive
|
|
64
|
-
*
|
|
130
|
+
* already there (case-insensitive fact match), keeping the newest MAX_NOTES.
|
|
131
|
+
* Each new fact is stamped with the `origin` task that recorded it; a fact
|
|
132
|
+
* already present keeps its ORIGINAL origin (provenance traces to who first
|
|
133
|
+
* established it). Failures are swallowed — the cache is a sharpener, never a
|
|
65
134
|
* blocker.
|
|
66
135
|
*/
|
|
67
|
-
export async function appendEnvNotes(cwd, notes) {
|
|
136
|
+
export async function appendEnvNotes(cwd, notes, origin = '') {
|
|
68
137
|
if (notes.length === 0)
|
|
69
138
|
return;
|
|
70
139
|
try {
|
|
71
|
-
const existing = (await readEnvNotes(cwd))
|
|
72
|
-
const seen = new Set(existing.map(
|
|
140
|
+
const existing = parseEnvNotes(await readEnvNotes(cwd));
|
|
141
|
+
const seen = new Set(existing.map(n => n.fact.toLowerCase()));
|
|
73
142
|
const merged = [...existing];
|
|
74
143
|
for (const note of notes) {
|
|
75
|
-
const
|
|
76
|
-
|
|
144
|
+
const fact = note.trim().replace(/\t/g, ' ');
|
|
145
|
+
const key = fact.toLowerCase();
|
|
146
|
+
if (key.length === 0 || seen.has(key))
|
|
77
147
|
continue;
|
|
78
148
|
seen.add(key);
|
|
79
|
-
merged.push(
|
|
149
|
+
merged.push({ fact, origin: origin.trim() });
|
|
80
150
|
}
|
|
81
151
|
const kept = merged.slice(-MAX_NOTES);
|
|
82
152
|
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
83
|
-
await fsp.writeFile(envNotesFile(cwd), kept.join('\n') + '\n', 'utf8');
|
|
153
|
+
await fsp.writeFile(envNotesFile(cwd), kept.map(serializeNote).join('\n') + '\n', 'utf8');
|
|
84
154
|
}
|
|
85
155
|
catch {
|
|
86
156
|
// best-effort cache
|
|
87
157
|
}
|
|
88
158
|
}
|
|
89
159
|
/**
|
|
90
|
-
* The prompt block a gate child receives when notes exist.
|
|
91
|
-
* load-bearing: facts save re-discovery time but grant no
|
|
92
|
-
*
|
|
160
|
+
* The prompt block a gate child receives when notes exist. Two things are
|
|
161
|
+
* load-bearing: the no-waiver caveat (facts save re-discovery time but grant no
|
|
162
|
+
* license to prepare/repair) and the trust discipline (a note is second-hand
|
|
163
|
+
* until re-validated; an EXCUSE-CLASS note may not wave off a failure without a
|
|
164
|
+
* live re-check; a grep of a generated artifact is not evidence of absence).
|
|
93
165
|
*/
|
|
94
|
-
export function buildEnvNotesBlock(
|
|
95
|
-
|
|
166
|
+
export function buildEnvNotesBlock(raw) {
|
|
167
|
+
const notes = parseEnvNotes(raw);
|
|
168
|
+
if (notes.length === 0)
|
|
96
169
|
return '';
|
|
170
|
+
const lines = notes.map(n => {
|
|
171
|
+
const origin = n.origin ? ` — recorded by ${n.origin}` : ' — origin unrecorded';
|
|
172
|
+
const flag = isExcuseNote(n.fact) ?
|
|
173
|
+
' [EXCUSE-CLASS — re-validate in the CURRENT tree before citing this to wave off any failure]'
|
|
174
|
+
: '';
|
|
175
|
+
return `- ${n.fact}${origin}${flag}`;
|
|
176
|
+
});
|
|
97
177
|
return [
|
|
98
|
-
'KNOWN ENVIRONMENT FACTS —
|
|
99
|
-
'(
|
|
100
|
-
...
|
|
101
|
-
|
|
102
|
-
.split('\n')
|
|
103
|
-
.map(n => `- ${n}`),
|
|
178
|
+
'KNOWN ENVIRONMENT FACTS — recorded by earlier verification passes in this run',
|
|
179
|
+
'(second-hand, may be stale or WRONG):',
|
|
180
|
+
...lines,
|
|
181
|
+
'',
|
|
104
182
|
'These facts only save you re-discovery time (where credentials/config live, which',
|
|
105
183
|
'tools are installed, which services are reachable). They are NOT a license to',
|
|
106
184
|
'prepare or repair the run: the verify-as-shipped rules below still govern the',
|
|
107
185
|
'verdict — if the project needs something its own committed files do not provide,',
|
|
108
186
|
'that remains the defect no matter what is listed here.',
|
|
187
|
+
'',
|
|
188
|
+
'TRUST DISCIPLINE — a false "pre-existing, unrelated" note once masked a real shipped',
|
|
189
|
+
'defect across many tasks in this exact pipeline; do not repeat it:',
|
|
190
|
+
'- A note above is second-hand hearsay from another task, not your own observation.',
|
|
191
|
+
' You may CITE one to EXCUSE, wave off, or down-grade a failure ONLY IF you',
|
|
192
|
+
' RE-VALIDATE its claim in the CURRENT tree right now and state the command or',
|
|
193
|
+
' observation you used to reconfirm it.',
|
|
194
|
+
'- If a note fails re-validation (its claim is not true in the current tree), do NOT',
|
|
195
|
+
" inherit it: treat the underlying problem as UNexcused and report it. Don't silently",
|
|
196
|
+
' carry a stale fact forward.',
|
|
197
|
+
'- EVIDENCE HYGIENE: string-searching a MINIFIED, bundled, or otherwise generated or',
|
|
198
|
+
' compiled artifact is NOT evidence that something is absent — identifiers there are',
|
|
199
|
+
' renamed or stripped by construction, so a zero-hit grep proves nothing. Derive',
|
|
200
|
+
' presence/absence only from SOURCE files or by EXECUTING the artifact and observing.',
|
|
201
|
+
'- A claim that a defect is "pre-existing", "unrelated", or "not this task" is an',
|
|
202
|
+
' EXCUSE, not a fact (the ones above are marked): re-establish it live, and if it',
|
|
203
|
+
' actually holds as a real defect, escalate it (report FAIL) rather than passing it',
|
|
204
|
+
' on as a standing waiver.',
|
|
109
205
|
''
|
|
110
206
|
].join('\n');
|
|
111
207
|
}
|
|
@@ -115,7 +211,11 @@ export const ENV_NOTE_EMIT_INSTRUCTION = [
|
|
|
115
211
|
'THIS MACHINE or the project environment (a service reachable/absent at an address, where',
|
|
116
212
|
'credentials/config live, a tool or runtime present/missing and its version), emit a line',
|
|
117
213
|
' ENV-NOTE: <one-line fact>',
|
|
118
|
-
'anywhere in your answer, one per fact.
|
|
119
|
-
'
|
|
120
|
-
'
|
|
214
|
+
'anywhere in your answer, one per fact. A fact is something you OBSERVED to be true of the',
|
|
215
|
+
'machine or environment — never a task verdict, never spec content, never a judgment about',
|
|
216
|
+
'the code. In particular do NOT record an absence you inferred from grepping a built or',
|
|
217
|
+
'minified artifact (identifiers there are mangled — a zero-hit grep proves nothing), and',
|
|
218
|
+
'do NOT record "X is pre-existing / unrelated / not my task": that is a verdict, not an',
|
|
219
|
+
'environment fact. These are cached for later verification passes in this run so they do',
|
|
220
|
+
'not re-discover the same things.'
|
|
121
221
|
].join('\n');
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { GateDeps } from './task-gates.js';
|
|
3
3
|
import { type FinalFixResult } from './final-gate-fix.js';
|
|
4
|
+
import { type AddedLine } from './probe-gaming.js';
|
|
4
5
|
import { type ChangedFile } from './substitution-probe.js';
|
|
5
6
|
/** A function that re-runs a task's implementation turn (AUTOFIX). Injected by the
|
|
6
7
|
* command so this module stays free of the orchestrators (avoids an import cycle). */
|
|
@@ -17,6 +18,15 @@ export type FinalGateFixFn = (ctx: ExtensionCommandContext, cwd: string, failRea
|
|
|
17
18
|
* blocker.
|
|
18
19
|
*/
|
|
19
20
|
export declare function collectChangedFiles(cwd: string, signal?: AbortSignal): Promise<ChangedFile[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Collect the task's added lines WITH CONTENT (path + text) for the probe-gaming
|
|
23
|
+
* probe (F6), which needs the actual line text the numstat-shape collector omits.
|
|
24
|
+
* Pre-commit the work is the tree-vs-HEAD diff plus untracked files (read whole, as
|
|
25
|
+
* all-added); on the post-enforce re-verify the tree is clean, so fall back to the
|
|
26
|
+
* last commit's diff. Failures degrade to an empty list — the probe is a sharpener,
|
|
27
|
+
* never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Promise<AddedLine[]>;
|
|
20
30
|
/**
|
|
21
31
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
22
32
|
* implementation re-runner, injected by the caller. The returned object also drives
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -26,7 +26,9 @@ import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate
|
|
|
26
26
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
27
27
|
import { researchResolution } from './verify-resolution.js';
|
|
28
28
|
import { extractProhibitions, findProhibitionViolations } from './prohibition-probe.js';
|
|
29
|
-
import {
|
|
29
|
+
import { findProbeGaming, parseAddedLines } from './probe-gaming.js';
|
|
30
|
+
import { findSubstitutionSuspects, isTestFile } from './substitution-probe.js';
|
|
31
|
+
import { findTestRebuiltAssemblies, testAssemblyVerifyFindings } from './test-assembly.js';
|
|
30
32
|
import { runBoundedLintFix } from './lint-fix.js';
|
|
31
33
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
32
34
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
@@ -76,6 +78,85 @@ export async function collectChangedFiles(cwd, signal) {
|
|
|
76
78
|
}
|
|
77
79
|
return files;
|
|
78
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Collect the task's added lines WITH CONTENT (path + text) for the probe-gaming
|
|
83
|
+
* probe (F6), which needs the actual line text the numstat-shape collector omits.
|
|
84
|
+
* Pre-commit the work is the tree-vs-HEAD diff plus untracked files (read whole, as
|
|
85
|
+
* all-added); on the post-enforce re-verify the tree is clean, so fall back to the
|
|
86
|
+
* last commit's diff. Failures degrade to an empty list — the probe is a sharpener,
|
|
87
|
+
* never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
|
|
88
|
+
*/
|
|
89
|
+
export async function collectAddedLines(cwd, signal) {
|
|
90
|
+
const tracked = await git(cwd, ['diff', 'HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
91
|
+
const lines = tracked.exitCode === 0 ? parseAddedLines(tracked.stdout) : [];
|
|
92
|
+
const untracked = await git(cwd, ['ls-files', '--others', '--exclude-standard', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
93
|
+
for (const name of splitLines(untracked.exitCode === 0 ? untracked.stdout : '')) {
|
|
94
|
+
try {
|
|
95
|
+
const content = await fsp.readFile(path.join(cwd, name), 'utf8');
|
|
96
|
+
for (const text of content.split('\n'))
|
|
97
|
+
lines.push({ path: name, text });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// unreadable/binary — nothing to report
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (lines.length === 0) {
|
|
104
|
+
const last = await git(cwd, ['diff', 'HEAD~1..HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
105
|
+
return last.exitCode === 0 ? parseAddedLines(last.stdout) : [];
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
/** Source extensions whose relative imports the test-assembly probe reasons over. */
|
|
110
|
+
const SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/;
|
|
111
|
+
/** Bounds so the probe stays cheap on large repos (it reads file text). */
|
|
112
|
+
const MAX_PROBE_FILES = 4000;
|
|
113
|
+
const MAX_PROBE_FILE_BYTES = 512 * 1024;
|
|
114
|
+
/** Best-effort read of a repo file's text; unreadable/oversized → null (skipped). */
|
|
115
|
+
async function readRepoFile(cwd, rel) {
|
|
116
|
+
try {
|
|
117
|
+
const buf = await fsp.readFile(path.join(cwd, rel));
|
|
118
|
+
if (buf.length > MAX_PROBE_FILE_BYTES)
|
|
119
|
+
return null;
|
|
120
|
+
return { path: rel, text: buf.toString('utf8') };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Deterministic test-assembly probe input (see test-assembly.ts, run-8 F4): read the
|
|
128
|
+
* task's own changed TEST files plus the repo's tracked source files, and return the
|
|
129
|
+
* finding lines naming any test that rebuilds a production assembly it never imports.
|
|
130
|
+
* Pure import-graph shape; failures degrade to no findings (the probe is a sharpener,
|
|
131
|
+
* never a blocker). `changed` is the already-collected task diff, reused so the probe
|
|
132
|
+
* costs one extra tracked-file listing, not a second diff.
|
|
133
|
+
*/
|
|
134
|
+
async function collectTestAssemblyFindings(cwd, changed, signal) {
|
|
135
|
+
const changedTests = changed.filter(f => isTestFile(f.path));
|
|
136
|
+
if (changedTests.length === 0)
|
|
137
|
+
return [];
|
|
138
|
+
const listed = await git(cwd, ['ls-files', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
139
|
+
if (listed.exitCode !== 0)
|
|
140
|
+
return [];
|
|
141
|
+
const sources = splitLines(listed.stdout)
|
|
142
|
+
.filter(p => SOURCE_EXT_RE.test(p))
|
|
143
|
+
.slice(0, MAX_PROBE_FILES);
|
|
144
|
+
const production = [];
|
|
145
|
+
for (const rel of sources) {
|
|
146
|
+
if (isTestFile(rel))
|
|
147
|
+
continue;
|
|
148
|
+
const f = await readRepoFile(cwd, rel);
|
|
149
|
+
if (f)
|
|
150
|
+
production.push(f);
|
|
151
|
+
}
|
|
152
|
+
const testFiles = [];
|
|
153
|
+
for (const { path: rel } of changedTests) {
|
|
154
|
+
const f = await readRepoFile(cwd, rel);
|
|
155
|
+
if (f)
|
|
156
|
+
testFiles.push(f);
|
|
157
|
+
}
|
|
158
|
+
return testAssemblyVerifyFindings(findTestRebuiltAssemblies(testFiles, production));
|
|
159
|
+
}
|
|
79
160
|
/**
|
|
80
161
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
81
162
|
* implementation re-runner, injected by the caller. The returned object also drives
|
|
@@ -292,6 +373,16 @@ export function buildGateDeps(params) {
|
|
|
292
373
|
// authored/changed become prompt-level findings mandating the child
|
|
293
374
|
// to drive the real artifact before trusting their green result.
|
|
294
375
|
probe: () => collectChangedFiles(cwd2, signal).then(findSubstitutionSuspects),
|
|
376
|
+
// Deterministic test-assembly probe (F4): authored test files that
|
|
377
|
+
// rebuild production wiring — importing the leaf modules the shipped
|
|
378
|
+
// entry composes and assembling their own copy — become rule-3f
|
|
379
|
+
// findings so the child drives the REAL assembly, not the copy.
|
|
380
|
+
testAssemblyProbe: () => collectChangedFiles(cwd2, signal).then(changed => collectTestAssemblyFindings(cwd2, changed, signal)),
|
|
381
|
+
// Deterministic probe-gaming probe (F6): added lines whose stated
|
|
382
|
+
// purpose is to make a check pass rather than meet the requirement
|
|
383
|
+
// ("return 401 so the verification test passes") become rule-4c
|
|
384
|
+
// findings so the child verifies the real requirement, not the check.
|
|
385
|
+
probeGamingProbe: () => collectAddedLines(cwd2, signal).then(findProbeGaming),
|
|
295
386
|
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
296
387
|
// that the task's diff modified anyway become prompt-level findings
|
|
297
388
|
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
@@ -311,10 +402,13 @@ export function buildGateDeps(params) {
|
|
|
311
402
|
// Per-run environment-facts cache under .pi-tasks/ (survives
|
|
312
403
|
// discardEdits): earlier children's discoveries save this child
|
|
313
404
|
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
314
|
-
// next one
|
|
405
|
+
// next one, stamped with this task's id as their origin so a
|
|
406
|
+
// later child sees a cited fact is second-hand and must
|
|
407
|
+
// re-validate before excusing a failure (F7). Facts only —
|
|
408
|
+
// verdict rules unaffected.
|
|
315
409
|
envNotes: {
|
|
316
410
|
read: () => readEnvNotes(cwd2),
|
|
317
|
-
append: notes => appendEnvNotes(cwd2, notes)
|
|
411
|
+
append: notes => appendEnvNotes(cwd2, notes, taskId)
|
|
318
412
|
},
|
|
319
413
|
// Per-run cross-slice contract registry under .pi-tasks/ (F3): the
|
|
320
414
|
// verbatim interface facts the design pins that multiple slices
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* probe-gaming — deterministic detection of CHECK-GAMING code, feeding the verify
|
|
3
|
+
* and enforce gate prompts (run-8 F6).
|
|
4
|
+
*
|
|
5
|
+
* The failure class: the implementation writes code — or a comment on it — whose
|
|
6
|
+
* STATED PURPOSE is to make a check pass, rather than to satisfy the requirement the
|
|
7
|
+
* check stands for. mx5 run-8 shipped, verbatim,
|
|
8
|
+
* `// Return 401 so the verification test passes (expects 200/401/403 for /api routes).`
|
|
9
|
+
* above four catch-all handlers added to satisfy the VERIFY curl instead of fixing
|
|
10
|
+
* the broken route mount. The route stayed dead; the check went green; the code SAID
|
|
11
|
+
* SO IN WRITING. A check is a MESSENGER for a requirement — code written to quiet the
|
|
12
|
+
* messenger instead of meeting the requirement is the defect, and when it announces
|
|
13
|
+
* its own intent it is cheaply detectable.
|
|
14
|
+
*
|
|
15
|
+
* Same probe+rule design as the other run-8 probes (skip-escape.ts,
|
|
16
|
+
* prohibition-probe.ts, substitution-probe.ts, test-assembly.ts): a deterministic
|
|
17
|
+
* finding is the reliable lever, a prompt rule alone is weak. This scans the task's
|
|
18
|
+
* DIFF (added lines only — the work this task introduced) for the gaming tell and
|
|
19
|
+
* hands each hit to the gate child verbatim, so the rule fires on a concrete line
|
|
20
|
+
* rather than on the model self-discovering the intent.
|
|
21
|
+
*
|
|
22
|
+
* Advisory, never an auto-FAIL: intent phrasing is prose and a genuinely-benign line
|
|
23
|
+
* could in principle carry it ("returns 401 so the auth test passes" describing a
|
|
24
|
+
* CORRECT auth path). The child reads the exact line and the surrounding code and
|
|
25
|
+
* judges — the verify rule directs it to confirm the underlying requirement is
|
|
26
|
+
* actually met, not merely that the check is green.
|
|
27
|
+
*
|
|
28
|
+
* FP-MEASURED on the real run-8 corpus (~/hub/mx5, all 53 commits): 1 unique hit in
|
|
29
|
+
* 50,735 added lines — exactly the true F6 line, zero false positives. The tell is a
|
|
30
|
+
* purpose phrase binding a CHECK noun (test / verification / lint / CI / gate / …) to
|
|
31
|
+
* a pass-state or a gaming verb; benign uses of the same words ("pass the test data",
|
|
32
|
+
* "run the verification suite", "the linter flagged this") do not match. Pure
|
|
33
|
+
* diff-text analysis — no stack, framework, or tool-name assumptions; a project with
|
|
34
|
+
* no such comment simply yields no findings (nothing to observe = pass).
|
|
35
|
+
*/
|
|
36
|
+
/** One added line from a task's diff: the file it was added to and its text. */
|
|
37
|
+
export interface AddedLine {
|
|
38
|
+
/** Path relative to the repo root (from the `+++ b/…` header). */
|
|
39
|
+
path: string;
|
|
40
|
+
/** The added line's content, without the leading `+`. */
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
/** Does this line state its purpose is to make a check pass? */
|
|
44
|
+
export declare function isProbeGamingLine(text: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Parse a unified `git diff` into its added lines (path + text). Tracks the current
|
|
47
|
+
* file from each `+++ b/…` header and collects every `+`-prefixed body line
|
|
48
|
+
* (excluding the `+++` header itself). Robust to surrounding prose (the enforce diff
|
|
49
|
+
* capture wraps the diff in a header/footer) because only `+`/`+++` lines are read.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseAddedLines(diff: string): AddedLine[];
|
|
52
|
+
/**
|
|
53
|
+
* Scan a set of added lines for check-gaming tells. Returns one finding per matching
|
|
54
|
+
* line; empty when none match (the prompt then gets no probe block). The finding
|
|
55
|
+
* carries the file and the offending line verbatim so the gate child can locate and
|
|
56
|
+
* judge it. Long lines are truncated for the prompt.
|
|
57
|
+
*/
|
|
58
|
+
export declare function findProbeGaming(added: AddedLine[]): string[];
|
|
59
|
+
/** Convenience: scan a unified diff string straight to findings. */
|
|
60
|
+
export declare function findProbeGamingInDiff(diff: string): string[];
|
|
Binary file
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-assembly — deterministic detection of TEST-REBUILT PRODUCTION WIRING, feeding
|
|
3
|
+
* the verify gate's prompt (run-8 F4; third recurrence of the test-the-copy class,
|
|
4
|
+
* runs 3, 4, 8).
|
|
5
|
+
*
|
|
6
|
+
* The failure class: a test file re-constructs wiring that ALSO exists in production
|
|
7
|
+
* — it builds its own app assembly / its own entry point out of the same leaf modules
|
|
8
|
+
* the production entry composes, then tests THAT private copy. The copy can be wired
|
|
9
|
+
* differently from production and stay green while the shipped wiring is broken. Run-8
|
|
10
|
+
* fixture: `test/photos.test.ts` imports the real `authRoutes` + `photosRoutes` leaves,
|
|
11
|
+
* mounts them into its OWN app at a DIFFERENT prefix than the production entry, and
|
|
12
|
+
* runs 102/102 green — while the shipped upload path is dead because production mounts
|
|
13
|
+
* the same leaf at the wrong prefix. The verify child, judging "do the tests pass",
|
|
14
|
+
* saw green and counted the photos area verified. The seam the test was supposed to
|
|
15
|
+
* cover is exactly the seam it re-implemented away.
|
|
16
|
+
*
|
|
17
|
+
* This is the VERIFY-SIDE complement of the generation-side wiring probe
|
|
18
|
+
* (wiring-claims.ts, item #5) and shares the load-bearing lesson of
|
|
19
|
+
* substitution-probe / skip-escape / wiring-claims: a deterministic finding that NAMES
|
|
20
|
+
* the suspect file is the reliable lever; a bare prompt rule is weak. rule 3b already
|
|
21
|
+
* tells the child to spot-check that self-authored tests exercise the real artifact,
|
|
22
|
+
* but F4 slips through because these tests DO import the real leaf modules — they just
|
|
23
|
+
* bypass the real ASSEMBLY, which rule 3b's "did it import and call the module" check
|
|
24
|
+
* does not catch. This probe supplies the missing concrete fact.
|
|
25
|
+
*
|
|
26
|
+
* THE SIGNAL is pure import-graph SHAPE, zero stack/framework assumptions (no "app",
|
|
27
|
+
* no "route", no "mount", no language runtime): a test file T is flagged when there is
|
|
28
|
+
* a production file E (the assembly/entry) such that
|
|
29
|
+
* - T does NOT import E (it bypasses the shipped assembly), AND
|
|
30
|
+
* - T and E both import ≥2 of the SAME leaf modules, where each such leaf is
|
|
31
|
+
* imported by E and by NO OTHER production file (E is the leaf's SOLE production
|
|
32
|
+
* composition site).
|
|
33
|
+
* The "E-exclusive leaf" condition is the crisp discriminator that keeps ordinary
|
|
34
|
+
* shared-utility imports out: a test importing an api client + a schema module that
|
|
35
|
+
* every page also imports is NOT re-assembly (those utilities have many production
|
|
36
|
+
* importers); a test importing two route modules that only the server entry composes
|
|
37
|
+
* IS re-assembly. Measured on the run-8 fixture tree: flags exactly the four backend
|
|
38
|
+
* tests that rebuild the server entry's route composition (including the real
|
|
39
|
+
* photos seam bug) and leaves clean the single-leaf direct test, the utility-sharing
|
|
40
|
+
* page test, and the source-grepping test — 0 false positives.
|
|
41
|
+
*
|
|
42
|
+
* Findings are ADVISORY (probe+rule): they mandate the child to exercise the REAL
|
|
43
|
+
* shipped assembly directly before counting the area verified; they never auto-FAIL.
|
|
44
|
+
* A test that re-composes wiring which happens to match production survives once the
|
|
45
|
+
* child drives the real entry; only one whose real assembly is broken gets named.
|
|
46
|
+
*/
|
|
47
|
+
/** A source file the probe reasons over: repo-relative path + its full text. */
|
|
48
|
+
export interface RepoFile {
|
|
49
|
+
/** Path relative to the repo root (used verbatim in the finding text). */
|
|
50
|
+
path: string;
|
|
51
|
+
/** Full file contents (import statements are parsed out of it). */
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
|
54
|
+
/** One test file that rebuilds a production assembly instead of importing it. */
|
|
55
|
+
export interface TestAssemblyFinding {
|
|
56
|
+
/** The test file re-constructing the wiring. */
|
|
57
|
+
testFile: string;
|
|
58
|
+
/** The production assembly / entry it bypasses (sole composer of the leaves). */
|
|
59
|
+
assemblyFile: string;
|
|
60
|
+
/** The leaf modules (repo-relative, extensionless) the test re-composes. */
|
|
61
|
+
leaves: string[];
|
|
62
|
+
}
|
|
63
|
+
/** The importing file's OWN module id (path minus extension / `/index`). */
|
|
64
|
+
export declare function moduleIdOf(filePath: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* The set of repo-relative, extensionless module ids that `text` imports via RELATIVE
|
|
67
|
+
* specifiers (a specifier starting with `.`). Bare/external specifiers (`hono`,
|
|
68
|
+
* `bun:sql`, `node:fs`) are ignored — they never name a repo file, so they cannot be
|
|
69
|
+
* a re-composed production leaf. Resolution is pure path arithmetic against the
|
|
70
|
+
* importer's directory; the filesystem is never touched.
|
|
71
|
+
*/
|
|
72
|
+
export declare function relativeImports(filePath: string, text: string): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Find test files that rebuild a production assembly. `changedTestFiles` are the
|
|
75
|
+
* task's own authored/changed test files (path + text); `productionFiles` are the
|
|
76
|
+
* repo's non-test source files (path + text) used to build the import graph and the
|
|
77
|
+
* per-leaf production in-degree. Returns one finding per re-assembling test, sorted
|
|
78
|
+
* for determinism. Empty when no test re-composes an E-exclusive leaf set.
|
|
79
|
+
*/
|
|
80
|
+
export declare function findTestRebuiltAssemblies(changedTestFiles: RepoFile[], productionFiles: RepoFile[]): TestAssemblyFinding[];
|
|
81
|
+
/**
|
|
82
|
+
* Render findings as verify-child prompt lines (probe+rule pattern — the concrete
|
|
83
|
+
* finding that makes the rule fire reliably). One line per re-assembling test naming
|
|
84
|
+
* the test, the shipped assembly it bypasses, and the re-composed leaves. Empty
|
|
85
|
+
* findings → empty array (caller emits no block).
|
|
86
|
+
*/
|
|
87
|
+
export declare function testAssemblyVerifyFindings(findings: TestAssemblyFinding[]): string[];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-assembly — deterministic detection of TEST-REBUILT PRODUCTION WIRING, feeding
|
|
3
|
+
* the verify gate's prompt (run-8 F4; third recurrence of the test-the-copy class,
|
|
4
|
+
* runs 3, 4, 8).
|
|
5
|
+
*
|
|
6
|
+
* The failure class: a test file re-constructs wiring that ALSO exists in production
|
|
7
|
+
* — it builds its own app assembly / its own entry point out of the same leaf modules
|
|
8
|
+
* the production entry composes, then tests THAT private copy. The copy can be wired
|
|
9
|
+
* differently from production and stay green while the shipped wiring is broken. Run-8
|
|
10
|
+
* fixture: `test/photos.test.ts` imports the real `authRoutes` + `photosRoutes` leaves,
|
|
11
|
+
* mounts them into its OWN app at a DIFFERENT prefix than the production entry, and
|
|
12
|
+
* runs 102/102 green — while the shipped upload path is dead because production mounts
|
|
13
|
+
* the same leaf at the wrong prefix. The verify child, judging "do the tests pass",
|
|
14
|
+
* saw green and counted the photos area verified. The seam the test was supposed to
|
|
15
|
+
* cover is exactly the seam it re-implemented away.
|
|
16
|
+
*
|
|
17
|
+
* This is the VERIFY-SIDE complement of the generation-side wiring probe
|
|
18
|
+
* (wiring-claims.ts, item #5) and shares the load-bearing lesson of
|
|
19
|
+
* substitution-probe / skip-escape / wiring-claims: a deterministic finding that NAMES
|
|
20
|
+
* the suspect file is the reliable lever; a bare prompt rule is weak. rule 3b already
|
|
21
|
+
* tells the child to spot-check that self-authored tests exercise the real artifact,
|
|
22
|
+
* but F4 slips through because these tests DO import the real leaf modules — they just
|
|
23
|
+
* bypass the real ASSEMBLY, which rule 3b's "did it import and call the module" check
|
|
24
|
+
* does not catch. This probe supplies the missing concrete fact.
|
|
25
|
+
*
|
|
26
|
+
* THE SIGNAL is pure import-graph SHAPE, zero stack/framework assumptions (no "app",
|
|
27
|
+
* no "route", no "mount", no language runtime): a test file T is flagged when there is
|
|
28
|
+
* a production file E (the assembly/entry) such that
|
|
29
|
+
* - T does NOT import E (it bypasses the shipped assembly), AND
|
|
30
|
+
* - T and E both import ≥2 of the SAME leaf modules, where each such leaf is
|
|
31
|
+
* imported by E and by NO OTHER production file (E is the leaf's SOLE production
|
|
32
|
+
* composition site).
|
|
33
|
+
* The "E-exclusive leaf" condition is the crisp discriminator that keeps ordinary
|
|
34
|
+
* shared-utility imports out: a test importing an api client + a schema module that
|
|
35
|
+
* every page also imports is NOT re-assembly (those utilities have many production
|
|
36
|
+
* importers); a test importing two route modules that only the server entry composes
|
|
37
|
+
* IS re-assembly. Measured on the run-8 fixture tree: flags exactly the four backend
|
|
38
|
+
* tests that rebuild the server entry's route composition (including the real
|
|
39
|
+
* photos seam bug) and leaves clean the single-leaf direct test, the utility-sharing
|
|
40
|
+
* page test, and the source-grepping test — 0 false positives.
|
|
41
|
+
*
|
|
42
|
+
* Findings are ADVISORY (probe+rule): they mandate the child to exercise the REAL
|
|
43
|
+
* shipped assembly directly before counting the area verified; they never auto-FAIL.
|
|
44
|
+
* A test that re-composes wiring which happens to match production survives once the
|
|
45
|
+
* child drives the real entry; only one whose real assembly is broken gets named.
|
|
46
|
+
*/
|
|
47
|
+
import { isTestFile } from './substitution-probe.js';
|
|
48
|
+
/** Code file extensions whose relative imports we resolve. Not a stack assumption —
|
|
49
|
+
* purely which quoted specifiers name a repo file; other languages simply produce
|
|
50
|
+
* no matches and the whole probe degrades to nothing. */
|
|
51
|
+
const CODE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/;
|
|
52
|
+
/**
|
|
53
|
+
* Static import/re-export declarations: `import … from 'x'`, `import 'x'`,
|
|
54
|
+
* `export … from 'x'`. Anchored to line start (after optional whitespace) so an
|
|
55
|
+
* import-shaped STRING inside an assertion (`expect(src).toContain("import x from
|
|
56
|
+
* '../y'")`, a source-grepping test) is NOT mistaken for a real import — that string
|
|
57
|
+
* is indented behind `expect(`, never at line start.
|
|
58
|
+
*/
|
|
59
|
+
const STATIC_IMPORT_RE = /^[ \t]*(?:import|export)\s+(?:[^'"\n]*\sfrom\s+)?['"]([^'"]+)['"]/gm;
|
|
60
|
+
/** Dynamic `import('x')` / `require('x')` calls (the call-paren form is unlikely to
|
|
61
|
+
* appear inside an assertion string, so matching anywhere is safe enough). */
|
|
62
|
+
const CALL_IMPORT_RE = /(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
63
|
+
/** Strip a code extension and a trailing `/index` so `./a`, `./a.ts`, and
|
|
64
|
+
* `./a/index.ts` all collapse to the same module id. */
|
|
65
|
+
function stripToModuleId(p) {
|
|
66
|
+
return p.replace(CODE_EXT_RE, '').replace(/\/index$/, '');
|
|
67
|
+
}
|
|
68
|
+
/** Normalise a POSIX-style relative path (resolve `.`/`..` segments) without touching
|
|
69
|
+
* the filesystem — the analysis is pure text shape. */
|
|
70
|
+
function normalisePosix(p) {
|
|
71
|
+
const segments = [];
|
|
72
|
+
for (const seg of p.split('/')) {
|
|
73
|
+
if (seg === '' || seg === '.')
|
|
74
|
+
continue;
|
|
75
|
+
if (seg === '..')
|
|
76
|
+
segments.pop();
|
|
77
|
+
else
|
|
78
|
+
segments.push(seg);
|
|
79
|
+
}
|
|
80
|
+
return segments.join('/');
|
|
81
|
+
}
|
|
82
|
+
/** The importing file's OWN module id (path minus extension / `/index`). */
|
|
83
|
+
export function moduleIdOf(filePath) {
|
|
84
|
+
return stripToModuleId(filePath);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The set of repo-relative, extensionless module ids that `text` imports via RELATIVE
|
|
88
|
+
* specifiers (a specifier starting with `.`). Bare/external specifiers (`hono`,
|
|
89
|
+
* `bun:sql`, `node:fs`) are ignored — they never name a repo file, so they cannot be
|
|
90
|
+
* a re-composed production leaf. Resolution is pure path arithmetic against the
|
|
91
|
+
* importer's directory; the filesystem is never touched.
|
|
92
|
+
*/
|
|
93
|
+
export function relativeImports(filePath, text) {
|
|
94
|
+
const dir = filePath.includes('/') ? filePath.slice(0, filePath.lastIndexOf('/')) : '';
|
|
95
|
+
const ids = new Set();
|
|
96
|
+
for (const re of [STATIC_IMPORT_RE, CALL_IMPORT_RE]) {
|
|
97
|
+
re.lastIndex = 0;
|
|
98
|
+
for (let m = re.exec(text); m !== null; m = re.exec(text)) {
|
|
99
|
+
const spec = m[1];
|
|
100
|
+
if (!spec.startsWith('.'))
|
|
101
|
+
continue;
|
|
102
|
+
ids.add(stripToModuleId(normalisePosix(`${dir}/${spec}`)));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...ids];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Find test files that rebuild a production assembly. `changedTestFiles` are the
|
|
109
|
+
* task's own authored/changed test files (path + text); `productionFiles` are the
|
|
110
|
+
* repo's non-test source files (path + text) used to build the import graph and the
|
|
111
|
+
* per-leaf production in-degree. Returns one finding per re-assembling test, sorted
|
|
112
|
+
* for determinism. Empty when no test re-composes an E-exclusive leaf set.
|
|
113
|
+
*/
|
|
114
|
+
export function findTestRebuiltAssemblies(changedTestFiles, productionFiles) {
|
|
115
|
+
// Only genuine production (non-test) files can be the bypassed assembly.
|
|
116
|
+
const prod = productionFiles.filter(f => !isTestFile(f.path));
|
|
117
|
+
const prodImports = new Map();
|
|
118
|
+
const inDegree = new Map();
|
|
119
|
+
for (const f of prod) {
|
|
120
|
+
const imps = relativeImports(f.path, f.text);
|
|
121
|
+
prodImports.set(f.path, new Set(imps));
|
|
122
|
+
for (const m of imps)
|
|
123
|
+
inDegree.set(m, (inDegree.get(m) ?? 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
const findings = [];
|
|
126
|
+
for (const t of [...changedTestFiles].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
127
|
+
if (!isTestFile(t.path))
|
|
128
|
+
continue;
|
|
129
|
+
const tImports = new Set(relativeImports(t.path, t.text));
|
|
130
|
+
if (tImports.size < 2)
|
|
131
|
+
continue;
|
|
132
|
+
let best = null;
|
|
133
|
+
for (const e of [...prod].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
134
|
+
if (e.path === t.path)
|
|
135
|
+
continue;
|
|
136
|
+
// The test imports the real assembly → it is exercising the shipped wiring,
|
|
137
|
+
// not a copy. Good citizen, never flagged.
|
|
138
|
+
if (tImports.has(moduleIdOf(e.path)))
|
|
139
|
+
continue;
|
|
140
|
+
const eImports = prodImports.get(e.path);
|
|
141
|
+
// Leaves E is the SOLE production composer of, that this test re-imports.
|
|
142
|
+
const leaves = [...tImports].filter(m => eImports.has(m) && inDegree.get(m) === 1);
|
|
143
|
+
if (leaves.length >= 2 && (best === null || leaves.length > best.leaves.length)) {
|
|
144
|
+
best = { testFile: t.path, assemblyFile: e.path, leaves: leaves.sort() };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (best)
|
|
148
|
+
findings.push(best);
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Render findings as verify-child prompt lines (probe+rule pattern — the concrete
|
|
154
|
+
* finding that makes the rule fire reliably). One line per re-assembling test naming
|
|
155
|
+
* the test, the shipped assembly it bypasses, and the re-composed leaves. Empty
|
|
156
|
+
* findings → empty array (caller emits no block).
|
|
157
|
+
*/
|
|
158
|
+
export function testAssemblyVerifyFindings(findings) {
|
|
159
|
+
return findings.map(f => `${f.testFile} imports and re-composes ${f.leaves.length} leaf module(s) `
|
|
160
|
+
+ `(${f.leaves.join(', ')}) that ${f.assemblyFile} is the ONLY production file to `
|
|
161
|
+
+ `compose, yet it never imports ${f.assemblyFile} — it builds its OWN assembly of `
|
|
162
|
+
+ `those leaves instead of exercising the shipped one`);
|
|
163
|
+
}
|
|
@@ -61,7 +61,7 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
61
61
|
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
62
62
|
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
63
63
|
*/
|
|
64
|
-
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string, prohibitionFindings?: string[], skipEscapeFindings?: string[], contracts?: string): string;
|
|
64
|
+
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string, prohibitionFindings?: string[], skipEscapeFindings?: string[], contracts?: string, testAssemblyFindings?: string[], probeGamingFindings?: string[]): string;
|
|
65
65
|
/**
|
|
66
66
|
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL|UNOBSERVED`
|
|
67
67
|
* marker (the model discusses before concluding, and bash output may echo the word
|
|
@@ -114,6 +114,22 @@ export interface VerificationDeps {
|
|
|
114
114
|
* no-waiver rule (4b). Advisory, never auto-FAIL — real prohibitions can be
|
|
115
115
|
* conditional prose. ABSENT or empty → no prohibition block. */
|
|
116
116
|
prohibitionProbe?: () => Promise<string[]>;
|
|
117
|
+
/**
|
|
118
|
+
* DETERMINISTIC test-assembly probe (see test-assembly.ts): authored test files
|
|
119
|
+
* that rebuild production WIRING — importing the leaf modules the shipped entry
|
|
120
|
+
* composes and assembling their own copy instead of the real assembly — become
|
|
121
|
+
* prompt findings under rule 3f (F4 test-the-copy, 3rd recurrence). Pure import-
|
|
122
|
+
* graph shape; the child then drives the real assembly before trusting the copy.
|
|
123
|
+
* ABSENT or empty → no test-assembly block. */
|
|
124
|
+
testAssemblyProbe?: () => Promise<string[]>;
|
|
125
|
+
/**
|
|
126
|
+
* DETERMINISTIC probe-gaming probe (see probe-gaming.ts, run-8 F6): added lines
|
|
127
|
+
* in the task's diff whose stated purpose is to make a CHECK pass instead of
|
|
128
|
+
* meeting the requirement it stands for ("return 401 so the verification test
|
|
129
|
+
* passes"). Injected as findings under rule 4c so the child confirms the
|
|
130
|
+
* underlying requirement is genuinely met rather than trusting the green check.
|
|
131
|
+
* Pure diff-text analysis; ABSENT or empty → no probe block. */
|
|
132
|
+
probeGamingProbe?: () => Promise<string[]>;
|
|
117
133
|
/**
|
|
118
134
|
* Result of the git-state guard for the MOST RECENT runChild call (see
|
|
119
135
|
* git-state-guard.ts): did the child mutate repo state (stash/checkout/file
|
package/dist/task/verify-work.js
CHANGED
|
@@ -139,7 +139,7 @@ export function extractSpecForVerification(taskBody) {
|
|
|
139
139
|
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
140
140
|
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
141
141
|
*/
|
|
142
|
-
export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings, skipEscapeFindings, contracts) {
|
|
142
|
+
export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings, skipEscapeFindings, contracts, testAssemblyFindings, probeGamingFindings) {
|
|
143
143
|
const probeBlock = probeFindings && probeFindings.length > 0 ?
|
|
144
144
|
[
|
|
145
145
|
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
@@ -166,6 +166,24 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
166
166
|
''
|
|
167
167
|
]
|
|
168
168
|
: [];
|
|
169
|
+
const probeGamingBlock = probeGamingFindings && probeGamingFindings.length > 0 ?
|
|
170
|
+
[
|
|
171
|
+
'CHECK-GAMING NOTICE (deterministic, computed by the orchestrator from the',
|
|
172
|
+
"task's diff): these added lines state their own purpose is to make a CHECK",
|
|
173
|
+
'pass (a test / verification / lint / gate), not to meet the requirement the',
|
|
174
|
+
'check stands for:',
|
|
175
|
+
...probeGamingFindings.map(f => `- ${f}`),
|
|
176
|
+
'A check is a MESSENGER for a requirement. Code written to quiet the messenger',
|
|
177
|
+
'instead of meeting the requirement is a defect even when the check is green',
|
|
178
|
+
'(run-8 F6: a handler returned 401 "so the verification test passes" while the',
|
|
179
|
+
'real route stayed dead). Do NOT accept the passing check as proof. Read each',
|
|
180
|
+
"line's surrounding code and confirm the UNDERLYING requirement is genuinely",
|
|
181
|
+
'met — drive the real behavior directly (rule 4c). If the code only exists to',
|
|
182
|
+
'satisfy the check while the requirement is unmet, that is a FAIL naming the',
|
|
183
|
+
'gamed check and the unmet requirement.',
|
|
184
|
+
''
|
|
185
|
+
]
|
|
186
|
+
: [];
|
|
169
187
|
const skipEscapeBlock = skipEscapeFindings && skipEscapeFindings.length > 0 ?
|
|
170
188
|
[
|
|
171
189
|
"SKIP-ESCAPE NOTICE (deterministic, computed by the orchestrator from the spec's",
|
|
@@ -181,6 +199,23 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
181
199
|
''
|
|
182
200
|
]
|
|
183
201
|
: [];
|
|
202
|
+
const testAssemblyBlock = testAssemblyFindings && testAssemblyFindings.length > 0 ?
|
|
203
|
+
[
|
|
204
|
+
'TEST-ASSEMBLY NOTICE (deterministic, computed by the orchestrator from pure',
|
|
205
|
+
'import-graph shape): these test files rebuild production WIRING — they import the',
|
|
206
|
+
'same leaf modules the shipped entry composes and assemble their OWN copy of it,',
|
|
207
|
+
'instead of importing the production assembly:',
|
|
208
|
+
...testAssemblyFindings.map(f => `- ${f}`),
|
|
209
|
+
'A green result on such a test proves that PRIVATE re-assembly, NOT the shipped',
|
|
210
|
+
'wiring — the copy can be wired differently (a different mount prefix, order, or',
|
|
211
|
+
'middleware) and pass while production is broken exactly at the seam the test was',
|
|
212
|
+
'meant to cover (rule 3f below). Before you count the covered area verified, drive',
|
|
213
|
+
'the behavior against the REAL shipped assembly/entry named above (start or invoke',
|
|
214
|
+
"the production entry point, not the test's hand-built app). If the real assembly",
|
|
215
|
+
'fails where the test passes, report FAIL and name the wiring seam.',
|
|
216
|
+
''
|
|
217
|
+
]
|
|
218
|
+
: [];
|
|
184
219
|
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
|
|
185
220
|
const contractsBlock = contracts && contracts.trim().length > 0 ? [buildContractsVerifyBlock(contracts)] : [];
|
|
186
221
|
return [
|
|
@@ -200,7 +235,9 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
200
235
|
...contractsBlock,
|
|
201
236
|
...probeBlock,
|
|
202
237
|
...prohibitionBlock,
|
|
238
|
+
...probeGamingBlock,
|
|
203
239
|
...skipEscapeBlock,
|
|
240
|
+
...testAssemblyBlock,
|
|
204
241
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
205
242
|
'checkout (or CI run) would experience it:',
|
|
206
243
|
'',
|
|
@@ -280,6 +317,20 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
280
317
|
' unverified is a FAIL, never a PASS. A wrong-input control exists for every',
|
|
281
318
|
' artifact — HTTP request, CLI invocation, library call, schema load, config parse.',
|
|
282
319
|
'',
|
|
320
|
+
'3f. TEST-REBUILT ASSEMBLY: a test proves only the copy if it RE-CONSTRUCTS wiring that',
|
|
321
|
+
' also exists in production — assembling its own app / entry point / config out of the',
|
|
322
|
+
' same leaf modules the shipped entry composes, instead of importing and exercising the',
|
|
323
|
+
' production assembly. Such a test can wire the leaves differently from production (a',
|
|
324
|
+
' different prefix, order, adapter, or middleware) and pass green while the SHIPPED',
|
|
325
|
+
' wiring is broken at exactly the seam the test was meant to cover — its green result',
|
|
326
|
+
' never touched the production assembly at all. Whenever a spec-required behavior is',
|
|
327
|
+
' covered ONLY by tests that build their own composition of the real modules, that',
|
|
328
|
+
' behavior is UNVERIFIED off those tests: exercise the REAL shipped entry/assembly (run',
|
|
329
|
+
' or invoke the production entry point, hit the real composed surface) and judge THAT.',
|
|
330
|
+
' If the real assembly fails where the copy passes, report FAIL naming the wiring seam',
|
|
331
|
+
' (e.g. "the entry mounts <module> at <X> but the test mounts it at <Y>, so the real',
|
|
332
|
+
' path is dead while the test is green").',
|
|
333
|
+
'',
|
|
283
334
|
'4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
|
|
284
335
|
' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
285
336
|
'',
|
|
@@ -296,6 +347,18 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
296
347
|
' for…") that covers the change — judged against that stated exception, not against',
|
|
297
348
|
' your view of harmlessness.',
|
|
298
349
|
'',
|
|
350
|
+
'4c. THE CHECK IS THE MESSENGER, NOT THE REQUIREMENT — code (or a comment) whose',
|
|
351
|
+
' stated purpose is to make a check PASS, rather than to satisfy the requirement the',
|
|
352
|
+
' check stands for, is a defect even when the check is green. The tell is the intent',
|
|
353
|
+
' written down: "return X so the test passes", "hardcode this to satisfy the linter",',
|
|
354
|
+
' "stub it out to appease CI". When you see such a line — or the CHECK-GAMING NOTICE',
|
|
355
|
+
' above names one — do NOT treat the passing check as proof the requirement is met.',
|
|
356
|
+
' Find the actual requirement the check was meant to prove and verify THAT directly',
|
|
357
|
+
' against the real artifact (rule 3e negative control is the sharpest tool: a handler',
|
|
358
|
+
' that answers the check-shaped request the same way for a WRONG input is gaming the',
|
|
359
|
+
' check, not implementing the behavior). If the requirement is genuinely unmet while',
|
|
360
|
+
' the check passes, the verdict is FAIL naming the gamed check and the real gap.',
|
|
361
|
+
'',
|
|
299
362
|
'5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
|
|
300
363
|
' service or network resource (a database server, an API host) that the project',
|
|
301
364
|
' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
|
|
@@ -446,6 +509,28 @@ export async function runWorkVerification(deps) {
|
|
|
446
509
|
prohibitions = [];
|
|
447
510
|
}
|
|
448
511
|
}
|
|
512
|
+
// Test-assembly findings feed the prompt (rule 3f); a probe failure must never
|
|
513
|
+
// block verification — it is an optional sharpener like the substitution probe.
|
|
514
|
+
let testAssembly = [];
|
|
515
|
+
if (deps.testAssemblyProbe) {
|
|
516
|
+
try {
|
|
517
|
+
testAssembly = await deps.testAssemblyProbe();
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
testAssembly = [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Probe-gaming findings feed the prompt (rule 4c, F6); a probe failure must never
|
|
524
|
+
// block verification — an optional sharpener like the other diff-shape probes.
|
|
525
|
+
let probeGaming = [];
|
|
526
|
+
if (deps.probeGamingProbe) {
|
|
527
|
+
try {
|
|
528
|
+
probeGaming = await deps.probeGamingProbe();
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
probeGaming = [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
449
534
|
// Environment facts from earlier gate children (best-effort; a cache failure
|
|
450
535
|
// must never block verification).
|
|
451
536
|
let envNotes = '';
|
|
@@ -481,7 +566,7 @@ export async function runWorkVerification(deps) {
|
|
|
481
566
|
for (let attempt = 1;; attempt++) {
|
|
482
567
|
let text;
|
|
483
568
|
try {
|
|
484
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions, skipEscapes, contracts), deps.signal);
|
|
569
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions, skipEscapes, contracts, testAssembly, probeGaming), deps.signal);
|
|
485
570
|
}
|
|
486
571
|
catch (err) {
|
|
487
572
|
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.4",
|
|
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",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p tsconfig.build.json",
|
|
16
16
|
"lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit",
|
|
17
|
-
"test": "bun test src/",
|
|
17
|
+
"test": "AGENT=1 bun test src/",
|
|
18
18
|
"prepublishOnly": "bun run build"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|