@mjasnikovs/pi-task 0.37.6 → 0.38.0
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/README.md +6 -3
- package/dist/shared/child-output.d.ts +19 -3
- package/dist/shared/child-output.js +21 -5
- package/dist/shared/git-runner.d.ts +39 -0
- package/dist/shared/git-runner.js +38 -0
- package/dist/task/accept-debt.d.ts +27 -58
- package/dist/task/accept-debt.js +60 -130
- package/dist/task/auto-orchestrator.d.ts +7 -57
- package/dist/task/auto-orchestrator.js +25 -499
- package/dist/task/child-runner.d.ts +2 -0
- package/dist/task/child-runner.js +74 -70
- package/dist/task/enforce-guidelines.d.ts +1 -1
- package/dist/task/enforce-guidelines.js +2 -2
- package/dist/task/external-context.d.ts +85 -7
- package/dist/task/external-context.js +100 -63
- package/dist/task/file-inventory.js +22 -41
- package/dist/task/final-gate.d.ts +80 -0
- package/dist/task/final-gate.js +102 -49
- package/dist/task/gate-deps.js +6 -23
- package/dist/task/git-state-guard.d.ts +1 -1
- package/dist/task/git-state-guard.js +1 -7
- package/dist/task/phases.js +40 -83
- package/dist/task/run-final-gate.d.ts +127 -0
- package/dist/task/run-final-gate.js +492 -0
- package/dist/task/task-gates.d.ts +20 -57
- package/dist/task/task-gates.js +11 -11
- package/dist/task/verify-work.d.ts +40 -32
- package/dist/task/verify-work.js +301 -241
- package/dist/workers/docs-core.d.ts +14 -0
- package/dist/workers/docs-core.js +28 -16
- package/dist/workers/fetch-core.d.ts +6 -1
- package/dist/workers/fetch-core.js +26 -33
- package/dist/workers/focused-extractor.d.ts +73 -0
- package/dist/workers/focused-extractor.js +72 -0
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +48 -42
- package/dist/workers/pi-worker-fetch.js +6 -8
- package/dist/workers/typeonly-log.d.ts +13 -0
- package/package.json +1 -1
package/dist/task/verify-work.js
CHANGED
|
@@ -116,43 +116,63 @@ export function extractSpecForVerification(taskBody) {
|
|
|
116
116
|
return spec.length > 0 ? spec : null;
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
* Build the
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* `probeFindings` are the deterministic self-verification probe results (see
|
|
124
|
-
* substitution-probe.ts): the TEST-THE-COPY class is caught 5/5 only when the
|
|
125
|
-
* prompt carries both the rule (3b) AND a concrete finding naming the suspect
|
|
126
|
-
* file — the rule alone got 2/5 attention on the local model. The findings are
|
|
127
|
-
* pure git shape (test files the task itself changed), so the mandate is
|
|
128
|
-
* language- and framework-agnostic.
|
|
129
|
-
*
|
|
130
|
-
* `prohibitionFindings` are the deterministic prohibition probe results (see
|
|
131
|
-
* prohibition-probe.ts): spec-forbidden paths the task's diff modified anyway.
|
|
132
|
-
* Same probe+rule design, same reason: the VIOLATION-EXCUSAL class (mx5 run 7:
|
|
133
|
-
* child saw "Do NOT modify server-side code" violated, waived it as "additive,
|
|
134
|
-
* tests pass", PASSed) needs both the no-waiver rule (4b) AND the concrete diff
|
|
135
|
-
* fact — the baseline child usually never runs `git diff` at all, so without the
|
|
136
|
-
* finding it cannot even SEE the violation. A/B on the live local model
|
|
137
|
-
* (violated-but-working fixture, everything green, forbidden file modified
|
|
138
|
-
* additively): old prompt 5/5 false-PASS (several runs affirmatively claimed the
|
|
139
|
-
* forbidden file was untouched); rule+finding 5/5 FAIL naming the constraint.
|
|
140
|
-
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
141
|
-
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
119
|
+
* Build one row. Generic in the probe's return type so `empty`/`findings` stay
|
|
120
|
+
* type-checked against the dep's real signature; the result is the erased
|
|
121
|
+
* `ProbeAdapter` the table stores.
|
|
142
122
|
*/
|
|
143
|
-
|
|
123
|
+
function probeAdapter(row) {
|
|
124
|
+
const { key, stage, block, ruleId, rule } = row;
|
|
125
|
+
return {
|
|
126
|
+
key,
|
|
127
|
+
stage,
|
|
128
|
+
block,
|
|
129
|
+
ruleId,
|
|
130
|
+
rule,
|
|
131
|
+
run: async (deps, onStage) => {
|
|
132
|
+
const probe = row.dep(deps);
|
|
133
|
+
// Probes are INDEPENDENTLY OPTIONAL: an absent dep is "skipped", and a
|
|
134
|
+
// probe that throws degrades to its own empty value — it is a sharpener,
|
|
135
|
+
// never a blocker, so a fault in one can neither block the gate nor
|
|
136
|
+
// leak into another row.
|
|
137
|
+
if (!probe)
|
|
138
|
+
return { raw: row.empty, findings: row.findings(row.empty) };
|
|
139
|
+
if (stage)
|
|
140
|
+
onStage(stage);
|
|
141
|
+
try {
|
|
142
|
+
const raw = await probe();
|
|
143
|
+
return { raw, findings: row.findings(raw) };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { raw: row.empty, findings: row.findings(row.empty) };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Identity transform for the rows whose probe already returns finding lines. */
|
|
152
|
+
const asLines = (raw) => raw;
|
|
144
153
|
/**
|
|
145
|
-
* The
|
|
146
|
-
*
|
|
147
|
-
* an independent finding list; absent/empty emits no block.
|
|
154
|
+
* The table. ROW ORDER IS THE NOTICE-BLOCK ORDER IN THE PROMPT — do not reorder
|
|
155
|
+
* without re-running the byte-identity check.
|
|
148
156
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
const PROBE_ADAPTERS = [
|
|
158
|
+
/**
|
|
159
|
+
* Deterministic self-verification probe (see substitution-probe.ts): the
|
|
160
|
+
* TEST-THE-COPY class is caught 5/5 only when the prompt carries both the rule
|
|
161
|
+
* (3b) AND a concrete finding naming the suspect file — the rule alone got 2/5
|
|
162
|
+
* attention on the local model. The findings are pure git shape (test files the
|
|
163
|
+
* task itself changed), so the mandate is language- and framework-agnostic.
|
|
164
|
+
*/
|
|
165
|
+
probeAdapter({
|
|
166
|
+
key: 'substitution',
|
|
167
|
+
stage: 'substitution probe',
|
|
168
|
+
dep: deps => deps.probe,
|
|
169
|
+
empty: [],
|
|
170
|
+
findings: asLines,
|
|
171
|
+
ruleId: '3b',
|
|
172
|
+
block: findings => [
|
|
153
173
|
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
154
174
|
'this task wrote or changed the very tests whose green result would bless it:',
|
|
155
|
-
...
|
|
175
|
+
...findings.map(f => `- ${f}`),
|
|
156
176
|
'A green run of self-authored tests is NOT sufficient verification. Before any PASS',
|
|
157
177
|
'you MUST confirm these tests exercise the REAL shipped artifact: drive 1-2 tested',
|
|
158
178
|
'behaviors directly against the real app / module / entry point and judge what IT',
|
|
@@ -160,26 +180,73 @@ projectSurface = {}) {
|
|
|
160
180
|
'copy (rule 3b below).',
|
|
161
181
|
''
|
|
162
182
|
]
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
}),
|
|
184
|
+
/**
|
|
185
|
+
* Deterministic prohibition probe (see prohibition-probe.ts): spec-forbidden
|
|
186
|
+
* paths the task's diff modified anyway. Same probe+rule design, same reason:
|
|
187
|
+
* the VIOLATION-EXCUSAL class (mx5 run 7: child saw "Do NOT modify server-side
|
|
188
|
+
* code" violated, waived it as "additive, tests pass", PASSed) needs both the
|
|
189
|
+
* no-waiver rule (4b) AND the concrete diff fact — the baseline child usually
|
|
190
|
+
* never runs `git diff` at all, so without the finding it cannot even SEE the
|
|
191
|
+
* violation. A/B on the live local model (violated-but-working fixture,
|
|
192
|
+
* everything green, forbidden file modified additively): old prompt 5/5
|
|
193
|
+
* false-PASS (several runs affirmatively claimed the forbidden file was
|
|
194
|
+
* untouched); rule+finding 5/5 FAIL naming the constraint. Guard: honest-clean
|
|
195
|
+
* fixture (prohibition in spec, probe silent) 5/5 PASS — no paranoia.
|
|
196
|
+
* Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
197
|
+
*/
|
|
198
|
+
probeAdapter({
|
|
199
|
+
key: 'prohibition',
|
|
200
|
+
stage: 'prohibition probe',
|
|
201
|
+
dep: deps => deps.prohibitionProbe,
|
|
202
|
+
empty: [],
|
|
203
|
+
findings: asLines,
|
|
204
|
+
ruleId: '4b',
|
|
205
|
+
block: findings => [
|
|
166
206
|
'PROHIBITION NOTICE (deterministic, computed by the orchestrator from the',
|
|
167
207
|
"spec's own constraint lines and the task's diff): this task MODIFIED paths",
|
|
168
208
|
'the spec explicitly forbids modifying:',
|
|
169
|
-
...
|
|
209
|
+
...findings.map(f => `- ${f}`),
|
|
170
210
|
'Read the exact constraint wording in the spec. Unless that wording itself',
|
|
171
211
|
'states an exception that covers this change, this is a violated prohibition:',
|
|
172
212
|
'rule 4b applies and the verdict is FAIL naming the forbidden path — even if',
|
|
173
213
|
'every test passes and the change looks harmless.',
|
|
174
214
|
''
|
|
215
|
+
],
|
|
216
|
+
rule: [
|
|
217
|
+
'4b. SPEC PROHIBITIONS ARE PART OF THE BAR — YOU HAVE NO WAIVER AUTHORITY: when the',
|
|
218
|
+
' spec explicitly forbids something ("Do NOT modify X", "MUST NOT touch Y") and the',
|
|
219
|
+
' shipped work does it anyway, that is a FAIL naming the violated constraint. You',
|
|
220
|
+
' may not excuse a violation because it is additive, small, harmless, an improvement,',
|
|
221
|
+
' or because every test still passes — "it works anyway" is exactly the waiver you do',
|
|
222
|
+
" not have; relaxing a constraint is the spec owner's call, not yours. Check the",
|
|
223
|
+
" task's own diff (git) against the spec's prohibitions — a forbidden file can be",
|
|
224
|
+
' modified without any test noticing. Only two outcomes are not a FAIL: the',
|
|
225
|
+
' violation was fully REVERTED (the shipped tree no longer violates), or the',
|
|
226
|
+
' prohibition\'s own wording states an exception ("except…", "beyond what is needed',
|
|
227
|
+
' for…") that covers the change — judged against that stated exception, not against',
|
|
228
|
+
' your view of harmlessness.'
|
|
175
229
|
]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
230
|
+
}),
|
|
231
|
+
/**
|
|
232
|
+
* Deterministic cross-task deletion probe (see task-provenance.ts, mx5 run 12
|
|
233
|
+
* PROMPT 2): tracked files the task's diff DELETES whose introducing task (git
|
|
234
|
+
* provenance) differs from the current task. The only row whose probe does NOT
|
|
235
|
+
* return finding lines — the structured value also rides on a FAIL outcome so
|
|
236
|
+
* an ACCEPT records each deletion as a durable debt.
|
|
237
|
+
*/
|
|
238
|
+
probeAdapter({
|
|
239
|
+
key: 'crossTaskDeletion',
|
|
240
|
+
stage: 'cross-task deletion probe',
|
|
241
|
+
dep: deps => deps.crossTaskDeletionProbe,
|
|
242
|
+
empty: [],
|
|
243
|
+
findings: crossTaskDeletionVerifyFindings,
|
|
244
|
+
ruleId: '4d',
|
|
245
|
+
block: findings => [
|
|
179
246
|
'CROSS-TASK DELETION NOTICE (deterministic, computed by the orchestrator from',
|
|
180
247
|
"git provenance over the task's diff): this task's work DELETED committed",
|
|
181
248
|
'deliverable(s) that a DIFFERENT, already-completed task introduced:',
|
|
182
|
-
...
|
|
249
|
+
...findings.map(f => `- ${f}`),
|
|
183
250
|
"A sibling task's verified, committed deliverable is not this task's to remove.",
|
|
184
251
|
'Deleting the file a checker complains about is the cheapest way to turn a red',
|
|
185
252
|
'check green — the finding vanishes with the file — and it destroys finished',
|
|
@@ -188,15 +255,38 @@ projectSurface = {}) {
|
|
|
188
255
|
'verdict is FAIL naming the deleted file and its owning task — even if every',
|
|
189
256
|
'check passes and the deletion made them pass.',
|
|
190
257
|
''
|
|
258
|
+
],
|
|
259
|
+
rule: [
|
|
260
|
+
"4d. A SIBLING TASK'S COMMITTED DELIVERABLE IS NOT THIS TASK'S TO DELETE — check the",
|
|
261
|
+
" task's own diff (git) for DELETED tracked files. A file that a DIFFERENT completed",
|
|
262
|
+
" task introduced and committed, deleted by this task's work, is finished work",
|
|
263
|
+
' destroyed — usually to make a red check go green (the file the checker complains',
|
|
264
|
+
' about simply disappears; that is quieting the messenger, rule 4c, not fixing the',
|
|
265
|
+
' defect). A passing check suite on the shrunken tree is not a waiver. Only two',
|
|
266
|
+
" outcomes are not a FAIL: THIS task's spec explicitly requires that removal, or",
|
|
267
|
+
' the deletion is a genuine relocation (the same file lives on elsewhere in the',
|
|
268
|
+
' tree). Otherwise the verdict is FAIL naming the deleted file and the task that',
|
|
269
|
+
' owns it.'
|
|
191
270
|
]
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
271
|
+
}),
|
|
272
|
+
/**
|
|
273
|
+
* Deterministic probe-gaming probe (see probe-gaming.ts, run-8 F6): added lines
|
|
274
|
+
* whose stated purpose is to make a CHECK pass instead of meeting the
|
|
275
|
+
* requirement it stands for ("return 401 so the verification test passes").
|
|
276
|
+
*/
|
|
277
|
+
probeAdapter({
|
|
278
|
+
key: 'probeGaming',
|
|
279
|
+
stage: 'probe-gaming probe',
|
|
280
|
+
dep: deps => deps.probeGamingProbe,
|
|
281
|
+
empty: [],
|
|
282
|
+
findings: asLines,
|
|
283
|
+
ruleId: '4c',
|
|
284
|
+
block: findings => [
|
|
195
285
|
'CHECK-GAMING NOTICE (deterministic, computed by the orchestrator from the',
|
|
196
286
|
"task's diff): these added lines state their own purpose is to make a CHECK",
|
|
197
287
|
'pass (a test / verification / lint / gate), not to meet the requirement the',
|
|
198
288
|
'check stands for:',
|
|
199
|
-
...
|
|
289
|
+
...findings.map(f => `- ${f}`),
|
|
200
290
|
'A check is a MESSENGER for a requirement. Code written to quiet the messenger',
|
|
201
291
|
'instead of meeting the requirement is a defect even when the check is green',
|
|
202
292
|
'(run-8 F6: a handler returned 401 "so the verification test passes" while the',
|
|
@@ -206,15 +296,42 @@ projectSurface = {}) {
|
|
|
206
296
|
'satisfy the check while the requirement is unmet, that is a FAIL naming the',
|
|
207
297
|
'gamed check and the unmet requirement.',
|
|
208
298
|
''
|
|
299
|
+
],
|
|
300
|
+
rule: [
|
|
301
|
+
'4c. THE CHECK IS THE MESSENGER, NOT THE REQUIREMENT — code (or a comment) whose',
|
|
302
|
+
' stated purpose is to make a check PASS, rather than to satisfy the requirement the',
|
|
303
|
+
' check stands for, is a defect even when the check is green. The tell is the intent',
|
|
304
|
+
' written down: "return X so the test passes", "hardcode this to satisfy the linter",',
|
|
305
|
+
' "stub it out to appease CI". When you see such a line — or the CHECK-GAMING NOTICE',
|
|
306
|
+
' above names one — do NOT treat the passing check as proof the requirement is met.',
|
|
307
|
+
' Find the actual requirement the check was meant to prove and verify THAT directly',
|
|
308
|
+
' against the real artifact (rule 3e negative control is the sharpest tool: a handler',
|
|
309
|
+
' that answers the check-shaped request the same way for a WRONG input is gaming the',
|
|
310
|
+
' check, not implementing the behavior). If the requirement is genuinely unmet while',
|
|
311
|
+
' the check passes, the verdict is FAIL naming the gamed check and the real gap.'
|
|
209
312
|
]
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
313
|
+
}),
|
|
314
|
+
/**
|
|
315
|
+
* DETERMINISTIC skip-escape finding, computed purely from the spec's own VERIFY
|
|
316
|
+
* block (see skip-escape.ts): a required check wrapped in a skip-announcing `||`
|
|
317
|
+
* fallback. Injected so rule 5c fires reliably — the model does not self-discover
|
|
318
|
+
* a graceful skip-escape (A/B: rule alone ~1-3/5), but acts on a finding naming
|
|
319
|
+
* the exact line, per the proven probe+rule pattern. Pure text analysis over
|
|
320
|
+
* `deps.spec`, so this row needs no dep and reports no stage: it is the one probe
|
|
321
|
+
* that is never absent and never costs a git call.
|
|
322
|
+
*/
|
|
323
|
+
probeAdapter({
|
|
324
|
+
key: 'skipEscape',
|
|
325
|
+
dep: deps => () => Promise.resolve(skipEscapeVerifyFindings(findSkipEscapes(deps.spec ?? ''))),
|
|
326
|
+
empty: [],
|
|
327
|
+
findings: asLines,
|
|
328
|
+
ruleId: '5c',
|
|
329
|
+
block: findings => [
|
|
213
330
|
"SKIP-ESCAPE NOTICE (deterministic, computed by the orchestrator from the spec's",
|
|
214
331
|
'OWN VERIFY block): these VERIFY commands wrap a required check in a fallback that',
|
|
215
332
|
'ANNOUNCES skipping it when a tool is absent — so the check can "pass" while never',
|
|
216
333
|
'actually running:',
|
|
217
|
-
...
|
|
334
|
+
...findings.map(f => `- ${f}`),
|
|
218
335
|
'Do NOT accept a skipped check as a passed check. For each, determine whether it',
|
|
219
336
|
'ACTUALLY ran and observed the real behavior. If its tool is absent so the required',
|
|
220
337
|
'behavior was never observed, that area is UNOBSERVED (rule 5c) — verdict UNOBSERVED,',
|
|
@@ -222,14 +339,26 @@ projectSurface = {}) {
|
|
|
222
339
|
'artifact directly) may it count as verified.',
|
|
223
340
|
''
|
|
224
341
|
]
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
342
|
+
}),
|
|
343
|
+
/**
|
|
344
|
+
* Deterministic sandbox-path-leak probe (see foreign-path.ts, mx5 run 13 PROMPT
|
|
345
|
+
* 4 item 1): absolute paths this task committed that exist only inside the
|
|
346
|
+
* authoring child's own environment — `/workspace/src/shared` in a vite alias —
|
|
347
|
+
* while the real file sits at `src/shared` here.
|
|
348
|
+
*/
|
|
349
|
+
probeAdapter({
|
|
350
|
+
key: 'foreignPath',
|
|
351
|
+
stage: 'foreign-path probe',
|
|
352
|
+
dep: deps => deps.foreignPathProbe,
|
|
353
|
+
empty: [],
|
|
354
|
+
findings: asLines,
|
|
355
|
+
ruleId: '4e',
|
|
356
|
+
block: findings => [
|
|
228
357
|
'SANDBOX PATH LEAK NOTICE (deterministic, computed by the orchestrator by',
|
|
229
358
|
"resolving every absolute path in the task's diff against THIS machine): this",
|
|
230
359
|
'task committed absolute paths that do not exist here, while the real file they',
|
|
231
360
|
'name sits inside this repo:',
|
|
232
|
-
...
|
|
361
|
+
...findings.map(f => `- ${f}`),
|
|
233
362
|
"These are paths from the authoring agent's OWN environment, baked into a file",
|
|
234
363
|
'that ships. The command that reads such a path does not fail a check — it fails',
|
|
235
364
|
'to BUILD, so the checks that would have caught it never run and report nothing',
|
|
@@ -240,14 +369,38 @@ projectSurface = {}) {
|
|
|
240
369
|
'the tests/steps that ran, not the exit code. Unless the leaked path resolves on',
|
|
241
370
|
'this machine, the verdict is FAIL naming the file and the path (rule 4e).',
|
|
242
371
|
''
|
|
372
|
+
],
|
|
373
|
+
rule: [
|
|
374
|
+
'4e. AN ABSOLUTE PATH TO PROJECT FILES IS A DEFECT — a committed path like',
|
|
375
|
+
" `/workspace/src/shared` or `/home/<someone>/proj/src` names the authoring agent's",
|
|
376
|
+
' OWN machine, not this one. Its distinctive damage is that it breaks the run BEFORE',
|
|
377
|
+
' any check reports: a bad alias/config path makes the tool fail to RESOLVE or BUILD,',
|
|
378
|
+
' so a suite "passes" having executed nothing. Treat a command that reports no',
|
|
379
|
+
' failures but also no WORK — 0 tests run, 0 files emitted, an empty report — as',
|
|
380
|
+
' unverified, never as green. Confirm the count of things that actually ran. A path',
|
|
381
|
+
' to project-internal files must be relative to the file that carries it, or computed',
|
|
382
|
+
' at runtime; if one does not resolve here, the verdict is FAIL naming file and path.'
|
|
243
383
|
]
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
384
|
+
}),
|
|
385
|
+
/**
|
|
386
|
+
* Deterministic neutered-check-script probe (see script-escape.ts, mx5 run 13
|
|
387
|
+
* PROMPT 4 item 4): check-class scripts in a manifest THIS task changed whose
|
|
388
|
+
* exit status cannot be non-zero (`… || true`, an inverted-grep launder). The
|
|
389
|
+
* damage is second-order — the script still "passes" — which is exactly why the
|
|
390
|
+
* child cannot discover it by running the check.
|
|
391
|
+
*/
|
|
392
|
+
probeAdapter({
|
|
393
|
+
key: 'scriptEscape',
|
|
394
|
+
stage: 'script-escape probe',
|
|
395
|
+
dep: deps => deps.scriptEscapeProbe,
|
|
396
|
+
empty: [],
|
|
397
|
+
findings: asLines,
|
|
398
|
+
ruleId: '4f',
|
|
399
|
+
block: findings => [
|
|
247
400
|
'NEUTERED CHECK SCRIPT NOTICE (deterministic, computed by the orchestrator from',
|
|
248
401
|
'the manifest THIS task changed): these check scripts cannot report failure —',
|
|
249
402
|
'their exit status is 0 no matter what the checker finds:',
|
|
250
|
-
...
|
|
403
|
+
...findings.map(f => `- ${f}`),
|
|
251
404
|
'You CANNOT discover this by running the script: it passes, which is the whole',
|
|
252
405
|
'defect (mx5 run 13 shipped a `lint` whose typecheck was disarmed by an inverted',
|
|
253
406
|
'grep and a `|| true` tail; every gate that ran it reported success without',
|
|
@@ -257,14 +410,36 @@ projectSurface = {}) {
|
|
|
257
410
|
'`npm run lint`) and judge THAT output. Unless the task spec explicitly requires',
|
|
258
411
|
'the script to tolerate failure, the verdict is FAIL naming the script (rule 4f).',
|
|
259
412
|
''
|
|
413
|
+
],
|
|
414
|
+
rule: [
|
|
415
|
+
'4f. A CHECK THAT CANNOT FAIL PROVES NOTHING — before you cite a check script',
|
|
416
|
+
" (`npm run lint`, `bun run test`) as evidence, read its DEFINITION in the project's",
|
|
417
|
+
' manifest. A script ending in `|| true`, `; exit 0`, or piping a checker into an',
|
|
418
|
+
' inverted grep exits 0 unconditionally: its green result is a constant, not a',
|
|
419
|
+
' measurement, and running it again only reproduces the constant. When a script is',
|
|
420
|
+
' built that way, run the underlying checker directly and judge its real output; and',
|
|
421
|
+
' unless the spec required that tolerance, the script itself is a defect — the',
|
|
422
|
+
' verdict is FAIL naming it.'
|
|
260
423
|
]
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
424
|
+
}),
|
|
425
|
+
/**
|
|
426
|
+
* Deterministic test-runner glob-collision probe (see runner-globs.ts, mx5 runs
|
|
427
|
+
* 7 AND 13, PROMPT 4 item 2): the manifest declares two runners whose file sets
|
|
428
|
+
* are not provably disjoint, so the scanning one imports the other's specs and
|
|
429
|
+
* dies during COLLECTION.
|
|
430
|
+
*/
|
|
431
|
+
probeAdapter({
|
|
432
|
+
key: 'runnerGlob',
|
|
433
|
+
stage: 'runner-glob probe',
|
|
434
|
+
dep: deps => deps.runnerGlobProbe,
|
|
435
|
+
empty: [],
|
|
436
|
+
findings: asLines,
|
|
437
|
+
ruleId: '4g',
|
|
438
|
+
block: findings => [
|
|
264
439
|
'TEST-RUNNER GLOB COLLISION NOTICE (deterministic, computed by the orchestrator',
|
|
265
440
|
"from the manifest's declared runners and their config): two test runners claim",
|
|
266
441
|
'the same files:',
|
|
267
|
-
...
|
|
442
|
+
...findings.map(f => `- ${f}`),
|
|
268
443
|
"The scanning runner will import the other's spec files and abort on a module",
|
|
269
444
|
'loaded outside its own runner — so the suite dies WHOLESALE rather than',
|
|
270
445
|
'reporting failures (this is the THIRD occurrence: mx5 runs 7 and 13). Run both',
|
|
@@ -272,15 +447,35 @@ projectSurface = {}) {
|
|
|
272
447
|
'its own. A run that errors during collection has verified nothing, whatever its',
|
|
273
448
|
'exit code says (rule 4g).',
|
|
274
449
|
''
|
|
450
|
+
],
|
|
451
|
+
rule: [
|
|
452
|
+
'4g. TWO RUNNERS, ONE FILE SET, NO RESULTS — when a project declares more than one',
|
|
453
|
+
' test runner, check that each collects only its own files. A runner that scans for',
|
|
454
|
+
" `*.test.*` / `*.spec.*` project-wide will import another runner's specs and abort",
|
|
455
|
+
' during COLLECTION. That failure mode looks nothing like a test failure: you get an',
|
|
456
|
+
' import error, or a suite that reports zero tests. Always read how many tests each',
|
|
457
|
+
' command actually COLLECTED and RAN; zero collected is never a pass.'
|
|
275
458
|
]
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
459
|
+
}),
|
|
460
|
+
/**
|
|
461
|
+
* Deterministic test-assembly probe (see test-assembly.ts): authored test files
|
|
462
|
+
* that rebuild production WIRING — importing the leaf modules the shipped entry
|
|
463
|
+
* composes and assembling their own copy instead of the real assembly — under
|
|
464
|
+
* rule 3f (F4 test-the-copy, 3rd recurrence). Pure import-graph shape.
|
|
465
|
+
*/
|
|
466
|
+
probeAdapter({
|
|
467
|
+
key: 'testAssembly',
|
|
468
|
+
stage: 'test-assembly probe',
|
|
469
|
+
dep: deps => deps.testAssemblyProbe,
|
|
470
|
+
empty: [],
|
|
471
|
+
findings: asLines,
|
|
472
|
+
ruleId: '3f',
|
|
473
|
+
block: findings => [
|
|
279
474
|
'TEST-ASSEMBLY NOTICE (deterministic, computed by the orchestrator from pure',
|
|
280
475
|
'import-graph shape): these test files rebuild production WIRING — they import the',
|
|
281
476
|
'same leaf modules the shipped entry composes and assemble their OWN copy of it,',
|
|
282
477
|
'instead of importing the production assembly:',
|
|
283
|
-
...
|
|
478
|
+
...findings.map(f => `- ${f}`),
|
|
284
479
|
'A green result on such a test proves that PRIVATE re-assembly, NOT the shipped',
|
|
285
480
|
'wiring — the copy can be wired differently (a different mount prefix, order, or',
|
|
286
481
|
'middleware) and pass while production is broken exactly at the seam the test was',
|
|
@@ -290,7 +485,29 @@ projectSurface = {}) {
|
|
|
290
485
|
'fails where the test passes, report FAIL and name the wiring seam.',
|
|
291
486
|
''
|
|
292
487
|
]
|
|
293
|
-
|
|
488
|
+
})
|
|
489
|
+
];
|
|
490
|
+
/**
|
|
491
|
+
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
492
|
+
* without spawning pi. The contract: run the spec's own verification in the real
|
|
493
|
+
* workspace, judge against ACCEPTANCE, and end on exactly one verdict line.
|
|
494
|
+
*
|
|
495
|
+
* `findings` is the probe bag: one key per PROBE_ADAPTERS row (see the table
|
|
496
|
+
* above for what each channel means and the A/B evidence behind it). A key that
|
|
497
|
+
* is absent or empty emits no block — the probes are independently optional.
|
|
498
|
+
*/
|
|
499
|
+
export function buildVerifyPrompt(spec, findings = {}, context = {}) {
|
|
500
|
+
const { envNotes, contracts } = context;
|
|
501
|
+
// NOTICE BLOCKS in table order; RULES sorted by their hand-assigned number.
|
|
502
|
+
const noticeBlocks = PROBE_ADAPTERS.flatMap(adapter => {
|
|
503
|
+
const lines = findings[adapter.key];
|
|
504
|
+
return lines && lines.length > 0 ? adapter.block(lines) : [];
|
|
505
|
+
});
|
|
506
|
+
const probeRules = PROBE_ADAPTERS.filter(a => a.rule)
|
|
507
|
+
.sort((a, b) => a.ruleId < b.ruleId ? -1
|
|
508
|
+
: a.ruleId > b.ruleId ? 1
|
|
509
|
+
: 0)
|
|
510
|
+
.flatMap(a => [...(a.rule ?? []), '']);
|
|
294
511
|
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
|
|
295
512
|
const contractsBlock = contracts && contracts.trim().length > 0 ? [buildContractsVerifyBlock(contracts)] : [];
|
|
296
513
|
return [
|
|
@@ -308,15 +525,7 @@ projectSurface = {}) {
|
|
|
308
525
|
'',
|
|
309
526
|
...envBlock,
|
|
310
527
|
...contractsBlock,
|
|
311
|
-
...
|
|
312
|
-
...prohibitionBlock,
|
|
313
|
-
...crossTaskDeletionBlock,
|
|
314
|
-
...probeGamingBlock,
|
|
315
|
-
...skipEscapeBlock,
|
|
316
|
-
...foreignPathBlock,
|
|
317
|
-
...scriptEscapeBlock,
|
|
318
|
-
...runnerGlobBlock,
|
|
319
|
-
...testAssemblyBlock,
|
|
528
|
+
...noticeBlocks,
|
|
320
529
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
321
530
|
'checkout (or CI run) would experience it:',
|
|
322
531
|
'',
|
|
@@ -413,68 +622,9 @@ projectSurface = {}) {
|
|
|
413
622
|
'4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
|
|
414
623
|
' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
415
624
|
'',
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
' may not excuse a violation because it is additive, small, harmless, an improvement,',
|
|
420
|
-
' or because every test still passes — "it works anyway" is exactly the waiver you do',
|
|
421
|
-
" not have; relaxing a constraint is the spec owner's call, not yours. Check the",
|
|
422
|
-
" task's own diff (git) against the spec's prohibitions — a forbidden file can be",
|
|
423
|
-
' modified without any test noticing. Only two outcomes are not a FAIL: the',
|
|
424
|
-
' violation was fully REVERTED (the shipped tree no longer violates), or the',
|
|
425
|
-
' prohibition\'s own wording states an exception ("except…", "beyond what is needed',
|
|
426
|
-
' for…") that covers the change — judged against that stated exception, not against',
|
|
427
|
-
' your view of harmlessness.',
|
|
428
|
-
'',
|
|
429
|
-
'4c. THE CHECK IS THE MESSENGER, NOT THE REQUIREMENT — code (or a comment) whose',
|
|
430
|
-
' stated purpose is to make a check PASS, rather than to satisfy the requirement the',
|
|
431
|
-
' check stands for, is a defect even when the check is green. The tell is the intent',
|
|
432
|
-
' written down: "return X so the test passes", "hardcode this to satisfy the linter",',
|
|
433
|
-
' "stub it out to appease CI". When you see such a line — or the CHECK-GAMING NOTICE',
|
|
434
|
-
' above names one — do NOT treat the passing check as proof the requirement is met.',
|
|
435
|
-
' Find the actual requirement the check was meant to prove and verify THAT directly',
|
|
436
|
-
' against the real artifact (rule 3e negative control is the sharpest tool: a handler',
|
|
437
|
-
' that answers the check-shaped request the same way for a WRONG input is gaming the',
|
|
438
|
-
' check, not implementing the behavior). If the requirement is genuinely unmet while',
|
|
439
|
-
' the check passes, the verdict is FAIL naming the gamed check and the real gap.',
|
|
440
|
-
'',
|
|
441
|
-
"4d. A SIBLING TASK'S COMMITTED DELIVERABLE IS NOT THIS TASK'S TO DELETE — check the",
|
|
442
|
-
" task's own diff (git) for DELETED tracked files. A file that a DIFFERENT completed",
|
|
443
|
-
" task introduced and committed, deleted by this task's work, is finished work",
|
|
444
|
-
' destroyed — usually to make a red check go green (the file the checker complains',
|
|
445
|
-
' about simply disappears; that is quieting the messenger, rule 4c, not fixing the',
|
|
446
|
-
' defect). A passing check suite on the shrunken tree is not a waiver. Only two',
|
|
447
|
-
" outcomes are not a FAIL: THIS task's spec explicitly requires that removal, or",
|
|
448
|
-
' the deletion is a genuine relocation (the same file lives on elsewhere in the',
|
|
449
|
-
' tree). Otherwise the verdict is FAIL naming the deleted file and the task that',
|
|
450
|
-
' owns it.',
|
|
451
|
-
'',
|
|
452
|
-
'4e. AN ABSOLUTE PATH TO PROJECT FILES IS A DEFECT — a committed path like',
|
|
453
|
-
" `/workspace/src/shared` or `/home/<someone>/proj/src` names the authoring agent's",
|
|
454
|
-
' OWN machine, not this one. Its distinctive damage is that it breaks the run BEFORE',
|
|
455
|
-
' any check reports: a bad alias/config path makes the tool fail to RESOLVE or BUILD,',
|
|
456
|
-
' so a suite "passes" having executed nothing. Treat a command that reports no',
|
|
457
|
-
' failures but also no WORK — 0 tests run, 0 files emitted, an empty report — as',
|
|
458
|
-
' unverified, never as green. Confirm the count of things that actually ran. A path',
|
|
459
|
-
' to project-internal files must be relative to the file that carries it, or computed',
|
|
460
|
-
' at runtime; if one does not resolve here, the verdict is FAIL naming file and path.',
|
|
461
|
-
'',
|
|
462
|
-
'4f. A CHECK THAT CANNOT FAIL PROVES NOTHING — before you cite a check script',
|
|
463
|
-
" (`npm run lint`, `bun run test`) as evidence, read its DEFINITION in the project's",
|
|
464
|
-
' manifest. A script ending in `|| true`, `; exit 0`, or piping a checker into an',
|
|
465
|
-
' inverted grep exits 0 unconditionally: its green result is a constant, not a',
|
|
466
|
-
' measurement, and running it again only reproduces the constant. When a script is',
|
|
467
|
-
' built that way, run the underlying checker directly and judge its real output; and',
|
|
468
|
-
' unless the spec required that tolerance, the script itself is a defect — the',
|
|
469
|
-
' verdict is FAIL naming it.',
|
|
470
|
-
'',
|
|
471
|
-
'4g. TWO RUNNERS, ONE FILE SET, NO RESULTS — when a project declares more than one',
|
|
472
|
-
' test runner, check that each collects only its own files. A runner that scans for',
|
|
473
|
-
" `*.test.*` / `*.spec.*` project-wide will import another runner's specs and abort",
|
|
474
|
-
' during COLLECTION. That failure mode looks nothing like a test failure: you get an',
|
|
475
|
-
' import error, or a suite that reports zero tests. Always read how many tests each',
|
|
476
|
-
' command actually COLLECTED and RAN; zero collected is never a pass.',
|
|
477
|
-
'',
|
|
625
|
+
// The 4b…4g band: one rule per probe row that owns a numbered rule, emitted
|
|
626
|
+
// in rule-number order (see the table above — NOT the notice-block order).
|
|
627
|
+
...probeRules,
|
|
478
628
|
'5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
|
|
479
629
|
' service or network resource (a database server, an API host) that the project',
|
|
480
630
|
' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
|
|
@@ -614,101 +764,21 @@ export async function runWorkVerification(deps) {
|
|
|
614
764
|
if (!deps.spec || deps.spec.trim().length === 0) {
|
|
615
765
|
return { ok: true, reason: 'no spec to verify' };
|
|
616
766
|
}
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
let prohibitions = [];
|
|
630
|
-
if (deps.prohibitionProbe) {
|
|
631
|
-
stage('prohibition probe');
|
|
632
|
-
try {
|
|
633
|
-
prohibitions = await deps.prohibitionProbe();
|
|
634
|
-
}
|
|
635
|
-
catch {
|
|
636
|
-
prohibitions = [];
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
// Test-assembly findings feed the prompt (rule 3f); a probe failure must never
|
|
640
|
-
// block verification — it is an optional sharpener like the substitution probe.
|
|
641
|
-
let testAssembly = [];
|
|
642
|
-
if (deps.testAssemblyProbe) {
|
|
643
|
-
stage('test-assembly probe');
|
|
644
|
-
try {
|
|
645
|
-
testAssembly = await deps.testAssemblyProbe();
|
|
646
|
-
}
|
|
647
|
-
catch {
|
|
648
|
-
testAssembly = [];
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
// Probe-gaming findings feed the prompt (rule 4c, F6); a probe failure must never
|
|
652
|
-
// block verification — an optional sharpener like the other diff-shape probes.
|
|
653
|
-
let probeGaming = [];
|
|
654
|
-
if (deps.probeGamingProbe) {
|
|
655
|
-
stage('probe-gaming probe');
|
|
656
|
-
try {
|
|
657
|
-
probeGaming = await deps.probeGamingProbe();
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
probeGaming = [];
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Cross-task deletion findings feed the prompt (rule 4d) AND ride on a FAIL
|
|
664
|
-
// outcome (structured) so an ACCEPT can record them as durable debts. A probe
|
|
665
|
-
// failure must never block verification.
|
|
666
|
-
let crossDeletions = [];
|
|
667
|
-
if (deps.crossTaskDeletionProbe) {
|
|
668
|
-
stage('cross-task deletion probe');
|
|
669
|
-
try {
|
|
670
|
-
crossDeletions = await deps.crossTaskDeletionProbe();
|
|
671
|
-
}
|
|
672
|
-
catch {
|
|
673
|
-
crossDeletions = [];
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
// Sandbox-path-leak findings the deterministic repair could NOT fix, injected
|
|
677
|
-
// under rule 4e. A probe failure must never block verification.
|
|
678
|
-
let foreignPaths = [];
|
|
679
|
-
if (deps.foreignPathProbe) {
|
|
680
|
-
stage('foreign-path probe');
|
|
681
|
-
try {
|
|
682
|
-
foreignPaths = await deps.foreignPathProbe();
|
|
683
|
-
}
|
|
684
|
-
catch {
|
|
685
|
-
foreignPaths = [];
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
// Neutered check scripts in a manifest this task changed, injected under rule
|
|
689
|
-
// 4f. A probe failure must never block verification.
|
|
690
|
-
let scriptEscapes = [];
|
|
691
|
-
if (deps.scriptEscapeProbe) {
|
|
692
|
-
stage('script-escape probe');
|
|
693
|
-
try {
|
|
694
|
-
scriptEscapes = await deps.scriptEscapeProbe();
|
|
695
|
-
}
|
|
696
|
-
catch {
|
|
697
|
-
scriptEscapes = [];
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
// Colliding test-runner globs, injected under rule 4g. A probe failure must
|
|
701
|
-
// never block verification.
|
|
702
|
-
let runnerGlobs = [];
|
|
703
|
-
if (deps.runnerGlobProbe) {
|
|
704
|
-
stage('runner-glob probe');
|
|
705
|
-
try {
|
|
706
|
-
runnerGlobs = await deps.runnerGlobProbe();
|
|
707
|
-
}
|
|
708
|
-
catch {
|
|
709
|
-
runnerGlobs = [];
|
|
710
|
-
}
|
|
767
|
+
// Every deterministic probe, one table row each (see PROBE_ADAPTERS): the row
|
|
768
|
+
// knows which dep it reads, what it degrades to, and which notice block its
|
|
769
|
+
// findings become. Each is an optional SHARPENER — an absent dep is skipped and
|
|
770
|
+
// a throwing probe degrades to its empty value, so no probe can block the gate.
|
|
771
|
+
const findings = {};
|
|
772
|
+
const rawResults = new Map();
|
|
773
|
+
for (const adapter of PROBE_ADAPTERS) {
|
|
774
|
+
const result = await adapter.run(deps, stage);
|
|
775
|
+
findings[adapter.key] = result.findings;
|
|
776
|
+
rawResults.set(adapter.key, result.raw);
|
|
711
777
|
}
|
|
778
|
+
// The one probe whose RAW value is needed beyond the prompt: cross-task deletion
|
|
779
|
+
// findings ride on a FAIL outcome (structured) so an ACCEPT can record them as
|
|
780
|
+
// durable debts. The row's `empty` is `[]`, so this is always an array.
|
|
781
|
+
const crossDeletions = (rawResults.get('crossTaskDeletion') ?? []);
|
|
712
782
|
// Environment facts from earlier gate children (best-effort; a cache failure
|
|
713
783
|
// must never block verification).
|
|
714
784
|
let envNotes = '';
|
|
@@ -731,12 +801,6 @@ export async function runWorkVerification(deps) {
|
|
|
731
801
|
contracts = '';
|
|
732
802
|
}
|
|
733
803
|
}
|
|
734
|
-
// DETERMINISTIC skip-escape finding, computed purely from the spec's own VERIFY
|
|
735
|
-
// block (see skip-escape.ts): a required check wrapped in a skip-announcing `||`
|
|
736
|
-
// fallback. Injected so rule 5c fires reliably — the model does not self-discover a
|
|
737
|
-
// graceful skip-escape (A/B: rule alone ~1-3/5), but acts on a finding naming the
|
|
738
|
-
// exact line, per the proven probe+rule pattern. Pure text analysis, no dep needed.
|
|
739
|
-
const skipEscapes = skipEscapeVerifyFindings(findSkipEscapes(deps.spec));
|
|
740
804
|
// A child that emits NO verdict never judged the work (budget/context death mid-
|
|
741
805
|
// investigation — seen live: an 11-minute verify wandered, died verdict-less, and
|
|
742
806
|
// the resulting FAIL burned a full implementation re-run on an unjudged artifact).
|
|
@@ -744,11 +808,7 @@ export async function runWorkVerification(deps) {
|
|
|
744
808
|
for (let attempt = 1;; attempt++) {
|
|
745
809
|
let text;
|
|
746
810
|
try {
|
|
747
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes,
|
|
748
|
-
foreignPaths,
|
|
749
|
-
scriptEscapes,
|
|
750
|
-
runnerGlobs
|
|
751
|
-
}), deps.signal);
|
|
811
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, { envNotes, contracts }), deps.signal);
|
|
752
812
|
}
|
|
753
813
|
catch (err) {
|
|
754
814
|
if (err instanceof Error && err.message === USER_CANCELLED)
|