@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.
@@ -69,6 +69,8 @@
69
69
  */
70
70
  import { USER_CANCELLED } from './child-runner.js';
71
71
  import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from './env-notes.js';
72
+ import { buildContractsVerifyBlock } from './contracts.js';
73
+ import { findSkipEscapes, skipEscapeVerifyFindings } from './skip-escape.js';
72
74
  /**
73
75
  * The verification child gets exactly two tools: `read` and `bash`.
74
76
  *
@@ -137,7 +139,7 @@ export function extractSpecForVerification(taskBody) {
137
139
  * Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
138
140
  * paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
139
141
  */
140
- export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings) {
142
+ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings, skipEscapeFindings, contracts, testAssemblyFindings, probeGamingFindings) {
141
143
  const probeBlock = probeFindings && probeFindings.length > 0 ?
142
144
  [
143
145
  'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
@@ -164,7 +166,58 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
164
166
  ''
165
167
  ]
166
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
+ : [];
187
+ const skipEscapeBlock = skipEscapeFindings && skipEscapeFindings.length > 0 ?
188
+ [
189
+ "SKIP-ESCAPE NOTICE (deterministic, computed by the orchestrator from the spec's",
190
+ 'OWN VERIFY block): these VERIFY commands wrap a required check in a fallback that',
191
+ 'ANNOUNCES skipping it when a tool is absent — so the check can "pass" while never',
192
+ 'actually running:',
193
+ ...skipEscapeFindings.map(f => `- ${f}`),
194
+ 'Do NOT accept a skipped check as a passed check. For each, determine whether it',
195
+ 'ACTUALLY ran and observed the real behavior. If its tool is absent so the required',
196
+ 'behavior was never observed, that area is UNOBSERVED (rule 5c) — verdict UNOBSERVED,',
197
+ 'not PASS. Only if you observe the required behavior another way (running the real',
198
+ 'artifact directly) may it count as verified.',
199
+ ''
200
+ ]
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
+ : [];
167
219
  const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
220
+ const contractsBlock = contracts && contracts.trim().length > 0 ? [buildContractsVerifyBlock(contracts)] : [];
168
221
  return [
169
222
  'You are a strict verification pass running right after an AI coding agent',
170
223
  'finished a task and committed it. The agent is known to mark work "done"',
@@ -179,8 +232,12 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
179
232
  spec.trim(),
180
233
  '',
181
234
  ...envBlock,
235
+ ...contractsBlock,
182
236
  ...probeBlock,
183
237
  ...prohibitionBlock,
238
+ ...probeGamingBlock,
239
+ ...skipEscapeBlock,
240
+ ...testAssemblyBlock,
184
241
  'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
185
242
  'checkout (or CI run) would experience it:',
186
243
  '',
@@ -242,6 +299,38 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
242
299
  ' server never starts, no entrypoint exists — that inability IS the defect:',
243
300
  ' report FAIL naming it. Do not stand up a substitute to get to green.',
244
301
  '',
302
+ '3e. NEGATIVE CONTROL IS MANDATORY — a success you cannot make fail is not evidence,',
303
+ ' and a check that only ever ran on the RIGHT input has not been shown to',
304
+ ' discriminate working from broken. For EACH behavioral check whose passing outcome',
305
+ ' you rely on (an endpoint returned success, a command exited 0, a call returned the',
306
+ ' expected value) you MUST also run that SAME check with one input deliberately',
307
+ ' wrong — a nonsense path, a bogus flag or subcommand, a malformed or absent',
308
+ ' argument, a target that does not exist — recreating whatever setup the check needs',
309
+ ' to do so (restart the server if a prior step killed it, re-invoke the command).',
310
+ ' Require a DIFFERENT, failing outcome from the wrong input. If the wrong input',
311
+ ' yields the SAME success (same status code, same body, same exit 0), the check',
312
+ ' cannot tell working from broken: it is VOID and the behavior it was meant to prove',
313
+ ' is UNVERIFIED — a FAIL naming the indiscriminate check (e.g. "a POST to a nonsense',
314
+ ' path returns the same 200 and body as the real endpoint, so the route is unproven',
315
+ ' — a catch-all fallback is masking it"). Skipping the control is not neutral: a',
316
+ ' required behavior left without a discriminating check is UNVERIFIED, and',
317
+ ' unverified is a FAIL, never a PASS. A wrong-input control exists for every',
318
+ ' artifact — HTTP request, CLI invocation, library call, schema load, config parse.',
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
+ '',
245
334
  '4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
246
335
  ' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
247
336
  '',
@@ -258,6 +347,18 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
258
347
  ' for…") that covers the change — judged against that stated exception, not against',
259
348
  ' your view of harmlessness.',
260
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
+ '',
261
362
  '5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
262
363
  ' service or network resource (a database server, an API host) that the project',
263
364
  ' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
@@ -282,8 +383,36 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
282
383
  ' that is a FAIL naming the gap. Any schema surgery you performed to reach green IS the',
283
384
  ' defect.',
284
385
  '',
285
- '6. If the spec legitimately has no runnable verification (a pure docs / config change',
286
- ' with nothing to build or run), validating it cleanly is a PASS.',
386
+ '5c. A SPEC-REQUIRED CHECK THAT DID NOT ACTUALLY RUN IS NOT VERIFIED. The env-gap',
387
+ ' exception (rule 5) covers ONLY a genuinely EXTERNAL service the finished product',
388
+ " connects to (a database, an API host). It does NOT cover a check the SPEC's own",
389
+ " VERIFY block authored to OBSERVE the deliverable's required behavior. If such a",
390
+ ' check did not actually execute — the tool or runner it needs is not installed, or',
391
+ ' the VERIFY line short-circuits ITSELF with a skip-escape (`|| true`, `|| echo',
392
+ ' skipping`, `2>/dev/null || exit 0`, `command -v X ||` …) so a missing tool passes',
393
+ ' silently — then that behavior was NEVER OBSERVED. A skipped check is not a passed',
394
+ ' check: you may not count the area as verified, and "correctly skipped" is NOT a',
395
+ " PASS. Do not let the work's own skip-escape waive the work's own gate. When a",
396
+ ' REQUIRED behavior could not be observed because its observation tooling is absent,',
397
+ ' the verdict is UNOBSERVED — report exactly what could not be observed and which',
398
+ ' tool was missing, so a human decides whether to provision the tool or accept the',
399
+ ' unproven behavior.',
400
+ ' DISCRIMINATOR (rule 5 vs rule 5c) — when something needed is absent, ask which it',
401
+ ' is: (rule 5, env-gap, do NOT fail the code) a SERVICE the FINISHED PRODUCT itself',
402
+ ' connects to at runtime to FUNCTION — a database, an API host, a message broker —',
403
+ ' whose absence is an environment gap; versus (rule 5c, UNOBSERVED) a HARNESS that',
404
+ ' exists only to OBSERVE or DRIVE the product during a check — a browser driver, a',
405
+ " UI/terminal smoke, a snapshot or fuzz tool — whose absence leaves the product's own",
406
+ ' behavior unproven. The product NEEDS the former to work at all; it needs the latter',
407
+ ' only to be CHECKED. A command that fails because such a runtime SERVICE is absent',
408
+ ' stays a rule-5 env-gap and is NOT UNOBSERVED; a required behavior you could not',
409
+ ' OBSERVE because its test harness is absent IS UNOBSERVED.',
410
+ '',
411
+ '6. If the spec legitimately has no runnable verification at all (a pure docs / config',
412
+ ' change with nothing to build or run), validating it cleanly is a PASS. This is NOT',
413
+ ' the same as a required behavioral check that self-skipped or whose tool is absent',
414
+ ' (that is UNOBSERVED, rule 5c) — "nothing to verify" means the spec never demanded',
415
+ ' an observation, not that an observation was demanded and then dodged.',
287
416
  '',
288
417
  '7. Do NOT edit anything to make a check pass. Report what you actually saw.',
289
418
  '',
@@ -291,32 +420,53 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
291
420
  'acceptance criterion is unmet — a required function absent, required data not persisted,',
292
421
  'a required behavior missing — the verdict is FAIL, even if typecheck and lint are green',
293
422
  'and even if the gap seems minor. Never downgrade an unmet criterion to a warning note.',
423
+ 'Before concluding PASS, confirm every behavioral check you relied on has its paired',
424
+ 'negative control (rule 3e) showing it CAN fail; a required behavior with no',
425
+ 'discriminating check is UNVERIFIED, and the verdict is FAIL.',
294
426
  '',
295
427
  ENV_NOTE_EMIT_INSTRUCTION,
296
428
  '',
297
429
  'When you are done, output EXACTLY ONE of these as the final line:',
298
430
  " WORK-VERIFIED: PASS (the project's own command, run unaided, met the spec)",
299
431
  ' WORK-VERIFIED: FAIL <text> (the shipped command failed or did not meet the spec; say what failed)',
432
+ ' WORK-VERIFIED: UNOBSERVED <text> (a spec-required behavioral check could not run because',
433
+ ' its observation tooling is absent — the behavior is',
434
+ ' unproven, not passed and not a code failure; rule 5c)',
300
435
  'Output the verdict line verbatim — it is parsed mechanically.'
301
436
  ].join('\n');
302
437
  }
