@mjasnikovs/pi-task 0.42.3 → 0.42.5

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.
@@ -42,7 +42,8 @@ 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';
45
+ import { classifyHealthDelta, inheritedHealthFindings, regressedCommands } from './health-baseline.js';
46
+ import { describeHealthFailures } from './repo-health-check.js';
46
47
  import { parseSpec, sliceSpecSection } from './spec-model.js';
47
48
  import { qaKindsFromRecord } from './qa-transcript.js';
48
49
  import { annotateConstraints, anyBinding, renderConstraintPolicy } from './constraint-policy.js';
@@ -72,6 +73,7 @@ const VERIFY_TOOLS = 'read,bash';
72
73
  export const VERIFY_FAIL_PREFIX = {
73
74
  'repo-health': 'repo health:',
74
75
  'static-checks': 'static checks:',
76
+ 'test-suite': 'test suite:',
75
77
  unobserved: 'work unobserved:',
76
78
  'model-verdict': 'work did not verify:',
77
79
  'harness-fault': 'verification pass could not run:'
@@ -103,6 +105,14 @@ export function failClassOfReason(reason) {
103
105
  export function isStaticClass(cls) {
104
106
  return cls === 'repo-health' || cls === 'static-checks';
105
107
  }
108
+ /** The class a red health result is minted under: the suite's when any test is red. */
109
+ function healthFailClass(failing) {
110
+ return failing.some(c => c.kind === 'test') ? 'test-suite' : 'repo-health';
111
+ }
112
+ /** Does this class name the deterministic whole-repo check, suite included? */
113
+ export function isHealthClass(cls) {
114
+ return isStaticClass(cls) || cls === 'test-suite';
115
+ }
106
116
  /**
107
117
  * The delivered spec's TEXT, for the children that must read its prose verbatim.
108
118
  * The slicing itself lives in spec-model.ts beside the parser, so "the spec
@@ -182,8 +192,8 @@ const PROBE_ADAPTERS = [
182
192
  ]
183
193
  }),
184
194
  /**
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
195
+ * PRE-EXISTING repo health (see health-baseline.ts): checks that were ALREADY
196
+ * failing, with the same exit code, before this task started. They used to be an
187
197
  * absolute FAIL that short-circuited the whole pass, so the task answered for
188
198
  * a sibling's defect and its own probe findings were never computed. As a row
189
199
  * they are stated to the child instead: judge this task's work, and do not
@@ -197,7 +207,7 @@ const PROBE_ADAPTERS = [
197
207
  ruleId: '4h',
198
208
  block: findings => [
199
209
  'INHERITED REPO-HEALTH NOTICE (deterministic, computed by the orchestrator by',
200
- "re-running the project's own static checks and comparing them against the",
210
+ "re-running the project's own checks and comparing their exit codes against the",
201
211
  'baseline taken before this task started): these checks were ALREADY failing,',
202
212
  'with the same exit code, before any of this work existed:',
203
213
  ...findings.map(f => `- ${f}`),
@@ -208,13 +218,16 @@ const PROBE_ADAPTERS = [
208
218
  ],
209
219
  rule: [
210
220
  "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',
221
+ ' INHERITED REPO-HEALTH NOTICE above names a check, that check exited the same way',
212
222
  ' before this task ran. Do NOT fail this work for it: the defect belongs to',
213
223
  ' whatever put it there, it is recorded as durable debt, and the run-end gate',
214
224
  ' re-checks it. Do NOT lean on it either — a command that was already exiting',
215
225
  " non-zero tells you nothing about this task's behavior, so verify that behavior",
216
226
  ' 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."
227
+ " is this task's to answer for in the ordinary way. A TEST command is the",
228
+ ' exception to the exit code: it exits the same way for one failing test or',
229
+ " fifty. Run it, and a test that fails because of THIS work is this task's FAIL,",
230
+ ' whatever the spec says about it.'
218
231
  ]
219
232
  }),
220
233
  /**
@@ -905,18 +918,47 @@ export async function runWorkVerification(deps) {
905
918
  if (deps.repoHealth) {
906
919
  stage('repo health');
907
920
  const h = await deps.repoHealth();
908
- if (!h.ok) {
921
+ // `ok` is not the whole verdict: a suite this tree no longer finds observes
922
+ // nothing, so nothing fails, and only the differential sees it went away.
923
+ // Establishing a baseline can cost a worktree health run, so it is asked for
924
+ // only in the two shapes it can speak to.
925
+ const suiteGone = (h.commands ?? []).some(c => c.gap === 'empty-suite');
926
+ if (!h.ok || suiteGone) {
909
927
  const baseline = deps.healthBaseline ? await deps.healthBaseline() : null;
910
- if (classifyHealthDelta(baseline?.outcome ?? null, h) === 'regressed') {
928
+ const before = baseline?.outcome ?? null;
929
+ if (classifyHealthDelta(before, h) === 'regressed') {
930
+ // Named after what REGRESSED, not after whatever failed first: a lint
931
+ // red on arrival would otherwise stand in for the suite this task broke.
932
+ const regressed = regressedCommands(before, h);
933
+ if (regressed.length === 0) {
934
+ return {
935
+ ok: false,
936
+ failClass: 'repo-health',
937
+ reason: `repo health: ${h.reason}`,
938
+ health: h
939
+ };
940
+ }
941
+ const failClass = healthFailClass(regressed);
911
942
  return {
912
943
  ok: false,
913
- failClass: 'repo-health',
914
- reason: `repo health: ${h.reason}`,
915
- health: h
944
+ failClass,
945
+ reason: `${VERIFY_FAIL_PREFIX[failClass]} ${describeHealthFailures(regressed)}`,
946
+ health: {
947
+ ...h,
948
+ ok: false,
949
+ commands: regressed,
950
+ output: regressed[0].output ?? h.output
951
+ }
916
952
  };
917
953
  }
918
- pre.repoHealth = inheritedHealthFindings(h);
919
- inheritedHealth = `repo health: ${h.reason} — already failing before this task`;
954
+ if (!h.ok) {
955
+ pre.repoHealth = inheritedHealthFindings(h);
956
+ const failing = h.commands?.filter(c => c.outcome === 'fail') ?? [];
957
+ inheritedHealth =
958
+ failing.length > 0 ?
959
+ `${VERIFY_FAIL_PREFIX[healthFailClass(failing)]} ${describeHealthFailures(failing)} — already failing before this task`
960
+ : `repo health: ${h.reason} — already failing before this task`;
961
+ }
920
962
  }
921
963
  }
922
964
  const inherited = inheritedHealth === undefined ? {} : { inheritedHealth };
@@ -36,6 +36,10 @@ export type YoloPick = {
36
36
  * demotion — an answer proven to name an unverified API identifier. Auto-accepting
37
37
  * that would re-promote exactly the invention the demotion exists to stop, so a
38
38
  * machine may never take it; a human still can.
39
+ *
40
+ * An option that defers a breakage to an owner nobody is, is passed over here
41
+ * rather than at each caller: the clarify generator, the plan review and an
42
+ * UNKNOWN's own suggestion reach a machine with no guard in front of them.
39
43
  */
40
44
  export declare function yoloPickAnswer(enabled: boolean, opts: {
41
45
  suggested?: string;
@@ -45,11 +49,11 @@ export declare function yoloPickAnswer(enabled: boolean, opts: {
45
49
  /**
46
50
  * The same policy expressed over an {@link AutoAnswer}, for the grill site. Of the
47
51
  * five `reason` tags an unknown can carry — `api-synthesis`, `deferred-breakage`,
48
- * `integration`, `threw`, `model-unknown` — the first two are unsafe: one names an
49
- * API nobody verified, the other hands a red suite to an owner nobody is. The other
52
+ * `integration`, `threw`, `model-unknown` — `api-synthesis` is unsafe by its tag:
53
+ * the answer names an API nobody verified. A `deferred-breakage` suggestion is the
54
+ * deferral itself, which `yoloPickAnswer` passes over for every site. The other
50
55
  * three carry an ordinary best-effort recommendation, which is precisely what a
51
- * human would be shown as the green card. The variants are told apart by that tag,
52
- * never by pattern-matching the answer text.
56
+ * human would be shown as the green card.
53
57
  */
54
58
  export declare function yoloPickAutoAnswer(enabled: boolean, auto: AutoAnswer): YoloPick;
55
59
  /**
package/dist/task/yolo.js CHANGED
@@ -29,6 +29,7 @@
29
29
  * the config.
30
30
  */
31
31
  import { getConfig } from '../config/config.js';
32
+ import { defersBreakage } from './deferred-breakage.js';
32
33
  /**
33
34
  * Visible provenance marker on the artifacts an auto-pick writes: gate trail lines
34
35
  * and the task file's Q&A record (qa-transcript.ts `QA_PROVENANCE`). A later audit
@@ -53,6 +54,10 @@ export function isYoloMode() {
53
54
  * demotion — an answer proven to name an unverified API identifier. Auto-accepting
54
55
  * that would re-promote exactly the invention the demotion exists to stop, so a
55
56
  * machine may never take it; a human still can.
57
+ *
58
+ * An option that defers a breakage to an owner nobody is, is passed over here
59
+ * rather than at each caller: the clarify generator, the plan review and an
60
+ * UNKNOWN's own suggestion reach a machine with no guard in front of them.
56
61
  */
57
62
  export function yoloPickAnswer(enabled, opts) {
58
63
  if (!enabled)
@@ -60,20 +65,26 @@ export function yoloPickAnswer(enabled, opts) {
60
65
  if (opts.unsafe !== undefined && opts.unsafe.length > 0) {
61
66
  return { kind: 'skip', note: opts.unsafe };
62
67
  }
63
- const pick = opts.suggested ?? opts.alt;
64
- if (pick === undefined || pick.trim().length === 0) {
68
+ const offered = [opts.suggested, opts.alt].filter((o) => o !== undefined && o.trim().length > 0);
69
+ if (offered.length === 0)
65
70
  return { kind: 'skip', note: 'no recommended option to take' };
71
+ const pick = offered.find(o => !defersBreakage(o));
72
+ if (pick === undefined) {
73
+ return {
74
+ kind: 'skip',
75
+ note: 'the recommended answer leaves a test or build failing for an owner that does not exist — needs a human'
76
+ };
66
77
  }
67
78
  return { kind: 'answer', answer: pick };
68
79
  }
69
80
  /**
70
81
  * The same policy expressed over an {@link AutoAnswer}, for the grill site. Of the
71
82
  * five `reason` tags an unknown can carry — `api-synthesis`, `deferred-breakage`,
72
- * `integration`, `threw`, `model-unknown` — the first two are unsafe: one names an
73
- * API nobody verified, the other hands a red suite to an owner nobody is. The other
83
+ * `integration`, `threw`, `model-unknown` — `api-synthesis` is unsafe by its tag:
84
+ * the answer names an API nobody verified. A `deferred-breakage` suggestion is the
85
+ * deferral itself, which `yoloPickAnswer` passes over for every site. The other
74
86
  * three carry an ordinary best-effort recommendation, which is precisely what a
75
- * human would be shown as the green card. The variants are told apart by that tag,
76
- * never by pattern-matching the answer text.
87
+ * human would be shown as the green card.
77
88
  */
78
89
  export function yoloPickAutoAnswer(enabled, auto) {
79
90
  if (!enabled)
@@ -85,9 +96,6 @@ export function yoloPickAutoAnswer(enabled, auto) {
85
96
  ...(auto.alt !== undefined && { alt: auto.alt }),
86
97
  ...(auto.reason === 'api-synthesis' && {
87
98
  unsafe: 'the suggested answer names an unverified API identifier — needs a human'
88
- }),
89
- ...(auto.reason === 'deferred-breakage' && {
90
- unsafe: 'the suggested answer leaves a test or build failing for an owner that does not exist — needs a human'
91
99
  })
92
100
  });
93
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.42.3",
3
+ "version": "0.42.5",
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",