@mjasnikovs/pi-task 0.40.50 → 0.41.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.
Files changed (123) hide show
  1. package/README.md +7 -4
  2. package/dist/config/config.d.ts +61 -18
  3. package/dist/config/config.js +50 -6
  4. package/dist/config/register.js +18 -5
  5. package/dist/shared/content-tokens.d.ts +16 -0
  6. package/dist/shared/content-tokens.js +137 -0
  7. package/dist/task/accept-debt.d.ts +15 -1
  8. package/dist/task/accept-debt.js +4 -0
  9. package/dist/task/artifact-closure.js +7 -1
  10. package/dist/task/auto-io.d.ts +59 -7
  11. package/dist/task/auto-io.js +140 -47
  12. package/dist/task/auto-orchestrator.d.ts +22 -19
  13. package/dist/task/auto-orchestrator.js +126 -112
  14. package/dist/task/child-runner.d.ts +18 -0
  15. package/dist/task/child-runner.js +7 -8
  16. package/dist/task/command-run.d.ts +4 -0
  17. package/dist/task/command-run.js +4 -2
  18. package/dist/task/constraint-policy.d.ts +52 -0
  19. package/dist/task/constraint-policy.js +66 -0
  20. package/dist/task/context-silence.d.ts +5 -4
  21. package/dist/task/context-silence.js +13 -5
  22. package/dist/task/coverage-loop.d.ts +7 -4
  23. package/dist/task/coverage-loop.js +16 -138
  24. package/dist/task/debug-log.d.ts +25 -2
  25. package/dist/task/debug-log.js +64 -10
  26. package/dist/task/decompose-fidelity.d.ts +17 -4
  27. package/dist/task/decompose-fidelity.js +95 -55
  28. package/dist/task/decompose-granularity.d.ts +7 -7
  29. package/dist/task/decompose-granularity.js +8 -8
  30. package/dist/task/env-notes.d.ts +65 -19
  31. package/dist/task/env-notes.js +152 -50
  32. package/dist/task/external-context.js +25 -5
  33. package/dist/task/fix-context.d.ts +39 -0
  34. package/dist/task/fix-context.js +58 -0
  35. package/dist/task/gate-deps.d.ts +49 -0
  36. package/dist/task/gate-deps.js +199 -38
  37. package/dist/task/gate-evidence.d.ts +51 -0
  38. package/dist/task/gate-evidence.js +90 -0
  39. package/dist/task/gate-resolution.d.ts +119 -0
  40. package/dist/task/gate-resolution.js +120 -0
  41. package/dist/task/git-state-guard.js +3 -26
  42. package/dist/task/handoff.d.ts +35 -0
  43. package/dist/task/handoff.js +100 -0
  44. package/dist/task/health-baseline.d.ts +87 -0
  45. package/dist/task/health-baseline.js +158 -0
  46. package/dist/task/ledger.d.ts +10 -0
  47. package/dist/task/ledger.js +22 -11
  48. package/dist/task/lint-fix.d.ts +11 -0
  49. package/dist/task/lint-fix.js +14 -8
  50. package/dist/task/loop-detector.d.ts +52 -3
  51. package/dist/task/loop-detector.js +81 -5
  52. package/dist/task/mentions.d.ts +17 -0
  53. package/dist/task/mentions.js +86 -0
  54. package/dist/task/orchestrator.d.ts +53 -9
  55. package/dist/task/orchestrator.js +104 -28
  56. package/dist/task/orientation.d.ts +135 -29
  57. package/dist/task/orientation.js +259 -67
  58. package/dist/task/owned-freeze-reassign.d.ts +7 -3
  59. package/dist/task/owned-freeze-reassign.js +13 -5
  60. package/dist/task/parsers.d.ts +6 -1
  61. package/dist/task/parsers.js +21 -4
  62. package/dist/task/phases.d.ts +1 -1
  63. package/dist/task/phases.js +125 -153
  64. package/dist/task/plan-orchestrator.js +5 -6
  65. package/dist/task/plan-rounds.d.ts +1 -0
  66. package/dist/task/plan-rounds.js +8 -1
  67. package/dist/task/prohibition-probe.d.ts +6 -1
  68. package/dist/task/prohibition-probe.js +26 -6
  69. package/dist/task/prompts.js +13 -2
  70. package/dist/task/qa-transcript.d.ts +17 -0
  71. package/dist/task/qa-transcript.js +28 -0
  72. package/dist/task/question-source.d.ts +16 -2
  73. package/dist/task/question-source.js +21 -8
  74. package/dist/task/repo-health-check.d.ts +27 -0
  75. package/dist/task/repo-health-check.js +39 -5
  76. package/dist/task/requirements.d.ts +67 -13
  77. package/dist/task/requirements.js +129 -67
  78. package/dist/task/research-worker.d.ts +43 -2
  79. package/dist/task/research-worker.js +103 -8
  80. package/dist/task/run-bracket.d.ts +9 -4
  81. package/dist/task/run-bracket.js +9 -0
  82. package/dist/task/run-context.d.ts +152 -0
  83. package/dist/task/run-context.js +277 -0
  84. package/dist/task/spec-doc.d.ts +78 -0
  85. package/dist/task/spec-doc.js +205 -0
  86. package/dist/task/spec-model.d.ts +70 -0
  87. package/dist/task/spec-model.js +139 -0
  88. package/dist/task/stall-detector.d.ts +1 -1
  89. package/dist/task/stall-detector.js +9 -5
  90. package/dist/task/state-dir.d.ts +29 -0
  91. package/dist/task/state-dir.js +80 -0
  92. package/dist/task/suppression-probe.d.ts +67 -0
  93. package/dist/task/suppression-probe.js +78 -0
  94. package/dist/task/task-gates.d.ts +39 -57
  95. package/dist/task/task-gates.js +115 -195
  96. package/dist/task/task-io.d.ts +10 -0
  97. package/dist/task/task-io.js +69 -18
  98. package/dist/task/task-parsers.js +6 -6
  99. package/dist/task/task-types.d.ts +7 -0
  100. package/dist/task/timings.d.ts +11 -0
  101. package/dist/task/timings.js +21 -0
  102. package/dist/task/tree-hash.d.ts +8 -0
  103. package/dist/task/tree-hash.js +55 -0
  104. package/dist/task/verify-reconcile.d.ts +4 -3
  105. package/dist/task/verify-reconcile.js +7 -8
  106. package/dist/task/verify-resolution.d.ts +28 -5
  107. package/dist/task/verify-resolution.js +70 -13
  108. package/dist/task/verify-work.d.ts +98 -35
  109. package/dist/task/verify-work.js +227 -60
  110. package/dist/task/yolo.d.ts +0 -13
  111. package/dist/task/yolo.js +9 -21
  112. package/dist/workers/pi-worker-core.d.ts +20 -0
  113. package/dist/workers/pi-worker-core.js +16 -11
  114. package/dist/workers/pi-worker-docs.d.ts +6 -5
  115. package/dist/workers/pi-worker-docs.js +8 -7
  116. package/dist/workers/pi-worker-fetch.d.ts +3 -2
  117. package/dist/workers/pi-worker-fetch.js +5 -4
  118. package/dist/workers/pi-worker-search.js +5 -4
  119. package/dist/workers/research-cache.d.ts +23 -1
  120. package/dist/workers/research-cache.js +63 -3
  121. package/dist/workers/shared.d.ts +18 -0
  122. package/dist/workers/shared.js +38 -22
  123. package/package.json +1 -1