303
438
  /**
304
- * Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL` marker
305
- * (the model discusses before concluding, and bash output may echo the word
439
+ * Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL|UNOBSERVED`
440
+ * marker (the model discusses before concluding, and bash output may echo the word
306
441
  * "VERIFY", so a distinct token and last-match win matter).
307
442
  *
443
+ * UNOBSERVED (rule 5c) is a distinct third outcome: a spec-required behavioral check
444
+ * could not run because its observation tooling is absent, so the behavior is neither
445
+ * proven nor shown broken. It is NOT a pass (`pass: false`) but carries `unobserved`
446
+ * so the gate can route it straight to the human — an unattended AUTOFIX re-run cannot
447
+ * provision a missing tool, so it must never auto-loop on this.
448
+ *
308
449
  * No marker at all is NOT a pass: a verification that cannot state a verdict is a
309
450
  * gray area, and the contract is that unverified work is reported as such.
310
451
  */
311
452
  export function parseVerifyVerdict(text) {
312
- const re = /WORK-VERIFIED:\s*(PASS|FAIL)\b[ \t]*(.*)/gi;
453
+ const re = /WORK-VERIFIED:\s*(PASS|FAIL|UNOBSERVED)\b[ \t]*(.*)/gi;
313
454
  let last = null;
314
455
  for (let m = re.exec(text); m !== null; m = re.exec(text))
315
456
  last = m;
316
457
  if (!last)
317
458
  return { pass: false, detail: 'no verdict emitted' };
318
- const pass = last[1].toUpperCase() === 'PASS';
319
- return { pass, detail: pass ? '' : last[2].trim() || 'unspecified failure' };
459
+ const kind = last[1].toUpperCase();
460
+ if (kind === 'PASS')
461
+ return { pass: true, detail: '' };
462
+ if (kind === 'UNOBSERVED') {
463
+ return {
464
+ pass: false,
465
+ unobserved: true,
466
+ detail: last[2].trim() || 'a required behavior could not be observed'
467
+ };
468
+ }
469
+ return { pass: false, detail: last[2].trim() || 'unspecified failure' };
320
470
  }
