@pushpalsdev/cli 1.1.21 → 1.1.23

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.
@@ -32,7 +32,14 @@ import {
32
32
  type ToolRequirement,
33
33
  } from "shared";
34
34
  import { resolveExecutor, type WorkerpalsRuntimeConfig } from "./common/executor_backend.js";
35
- import type { JobPublishBlockedInfo, JobResult } from "./common/types.js";
35
+ import type {
36
+ JobDiagnostics,
37
+ JobPatchSnapshotDiagnostics,
38
+ JobPublishBlockedInfo,
39
+ JobResult,
40
+ JobTerminalDiagnostics,
41
+ JobValidationRunDiagnostics,
42
+ } from "./common/types.js";
36
43
  import {
37
44
  compactJobOutput,
38
45
  truncate,
@@ -191,6 +198,8 @@ export interface QualityGatePolicy {
191
198
 
192
199
  const BROWSER_VALIDATION_MAX_AUTO_REVISIONS = 3;
193
200
  const CRITIC_COMPACT_RETRY_MIN_REDUCTION_RATIO = 0.25;
201
+ const MAX_DIAGNOSTIC_PATH_SAMPLES = 50;
202
+ const MAX_DIAGNOSTIC_TEXT_CHARS = 8_000;
194
203
 
195
204
  export function qualityRevisionLoopUpperBound(policy: {
196
205
  maxAutoRevisions: number;
@@ -233,6 +242,58 @@ export function qualityRevisionBudgetDecision(opts: {
233
242
  };
234
243
  }
235
244
 
245
+ const MERGE_CONFLICT_RETRY_EXECUTION_BUDGET_MS = 300_000;
246
+ const MERGE_CONFLICT_RETRY_FINALIZATION_BUDGET_MS = 60_000;
247
+ const MERGE_CONFLICT_MIN_RETRY_EXECUTION_BUDGET_MS = 120_000;
248
+
249
+ export function mergeConflictResolverRetryBudgetDecision(opts: {
250
+ jobElapsedMs: number;
251
+ executionBudgetMs: number;
252
+ finalizationBudgetMs: number;
253
+ }): {
254
+ shouldStart: boolean;
255
+ executionBudgetMs: number;
256
+ finalizationBudgetMs: number;
257
+ remainingTotalBudgetMs: number;
258
+ minimumExecutionBudgetMs: number;
259
+ } {
260
+ const configuredExecutionBudgetMs = Number(opts.executionBudgetMs);
261
+ if (!Number.isFinite(configuredExecutionBudgetMs) || configuredExecutionBudgetMs <= 0) {
262
+ return {
263
+ shouldStart: true,
264
+ executionBudgetMs: MERGE_CONFLICT_RETRY_EXECUTION_BUDGET_MS,
265
+ finalizationBudgetMs: MERGE_CONFLICT_RETRY_FINALIZATION_BUDGET_MS,
266
+ remainingTotalBudgetMs: Number.POSITIVE_INFINITY,
267
+ minimumExecutionBudgetMs: MERGE_CONFLICT_MIN_RETRY_EXECUTION_BUDGET_MS,
268
+ };
269
+ }
270
+
271
+ const configuredFinalizationBudgetMs = Math.max(0, Number(opts.finalizationBudgetMs) || 0);
272
+ const elapsedMs = Math.max(0, Number(opts.jobElapsedMs) || 0);
273
+ const remainingTotalBudgetMs = Math.max(
274
+ 0,
275
+ Math.floor(configuredExecutionBudgetMs + configuredFinalizationBudgetMs - elapsedMs),
276
+ );
277
+ const finalizationBudgetMs = Math.min(
278
+ MERGE_CONFLICT_RETRY_FINALIZATION_BUDGET_MS,
279
+ configuredFinalizationBudgetMs,
280
+ remainingTotalBudgetMs,
281
+ );
282
+ const availableExecutionBudgetMs = Math.max(0, remainingTotalBudgetMs - finalizationBudgetMs);
283
+ const executionBudgetMs = Math.min(
284
+ MERGE_CONFLICT_RETRY_EXECUTION_BUDGET_MS,
285
+ Math.floor(availableExecutionBudgetMs),
286
+ );
287
+
288
+ return {
289
+ shouldStart: executionBudgetMs >= MERGE_CONFLICT_MIN_RETRY_EXECUTION_BUDGET_MS,
290
+ executionBudgetMs: Math.max(10_000, executionBudgetMs),
291
+ finalizationBudgetMs,
292
+ remainingTotalBudgetMs,
293
+ minimumExecutionBudgetMs: MERGE_CONFLICT_MIN_RETRY_EXECUTION_BUDGET_MS,
294
+ };
295
+ }
296
+
236
297
  export function shouldRetryCriticTimeoutWithCompact(opts: {
237
298
  timeoutBehavior: string;
238
299
  qualityOk: boolean;
@@ -530,6 +591,162 @@ export function publishableChangedPaths(changedPaths: string[]): string[] {
530
591
  return changedPaths.filter((path) => !isNonPublishableArtifactPath(path));
531
592
  }
532
593
 
594
+ function compactDiagnosticText(value: unknown, maxChars = MAX_DIAGNOSTIC_TEXT_CHARS): string | null {
595
+ const text = String(value ?? "").replace(/\s+$/g, "");
596
+ if (!text.trim()) return null;
597
+ return text.length <= maxChars ? text : text.slice(Math.max(0, text.length - maxChars));
598
+ }
599
+
600
+ function diagnosticPathSample(paths: string[], limit = MAX_DIAGNOSTIC_PATH_SAMPLES): string[] {
601
+ const out: string[] = [];
602
+ const seen = new Set<string>();
603
+ for (const raw of paths) {
604
+ const path = String(raw ?? "").replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
605
+ if (!path || seen.has(path)) continue;
606
+ seen.add(path);
607
+ out.push(path);
608
+ if (out.length >= limit) break;
609
+ }
610
+ return out;
611
+ }
612
+
613
+ function diagnosticTopLevelDirs(paths: string[]): string[] {
614
+ const seen = new Set<string>();
615
+ for (const path of paths) {
616
+ const normalized = String(path ?? "").replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
617
+ if (!normalized) continue;
618
+ const top = normalized.includes("/") ? normalized.split("/", 1)[0] : normalized;
619
+ if (top) seen.add(top);
620
+ if (seen.size >= 20) break;
621
+ }
622
+ return [...seen];
623
+ }
624
+
625
+ function buildPatchSnapshotDiagnostics(
626
+ changedPaths: string[],
627
+ attempt: number,
628
+ phase: string,
629
+ ): JobPatchSnapshotDiagnostics {
630
+ const publishable = publishableChangedPaths(changedPaths);
631
+ const artifactOnly = changedPaths.filter((path) => isNonPublishableArtifactPath(path));
632
+ return {
633
+ attempt,
634
+ phase,
635
+ publishableFileCount: publishable.length,
636
+ artifactOnlyPathCount: artifactOnly.length,
637
+ changedPathSample: diagnosticPathSample(changedPaths),
638
+ topLevelDirs: diagnosticTopLevelDirs(publishable.length > 0 ? publishable : changedPaths),
639
+ capturedAt: new Date().toISOString(),
640
+ };
641
+ }
642
+
643
+ function classifyValidationRunFailure(run: ValidationExecutionResult): string | null {
644
+ if (run.ok) return null;
645
+ const combined = `${run.command}\n${run.stdout}\n${run.stderr}`.toLowerCase();
646
+ if (run.exitCode === 124 || combined.includes("timed out") || combined.includes("timeout")) {
647
+ return "timeout";
648
+ }
649
+ if (run.exitCode === 127 || combined.includes("missing tool") || combined.includes("not found")) {
650
+ return "missing_tool";
651
+ }
652
+ if (/browser|playwright|cypress|locator|page\.|screenshot|web:e2e/.test(combined)) {
653
+ return "browser_validation";
654
+ }
655
+ if (/cannot find module|import error|does not provide an export|no exported member|mock/.test(combined)) {
656
+ return "test_harness";
657
+ }
658
+ return "nonzero_exit";
659
+ }
660
+
661
+ function buildValidationRunDiagnostics(
662
+ runs: ValidationExecutionResult[],
663
+ attempt: number,
664
+ ): JobValidationRunDiagnostics[] {
665
+ return runs.slice(0, 20).map((run) => ({
666
+ attempt,
667
+ command: run.command,
668
+ exitCode: run.exitCode,
669
+ durationMs: run.elapsedMs,
670
+ passed: run.ok,
671
+ failureClass: classifyValidationRunFailure(run),
672
+ stdoutTail: compactDiagnosticText(run.stdout),
673
+ stderrTail: compactDiagnosticText(run.stderr),
674
+ }));
675
+ }
676
+
677
+ function inferTerminalFailureClass(result: JobResult, changedPaths: string[]): string {
678
+ if (result.ok) return "success";
679
+ const text = `${result.summary ?? ""}\n${result.stderr ?? ""}\n${result.stdout ?? ""}`.toLowerCase();
680
+ const publishableCount = publishableChangedPaths(changedPaths).length;
681
+ if (changedPaths.length > 0 && publishableCount === 0) return "artifact_only_no_publishable_patch";
682
+ if (result.exitCode === 124 || text.includes("timed out") || text.includes("timeout")) return "timeout";
683
+ if (text.includes("validationgate") || text.includes("validation")) return "validation";
684
+ if (text.includes("scopegate") || text.includes("scope")) return "scope";
685
+ if (text.includes("criticgate") || text.includes("critic")) return "critic";
686
+ if (text.includes("publish")) return "publish";
687
+ if (text.includes("shell-wrapper") || text.includes("command-router")) return "command_policy";
688
+ return "executor_failure";
689
+ }
690
+
691
+ function inferTerminalStage(result: JobResult, fallback: string): string {
692
+ const text = `${result.summary ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
693
+ if (text.includes("validationgate") || text.includes("validation")) return "validation";
694
+ if (text.includes("scopegate") || text.includes("scope")) return "scope";
695
+ if (text.includes("criticgate") || text.includes("critic")) return "critic";
696
+ if (text.includes("publish")) return "publish";
697
+ if (text.includes("quality gate")) return "quality";
698
+ if (text.includes("codex") || text.includes("executor")) return "executor";
699
+ return fallback;
700
+ }
701
+
702
+ function mergeJobDiagnostics(base: JobDiagnostics | undefined, extra: JobDiagnostics): JobDiagnostics {
703
+ return {
704
+ ...(base ?? {}),
705
+ ...extra,
706
+ attempts: [...(base?.attempts ?? []), ...(extra.attempts ?? [])],
707
+ phaseSpans: [...(base?.phaseSpans ?? []), ...(extra.phaseSpans ?? [])],
708
+ validationRuns: [...(base?.validationRuns ?? []), ...(extra.validationRuns ?? [])],
709
+ patchSnapshots: [...(base?.patchSnapshots ?? []), ...(extra.patchSnapshots ?? [])],
710
+ terminal: extra.terminal ?? base?.terminal,
711
+ metadata: {
712
+ ...(base?.metadata ?? {}),
713
+ ...(extra.metadata ?? {}),
714
+ },
715
+ };
716
+ }
717
+
718
+ function withJobDiagnostics(result: JobResult, diagnostics: JobDiagnostics): JobResult {
719
+ return {
720
+ ...result,
721
+ diagnostics: mergeJobDiagnostics(result.diagnostics, diagnostics),
722
+ };
723
+ }
724
+
725
+ function buildTerminalDiagnostics(args: {
726
+ result: JobResult;
727
+ executor: string;
728
+ changedPaths: string[];
729
+ terminalStage: string;
730
+ timeoutMs?: number | null;
731
+ metadata?: Record<string, unknown>;
732
+ }): JobTerminalDiagnostics {
733
+ const publishable = publishableChangedPaths(args.changedPaths);
734
+ const artifactOnly = args.changedPaths.filter((path) => isNonPublishableArtifactPath(path));
735
+ const text = `${args.result.summary ?? ""}\n${args.result.stderr ?? ""}\n${args.result.stdout ?? ""}`;
736
+ return {
737
+ failureClass: inferTerminalFailureClass(args.result, args.changedPaths),
738
+ terminalStage: inferTerminalStage(args.result, args.terminalStage),
739
+ executorBackend: args.executor,
740
+ summary: compactDiagnosticText(args.result.summary, 1_000),
741
+ watchdogFired: /watchdog|rollout coach/i.test(text),
742
+ timeoutMs: args.timeoutMs ?? null,
743
+ publishableFileCount: publishable.length,
744
+ artifactOnlyPathCount: artifactOnly.length,
745
+ changedPathSample: diagnosticPathSample(args.changedPaths),
746
+ metadata: args.metadata,
747
+ };
748
+ }
749
+
533
750
  function collectPlanningText(planning: TaskExecutePlanning): string {
534
751
  return [
535
752
  planning.intent,
@@ -7379,6 +7596,8 @@ export async function executeJob(
7379
7596
  const jobStartedAt = Date.now();
7380
7597
  const previousValidationFailureDigests = new Map<string, string>();
7381
7598
  const failureJobFamily = buildTaskFailureJobFamily(normalizedParams);
7599
+ const diagnosticValidationRuns: JobValidationRunDiagnostics[] = [];
7600
+ const diagnosticPatchSnapshots: JobPatchSnapshotDiagnostics[] = [];
7382
7601
  while (revisionAttempt <= qualityRevisionLoopMax) {
7383
7602
  const attemptStartedAt = Date.now();
7384
7603
  const attemptParams: Record<string, unknown> = { ...normalizedParams };
@@ -7388,7 +7607,7 @@ export async function executeJob(
7388
7607
  }
7389
7608
 
7390
7609
  const executor = resolveExecutor(runtimeConfig);
7391
- const executeBudgets = { executionBudgetMs, finalizationBudgetMs };
7610
+ const defaultExecuteBudgets = { executionBudgetMs, finalizationBudgetMs };
7392
7611
  const runExecutor = getBackendTaskExecutor(executor);
7393
7612
  if (!runExecutor) {
7394
7613
  return {
@@ -7400,14 +7619,17 @@ export async function executeJob(
7400
7619
  let result: Awaited<ReturnType<typeof runExecutor>> | null = null;
7401
7620
  let mergeConflictPass = 0;
7402
7621
  let executorElapsedMs = 0;
7622
+ let nextMergeConflictExecuteBudgets: typeof defaultExecuteBudgets | null = null;
7403
7623
  while (true) {
7624
+ const currentExecuteBudgets = nextMergeConflictExecuteBudgets ?? defaultExecuteBudgets;
7625
+ nextMergeConflictExecuteBudgets = null;
7404
7626
  const currentResult = await runExecutor(
7405
7627
  kind,
7406
7628
  attemptParams,
7407
7629
  repo,
7408
7630
  runtimeConfig,
7409
7631
  onLog,
7410
- executeBudgets,
7632
+ currentExecuteBudgets,
7411
7633
  );
7412
7634
  if (!currentResult.ok) return currentResult;
7413
7635
  result = currentResult;
@@ -7441,19 +7663,42 @@ export async function executeJob(
7441
7663
  exitCode: 4,
7442
7664
  };
7443
7665
  }
7666
+ const retryBudget = mergeConflictResolverRetryBudgetDecision({
7667
+ jobElapsedMs: Date.now() - attemptStartedAt,
7668
+ executionBudgetMs,
7669
+ finalizationBudgetMs,
7670
+ });
7671
+ if (!retryBudget.shouldStart) {
7672
+ const detail =
7673
+ "Merge-conflict rebase advanced into another conflicted commit, but remaining job budget " +
7674
+ `is ${retryBudget.remainingTotalBudgetMs}ms (< ${retryBudget.minimumExecutionBudgetMs}ms execution).`;
7675
+ onLog?.("stderr", `[MergeConflict] ${detail}`);
7676
+ return {
7677
+ ok: false,
7678
+ summary: detail,
7679
+ stdout: currentResult.stdout,
7680
+ stderr: [currentResult.stderr ?? "", resume.detail ?? detail].filter(Boolean).join("\n"),
7681
+ exitCode: 4,
7682
+ };
7683
+ }
7684
+ nextMergeConflictExecuteBudgets = {
7685
+ executionBudgetMs: retryBudget.executionBudgetMs,
7686
+ finalizationBudgetMs: retryBudget.finalizationBudgetMs,
7687
+ };
7444
7688
  onLog?.(
7445
7689
  "stdout",
7446
7690
  `[MergeConflict] Rebase surfaced another conflicted commit after auto-continue; rerunning resolver pass ${
7447
7691
  mergeConflictPass + 1
7448
- }.`,
7692
+ } with a capped completion budget (${retryBudget.executionBudgetMs}ms execution).`,
7449
7693
  );
7450
7694
  continue;
7451
7695
  }
7452
7696
  if (sequencer === "rebase" && !resume.resumed) {
7453
7697
  mergeConflictPass += 1;
7454
- const budget = qualityRevisionBudgetDecision({
7698
+ const budget = mergeConflictResolverRetryBudgetDecision({
7455
7699
  jobElapsedMs: Date.now() - attemptStartedAt,
7456
7700
  executionBudgetMs,
7701
+ finalizationBudgetMs,
7457
7702
  });
7458
7703
  if (mergeConflictPass < MAX_MERGE_CONFLICT_RESOLUTION_PASSES && budget.shouldStart) {
7459
7704
  const retryDetail =
@@ -7470,18 +7715,22 @@ export async function executeJob(
7470
7715
  ]
7471
7716
  .filter(Boolean)
7472
7717
  .join("\n\n");
7718
+ nextMergeConflictExecuteBudgets = {
7719
+ executionBudgetMs: budget.executionBudgetMs,
7720
+ finalizationBudgetMs: budget.finalizationBudgetMs,
7721
+ };
7473
7722
  onLog?.(
7474
7723
  "stdout",
7475
7724
  `[MergeConflict] ${retryDetail}; rerunning resolver pass ${
7476
7725
  mergeConflictPass + 1
7477
- } with focused rebase-completion guidance.`,
7726
+ } with focused rebase-completion guidance and capped budget (${budget.executionBudgetMs}ms execution).`,
7478
7727
  );
7479
7728
  continue;
7480
7729
  }
7481
7730
  if (!budget.shouldStart) {
7482
7731
  onLog?.(
7483
7732
  "stderr",
7484
- `[MergeConflict] Not rerunning unfinished rebase resolver: remaining execution budget is ${budget.remainingBudgetMs}ms (< ${budget.minimumRevisionBudgetMs}ms).`,
7733
+ `[MergeConflict] Not rerunning unfinished rebase resolver: remaining total budget is ${budget.remainingTotalBudgetMs}ms (< ${budget.minimumExecutionBudgetMs}ms execution).`,
7485
7734
  );
7486
7735
  }
7487
7736
  }
@@ -7511,6 +7760,11 @@ export async function executeJob(
7511
7760
  ? parseChangedPathsFromStatus(preQualityStatus.stdout)
7512
7761
  : [];
7513
7762
  const preQualityPublishablePaths = publishableChangedPaths(preQualityChangedPaths);
7763
+ if (preQualityChangedPaths.length > 0) {
7764
+ diagnosticPatchSnapshots.push(
7765
+ buildPatchSnapshotDiagnostics(preQualityChangedPaths, revisionAttempt, "executor"),
7766
+ );
7767
+ }
7514
7768
  const executorText = `${result.summary ?? ""}\n${result.stdout ?? ""}\n${result.stderr ?? ""}`;
7515
7769
  const shellWrapperReturn =
7516
7770
  /shell-wrapper command rejections|command-router shell-wrapper|command policy rejection/i.test(
@@ -7524,13 +7778,24 @@ export async function executeJob(
7524
7778
  "stderr",
7525
7779
  `[QualityGate] ${detail} Skipping ValidationGate/CriticGate because there is no PR-worthy patch to validate.`,
7526
7780
  );
7527
- return {
7781
+ const failure: JobResult = {
7528
7782
  ok: false,
7529
7783
  summary: `Executor produced no publishable code changes (${detail})`,
7530
7784
  stdout: result.stdout,
7531
7785
  stderr: [result.stderr ?? "", detail].filter(Boolean).join("\n"),
7532
7786
  exitCode: 4,
7533
7787
  };
7788
+ return withJobDiagnostics(failure, {
7789
+ terminal: buildTerminalDiagnostics({
7790
+ result: failure,
7791
+ executor,
7792
+ changedPaths: preQualityChangedPaths,
7793
+ terminalStage: "executor",
7794
+ timeoutMs: executionBudgetMs,
7795
+ metadata: { revisionAttempt, executorElapsedMs },
7796
+ }),
7797
+ patchSnapshots: [...diagnosticPatchSnapshots],
7798
+ });
7534
7799
  }
7535
7800
  if (
7536
7801
  preQualityPublishablePaths.length === 0 &&
@@ -7544,13 +7809,24 @@ export async function executeJob(
7544
7809
  "stderr",
7545
7810
  `[QualityGate] ${reason} Skipping ValidationGate/CriticGate and failing fast.`,
7546
7811
  );
7547
- return {
7812
+ const failure: JobResult = {
7548
7813
  ok: false,
7549
7814
  summary: reason,
7550
7815
  stdout: result.stdout,
7551
7816
  stderr: [result.stderr ?? "", reason].filter(Boolean).join("\n"),
7552
7817
  exitCode: 4,
7553
7818
  };
7819
+ return withJobDiagnostics(failure, {
7820
+ terminal: buildTerminalDiagnostics({
7821
+ result: failure,
7822
+ executor,
7823
+ changedPaths: preQualityChangedPaths,
7824
+ terminalStage: "executor",
7825
+ timeoutMs: executionBudgetMs,
7826
+ metadata: { revisionAttempt, executorElapsedMs, shellWrapperReturn },
7827
+ }),
7828
+ patchSnapshots: [...diagnosticPatchSnapshots],
7829
+ });
7554
7830
  }
7555
7831
 
7556
7832
  const qualityStartedAt = Date.now();
@@ -7566,6 +7842,12 @@ export async function executeJob(
7566
7842
  },
7567
7843
  );
7568
7844
  const qualityElapsedMs = Date.now() - qualityStartedAt;
7845
+ diagnosticPatchSnapshots.push(
7846
+ buildPatchSnapshotDiagnostics(quality.changedPaths, revisionAttempt, "quality"),
7847
+ );
7848
+ diagnosticValidationRuns.push(
7849
+ ...buildValidationRunDiagnostics(quality.validationRuns, revisionAttempt),
7850
+ );
7569
7851
  const validationCommandElapsedMs = quality.validationRuns.reduce(
7570
7852
  (total, run) => total + Math.max(0, Number(run.elapsedMs) || 0),
7571
7853
  0,
@@ -7626,6 +7908,30 @@ export async function executeJob(
7626
7908
  : executor === "openai_codex"
7627
7909
  ? await runCodexCriticReview(repo, attemptParams, qualityForCritic, runtimeConfig, onLog)
7628
7910
  : await runTaskCriticReview(repo, attemptParams, qualityForCritic, runtimeConfig, onLog);
7911
+ const annotateTerminalResult = (
7912
+ terminalResult: JobResult,
7913
+ terminalStage: string,
7914
+ changedPaths: string[] = quality.changedPaths,
7915
+ ): JobResult =>
7916
+ withJobDiagnostics(terminalResult, {
7917
+ terminal: buildTerminalDiagnostics({
7918
+ result: terminalResult,
7919
+ executor,
7920
+ changedPaths,
7921
+ terminalStage,
7922
+ timeoutMs: executionBudgetMs,
7923
+ metadata: {
7924
+ revisionAttempt,
7925
+ executorElapsedMs,
7926
+ qualityElapsedMs,
7927
+ validationFailureScope: quality.validationFailureScope,
7928
+ validationRuns: quality.validationRuns.length,
7929
+ criticScore: critic?.score ?? null,
7930
+ },
7931
+ }),
7932
+ validationRuns: [...diagnosticValidationRuns],
7933
+ patchSnapshots: [...diagnosticPatchSnapshots],
7934
+ });
7629
7935
  if (!qualityGatePolicy.criticGateEnabled) {
7630
7936
  onLog?.("stdout", "[CriticGate] Disabled by workerpals.quality_critic_gate_enabled=false.");
7631
7937
  } else if (skipCriticAfterExecutorTimeout) {
@@ -7685,7 +7991,7 @@ export async function executeJob(
7685
7991
  "stderr",
7686
7992
  "[PublishGate] Disabled by workerpals.quality_publish_gate_enabled=false; returning worker result despite gate failures.",
7687
7993
  );
7688
- return {
7994
+ const advisoryResult: JobResult = {
7689
7995
  ...result,
7690
7996
  summary: `${result.summary} (publish gate disabled; quality gate findings were advisory)`,
7691
7997
  stderr: truncate(
@@ -7700,6 +8006,7 @@ export async function executeJob(
7700
8006
  ),
7701
8007
  exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
7702
8008
  };
8009
+ return annotateTerminalResult(advisoryResult, "quality");
7703
8010
  }
7704
8011
 
7705
8012
  if (!deterministicRequiresRevision && !criticRequiresRevision) {
@@ -7718,13 +8025,14 @@ export async function executeJob(
7718
8025
  outputPolicyForRuntime(runtimeConfig),
7719
8026
  );
7720
8027
  onLog?.("stderr", `[QualityGate] ${requiredSummary}`);
7721
- return {
8028
+ const failure: JobResult = {
7722
8029
  ok: false,
7723
8030
  summary: requiredSummary,
7724
8031
  stdout: result.stdout,
7725
8032
  stderr: diagnostics,
7726
8033
  exitCode: 4,
7727
8034
  };
8035
+ return annotateTerminalResult(failure, "validation");
7728
8036
  }
7729
8037
  if (critic) {
7730
8038
  onLog?.(
@@ -7732,7 +8040,7 @@ export async function executeJob(
7732
8040
  `[CriticGate] review score ${critic.score.toFixed(1)}/10 (threshold ${qualityCriticMinScore}).`,
7733
8041
  );
7734
8042
  }
7735
- return result;
8043
+ return annotateTerminalResult(result, "completed");
7736
8044
  }
7737
8045
 
7738
8046
  const blockerIssue = quality.blocker
@@ -7792,13 +8100,14 @@ export async function executeJob(
7792
8100
  } else if (quality.requiredValidationFailures.length > 0) {
7793
8101
  const requiredSummary = `Required vision.md validation blocked publishing: ${quality.requiredValidationFailures.join("; ")}`;
7794
8102
  onLog?.("stderr", `[QualityGate] ${requiredSummary}`);
7795
- return {
8103
+ const failure: JobResult = {
7796
8104
  ok: false,
7797
8105
  summary: requiredSummary,
7798
8106
  stdout: result.stdout,
7799
8107
  stderr: blockerDiagnostics,
7800
8108
  exitCode: 4,
7801
8109
  };
8110
+ return annotateTerminalResult(failure, "validation");
7802
8111
  } else if (shouldSoftPassValidationBlocker(qualityGatePolicy, quality.blocker)) {
7803
8112
  onLog?.(
7804
8113
  "stderr",
@@ -7807,7 +8116,7 @@ export async function executeJob(
7807
8116
  260,
7808
8117
  )}`,
7809
8118
  );
7810
- return {
8119
+ const softPass: JobResult = {
7811
8120
  ...result,
7812
8121
  summary:
7813
8122
  `${result.summary} ` +
@@ -7815,15 +8124,17 @@ export async function executeJob(
7815
8124
  stderr: blockerDiagnostics,
7816
8125
  exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
7817
8126
  };
8127
+ return annotateTerminalResult(softPass, "quality");
7818
8128
  } else {
7819
8129
  onLog?.("stderr", `[QualityGate] ${blockerSummary}`);
7820
- return {
8130
+ const failure: JobResult = {
7821
8131
  ok: false,
7822
8132
  summary: blockerSummary,
7823
8133
  stdout: result.stdout,
7824
8134
  stderr: blockerDiagnostics,
7825
8135
  exitCode: 4,
7826
8136
  };
8137
+ return annotateTerminalResult(failure, "quality");
7827
8138
  }
7828
8139
  }
7829
8140
  if (revisionAttempt >= activeMaxAutoRevisions) {
@@ -7840,13 +8151,14 @@ export async function executeJob(
7840
8151
  );
7841
8152
  const requiredSummary = `Required vision.md validation failed after ${revisionAttempt} auto-revision attempt(s): ${quality.requiredValidationFailures.join("; ")}`;
7842
8153
  onLog?.("stderr", `[QualityGate] ${requiredSummary}`);
7843
- return {
8154
+ const failure: JobResult = {
7844
8155
  ok: false,
7845
8156
  summary: requiredSummary,
7846
8157
  stdout: result.stdout,
7847
8158
  stderr: diagnostics,
7848
8159
  exitCode: 4,
7849
8160
  };
8161
+ return annotateTerminalResult(failure, "validation");
7850
8162
  }
7851
8163
  if (qualitySoftPassOnExhausted) {
7852
8164
  const diagnostics = truncate(
@@ -7862,14 +8174,15 @@ export async function executeJob(
7862
8174
  260,
7863
8175
  )}`,
7864
8176
  );
7865
- return {
8177
+ const softPass: JobResult = {
7866
8178
  ...result,
7867
8179
  summary: `${result.summary} (quality gate soft-pass after ${revisionAttempt} auto-revision attempt(s))`,
7868
8180
  stderr: diagnostics,
7869
8181
  exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
7870
8182
  };
8183
+ return annotateTerminalResult(softPass, "quality");
7871
8184
  }
7872
- return {
8185
+ const failure: JobResult = {
7873
8186
  ok: false,
7874
8187
  summary: `Quality gate failed after ${revisionAttempt} auto-revision attempt(s): ${toSingleLine(
7875
8188
  issueSummary,
@@ -7884,6 +8197,7 @@ export async function executeJob(
7884
8197
  ),
7885
8198
  exitCode: 4,
7886
8199
  };
8200
+ return annotateTerminalResult(failure, "quality");
7887
8201
  }
7888
8202
 
7889
8203
  const revisionBudget = qualityRevisionBudgetDecision({
@@ -7900,7 +8214,7 @@ export async function executeJob(
7900
8214
  220,
7901
8215
  )}`;
7902
8216
  onLog?.("stderr", `[QualityGate] ${budgetSummary}`);
7903
- return {
8217
+ const failure: JobResult = {
7904
8218
  ok: false,
7905
8219
  summary: budgetSummary,
7906
8220
  stdout: result.stdout,
@@ -7916,6 +8230,7 @@ export async function executeJob(
7916
8230
  ),
7917
8231
  exitCode: 4,
7918
8232
  };
8233
+ return annotateTerminalResult(failure, "quality");
7919
8234
  }
7920
8235
 
7921
8236
  revisionAttempt += 1;
@@ -19,6 +19,7 @@
19
19
  import { executeJob, shouldCommit, createJobCommit } from "./execute_job.js";
20
20
  import { loadPushPalsConfig } from "shared";
21
21
  import { writeFileSync } from "fs";
22
+ import type { JobDiagnostics } from "./common/types.js";
22
23
  import {
23
24
  applyMergeConflictExecutionHints,
24
25
  isMergeConflictResolutionParams,
@@ -55,6 +56,7 @@ interface JobResult {
55
56
  sha: string;
56
57
  stage: "sync" | "push";
57
58
  };
59
+ diagnostics?: JobDiagnostics;
58
60
  }
59
61
 
60
62
  // ─── Logging helpers ────────────────────────────────────────────────────────
@@ -183,6 +185,7 @@ async function main(): Promise<void> {
183
185
  stdout: result.stdout,
184
186
  stderr: result.stderr,
185
187
  exitCode: result.exitCode,
188
+ diagnostics: result.diagnostics,
186
189
  };
187
190
  // Create commit for file-modifying jobs
188
191
  if (result.ok && shouldCommit(spec.kind, CONFIG)) {