@@ -42,6 +42,11 @@ import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from '
42
42
  import { buildContractsVerifyBlock } from './contracts.js';
43
43
  import { findSkipEscapes, skipEscapeVerifyFindings } from './skip-escape.js';
44
44
  import { crossTaskDeletionVerifyFindings } from './task-provenance.js';
45
+ import { classifyHealthDelta, inheritedHealthFindings } from './health-baseline.js';
46
+ import { parseSpec, sliceSpecSection } from './spec-model.js';
47
+ import { qaKindsFromRecord } from './qa-transcript.js';
48
+ import { annotateConstraints, anyBinding, renderConstraintPolicy } from './constraint-policy.js';
49
+ import { suppressionVerifyFindings } from './suppression-probe.js';
45
50
  /**
46
51
  * The verification child gets exactly two tools: `read` and `bash`.
47
52
  *
@@ -99,33 +104,12 @@ export function isStaticClass(cls) {
99
104
  return cls === 'repo-health' || cls === 'static-checks';
100
105
  }
101
106
  /**
102
- * Slice the delivered spec (GOAL / CONSTRAINTS / ACCEPTANCE / VERIFY) out of a
103
- * task file body. The compose and critique phases write the section named `spec`,
104
- * so the composed spec lives under `## spec` and runs to the next line matching
105
- * `## ` plus non-space (`## phase timings`); a `### ` subheading stays inside.
106
- * Returns null when there is no spec section or it is blank (a task that never
107
- * reached compose), which the caller treats as a pass — nothing to verify against.
107
+ * The delivered spec's TEXT, for the children that must read its prose verbatim.
108
+ * The slicing itself lives in spec-model.ts beside the parser, so "the spec
109
+ * section" is one definition rather than one per consumer.
108
110
  */
109
111
  export function extractSpecForVerification(taskBody) {
110
- const lines = taskBody.split('\n');
111
- let start = -1;
112
- for (let i = 0; i < lines.length; i++) {
113
- if (/^##\s+spec\s*$/i.test(lines[i])) {
114
- start = i + 1;
115
- break;
116
- }
117
- }
118
- if (start === -1)
119
- return null;
120
- let end = lines.length;
121
- for (let i = start; i < lines.length; i++) {
122
- if (/^##\s+\S/.test(lines[i])) {
123
- end = i;
124
- break;
125
- }
126
- }
127
- const spec = lines.slice(start, end).join('\n').trim();
128
- return spec.length > 0 ? spec : null;
112
+ return sliceSpecSection(taskBody);
129
113
  }
130
114
  /**
131
115
  * Build one row. Generic in the probe's return type so `empty`/`findings` stay
@@ -146,8 +130,8 @@ function probeAdapter(row) {
146
130
  block,
147
131
  ruleId,
148
132
  rule,
149
- run: async (deps, onStage) => {
150
- const probe = source(deps);
133
+ run: async (deps, onStage, pre) => {
134
+ const probe = source(deps, pre);
151
135
  // Probes are INDEPENDENTLY OPTIONAL: an absent probe is "skipped", and a
152
136
  // probe that throws degrades to its own empty value — it is a sharpener,
153
137
  // never a blocker, so a fault in one can neither block the gate nor
@@ -171,6 +155,68 @@ const asLines = (raw) => raw;
171
155
  /** The table. ROW ORDER IS THE NOTICE-BLOCK ORDER IN THE PROMPT: `buildVerifyPrompt`
172
156
  * flatMaps this array to build the notices, so reordering rows reorders the prompt. */