321
471
  /**
322
472
  * Run the verification pass for one task. A missing spec is a pass. Otherwise run
@@ -359,6 +509,28 @@ export async function runWorkVerification(deps) {
359
509
  prohibitions = [];
360
510
  }
361
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
+ }
362
534
  // Environment facts from earlier gate children (best-effort; a cache failure
363
535
  // must never block verification).
364
536
  let envNotes = '';
@@ -370,6 +542,23 @@ export async function runWorkVerification(deps) {
370
542
  envNotes = '';
371
543
  }
372
544
  }
545
+ // Cross-slice contracts from decompose-time extraction (best-effort; a read
546
+ // fault must never block verification).
547
+ let contracts = '';
548
+ if (deps.contracts) {
549
+ try {
550
+ contracts = await deps.contracts();
551
+ }
552
+ catch {
553
+ contracts = '';
554
+ }
555
+ }
556
+ // DETERMINISTIC skip-escape finding, computed purely from the spec's own VERIFY
557
+ // block (see skip-escape.ts): a required check wrapped in a skip-announcing `||`
558
+ // fallback. Injected so rule 5c fires reliably — the model does not self-discover a
559
+ // graceful skip-escape (A/B: rule alone ~1-3/5), but acts on a finding naming the
560
+ // exact line, per the proven probe+rule pattern. Pure text analysis, no dep needed.
561
+ const skipEscapes = skipEscapeVerifyFindings(findSkipEscapes(deps.spec));
373
562
  // A child that emits NO verdict never judged the work (budget/context death mid-
374
563
  // investigation — seen live: an 11-minute verify wandered, died verdict-less, and
375
564
  // the resulting FAIL burned a full implementation re-run on an unjudged artifact).
@@ -377,7 +566,7 @@ export async function runWorkVerification(deps) {
377
566
  for (let attempt = 1;; attempt++) {
378
567
  let text;
379
568
  try {
380
- text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions), deps.signal);
569
+ text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions, skipEscapes, contracts, testAssembly, probeGaming), deps.signal);
381
570
  }
382
571
  catch (err) {
383
572
  if (err instanceof Error && err.message === USER_CANCELLED)
@@ -414,6 +603,12 @@ export async function runWorkVerification(deps) {
414
603
  return { ok: true };
415
604
  if (verdict.detail === 'no verdict emitted' && attempt === 1)
416
605
  continue;
606
+ // UNOBSERVED (rule 5c): a spec-required behavioral check could not run because
607
+ // its tooling is absent. Block like any FAIL, but flag it so the gate hands it
608
+ // straight to the human — re-running the impl turn cannot install a missing tool.
609
+ if (verdict.unobserved) {
610
+ return { ok: false, unobserved: true, reason: `work unobserved: ${verdict.detail}` };
611
+ }
417
612
  return {
418
613
  ok: false,
419
614
  reason: `work did not verify: ${verdict.detail}${verdict.detail === 'no verdict emitted' ? ' (after verify retry)' : ''}`
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Deterministic synthesized-wiring scanner for a composed spec (run-8 F3, gen side).
3
+ *
4
+ * F3 (the dominant run-8 shipped defect): refine/compose invent a "uniform" wiring
5
+ * table — one module → one mount prefix, `/api/<x>` → `<x>Routes` for every module —
6
+ * though the design pins ENDPOINTS, not mounts, and one module's pinned endpoints do
7
+ * NOT all sit under a single prefix (photos: `POST /api/listings/:id/photos` AND
8
+ * `GET/DELETE /api/photos/:id`). Mounting that module at `/api/photos` double-prefixes
9
+ * the upload; consumers follow the pinned paths, assembly follows the invented table,
10
+ * the seam ships broken. See [[contract-registry-f3]] (#4, the verify-side + registry
11
+ * lever) — this is its GENERATION-side complement.
12
+ *
13
+ * A/B-measured on the live 27B (F3 critique trap): the CROSS-SLICE CONTRACTS registry
14
+ * is NECESSARY but the prompt+registry alone is a WEAK catcher (A/B arms 0/8, +registry
15
+ * only 1/8) — the model's attention goes to the obvious VERIFY weakness and it rarely
16
+ * does the path-composition reasoning even with the facts in front of it. The reliable
17
+ * lever is the SAME probe+rule pattern as [[skip-escape-scanner-f2]] / [[verify-
18
+ * substitution-ab]]: a deterministic finding that NAMES the exact synthesized mappings
19
+ * and juxtaposes the verbatim pinned facts, forcing focused reconciliation.
20
+ *
21
+ * This scanner does NOT decide which mapping is wrong — that needs routing-composition
22
+ * knowledge (a forbidden stack assumption). It surfaces every mapping that (a) is not a
23
+ * verbatim substring of the design/registry (so it is INFERRED, not cited) AND (b) touches
24
+ * a pinned cross-slice boundary (an operand appears in the registry) — i.e. it reshapes a
25
+ * shared contract. The 4 coincidentally-correct mappings are surfaced too, but framed as
26
+ * "reconcile each; keep the conforming ones" — the LLM decides, informed. Pure text/
27
+ * substring analysis; no stack, framework, or routing assumptions. Empty registry (single
28
+ * `/task`, or no shared boundary) ⇒ no-op.
29
+ */
30
+ export interface WiringClaim {
31
+ /** The offending mapping line, verbatim (trimmed). */
32
+ line: string;
33
+ /** Left operand (the mapped-from token, e.g. a mount prefix). */
34
+ from: string;
35
+ /** Right operand (the mapped-to token, e.g. a module name). */
36
+ to: string;
37
+ }
38
+ /**
39
+ * Find synthesized wiring mappings in `spec`: `A <arrow> B` lines whose whole mapping
40
+ * is NOT a verbatim substring of `grounding` (design ∪ registry) yet an operand appears
41
+ * in `registry` (so it reshapes a pinned cross-slice boundary). Returns [] when the
42
+ * registry is empty (no shared contracts to reshape) — the whole check is a no-op then.
43
+ */
44
+ export declare function findSynthesizedWiring(spec: string, grounding: string, registry: string): WiringClaim[];
45
+ /**
46
+ * Render findings as the critique probe (probe+rule pattern): NAME the inferred
47
+ * mappings and juxtapose the verbatim pinned facts, then instruct focused
48
+ * reconciliation. Deliberately does NOT accuse a specific mapping — the model decides
49
+ * which (if any) fails to reproduce a pinned fact. Empty findings ⇒ '' (no block).
50
+ */
51
+ export declare function wiringProbeText(findings: WiringClaim[], registry: string): string;
52
+ /**
53
+ * Render findings as a critique-rewrite defect block (fed into the FOCUS list): the
54
+ * rewrite must reconcile each mapping against the pinned facts and correct only the
55
+ * one(s) that break. Mirrors skipEscapeDefectText's shape.
56
+ */
57
+ export declare function wiringDefectText(findings: WiringClaim[], registry: string): string;
58
+ /**
59
+ * Concatenate the design/spec docs the given texts @-reference (best-effort, readable
60
+ * files only) as extra grounding for findSynthesizedWiring — so a mapping the design
61
+ * states verbatim is treated as CITED, not synthesized. Unreadable/absent mentions are
62
+ * skipped; returns '' when nothing resolves.
63
+ */
64
+ export declare function readReferencedDocs(cwd: string, ...texts: string[]): string;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Deterministic synthesized-wiring scanner for a composed spec (run-8 F3, gen side).
3
+ *
4
+ * F3 (the dominant run-8 shipped defect): refine/compose invent a "uniform" wiring
5
+ * table — one module → one mount prefix, `/api/<x>` → `<x>Routes` for every module —
6
+ * though the design pins ENDPOINTS, not mounts, and one module's pinned endpoints do
7
+ * NOT all sit under a single prefix (photos: `POST /api/listings/:id/photos` AND
8
+ * `GET/DELETE /api/photos/:id`). Mounting that module at `/api/photos` double-prefixes
9
+ * the upload; consumers follow the pinned paths, assembly follows the invented table,
10
+ * the seam ships broken. See [[contract-registry-f3]] (#4, the verify-side + registry
11
+ * lever) — this is its GENERATION-side complement.
12
+ *
13
+ * A/B-measured on the live 27B (F3 critique trap): the CROSS-SLICE CONTRACTS registry
14
+ * is NECESSARY but the prompt+registry alone is a WEAK catcher (A/B arms 0/8, +registry
15
+ * only 1/8) — the model's attention goes to the obvious VERIFY weakness and it rarely
16
+ * does the path-composition reasoning even with the facts in front of it. The reliable
17
+ * lever is the SAME probe+rule pattern as [[skip-escape-scanner-f2]] / [[verify-
18
+ * substitution-ab]]: a deterministic finding that NAMES the exact synthesized mappings
19
+ * and juxtaposes the verbatim pinned facts, forcing focused reconciliation.
20
+ *
21
+ * This scanner does NOT decide which mapping is wrong — that needs routing-composition
22
+ * knowledge (a forbidden stack assumption). It surfaces every mapping that (a) is not a
23
+ * verbatim substring of the design/registry (so it is INFERRED, not cited) AND (b) touches
24
+ * a pinned cross-slice boundary (an operand appears in the registry) — i.e. it reshapes a
25
+ * shared contract. The 4 coincidentally-correct mappings are surfaced too, but framed as
26
+ * "reconcile each; keep the conforming ones" — the LLM decides, informed. Pure text/
27
+ * substring analysis; no stack, framework, or routing assumptions. Empty registry (single
28
+ * `/task`, or no shared boundary) ⇒ no-op.
29
+ */
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ /** Mapping arrows a wiring/mount table uses across notations. A colon is deliberately
33
+ * NOT an arrow — it matches endpoint params (`/photos/:id`), section headers, and prose,
34
+ * which would drown the signal. */
35
+ const ARROW = '(?:→|->|=>|⇒|↦)';
36
+ /** A `left <arrow> right` mapping on one line, tolerating a leading list bullet. */
37
+ const MAPPING_LINE_RE = new RegExp(`^[ \\t]*[-*]?[ \\t]*(.+?)[ \\t]*${ARROW}[ \\t]*(.+?)[ \\t]*$`);
38
+ /** An operand shorter than this is too generic to anchor a boundary match. */
39
+ const MIN_OPERAND_LENGTH = 4;
40
+ /** Collapse whitespace + lowercase; drop markdown backticks so quoting formatting
41
+ * differences don't defeat the substring match. Mirrors contracts.ts's normalise. */
42
+ function normalise(s) {
43
+ return s.replace(/`/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
44
+ }
45
+ /**
46
+ * Find synthesized wiring mappings in `spec`: `A <arrow> B` lines whose whole mapping
47
+ * is NOT a verbatim substring of `grounding` (design ∪ registry) yet an operand appears
48
+ * in `registry` (so it reshapes a pinned cross-slice boundary). Returns [] when the
49
+ * registry is empty (no shared contracts to reshape) — the whole check is a no-op then.
50
+ */
51
+ export function findSynthesizedWiring(spec, grounding, registry) {
52
+ if (registry.trim().length === 0)
53
+ return [];
54
+ const groundHay = normalise(grounding + '\n' + registry);
55
+ const regHay = normalise(registry);
56
+ const found = [];
57
+ const seen = new Set();
58
+ for (const rawLine of spec.split('\n')) {
59
+ const m = MAPPING_LINE_RE.exec(rawLine);
60
+ if (!m)
61
+ continue;
62
+ const from = m[1].trim();
63
+ const to = m[2].trim();
64
+ const line = rawLine.replace(/^[ \t]*[-*][ \t]*/, '').trim();
65
+ const key = normalise(line);
66
+ if (key.length === 0 || seen.has(key))
67
+ continue;
68
+ // Cited, not synthesized: the whole mapping appears verbatim in the source.
69
+ if (groundHay.includes(key))
70
+ continue;
71
+ // Only a mapping that touches a PINNED shared boundary is an F3 suspect — this
72
+ // is the crisp discriminator that keeps internal-flow prose ("input → output")
73
+ // out. An operand (long enough to be specific) must appear in the registry.
74
+ const touchesBoundary = [from, to].some(op => {
75
+ const n = normalise(op);
76
+ return n.length >= MIN_OPERAND_LENGTH && regHay.includes(n);
77
+ });
78
+ if (!touchesBoundary)
79
+ continue;
80
+ seen.add(key);
81
+ found.push({ line, from: from.replace(/`/g, ''), to: to.replace(/`/g, '') });
82
+ }
83
+ return found;
84
+ }
85
+ /**
86
+ * Render findings as the critique probe (probe+rule pattern): NAME the inferred
87
+ * mappings and juxtapose the verbatim pinned facts, then instruct focused
88
+ * reconciliation. Deliberately does NOT accuse a specific mapping — the model decides
89
+ * which (if any) fails to reproduce a pinned fact. Empty findings ⇒ '' (no block).
90
+ */
91
+ export function wiringProbeText(findings, registry) {
92
+ if (findings.length === 0)
93
+ return '';
94
+ return [
95
+ 'SYNTHESIZED WIRING (deterministic finding) — the spec states these connect-the-',
96
+ 'modules mappings that are NOT quoted verbatim from the design (they are INFERRED,',
97
+ 'not cited) and that touch a pinned cross-slice boundary:',
98
+ ...findings.map((f, i) => ` ${i + 1}. ${f.line}`),
99
+ 'The design pins these interface FACTS instead (verbatim, authoritative):',
100
+ ...registry
101
+ .trim()
102
+ .split('\n')
103
+ .filter(l => l.trim().length > 0)
104
+ .map(l => ` - ${l.trim()}`),
105
+ 'For EACH mapping above, confirm it REPRODUCES the pinned facts EXACTLY. A module',
106
+ 'whose pinned facts do NOT all sit under the single prefix it is mapped to CANNOT be',
107
+ 'wired that way without breaking a path — that is a SEAM BUG: name the mapping and the',
108
+ 'pinned fact it fails to produce. KEEP every mapping that does reproduce its facts; do',
109
+ 'NOT alter a conforming one. If a boundary detail is genuinely unpinned, leave it',
110
+ 'unspecified rather than inventing a mapping.'
111
+ ].join('\n');
112
+ }
113
+ /**
114
+ * Render findings as a critique-rewrite defect block (fed into the FOCUS list): the
115
+ * rewrite must reconcile each mapping against the pinned facts and correct only the
116
+ * one(s) that break. Mirrors skipEscapeDefectText's shape.
117
+ */
118
+ export function wiringDefectText(findings, registry) {
119
+ return wiringProbeText(findings, registry);
120
+ }
121
+ // An @-file mention in a spec ("@DESIGN/foo.md"), minus trailing prose punctuation.
122
+ // Mirrors phantom-imports' mention rules so grounding sees the same source docs.
123
+ const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
124
+ const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
125
+ /**
126
+ * Concatenate the design/spec docs the given texts @-reference (best-effort, readable
127
+ * files only) as extra grounding for findSynthesizedWiring — so a mapping the design
128
+ * states verbatim is treated as CITED, not synthesized. Unreadable/absent mentions are
129
+ * skipped; returns '' when nothing resolves.
130
+ */
131
+ export function readReferencedDocs(cwd, ...texts) {
132
+ const seen = new Set();
133
+ const parts = [];
134
+ for (const text of texts) {
135
+ for (const m of text.matchAll(MENTION_RE)) {
136
+ const rel = m[1].replace(MENTION_TRAILING_PUNCT, '');
137
+ if (rel === '' || seen.has(rel))
138
+ continue;
139
+ seen.add(rel);
140
+ try {
141
+ parts.push(fs.readFileSync(path.resolve(cwd, rel), 'utf8'));
142
+ }
143
+ catch {
144
+ // not a readable file — skip
145
+ }
146
+ }
147
+ }
148
+ return parts.join('\n');
149
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.2",
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": {