@mjasnikovs/pi-task 0.18.23 → 0.18.24

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.
@@ -33,6 +33,7 @@ export interface AutoDeps extends GateDeps {
33
33
  finalGate?: (cwd: string, planText?: string) => Promise<{
34
34
  ok: boolean;
35
35
  reason: string;
36
+ failures?: string[];
36
37
  debtNote?: string;
37
38
  openDebts?: AcceptDebt[];
38
39
  }>;
@@ -913,13 +913,30 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
913
913
  // Hand the parent plan (the task list) to the gate so it can tell a
914
914
  // served app from a CLI: the boot check requires a listener only for
915
915
  // the former (mx5 run 10 — a CSS watcher satisfied "still alive").
916
+ // Trail EVERY aggregated failure entry (mx5 run 13): the gate now
917
+ // runs all sections and ranks the list; a single sliced reason
918
+ // line would re-hide everything past the first entry.
919
+ const trailGateFail = async (f) => {
920
+ const list = f.failures ?? [f.reason];
921
+ if (list.length <= 1) {
922
+ await recGate(`final-gate: FAIL — ${f.reason.slice(0, 300)}`);
923
+ return;
924
+ }
925
+ await recGate(`final-gate: FAIL — ${list.length} failures (ranked, most load-bearing first)`);
926
+ for (const [i, entry] of list.entries()) {
927
+ await recGate(`final-gate FAIL ${i + 1}/${list.length}: ${entry.slice(0, 300)}`);
928
+ }
929
+ };
916
930
  let fin = await deps.finalGate(cwd, body);
917
931
  // Record the outcome symmetrically (mx5 run 10 item 7): only FAIL was
918
932
  // ever trailed, so a PASSing gate was indistinguishable from a gate
919
933
  // that never ran. The PASS reason names the commands that were run.
920
- await recGate(fin.ok ?
921
- `final-gate: PASS — ${fin.reason.slice(0, 300)}`
922
- : `final-gate: FAIL — ${fin.reason.slice(0, 300)}`);
934
+ if (fin.ok) {
935
+ await recGate(`final-gate: PASS — ${fin.reason.slice(0, 300)}`);
936
+ }
937
+ else {
938
+ await trailGateFail(fin);
939
+ }
923
940
  // ACCEPT-debt re-check surfacing (mx5 run 4 B3 / run 8 TASK_0012):
924
941
  // tasks the user accepted despite a verify-FAIL that the gate could
925
942
  // not prove resolved against the current tree. Surface them at the
@@ -989,13 +1006,21 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
989
1006
  active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
990
1007
  // Work from the FRESH gate failure when the fix pass got
991
1008
  // as far as re-running the gate; otherwise keep the last.
992
- // The debt note is carried so the next picker still shows
993
- // the open claims (the seed never includes it).
1009
+ // The full ranked list rides along (and is re-trailed
1010
+ // when fresh) so the next picker and the next fix seed
1011
+ // still carry every entry, not just the first. The debt
1012
+ // note is carried so the next picker still shows the
1013
+ // open claims (the seed never includes it).
994
1014
  fin = {
995
1015
  ok: false,
996
1016
  reason: fix.gateReason ?? fin.reason,
1017
+ failures: fix.gateReason !== undefined ? fix.gateFailures : fin.failures,
997
1018
  debtNote: fin.debtNote
998
1019
  };
1020
+ if (fix.gateReason !== undefined
1021
+ && (fix.gateFailures?.length ?? 0) > 1) {
1022
+ await trailGateFail(fin);
1023
+ }
999
1024
  continue;
1000
1025
  }
1001
1026
  // Leave failed — the dismissal default, unchanged from the
@@ -42,7 +42,10 @@ export declare function extractFailingCommand(reason: string): string | null;
42
42
  /**
43
43
  * Build the fix child's prompt. Generic by construction: the only project facts
44
44
  * in it are the gate's own failure text — the command comes from the project's
45
- * discovered manifest, never from a hardcoded ecosystem.
45
+ * discovered manifest, never from a hardcoded ecosystem. The seed may carry
46
+ * SEVERAL failures (the gate aggregates every section since mx5 run 13, ranked
47
+ * most load-bearing first); convergence means the WHOLE list is empty, so the
48
+ * child is told to fix all of them.
46
49
  */