173
157
  const PROBE_ADAPTERS = [
158
+ /**
159
+ * The project's own check and build commands, ALREADY RUN by the parent against
160
+ * this exact tree (see gate-evidence.ts). The one row that supplies a RESULT
161
+ * rather than a warning, and the one row rule 1 switches on: a child handed the
162
+ * output spends its turn judging it instead of reproducing a suite the previous
163
+ * child already ran.
164
+ */
165
+ probeAdapter({
166
+ key: 'evidence',
167
+ stage: 'project checks',
168
+ empty: [],
169
+ findings: asLines,
170
+ ruleId: '1',
171
+ block: findings => [
172
+ 'EVIDENCE (deterministic, run by the orchestrator against THIS tree, just now):',
173
+ "the project's own check and build commands have ALREADY BEEN RUN for you. Each",
174
+ 'line is the command, how it ended, and the file holding its complete output:',
175
+ ...findings.map(f => `- ${f}`),
176
+ 'Read those files with your `read` tool. A command listed here does not need',
177
+ 'running again (rule 1) — re-run only a targeted subset: a single test file, or',
178
+ 'one command whose reported result you have a concrete reason to doubt. SKIPPED',
179
+ 'means the command could not run on this machine at all, so it is evidence of',
180
+ 'nothing in either direction — rules 5 and 5c decide what its absence means.',
181
+ ''
182
+ ]
183
+ }),
184
+ /**
185
+ * PRE-EXISTING repo health (see health-baseline.ts): static checks that were
186
+ * ALREADY failing, the same way, before this task started. They used to be an
187
+ * absolute FAIL that short-circuited the whole pass, so the task answered for
188
+ * a sibling's defect and its own probe findings were never computed. As a row
189
+ * they are stated to the child instead: judge this task's work, and do not
190
+ * read a check that was red on arrival as evidence about it either way.
191
+ */
192
+ probeAdapter({
193
+ key: 'repoHealth',
194
+ source: (_deps, pre) => () => Promise.resolve(pre.repoHealth),
195
+ empty: [],
196
+ findings: asLines,
197
+ ruleId: '4h',
198
+ block: findings => [
199
+ 'INHERITED REPO-HEALTH NOTICE (deterministic, computed by the orchestrator by',
200
+ "re-running the project's own static checks and comparing them against the",
201
+ 'baseline taken before this task started): these checks were ALREADY failing,',
202
+ 'with the same exit code, before any of this work existed:',
203
+ ...findings.map(f => `- ${f}`),
204
+ "They are NOT this task's defect and must not decide this verdict — rule 4h.",
205
+ 'Equally, a red check that was red before proves nothing about this work: do',
206
+ 'not read its output as evidence that the shipped behavior is correct.',
207
+ ''
208
+ ],
209
+ rule: [
210
+ "4h. AN INHERITED RED CHECK IS NOT THIS TASK'S FAIL, AND NOT ITS PROOF — when the",
211
+ ' INHERITED REPO-HEALTH NOTICE above names a check, that check failed identically',
212
+ ' before this task ran. Do NOT fail this work for it: the defect belongs to',
213
+ ' whatever put it there, it is recorded as durable debt, and the run-end gate',
214
+ ' re-checks it. Do NOT lean on it either — a command that was already exiting',
215
+ " non-zero tells you nothing about this task's behavior, so verify that behavior",
216
+ ' another way. A check that is failing DIFFERENTLY, or one absent from the notice,',
217
+ " is this task's to answer for in the ordinary way."
218
+ ]
219
+ }),
174
220
  /**
175
221
  * Deterministic self-verification probe (see substitution-probe.ts): test
176
222
  * files this task itself authored or changed, with their added-line counts.
@@ -212,27 +258,33 @@ const PROBE_ADAPTERS = [
212
258
  block: findings => [
213
259
  'PROHIBITION NOTICE (deterministic, computed by the orchestrator from the',
214
260
  "spec's own constraint lines and the task's diff): this task MODIFIED paths",
215
- 'the spec explicitly forbids modifying:',
261
+ 'the spec forbids modifying, each tagged with the weight of the constraint',
262
+ 'that forbids it:',
216
263
  ...findings.map(f => `- ${f}`),
217
- 'Read the exact constraint wording in the spec. Unless that wording itself',
218
- 'states an exception that covers this change, this is a violated prohibition:',
219
- 'rule 4b applies and the verdict is FAIL naming the forbidden path even if',
220
- 'every test passes and the change looks harmless.',
264
+ ...(anyBinding(findings) ?
265
+ [
266
+ 'Read the exact constraint wording in the spec. Unless that wording itself',
267
+ 'states an exception that covers this change, a BINDING one is a violated',
268
+ 'prohibition: rule 4b applies and the verdict is FAIL naming the forbidden',
269
+ 'path — even if every test passes and the change looks harmless.'
270
+ ]
271
+ : [
272
+ 'Every one of these is ADVISORY — automation, not a person, wrote the',
273
+ 'constraint that forbids it (rule 4b). Report each in your findings, and do',
274
+ 'NOT fail the work for it unless an ACCEPTANCE criterion also fails. The',
275
+ 'acceptance criteria are the bar here.'
276
+ ]),
221
277
  ''
222
278
  ],
223
279
  rule: [
224
- '4b. SPEC PROHIBITIONS ARE PART OF THE BAR — YOU HAVE NO WAIVER AUTHORITY: when the',
225
- ' spec explicitly forbids something ("Do NOT modify X", "MUST NOT touch Y") and the',
226
- ' shipped work does it anyway, that is a FAIL naming the violated constraint. You',
227
- ' may not excuse a violation because it is additive, small, harmless, an improvement,',
228
- ' or because every test still passes "it works anyway" is exactly the waiver you do',
229
- " not have; relaxing a constraint is the spec owner's call, not yours. Check the",
230
- " task's own diff (git) against the spec's prohibitions a forbidden file can be",
231
- ' modified without any test noticing. Only two outcomes are not a FAIL: the',
232
- ' violation was fully REVERTED (the shipped tree no longer violates), or the',
233
- ' prohibition\'s own wording states an exception ("except…", "beyond what is needed',
234
- ' for…") that covers the change — judged against that stated exception, not against',
235
- ' your view of harmlessness.'
280
+ ...renderConstraintPolicy('4b.'),
281
+ ' Applying that to a PROHIBITION ("Do NOT modify X", "MUST NOT touch Y"): check the',
282
+ " task's own diff against the spec's prohibitions a forbidden file can be",
283
+ ' modified without any test noticing. For a BINDING one, two outcomes are not a',
284
+ ' FAIL: the violation was fully REVERTED (the shipped tree no longer violates), or',
285
+ ' the prohibition\'s own wording states an exception ("except…", "beyond what is',
286
+ ' needed for…") that covers the changejudged against that stated exception, not',
287
+ ' against your view of harmlessness.'
236
288
  ]
237
289
  }),
238
290
  /**
@@ -485,6 +537,43 @@ const PROBE_ADAPTERS = [
485
537
  'fails where the test passes, report FAIL and name the wiring seam.',
486
538
  ''
487
539
  ]
540
+ }),
541
+ /**
542
+ * Deterministic suppression-widening probe (see suppression-probe.ts): checker
543
+ * suppressions this task NET added, per file. The child cannot find these by
544
+ * running the checks — a suppression's whole purpose is that the check goes
545
+ * quiet — so the count has to arrive as a finding (0034 shipped sixteen and
546
+ * every gate reported success).
547
+ */
548
+ probeAdapter({
549
+ key: 'suppressionWidening',
550
+ stage: 'suppression probe',
551
+ empty: [],
552
+ findings: suppressionVerifyFindings,
553
+ ruleId: '4i',
554
+ block: findings => [
555
+ 'SUPPRESSION WIDENING NOTICE (deterministic, computed by the orchestrator by',
556
+ "counting each suppression pattern's added minus removed lines in this task's",
557
+ 'diff): this task added checker suppressions:',
558
+ ...findings.map(f => `- ${f}`),
559
+ 'A suppression makes a checker SILENT, not satisfied. You cannot discover this',
560
+ "by running the project's checks — they are green because of these lines. For",
561
+ 'each, read the surrounding code and decide whether the underlying defect was',
562
+ 'FIXED or merely silenced, and whether the spec asked for the suppression at',
563
+ 'all (rule 4i).',
564
+ ''
565
+ ],
566
+ rule: [
567
+ '4i. A SILENCED CHECKER IS NOT A SATISFIED ONE — when the SUPPRESSION WIDENING',
568
+ ' NOTICE above names added suppressions (`@ts-expect-error`, `eslint-disable`,',
569
+ ' `# noqa`, `//nolint`, `#[allow(…)]`, `as unknown as` …), the green check they',
570
+ ' produce is not evidence about the code under them. Judge each one: a',
571
+ ' suppression the spec explicitly asked for, or one narrowly scoped to a',
572
+ ' documented third-party defect the task cannot fix, is fine and you say so. A',
573
+ ' suppression that exists so a checker would stop reporting a defect this task',
574
+ ' introduced is that defect, still shipped — verdict FAIL naming the file and the',
575
+ ' finding it hides. A blanket file-level disable is never the narrow case.'
576
+ ]
488
577
  })
489
578
  ];
