@mjasnikovs/pi-task 0.18.2 → 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/auto-orchestrator.js +22 -1
- package/dist/task/contracts.d.ts +54 -0
- package/dist/task/contracts.js +226 -0
- 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 +105 -4
- package/dist/task/phases.d.ts +8 -0
- package/dist/task/phases.js +62 -7
- package/dist/task/probe-gaming.d.ts +60 -0
- package/dist/task/probe-gaming.js +0 -0
- package/dist/task/prompts.d.ts +4 -4
- package/dist/task/prompts.js +12 -7
- package/dist/task/skip-escape.d.ts +25 -0
- package/dist/task/skip-escape.js +80 -0
- package/dist/task/task-gates.js +17 -5
- package/dist/task/test-assembly.d.ts +87 -0
- package/dist/task/test-assembly.js +163 -0
- package/dist/task/verify-work.d.ts +40 -3
- package/dist/task/verify-work.js +204 -9
- package/dist/task/wiring-claims.d.ts +64 -0
- package/dist/task/wiring-claims.js +149 -0
- package/package.json +2 -2
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
|
@@ -20,12 +20,15 @@ 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 { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
23
|
+
import { readContracts } from './contracts.js';
|
|
23
24
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
24
25
|
import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate.js';
|
|
25
26
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
26
27
|
import { researchResolution } from './verify-resolution.js';
|
|
27
28
|
import { extractProhibitions, findProhibitionViolations } from './prohibition-probe.js';
|
|
28
|
-
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';
|
|
29
32
|
import { runBoundedLintFix } from './lint-fix.js';
|
|
30
33
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
31
34
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
@@ -75,6 +78,85 @@ export async function collectChangedFiles(cwd, signal) {
|
|
|
75
78
|
}
|
|
76
79
|
return files;
|
|
77
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
|
+
}
|
|
78
160
|
/**
|
|
79
161
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
80
162
|
* implementation re-runner, injected by the caller. The returned object also drives
|
|
@@ -291,6 +373,16 @@ export function buildGateDeps(params) {
|
|
|
291
373
|
// authored/changed become prompt-level findings mandating the child
|
|
292
374
|
// to drive the real artifact before trusting their green result.
|
|
293
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),
|
|
294
386
|
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
295
387
|
// that the task's diff modified anyway become prompt-level findings
|
|
296
388
|
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
@@ -310,11 +402,20 @@ export function buildGateDeps(params) {
|
|
|
310
402
|
// Per-run environment-facts cache under .pi-tasks/ (survives
|
|
311
403
|
// discardEdits): earlier children's discoveries save this child
|
|
312
404
|
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
313
|
-
// 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.
|
|
314
409
|
envNotes: {
|
|
315
410
|
read: () => readEnvNotes(cwd2),
|
|
316
|
-
append: notes => appendEnvNotes(cwd2, notes)
|
|
317
|
-
}
|
|
411
|
+
append: notes => appendEnvNotes(cwd2, notes, taskId)
|
|
412
|
+
},
|
|
413
|
+
// Per-run cross-slice contract registry under .pi-tasks/ (F3): the
|
|
414
|
+
// verbatim interface facts the design pins that multiple slices
|
|
415
|
+
// share, so the verify child checks this slice's boundary against
|
|
416
|
+
// them. Empty on single-`/task` runs or a design with no shared
|
|
417
|
+
// boundary → no block.
|
|
418
|
+
contracts: () => readContracts(cwd2)
|
|
318
419
|
});
|
|
319
420
|
},
|
|
320
421
|
lintFix: (fixCtx, cwd2, taskTitle, failReason) => runBoundedLintFix({
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -50,6 +50,14 @@ export declare function replaceToolingWithVerified(research: string, verifiedCom
|
|
|
50
50
|
* a genuinely greenfield task is still free to create.
|
|
51
51
|
*/
|
|
52
52
|
export declare function refineExistingFilesBlock(deps: PhaseDeps): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* The read-only cross-slice contract block (see contracts.ts) for a phase that
|
|
55
|
+
* generates spec text — the verbatim interface facts the SOURCE design pins that
|
|
56
|
+
* more than one slice touches. Empty when the registry is absent/empty (single
|
|
57
|
+
* `/task` runs, or a design that pins no shared boundary), so this degrades to a
|
|
58
|
+
* no-op. Best-effort: a read fault yields '' rather than blocking the phase.
|
|
59
|
+
*/
|
|
60
|
+
export declare function phaseContractsBlock(deps: PhaseDeps): Promise<string>;
|
|
53
61
|
export declare const phaseRefine: (deps: PhaseDeps, raw: string, planContext?: string) => Promise<string>;
|
|
54
62
|
export declare function phaseVerifyTooling(deps: PhaseDeps, research: string): Promise<string>;
|
|
55
63
|
export interface PhaseResearchDeps extends ExternalContextDeps {
|
package/dist/task/phases.js
CHANGED
|
@@ -24,6 +24,9 @@ import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './quest
|
|
|
24
24
|
import { parseGrillQuestions, parseAutoAnswer, autoAnswerHasTag, parseVerifyToolingOutput, deriveTitle } from './parsers.js';
|
|
25
25
|
import { compressTitle } from './title-label.js';
|
|
26
26
|
import { parseVerifyBlock, validateSpecShape, stripSpecPreamble, isCritiqueClean } from './spec-validation.js';
|
|
27
|
+
import { findSkipEscapes, skipEscapeDefectText } from './skip-escape.js';
|
|
28
|
+
import { findSynthesizedWiring, wiringProbeText, readReferencedDocs } from './wiring-claims.js';
|
|
29
|
+
import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from './contracts.js';
|
|
27
30
|
import { runPhaseChild, runPhaseWithLoopGuard, runWithEmphasisRetry, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
28
31
|
import { SessionUI } from '../remote/bridge.js';
|
|
29
32
|
// ─── Re-export constants from their home modules ────────────────────────────
|
|
@@ -112,9 +115,21 @@ export async function refineExistingFilesBlock(deps) {
|
|
|
112
115
|
});
|
|
113
116
|
return block.trim().length === 0 ? '' : `${REFINE_PRESERVE_DIRECTIVE}\n\n${block.trim()}`;
|
|
114
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The read-only cross-slice contract block (see contracts.ts) for a phase that
|
|
120
|
+
* generates spec text — the verbatim interface facts the SOURCE design pins that
|
|
121
|
+
* more than one slice touches. Empty when the registry is absent/empty (single
|
|
122
|
+
* `/task` runs, or a design that pins no shared boundary), so this degrades to a
|
|
123
|
+
* no-op. Best-effort: a read fault yields '' rather than blocking the phase.
|
|
124
|
+
*/
|
|
125
|
+
export async function phaseContractsBlock(deps) {
|
|
126
|
+
const contracts = await readContracts(deps.cwd).catch(() => '');
|
|
127
|
+
return buildContractsBlock(contracts);
|
|
128
|
+
}
|
|
115
129
|
export const phaseRefine = async (deps, raw, planContext) => {
|
|
116
130
|
const existingFiles = await refineExistingFilesBlock(deps).catch(() => '');
|
|
117
|
-
|
|
131
|
+
const contracts = await phaseContractsBlock(deps);
|
|
132
|
+
return runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext, existingFiles, contracts))),
|
|
118
133
|
// refine's deliverable is a 4-section text rewrite that never strictly
|
|
119
134
|
// needs a successful read — on a test-writing task against a large
|
|
120
135
|
// existing codebase the model over-explores (re-reads source hunting for
|
|
@@ -714,7 +729,8 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
714
729
|
return out.join('\n');
|
|
715
730
|
}
|
|
716
731
|
export async function phaseCompose(deps, refined, research, qa) {
|
|
717
|
-
|
|
732
|
+
const contracts = await phaseContractsBlock(deps);
|
|
733
|
+
return runWithEmphasisRetry(deps, 'compose', 'read', problem => COMPOSE_PROMPT(refined, research, qa, problem, contracts), text => {
|
|
718
734
|
// Trim any "here's the spec:" preamble before validating, so a
|
|
719
735
|
// strippable lead-in doesn't burn a full retry — and the stored
|
|
720
736
|
// value starts at GOAL.
|
|
@@ -747,6 +763,35 @@ export async function phaseCritique(deps, spec, refined, qa) {
|
|
|
747
763
|
// When the draft is structurally sound and triage says CLEAN, return it as
|
|
748
764
|
// is. Otherwise fall through to the rewrite, feeding the triage defects in
|
|
749
765
|
// as a focus list. Triage failures are non-fatal — we just do the rewrite.
|
|
766
|
+
// DETERMINISTIC skip-escape gate (run-8 F2): a required VERIFY check wrapped in a
|
|
767
|
+
// skip-announcing `||` fallback (`… || echo "skipping (tool absent)"`) lets the
|
|
768
|
+
// check pass while never running. FP-measured 0/20 on the historical specs. When
|
|
769
|
+
// present, force the rewrite to strip it — never let the triage CLEAN short-circuit
|
|
770
|
+
// ship a self-waiving VERIFY block — and feed the offending lines in as defects.
|
|
771
|
+
const skipEscapes = findSkipEscapes(spec);
|
|
772
|
+
const skipDefects = skipEscapes.length > 0 ? skipEscapeDefectText(skipEscapes) : null;
|
|
773
|
+
// The cross-slice contract registry (run-8 F3): the design's pinned interface
|
|
774
|
+
// facts, quoted verbatim, that more than one slice touches. Threading them into
|
|
775
|
+
// critique lets the triage/rewrite RECONCILE a synthesized wiring specific (a
|
|
776
|
+
// fabricated uniform mount table) against the facts it must reproduce — the
|
|
777
|
+
// generation-side complement of the verify-side boundary check. Empty (single
|
|
778
|
+
// /task, or a design that pins no shared boundary) ⇒ no-op.
|
|
779
|
+
const registryRaw = await readContracts(deps.cwd).catch(() => '');
|
|
780
|
+
const contractsBlock = buildContractsVerifyBlock(registryRaw);
|
|
781
|
+
// DETERMINISTIC synthesized-wiring probe (run-8 F3, generation side). The registry
|
|
782
|
+
// alone is a WEAK catcher (live A/B: prompt+registry ~1/8) — the model's attention
|
|
783
|
+
// goes to the obvious VERIFY weakness and it rarely does the path-composition
|
|
784
|
+
// reasoning. The scanner NAMES the inferred mount mappings and juxtaposes the
|
|
785
|
+
// verbatim pinned facts, forcing focused reconciliation (probe+rule pattern, same
|
|
786
|
+
// lever as skip-escape / substitution-probe). FP-clean (1/18 files on the run-8
|
|
787
|
+
// trees). Grounding = the registry ∪ any design doc the spec/refined @-reference.
|
|
788
|
+
const wiring = registryRaw.trim().length > 0 ?
|
|
789
|
+
findSynthesizedWiring(spec, registryRaw + '\n' + readReferencedDocs(deps.cwd, refined, spec), registryRaw)
|
|
790
|
+
: [];
|
|
791
|
+
const wiringProbe = wiring.length > 0 ? wiringProbeText(wiring, registryRaw) : null;
|
|
792
|
+
if (wiringProbe) {
|
|
793
|
+
deps.logDebug?.(`synthesized wiring flagged in spec: ${wiring.map(w => w.line).join(' | ')}`);
|
|
794
|
+
}
|
|
750
795
|
let triageDefects = null;
|
|
751
796
|
if (parseVerifyBlock(spec) !== null) {
|
|
752
797
|
const tTriage = Date.now();
|
|
@@ -756,21 +801,31 @@ export async function phaseCritique(deps, spec, refined, qa) {
|
|
|
756
801
|
// Granting `read` here let it wander the repo to "verify" findings,
|
|
757
802
|
// which made the supposedly-cheap pass cost as much as a rewrite
|
|
758
803
|
// (observed ~133s). The judgement needs no file access.
|
|
759
|
-
verdict = await runPhaseChild(deps, 'critique-triage', '', appendNoThink(CRITIQUE_TRIAGE_PROMPT(spec, refined, qa)));
|
|
804
|
+
verdict = await runPhaseChild(deps, 'critique-triage', '', appendNoThink(CRITIQUE_TRIAGE_PROMPT(spec, refined, qa, contractsBlock)));
|
|
760
805
|
}
|
|
761
806
|
catch {
|
|
762
807
|
verdict = null;
|
|
763
808
|
}
|
|
764
809
|
deps.recordSubStep?.('triage', Date.now() - tTriage);
|
|
765
810
|
if (verdict !== null) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
811
|
+
// A deterministic skip-escape OR synthesized-wiring finding overrides a CLEAN
|
|
812
|
+
// triage: the draft must be rewritten to resolve it even if the model judged
|
|
813
|
+
// the rest clean (the model does not self-discover either reliably).
|
|
814
|
+
if (isCritiqueClean(verdict)) {
|
|
815
|
+
if (skipDefects === null && wiringProbe === null)
|
|
816
|
+
return spec;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
triageDefects = verdict.trim();
|
|
820
|
+
}
|
|
769
821
|
}
|
|
770
822
|
}
|
|
823
|
+
// Merge the deterministic skip-escape + synthesized-wiring defects with any triage
|
|
824
|
+
// defects for the rewrite (both are forced FOCUS items).
|
|
825
|
+
const rewriteDefects = [skipDefects, wiringProbe, triageDefects].filter(Boolean).join('\n\n') || null;
|
|
771
826
|
const tRewrite = Date.now();
|
|
772
827
|
try {
|
|
773
|
-
return await runWithEmphasisRetry(deps, 'critique', 'read', problem => CRITIQUE_PROMPT(spec, refined, qa, problem !== null,
|
|
828
|
+
return await runWithEmphasisRetry(deps, 'critique', 'read', problem => CRITIQUE_PROMPT(spec, refined, qa, problem !== null, rewriteDefects, contractsBlock), text => {
|
|
774
829
|
// The rewrite (thinking on) sometimes prepends narration before
|
|
775
830
|
// GOAL; the prompt forbids it but this validator only checks for
|
|
776
831
|
// a VERIFY block. Strip it so the delivered spec starts at GOAL.
|
|
@@ -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
|
package/dist/task/prompts.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare const COMPRESS_LABEL_PROMPT: (title: string, maxChars: number) =>
|
|
|
47
47
|
* "Scaffold …" title re-expands the entire design into one task (validated: a real
|
|
48
48
|
* /task-auto run implemented all 24 steps under step 1).
|
|
49
49
|
*/
|
|
50
|
-
declare const REFINE_PROMPT: (raw: string, planContext?: string, existingFiles?: string) => string;
|
|
50
|
+
declare const REFINE_PROMPT: (raw: string, planContext?: string, existingFiles?: string, contracts?: string) => string;
|
|
51
51
|
declare const RESEARCH_READ_ONLY_CONSTRAINT = "IMPORTANT: You are ONLY allowed to READ. Do NOT create, modify, or delete any files. Use the read, grep, find, and ls tools to inspect the repo.";
|
|
52
52
|
declare const RESEARCH_FILES_PROMPT: (refined: string) => string;
|
|
53
53
|
declare const RESEARCH_APIS_PROMPT: (refined: string, filesMap?: string) => string;
|
|
@@ -57,8 +57,8 @@ declare const GRILL_GEN_PROMPT: (refined: string, research: string, priorQA: str
|
|
|
57
57
|
declare const GRILL_AUTO_ANSWER_PROMPT: (refined: string, research: string, question: string) => string;
|
|
58
58
|
export declare const GRILL_AUTO_FORMAT_HINT: string;
|
|
59
59
|
declare function composeRetryEmphasis(problem: string): string;
|
|
60
|
-
declare const COMPOSE_PROMPT: (refined: string, research: string, qa: string, retryProblem: string | null) => string;
|
|
61
|
-
declare const CRITIQUE_TRIAGE_PROMPT: (spec: string, refined: string, qa: string) => string;
|
|
62
|
-
declare const CRITIQUE_PROMPT: (spec: string, refined: string, qa: string, addVerifyEmphasis: boolean, triageDefects?: string | null) => string;
|
|
60
|
+
declare const COMPOSE_PROMPT: (refined: string, research: string, qa: string, retryProblem: string | null, contracts?: string) => string;
|
|
61
|
+
declare const CRITIQUE_TRIAGE_PROMPT: (spec: string, refined: string, qa: string, contracts?: string) => string;
|
|
62
|
+
declare const CRITIQUE_PROMPT: (spec: string, refined: string, qa: string, addVerifyEmphasis: boolean, triageDefects?: string | null, contracts?: string) => string;
|
|
63
63
|
declare const VERIFY_TOOLING_PROMPT: (tooling: string) => string;
|
|
64
64
|
export { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, RESEARCH_READ_ONLY_CONSTRAINT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, composeRetryEmphasis };
|