47
50
  export declare function buildFinalFixPrompt(failReason: string): string;
48
51
  /**
@@ -63,6 +66,9 @@ export interface FinalFixResult {
63
66
  /** On a did-not-converge outcome: the FRESH gate failure, so the caller's next
64
67
  * picker (and next fix attempt) works from the current state, not the stale one. */
65
68
  gateReason?: string;
69
+ /** The fresh gate's individual ranked failures (see FinalGateOutcome.failures),
70
+ * so the caller can trail each entry — never just the first. */
71
+ gateFailures?: string[];
66
72
  }
67
73
  export interface FinalFixDeps {
68
74
  cwd: string;
@@ -72,10 +78,13 @@ export interface FinalFixDeps {
72
78
  failReason: string;
73
79
  /** Run the fix child; same closure shape the other gate children use. */
74
80
  runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
75
- /** Re-run the final integration gate — the only arbiter of convergence. */
81
+ /** Re-run the final integration gate — the only arbiter of convergence.
82
+ * Converges only when the gate's FULL aggregated failure list is empty
83
+ * (ok=true); `failures` rides through so the caller sees every entry. */
76
84
  gate: (cwd: string) => Promise<{
77
85
  ok: boolean;
78
86
  reason: string;
87
+ failures?: string[];
79
88
  }>;
80
89
  /** Labels of every currently-discoverable gate command (static + integration),
81
90
  * for the shrink guard. Pure discovery — nothing is executed. */
@@ -96,19 +96,24 @@ export function extractFailingCommand(reason) {
96
96
  /**
97
97
  * Build the fix child's prompt. Generic by construction: the only project facts
98
98
  * in it are the gate's own failure text — the command comes from the project's
99
- * discovered manifest, never from a hardcoded ecosystem.
99
+ * discovered manifest, never from a hardcoded ecosystem. The seed may carry
100
+ * SEVERAL failures (the gate aggregates every section since mx5 run 13, ranked
101
+ * most load-bearing first); convergence means the WHOLE list is empty, so the
102
+ * child is told to fix all of them.
100
103
  */
101
104
  export function buildFinalFixPrompt(failReason) {
102
105
  return [
103
106
  'You are a bounded fix pass for a FAILED whole-repo integration gate.',
104
107
  'Every task in this run is complete and committed; then the project’s own',
105
- 'integration command was run against the assembled repository and failed:',
108
+ 'integration commands were run against the assembled repository and failed:',
106
109
  '',
107
110
  failReason.trim(),
108
111
  '',
109
- 'Your ONLY job is to make that command pass by fixing the DEFECT it reveals.',
112
+ 'Your ONLY job is to fix the DEFECT(s) those failures reveal. When several',
113
+ 'failures are listed they are ranked most load-bearing first — fix ALL of',
114
+ 'them; the gate only converges when every one passes.',
110
115
  '',
111
- '1. Re-run the exact failing command first and read its full output.',
116
+ '1. Re-run each exact failing command first and read its full output.',
112
117
  '2. Diagnose the root cause, then fix it with the smallest correct change.',
113
118
  ' The project’s own manifests, configs and conventions define what',
114
119
  ' correct means — follow them, do not invent new structure.',
@@ -129,9 +134,9 @@ export function buildFinalFixPrompt(failReason) {
129
134
  ' revert, stash, clean). The work in this repository is finished and',
130
135
  ' committed — reverting it is destroying the run, not fixing it.',
131
136
  '',
132
- '4. Re-run the failing command after your fix and confirm it exits 0. The',
133
- ' gate is re-run mechanically after you finish — your claim is not the',
134
- ' verdict, the real exit code is.',
137
+ '4. Re-run the failing command(s) after your fix and confirm they exit 0.',
138
+ ' The gate is re-run mechanically after you finish — your claim is not',
139
+ ' the verdict, the real exit codes are.',
135
140
  '',
136
141
  'End with exactly one line:',
137
142
  ' FINAL-GATE-FIX: DONE',
@@ -240,7 +245,8 @@ export async function runFinalGateAutofix(deps) {
240
245
  return {
241
246
  ok: false,
242
247
  reason: `did not converge: ${fin.reason}`,
243
- gateReason: fin.reason
248
+ gateReason: fin.reason,
249
+ gateFailures: fin.failures
244
250
  };
245
251
  }
246
252
  return { ok: true, reason: fin.reason };
@@ -6,14 +6,25 @@ export interface FinalGateOutcome {
6
6
  /** true → statics and every runnable integration command passed (or nothing to run). */
7
7
  ok: boolean;
8
8
  /**
9
- * On a fail: the exact command, its exit code, and the tail of its output — the
10
- * MECHANICAL failure only. The accept-debt note is deliberately NOT folded in
9
+ * On a fail: the exact command(s), exit code(s), and output tail(s) — the
10
+ * MECHANICAL failures only. The accept-debt note is deliberately NOT folded in
11
11
  * here (mx5 run 11): this string seeds the final-gate AUTOFIX child's prompt,
12
12
  * and a debt included there is read as an instruction — the run-11 fix child
13
13
  * `rm`'d a sibling task's verified deliverable to satisfy a recorded claim. The
14
14
  * child cannot act on text it never receives; debts travel in `debtNote`.
15
+ * With multiple failures this is the numbered, ranked list (see `failures`).
15
16
  */
16
17
  reason: string;
18
+ /**
19
+ * On a fail: EVERY section failure individually, ranked most load-bearing
20
+ * first — boot/render ("the app does not serve/render") outranks any single
21
+ * test failure. The gate runs every section and aggregates rather than
22
+ * early-returning (mx5 run 13: a bun-test glob failure shadowed the boot +
23
+ * render probe, so the user accepted the FAIL having only ever seen 1 failing
24
+ * CT test while the shipped app 404'd on every non-API GET). Callers trail
25
+ * each entry and show the full list wherever an ACCEPT decision is made.
26
+ */
27
+ failures?: string[];
17
28
  /**
18
29
  * Human-facing suffix listing the still-open accepted-defect claims (see
19
30
  * buildAcceptDebtNote) — for the picker question and the trail, NEVER for the
@@ -146,6 +157,16 @@ export { taskThatIntroduced };
146
157
  * Run the final gate: static analysis first, then the lockfile consistency
147
158
  * checks, then the discovered integration commands, then one boot exercise of
148
159
  * the start command — whole-repo, verbatim, unaided. Deterministic (no model).
149
- * First real failure wins.
160
+ *
161
+ * EVERY section runs and failures AGGREGATE (mx5 run 13): the gate used to
162
+ * early-return on the first failing section, and the boot + render probe — built
163
+ * after run 11 exactly for "app serves blank/nothing" — was ordered last, so any
164
+ * earlier failure shadowed the most load-bearing signal. Run 13's user accepted
165
+ * the FAIL having seen only 1 failing CT test while the app 404'd on every
166
+ * non-API GET; boot/render never executed in any attempt. Now the outcome
167
+ * carries the full ranked failure list (boot/render first — "the app does not
168
+ * serve/render" outranks any single test), the ACCEPT decision is made on all of
169
+ * it, and autofix converges only when the whole list is empty. Per-section
170
+ * env-gap/INFRA_GAP skip semantics and orphan-port recovery are unchanged.
150
171
  */
151
172
  export declare function runFinalIntegrationGate(cwd: string, timeoutMs?: number, bootGraceMs?: number, bootDeps?: BootDeps, planText?: string): Promise<FinalGateOutcome>;
@@ -595,7 +595,17 @@ export { taskThatIntroduced };
595
595
  * Run the final gate: static analysis first, then the lockfile consistency
596
596
  * checks, then the discovered integration commands, then one boot exercise of
597
597
  * the start command — whole-repo, verbatim, unaided. Deterministic (no model).
598
- * First real failure wins.
598
+ *
599
+ * EVERY section runs and failures AGGREGATE (mx5 run 13): the gate used to
600
+ * early-return on the first failing section, and the boot + render probe — built
601
+ * after run 11 exactly for "app serves blank/nothing" — was ordered last, so any
602
+ * earlier failure shadowed the most load-bearing signal. Run 13's user accepted
603
+ * the FAIL having seen only 1 failing CT test while the app 404'd on every
604
+ * non-API GET; boot/render never executed in any attempt. Now the outcome
605
+ * carries the full ranked failure list (boot/render first — "the app does not
606
+ * serve/render" outranks any single test), the ACCEPT decision is made on all of
607
+ * it, and autofix converges only when the whole list is empty. Per-section
608
+ * env-gap/INFRA_GAP skip semantics and orphan-port recovery are unchanged.
599
609
  */
600
610
  export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGraceMs = 10_000, bootDeps = {}, planText) {
601
611
  const stat = runRepoHealthCheck(cwd);
@@ -630,8 +640,15 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
630
640
  ...(debtNote ? { debtNote } : {}),
631
641
  openDebts
632
642
  });
643
+ // Aggregated failures across ALL sections (mx5 run 13 — see the function doc).
644
+ // rank 0 = boot/render ("does not serve/render" is the most load-bearing
645
+ // signal); rank 1 = everything else, kept in execution order by stable sort.
646
+ const failures = [];
647
+ const fail = (text, rank = 1) => {
648
+ failures.push({ rank, text });
649
+ };
633
650
  if (!stat.ok)
634
- return withDebts({ ok: false, reason: `static checks: ${stat.reason}` });
651
+ fail(`static checks: ${stat.reason}`);
635
652
  // Launch-contract diff (mx5 run 10 item 4): the design declared `migrate`/`seed`
636
653
  // scripts that fell through decompose and shipped missing, unchecked. Diff the
637
654
  // plan-time-extracted declared scripts against the manifest; a missing one is a
@@ -640,16 +657,13 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
640
657
  if (declared.length > 0) {
641
658
  const missing = missingDeclaredScripts(declared, Object.keys(packageScripts(cwd)));
642
659
  if (missing.length > 0) {
643
- return withDebts({
644
- ok: false,
645
- reason: `launch contract: the design declares script(s) the shipped package.json does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`
646
- });
660
+ fail(`launch contract: the design declares script(s) the shipped package.json does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`);
647
661
  }
648
662
  }
649
663
  const lockCmds = discoverLockfileChecks(cwd);
650
664
  const { cmds } = discoverIntegrationCommands(cwd);
651
665
  const boot = discoverBootCommand(cwd);
652
- if (lockCmds.length === 0 && cmds.length === 0 && !boot) {
666
+ if (lockCmds.length === 0 && cmds.length === 0 && !boot && failures.length === 0) {
653
667
  return withDebts({ ok: true, reason: 'no integration command found (statics passed)' });
654
668
  }
655
669
  const ran = [];
@@ -663,10 +677,8 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
663
677
  if (r.outcome === 'skip')
664
678
  continue;
665
679
  if (r.outcome === 'fail') {
666
- return withDebts({
667
- ok: false,
668
- reason: `${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`
669
- });
680
+ fail(`${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
681
+ continue;
670
682
  }
671
683
  ran.push(label);
672
684
  }
@@ -685,7 +697,13 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
685
697
  if (declared.length > 0) {
686
698
  const covered = cmds.flatMap(([bin, args]) => (bin === 'bun' || bin === 'npm') && args[0] === 'run' && args[1] ? [args[1]] : []);
687
699
  const skippedLaunch = [];
700
+ // A declared script the manifest doesn't expose is already a launch-contract
701
+ // failure above; executing it too would double-report (pre-aggregation the
702
+ // contract diff early-returned, so this loop could assume presence).
703
+ const present = new Set(Object.keys(packageScripts(cwd)).map(s => s.toLowerCase()));
688
704
  for (const name of runnableDeclaredScripts(declared, covered)) {
705
+ if (!present.has(name.toLowerCase()))
706
+ continue;
689
707
  const cmd = ['bun', ['run', name]];
690
708
  const label = `${cmd[0]} ${cmd[1].join(' ')}`;
691
709
  const r = runGateCommand(cwd, cmd, Math.min(timeoutMs, 180_000), INFRA_GAP_OUTPUT_RE);
@@ -694,10 +712,8 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
694
712
  continue;
695
713
  }
696
714
  if (r.outcome === 'fail') {
697
- return withDebts({
698
- ok: false,
699
- reason: `launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`
700
- });
715
+ fail(`launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
716
+ continue;
701
717
  }
702
718
  ran.push(label);
703
719
  }
@@ -714,6 +730,9 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
714
730
  }
715
731
  }
716
732
  }
733
+ // Boot + render ALWAYS runs (mx5 run 13): it is independent of test results by
734
+ // construction, and it carries the run's most load-bearing signal — earlier
735
+ // failures no longer shadow it. Its failures rank FIRST in the aggregate.
717
736
  if (boot) {
718
737
  const label = `${boot[0]} ${boot[1].join(' ')}`;
719
738
  const expectServer = detectsServedApp(cwd, planText);
@@ -734,21 +753,18 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
734
753
  b = await recoverOrphanPort(cwd, boot, b, bootGraceMs, bootDepsWithRender, expectServer);
735
754
  }
736
755
  if (b.outcome === 'fail') {
737
- return withDebts({ ok: false, reason: `boot check: \`${label}\` ${b.detail}` });
756
+ fail(`boot check: \`${label}\` ${b.detail}`, 0);
738
757
  }
739
- if (b.outcome === 'orphan-port') {
758
+ else if (b.outcome === 'orphan-port') {
740
759
  // Could not clear the port. Distinct HARNESS diagnosis, never a bare app
741
760
  // FAIL: name the port and (when known) the process squatting on it.
742
761
  const holder = b.port !== null ? (bootDeps.findPortHolder ?? defaultFindPortHolder)(b.port) : null;
743
762
  const who = holder ? ` — held by an orphaned process (pid ${holder.pid}: ${holder.command})`
744
763
  : b.port !== null ? ` — port ${b.port} is held by another process`
745
764
  : '';
746
- return withDebts({
747
- ok: false,
748
- reason: `boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`
749
- });
765
+ fail(`boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`, 0);
750
766
  }
751
- if (b.outcome === 'pass') {
767
+ else if (b.outcome === 'pass') {
752
768
  ran.push(label);
753
769
  // A listener that served, but whose page could not be OBSERVED to render
754
770
  // (no browser, undeterminable port) → UNOBSERVED warning, not a silent pass.
@@ -756,6 +772,22 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
756
772
  warnings.push(b.renderNote);
757
773
  }
758
774
  }
775
+ if (failures.length > 0) {
776
+ // Stable sort: boot/render (rank 0) leads, everything else keeps execution
777
+ // order. One failure keeps the exact single-failure wording; several become
778
+ // a numbered list so the trail, the ACCEPT picker, and the autofix seed all
779
+ // carry the complete ranked picture.
780
+ const texts = [...failures].sort((a, b) => a.rank - b.rank).map(f => f.text);
781
+ return withDebts({
782
+ ok: false,
783
+ reason: texts.length === 1 ?
784
+ texts[0]
785
+ : `${texts.length} failures (ranked, most load-bearing first):\n${texts
786
+ .map((t, i) => `${i + 1}. ${t}`)
787
+ .join('\n')}`,
788
+ failures: texts
789
+ });
790
+ }
759
791
  const warningNote = warnings.length > 0 ? ` — WARNING: ${warnings.join('; WARNING: ')}` : '';
760
792
  return withDebts({
761
793
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.23",
3
+ "version": "0.18.24",
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",