490
579
  /**
@@ -493,6 +582,43 @@ const PROBE_ADAPTERS = [
493
582
  * checked against the table rather than against a hand-kept list.
494
583
  */
495
584
  export const BOUND_PROBE_KEYS = PROBE_ADAPTERS.filter(a => a.bound).map(a => a.key);
585
+ /**
586
+ * Rule 1 in its two states, picked by whether the evidence row supplied anything.
587
+ *
588
+ * A child told "run the project's own commands" directly under a block listing
589
+ * those commands' results runs them again — the instruction and the notice
590
+ * contradict each other, and the instruction is the numbered one. So the rule
591
+ * moves with the evidence rather than sitting beside it. Without evidence (no
592
+ * verified tooling, or a caller that binds no probes) the original obligation is
593
+ * the only correct one.
594
+ */
595
+ const RULE_1_RUN_THEM = [
596
+ "1. Run the project's OWN commands — the verbatim scripts / targets / binaries it",
597
+ ' ships (package.json scripts, Makefile targets, the command the spec names) —',
598
+ ' exactly as written, in the workspace as you found it. Judge the artifact or',
599
+ " output they actually PRODUCE. The project's own command and its real output are",
600
+ " the bar; also run the spec's VERIFY block, but it does not override that bar."
601
+ ];
602
+ const RULE_1_ALREADY_RUN = [
603
+ "1. The project's OWN check and build commands have ALREADY BEEN RUN against this",
604
+ ' exact tree, and the EVIDENCE block above names each one, its real exit code and',
605
+ ' the file holding its complete output. READ THOSE FILES — that output is the bar,',
606
+ ' and a command reported there does not need running again. Re-run only a TARGETED',
607
+ ' SUBSET: a single test file, or one command whose reported result you have a',
608
+ ' concrete reason to doubt (its output contradicts itself, or it never ran).',
609
+ " Judge the artifact or output the commands actually PRODUCED. Also run the spec's",
610
+ ' VERIFY block, and any command the evidence does not cover, but neither overrides',
611
+ ' that bar.'
612
+ ];
613
+ /**
614
+ * The spec as a VALUE, with each constraint's `[from:]` tag resolved against the
615
+ * task's own Q&A record. Shared by the verify prompt and the resolution judge, so
616
+ * one constraint cannot carry two weights.
617
+ */
618
+ export function specValue(spec, qaRecord) {
619
+ const kinds = qaRecord === undefined ? [] : qaKindsFromRecord(qaRecord);
620
+ return parseSpec(spec, n => kinds[n - 1] ?? null);
621
+ }
496
622
  /**
497
623
  * Build the verification child's prompt. Kept pure so the wording is unit-tested
498
624
  * without spawning pi. The contract: run the spec's own verification in the real
@@ -503,7 +629,7 @@ export const BOUND_PROBE_KEYS = PROBE_ADAPTERS.filter(a => a.bound).map(a => a.k
503
629
  * block — the probes are independently optional.
504
630
  */
505
631
  export function buildVerifyPrompt(spec, findings = {}, context = {}) {
506
- const { envNotes, contracts } = context;
632
+ const { envNotes, envRunId, contracts } = context;
507
633
  // NOTICE BLOCKS in table order; RULES sorted by their hand-assigned number.
508
634
  const noticeBlocks = PROBE_ADAPTERS.flatMap(adapter => {
509
635
  const lines = findings[adapter.key];
@@ -514,8 +640,16 @@ export function buildVerifyPrompt(spec, findings = {}, context = {}) {
514
640
  : a.ruleId > b.ruleId ? 1
515
641
  : 0)
516
642
  .flatMap(a => [...(a.rule ?? []), '']);
517
- const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
643
+ const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes, envRunId)] : [];
518
644
  const contractsBlock = contracts && contracts.trim().length > 0 ? [buildContractsVerifyBlock(contracts)] : [];
645
+ const weights = annotateConstraints(specValue(spec, context.qaRecord).constraints);
646
+ const weightsBlock = weights === null ?
647
+ []
648
+ : [
649
+ "THE SPEC'S CONSTRAINTS, BY WEIGHT (rule 4b says what each weight is worth):",
650
+ weights,
651
+ ''
652
+ ];
519
653
  return [
520
654
  'You are a strict verification pass running right after an AI coding agent',
521
655
  'finished a task and committed it. The agent is known to mark work "done"',
@@ -531,15 +665,12 @@ export function buildVerifyPrompt(spec, findings = {}, context = {}) {
531
665
  '',
532
666
  ...envBlock,
533
667
  ...contractsBlock,
668
+ ...weightsBlock,
534
669
  ...noticeBlocks,
535
670
  'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
536
671
  'checkout (or CI run) would experience it:',
537
672
  '',
538
- "1. Run the project's OWN commands the verbatim scripts / targets / binaries it",
539
- ' ships (package.json scripts, Makefile targets, the command the spec names) —',
540
- ' exactly as written, in the workspace as you found it. Judge the artifact or',
541
- " output they actually PRODUCE. The project's own command and its real output are",
542
- " the bar; also run the spec's VERIFY block, but it does not override that bar.",
673
+ ...((findings.evidence?.length ?? 0) > 0 ? RULE_1_ALREADY_RUN : RULE_1_RUN_THEM),
543
674
  '',
544
675
  '2. Do NOT prepare, repair, reconfigure, or stand in for the run to make a check',
545
676
  ' pass. Concretely, to reach a green result you must NOT: set or export an',
@@ -764,15 +895,28 @@ export async function runWorkVerification(deps) {
764
895
  // progress reporting must never break the gate
765
896
  }
766
897
  };
898
+ // DIFFERENTIAL, not absolute. A red check is this task's FAIL only when the
899
+ // baseline says it was not already red the same way; otherwise it is stated to
900
+ // the child as a probe row and recorded as inherited debt, and the pass
901
+ // continues — the short-circuit is what used to throw away every deterministic
902
+ // finding about the task's OWN work.
903
+ const pre = { repoHealth: [] };
904
+ let inheritedHealth;
767
905
  if (deps.repoHealth) {
768
906
  stage('repo health');
769
907
  const h = await deps.repoHealth();
770
908
  if (!h.ok) {
771
- return { ok: false, failClass: 'repo-health', reason: `repo health: ${h.reason}` };
909
+ const baseline = deps.healthBaseline ? await deps.healthBaseline() : null;
910
+ if (classifyHealthDelta(baseline?.outcome ?? null, h) === 'regressed') {
911
+ return { ok: false, failClass: 'repo-health', reason: `repo health: ${h.reason}` };
912
+ }
913
+ pre.repoHealth = inheritedHealthFindings(h);
914
+ inheritedHealth = `repo health: ${h.reason} — already failing before this task`;
772
915
  }
773
916
  }
917
+ const inherited = inheritedHealth === undefined ? {} : { inheritedHealth };
774
918
  if (!deps.spec || deps.spec.trim().length === 0) {
775
- return { ok: true, reason: 'no spec to verify' };
919
+ return { ok: true, reason: 'no spec to verify', ...inherited };
776
920
  }
777
921
  // Every deterministic probe, one table row each (see PROBE_ADAPTERS): the row
778
922
  // knows which dep it reads, what it degrades to, and which notice block its
@@ -781,10 +925,17 @@ export async function runWorkVerification(deps) {
781
925
  const findings = {};
782
926
  const rawResults = new Map();
783
927
  for (const adapter of PROBE_ADAPTERS) {
784
- const result = await adapter.run(deps, stage);
928
+ const result = await adapter.run(deps, stage, pre);
785
929
  findings[adapter.key] = result.findings;
786
930
  rawResults.set(adapter.key, result.raw);
787
931
  }
932
+ // Everything the gate now knows deterministically, carried on the outcome so
933
+ // an AUTOFIX re-run is TOLD it (fix-context.ts) rather than re-deriving it —
934
+ // which is what a re-run told only "lint fails" does by widening suppressions.
935
+ // Only channels that FOUND something: a bag of empty arrays is not knowledge,
936
+ // and every consumer would have to filter it back out.
937
+ const found = Object.fromEntries(Object.entries(findings).filter(([, lines]) => lines.length > 0));
938
+ const probed = Object.keys(found).length === 0 ? {} : { probes: found };
788
939
  // The one probe whose RAW value is needed beyond the prompt: cross-task deletion
789
940
  // findings ride on a FAIL outcome (structured) so an ACCEPT can record them as
790
941
  // durable debts. The row's `empty` is `[]`, so this is always an array.
@@ -818,7 +969,12 @@ export async function runWorkVerification(deps) {
818
969
  for (let attempt = 1;; attempt++) {
819
970
  let text;
820
971
  try {
821
- text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, { envNotes, contracts }), deps.signal);
972
+ text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, {
973
+ envNotes,
974
+ ...(deps.qaRecord === undefined ? {} : { qaRecord: deps.qaRecord }),
975
+ ...(deps.envNotes?.runId === undefined ? {} : { envRunId: deps.envNotes.runId }),
976
+ contracts
977
+ }), deps.signal);
822
978
  }
823
979
  catch (err) {
824
980
  if (err instanceof Error && err.message === USER_CANCELLED)
@@ -830,7 +986,9 @@ export async function runWorkVerification(deps) {
830
986
  return {
831
987
  ok: false,
832
988
  failClass: 'harness-fault',
833
- reason: `${VERIFY_FAIL_PREFIX['harness-fault']} ${msg}`
989
+ reason: `${VERIFY_FAIL_PREFIX['harness-fault']} ${msg}`,
990
+ ...inherited,
991
+ ...probed
834
992
  };
835
993
  }
836
994
  // Capture the environment facts the child shared — regardless of verdict
@@ -853,13 +1011,18 @@ export async function runWorkVerification(deps) {
853
1011
  continue;
854
1012
  return {
855
1013
  ok: false,
1014
+ // The child judged a tree it had itself changed — the FAULT is the
1015
+ // pass, not the work, so no implementation re-run can move it.
1016
+ failClass: 'harness-fault',
856
1017
  reason: 'verify child mutated repo state and its verdict was discarded '
857
- + `(state restored: ${mutation.detail.slice(0, 200)})`
1018
+ + `(state restored: ${mutation.detail.slice(0, 200)})`,
1019
+ ...inherited,
1020
+ ...probed
858
1021
  };
859
1022
  }
860
1023
  const verdict = parseVerifyVerdict(text);
861
1024
  if (verdict.pass)
862
- return { ok: true };
1025
+ return { ok: true, ...inherited, ...probed };
863
1026
  if (verdict.detail === 'no verdict emitted' && attempt === 1)
864
1027
  continue;
865
1028
  // Structured cross-task deletion findings ride on every FAIL outcome: if the
@@ -875,14 +1038,18 @@ export async function runWorkVerification(deps) {
875
1038
  unobserved: true,
876
1039
  failClass: 'unobserved',
877
1040
  reason: `work unobserved: ${verdict.detail}`,
878
- ...deletions
1041
+ ...deletions,
1042
+ ...inherited,
1043
+ ...probed
879
1044
  };
880
1045
  }
881
1046
  return {
882
1047
  ok: false,
883
1048
  failClass: 'model-verdict',
884
1049
  reason: `work did not verify: ${verdict.detail}${verdict.detail === 'no verdict emitted' ? ' (after verify retry)' : ''}`,
885
- ...deletions
1050
+ ...deletions,
1051
+ ...inherited,
1052
+ ...probed
886
1053
  };
887
1054
  }
888
1055
  }
@@ -1,5 +1,4 @@
1
1
  import type { AutoAnswer } from './parsers.js';
2
- import type { ResolutionChoice } from './verify-resolution.js';
3
2
  import type { FinalGateChoice } from './final-gate-fix.js';
4
3
  /**
5
4
  * Visible provenance marker on the artifacts an auto-pick writes: gate trail lines
@@ -52,18 +51,6 @@ export declare function yoloPickAnswer(enabled: boolean, opts: {
52
51
  * pattern-matching the answer text.
53
52
  */
54
53
  export declare function yoloPickAutoAnswer(enabled: boolean, auto: AutoAnswer): YoloPick;
55
- /**
56
- * The verify-FAIL picker policy: ACCEPT (and write a debt), never AUTOFIX.
57
- *
58
- * Not a preference — a bound. task-gates.ts consults this only after its own
59
- * unattended paths are exhausted: `autoFixNow` has run AUTOFIX up to
60
- * MAX_AUTO_AUTOFIX times while the research recommended it, and the YOLO rescue has
61
- * spent its one attempt on an ACCEPT recommendation with an untouched budget.
62
- * Answering AUTOFIX here would restart that budget from the very site that proves
63
- * it ran out. So YOLO takes the terminal option and records the defect under the
64
- * `'yolo-accepted'` debt origin, which a human decision never produces.
65
- */
66
- export declare function yoloVerifyResolution(enabled: boolean): ResolutionChoice | null;
67
54
  /**
68
55
  * The final-integration-gate policy: keep autofixing WHILE the picker still offers
69
56
  * that card — run-final-gate.ts withdraws it from `options` after
package/dist/task/yolo.js CHANGED
@@ -9,13 +9,15 @@
9
9
  * WHY PER-SITE, NOT ONE HOOK (the trap this module exists to avoid): the single
10
10
  * interactive choke point is SessionUI.ask() (remote/bridge.ts) — auto-picking
11
11
  * "index 0" inside it would be one tiny patch, and it would be wrong. One way the
12
- * verify-FAIL picker is reached is after MAX_AUTO_AUTOFIX unattended autofix
13
- * attempts have all failed, and it STILL tints AUTOFIX as recommended, because the
14
- * research still recommends it. A central hook would pick AUTOFIX forever and
15
- * defeat the exact cap that exists to break a non-converging loop. So every site
16
- * decides for itself, BEFORE ask() is called — which also means the prompt
17
- * notification (bridge.ts holds exactly one pushNotify, inside ask()) is
18
- * suppressed structurally, with zero suppression code.
12
+ * verify-FAIL picker is reached is after every unattended autofix attempt has
13
+ * failed, and it STILL tints AUTOFIX as recommended, because the research still
14
+ * recommends it. A central hook would pick AUTOFIX forever and defeat the exact
15
+ * cap that exists to break a non-converging loop. So every site decides for
16
+ * itself, BEFORE ask() is called — which also means the prompt notification
17
+ * (bridge.ts holds exactly one pushNotify, inside ask()) is suppressed
18
+ * structurally, with zero suppression code. The verify gate's own site is
19
+ * gate-resolution.ts's decision table, which reads {@link isYoloMode} as its
20
+ * `unattended` input.
19
21
  *
20
22
  * Guard direction (repo constraint): an auto-pick may cost time, never work.
21
23
  * Anything this module cannot stand behind — no recommendation to take, an answer
@@ -85,20 +87,6 @@ export function yoloPickAutoAnswer(enabled, auto) {
85
87
  })
86
88
  });
87
89
  }
88
- /**
89
- * The verify-FAIL picker policy: ACCEPT (and write a debt), never AUTOFIX.
90
- *
91
- * Not a preference — a bound. task-gates.ts consults this only after its own
92
- * unattended paths are exhausted: `autoFixNow` has run AUTOFIX up to
93
- * MAX_AUTO_AUTOFIX times while the research recommended it, and the YOLO rescue has
94
- * spent its one attempt on an ACCEPT recommendation with an untouched budget.
95
- * Answering AUTOFIX here would restart that budget from the very site that proves
96
- * it ran out. So YOLO takes the terminal option and records the defect under the
97
- * `'yolo-accepted'` debt origin, which a human decision never produces.
98
- */
99
- export function yoloVerifyResolution(enabled) {
100
- return enabled ? { action: 'accept' } : null;
101
- }
102
90
  /**
103
91
  * The final-integration-gate policy: keep autofixing WHILE the picker still offers
104
92
  * that card — run-final-gate.ts withdraws it from `options` after
@@ -399,6 +399,12 @@ export interface RunWorkerResult {
399
399
  */
400
400
  interface RestartState {
401
401
  loopHit?: LoopHit;
402
+ /**
403
+ * The killed attempt's read-set (loop-detector.ts). A runaway kill discards
404
+ * every byte the child read and nothing else records what that was, so it is
405
+ * what the loop and stall hints carry in place of answer text.
406
+ */
407
+ visited: readonly string[];
402
408
  commandKill?: CommandKill;
403
409
  streamStalled?: {
404
410
  idleMs: number;
@@ -462,5 +468,19 @@ interface RestartRule {
462
468
  /** Backoff before re-spawning, in ms. Only the connection rule waits. */
463
469
  backoffMs?: (s: RestartState) => number;
464
470
  }
471
+ /**
472
+ * The restart ladder, in precedence order. FIRST MATCH WINS.
473
+ *
474
+ * Read the `!loopHit` guards as "a loop kill outranks me even when it has no
475
+ * budget left". They are not redundant with row order: when a loop is detected
476
+ * but the shared budget is spent, row 1 declines, and without those guards row 2
477
+ * or 4 would then restart the same runaway child under a hint that does not
478
+ * describe why it died.
479
+ *
480
+ * The whole ritual — check the budget, set the hint, spend the counters, record
481
+ * and announce the discarded attempt, sleep, re-spawn — belongs to the loop in
482
+ * `runWorker`, so a new failure mode is one row here and cannot be added without
483
+ * becoming visible in `restarts`.
484
+ */
465
485
  export declare const RESTART_RULES: readonly RestartRule[];
466
486
  export declare function runWorker(input: RunWorkerInput): Promise<RunWorkerResult>;
@@ -102,7 +102,9 @@ const CARRY_FORWARD_LIMIT = 24_000;
102
102
  * `worker-timeout` and `connection-error` all discard work the model genuinely
103
103
  * did. A loop kill and a leaked tool call do not — the first is by definition the
104
104
  * same call repeated, the second is malformed protocol text, and replaying either
105
- * would feed the failure back to itself.
105
+ * would feed the failure back to itself. A loop kill also fires from the tool-call
106
+ * hook, before any answer text streams, so there is nothing to carry; what it
107
+ * hands the re-spawn is the READ-SET instead (`RestartState.visited`).
106
108
  */
107
109
  const CARRY_FORWARD_REASONS = CARRY_FORWARD_IDS;
108
110
  /**
@@ -245,6 +247,15 @@ absoluteCeilingMs) {
245
247
  }
246
248
  };
247
249
  }
250
+ /**
251
+ * A stall hit counts a streak or a byte total, not calls in a window, so printing
252
+ * the loop shape would misname why the attempt died.
253
+ */
254
+ function loopDetail(hit) {
255
+ if (hit.stall)
256
+ return `${hit.call.name} ${hit.stall} ×${hit.count}`;
257
+ return `${hit.call.name} ×${hit.count}${hit.windowSize === undefined ? '' : `/${hit.windowSize}`}`;
258
+ }
248
259
  /**
249
260
  * The restart ladder, in precedence order. FIRST MATCH WINS.
250
261
  *
@@ -259,15 +270,6 @@ absoluteCeilingMs) {
259
270
  * `runWorker`, so a new failure mode is one row here and cannot be added without
260
271
  * becoming visible in `restarts`.
261
272
  */
262
- /**
263
- * A stall hit carries no meaningful windowSize (rule 1 sets it to 0), so
264
- * printing the loop shape would misname why the attempt died.
265
- */
266
- function loopDetail(hit) {
267
- return hit.stall ?
268
- `${hit.call.name} ${hit.stall} ×${hit.count}`
269
- : `${hit.call.name} ×${hit.count}/${hit.windowSize}`;
270
- }
271
273
  export const RESTART_RULES = [
272
274
  {
273
275
  // A loop-kill is restarted with a hint naming the offending call so the
@@ -276,7 +278,9 @@ export const RESTART_RULES = [
276
278
  detect: s => s.loopHit && s.restartBudgetSpent < MAX_LOOP_RESTARTS ?
277
279
  { detail: loopDetail(s.loopHit) }
278
280
  : null,
279
- hint: s => s.loopHit.stall ? formatStallHint(s.loopHit.stall) : formatLoopHint(s.loopHit),
281
+ hint: s => s.loopHit.stall ?
282
+ formatStallHint(s.loopHit.stall, s.visited)
283
+ : formatLoopHint(s.loopHit, s.visited),
280
284
  counters: { shared: true }
281
285
  },
282
286
  {
@@ -599,6 +603,7 @@ export async function runWorker(input) {
599
603
  // cancel: a user who pressed ESC between attempts must not buy a spawn.
600
604
  const state = {
601
605
  ...(loopHit ? { loopHit } : {}),
606
+ visited: loopDetector?.visited() ?? [],
602
607
  ...(commandKill ? { commandKill } : {}),
603
608
  ...(streamStalled ? { streamStalled } : {}),
604
609
  stalled,
@@ -96,12 +96,13 @@ export declare function excerptFabricated(check: {
96
96
  * the test imports the rule it is checking.
97
97
  */
98
98
  export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated' | 'abstained'>): boolean;
99
- /** The docs cache key: a package's answer is per (module, question), with the question
100
- * lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
101
- * null for the project-source `.` lookup, which is never cached — the working tree
102
- * mutates as tasks implement.
99
+ /** The docs cache key: a package's answer is per (module, question), the question
100
+ * reduced to its sorted distinctive tokens so phrasing variants share one entry and a
101
+ * narrower question can be answered from a wider one's digest (research-cache.ts).
102
+ * Returns null for the project-source `.` lookup, which is never cached — the working
103
+ * tree mutates as tasks implement.
103
104
  *
104
- * The ecosystem joins the key only when the caller named one, so the keys of every
105
+ * The ecosystem joins the key only when the caller named one, so the subjects of every
105
106
  * call that lets the manifest decide are the ones they always were. */
106
107
  export declare function docsCacheKey(params: {
107
108
  module: string;
@@ -13,7 +13,7 @@ import { npmVersionLookup as defaultNpmVersionLookup, formatNpmVersionSection }
13
13
  import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
14
14
  import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
15
15
  import { logDocsAnswer } from './typeonly-log.js';
16
- import { normalizeQuery } from './research-cache.js';
16
+ import { normalizeQuery, queryTokenKey } from './research-cache.js';
17
17
  import { projectDocsRaw } from './docs-project.js';
18
18
  import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
19
19
  import { isAbstention } from './abstention.js';
@@ -433,18 +433,19 @@ export function docsCacheable(d) {
433
433
  // real answer and memoised the dead end for the whole run.
434
434
  return d.typeOnly !== true && d.excerptFabricated !== true && d.abstained !== true;
435
435
  }
436
- /** The docs cache key: a package's answer is per (module, question), with the question
437
- * lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
438
- * null for the project-source `.` lookup, which is never cached — the working tree
439
- * mutates as tasks implement.
436
+ /** The docs cache key: a package's answer is per (module, question), the question
437
+ * reduced to its sorted distinctive tokens so phrasing variants share one entry and a
438
+ * narrower question can be answered from a wider one's digest (research-cache.ts).
439
+ * Returns null for the project-source `.` lookup, which is never cached — the working
440
+ * tree mutates as tasks implement.
440
441
  *
441
- * The ecosystem joins the key only when the caller named one, so the keys of every
442
+ * The ecosystem joins the key only when the caller named one, so the subjects of every
442
443
  * call that lets the manifest decide are the ones they always were. */
443
444
  export function docsCacheKey(params) {
444
445
  if (params.module === '.')
445
446
  return null;
446
447
  const scope = params.ecosystem ? `${params.ecosystem}::` : '';
447
- return `${scope}${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`;
448
+ return `${scope}${normalizeQuery(params.module)}\u0000${queryTokenKey(params.query)}`;
448
449
  }
449
450
  /** Package provenance for per-entry resume invalidation: the package ROOT of the
450
451
  * specifier (`hono/client` → `hono`), and undefined for the project-source `.`.