session-orchestrator 3.20.0 → 3.21.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 (114) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +10 -8
  5. package/CHANGELOG.md +414 -0
  6. package/README.md +16 -11
  7. package/agents/analyst.md +1 -1
  8. package/agents/architect-reviewer.md +1 -1
  9. package/agents/code-implementer.md +4 -2
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/dialectic-deriver.md +1 -1
  12. package/agents/docs-writer.md +1 -1
  13. package/agents/memory-proposal-collector.md +1 -1
  14. package/agents/qa-strategist.md +1 -1
  15. package/agents/security-reviewer.md +1 -1
  16. package/agents/session-reviewer.md +42 -1
  17. package/agents/skill-applied-judge.md +1 -1
  18. package/agents/test-writer.md +1 -1
  19. package/agents/ui-developer.md +1 -1
  20. package/agents/ux-evaluator.md +1 -1
  21. package/commands/release.md +60 -0
  22. package/docs/migration-v3.md +9 -6
  23. package/docs/persona-panel.md +3 -1
  24. package/docs/scope-collision-guard.md +167 -0
  25. package/hooks/enforce-scope.mjs +103 -3
  26. package/hooks/hooks-codex.json +1 -1
  27. package/hooks/hooks.json +11 -1
  28. package/hooks/on-session-end.mjs +52 -5
  29. package/hooks/on-stop.mjs +127 -12
  30. package/hooks/post-bash-write-verify.mjs +8 -32
  31. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  32. package/package.json +1 -1
  33. package/pi/prompts/release.md +12 -0
  34. package/scripts/autopilot.mjs +3 -1
  35. package/scripts/emit-session.mjs +42 -0
  36. package/scripts/export-hw-learnings.mjs +61 -2
  37. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  38. package/scripts/lib/backlog-scan.mjs +106 -15
  39. package/scripts/lib/build-live-signals.mjs +7 -3
  40. package/scripts/lib/ci-status-banner.mjs +207 -23
  41. package/scripts/lib/git-config-drift.mjs +471 -0
  42. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  43. package/scripts/lib/io.mjs +193 -7
  44. package/scripts/lib/learnings/select.mjs +157 -3
  45. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  46. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  47. package/scripts/lib/named-vault-resolver.mjs +105 -16
  48. package/scripts/lib/peer-cards/schema.mjs +6 -2
  49. package/scripts/lib/reconcile/writer.mjs +69 -30
  50. package/scripts/lib/redact-spans.mjs +89 -0
  51. package/scripts/lib/scope-baseline.mjs +77 -17
  52. package/scripts/lib/scope-gate.mjs +658 -0
  53. package/scripts/lib/secret-masker.mjs +262 -0
  54. package/scripts/lib/session-lock.mjs +34 -10
  55. package/scripts/lib/session-registry.mjs +9 -1
  56. package/scripts/lib/state-md/mission-status.mjs +21 -12
  57. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  58. package/scripts/lib/validate/check-agents.mjs +77 -5
  59. package/scripts/lib/validate/check-commands.mjs +2 -20
  60. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  61. package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
  62. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  63. package/scripts/lib/validate/check-skills.mjs +191 -0
  64. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  65. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  66. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  67. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  68. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  69. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  70. package/scripts/lib/validate/tier-inference.mjs +46 -8
  71. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  72. package/scripts/lib/vault-mirror/process.mjs +264 -31
  73. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  74. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  75. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  76. package/scripts/lib/vcs-repo-spec.mjs +500 -19
  77. package/scripts/print-learnings-index.mjs +30 -3
  78. package/scripts/release.mjs +616 -61
  79. package/scripts/site-numbers.mjs +1049 -0
  80. package/scripts/validate-plugin.mjs +61 -0
  81. package/scripts/validate-wave-scope.mjs +281 -12
  82. package/scripts/vault-mirror.mjs +26 -1
  83. package/skills/_shared/monitor-patterns.md +24 -4
  84. package/skills/_shared/state-ownership.md +17 -0
  85. package/skills/claude-md-drift-check/SKILL.md +9 -1
  86. package/skills/debug/SKILL.md +4 -1
  87. package/skills/discovery/issue-templates.md +4 -4
  88. package/skills/discovery/probes-code.md +2 -2
  89. package/skills/discovery/probes-feature.md +6 -6
  90. package/skills/discovery/probes-infra.md +2 -2
  91. package/skills/discovery/probes-session.md +5 -5
  92. package/skills/dispatcher/SKILL.md +10 -1
  93. package/skills/frontmatter-guard/SKILL.md +9 -1
  94. package/skills/gitlab-ops/SKILL.md +49 -39
  95. package/skills/gitlab-portfolio/SKILL.md +10 -1
  96. package/skills/memory-cleanup/SKILL.md +18 -5
  97. package/skills/npm-publish/SKILL.md +22 -50
  98. package/skills/persona-panel/SKILL.md +3 -1
  99. package/skills/plan/mode-new.md +23 -5
  100. package/skills/repo-audit/SKILL.md +10 -1
  101. package/skills/session-end/SKILL.md +44 -2
  102. package/skills/session-end/metrics-collection.md +1 -1
  103. package/skills/session-end/session-metrics-write.md +4 -10
  104. package/skills/session-plan/SKILL.md +2 -2
  105. package/skills/session-plan/wave-template.md +1 -1
  106. package/skills/session-start/SKILL.md +13 -1
  107. package/skills/spinout/SKILL.md +5 -1
  108. package/skills/sunset-review/SKILL.md +11 -1
  109. package/skills/tmux-layout/SKILL.md +7 -2
  110. package/skills/vault-mirror/SKILL.md +10 -1
  111. package/skills/vault-sync/SKILL.md +10 -1
  112. package/skills/vault-sync/validator.mjs +55 -6
  113. package/skills/wave-executor/wave-loop.md +42 -12
  114. package/scripts/lib/mission-status-schema.mjs +0 -114
@@ -7,12 +7,21 @@
7
7
  * no-op condition (no VCS, CLI missing, timeout, parse failure).
8
8
  *
9
9
  * Supports GitLab (via glab) and GitHub (via gh).
10
- * VCS is auto-detected from git remote origin URL per gitlab-ops canonical logic.
10
+ * VCS is auto-detected from the repo's git remotes via
11
+ * `vcs-repo-spec.mjs`'s `detectVcsFamily` — NOT from a hard-coded `origin`
12
+ * lookup, which left the banner structurally dark in every repo whose remotes
13
+ * are named `gitlab`/`github` (#1039).
11
14
  */
12
15
 
13
16
  import { execFile as _execFile } from 'node:child_process';
14
17
  import { promisify } from 'node:util';
15
- import { resolveRepoSpec, resolveRepoHost } from './vcs-repo-spec.mjs';
18
+ import {
19
+ resolveRepoSpec,
20
+ resolveRepoHost,
21
+ redactUrlCredentials,
22
+ detectVcsFamily,
23
+ isQueryFailure,
24
+ } from './vcs-repo-spec.mjs';
16
25
 
17
26
  const execFileAsync = promisify(_execFile);
18
27
 
@@ -38,24 +47,130 @@ async function execWithTimeout(cmd, args, opts = {}) {
38
47
  }
39
48
 
40
49
  /**
41
- * Detect VCS from git remote URL.
42
- * Returns 'github' | 'gitlab'. Throws if git is unavailable or no origin.
50
+ * The one failure this probe can produce that the frozen
51
+ * `REMOTE_RESOLUTION_REASONS` set has no member for: the async timeout race
52
+ * every subprocess here runs under has no counterpart in the SYNCHRONOUS
53
+ * `vcs-repo-spec.mjs` core (it uses `execFileSync`, which cannot time out).
54
+ *
55
+ * Deliberately NOT a query-failure for warning purposes — a timeout stays
56
+ * SILENT, matching how `checkCiStatus`'s outer catch has always treated one
57
+ * (`msg === 'timeout'` → silent `null`). Warning on a hung `git` but not on a
58
+ * hung `glab` would be an inconsistency inside a single banner, and a hang is
59
+ * not a fact an operator can act on the way "git is not installed" is.
60
+ */
61
+ const PROBE_TIMEOUT = 'probe-timeout';
62
+
63
+ /**
64
+ * The ONE `isQueryFailure` member this banner deliberately keeps silent.
65
+ *
66
+ * `vcs-repo-spec.mjs` classifies `not-a-git-repo` as a query failure, and that
67
+ * is correct IN ITS DOMAIN: a remote-resolution helper genuinely could not
68
+ * enumerate remotes. For THIS consumer the same fact is a benign absence —
69
+ * session-start Phase 4 asking a non-repo directory about its CI has a complete
70
+ * and unsurprising answer ("there is no CI here"), and an operator cannot act on
71
+ * being told their directory is not a git repo. Warning would print on every
72
+ * such session-start and train exactly the reflex that makes the
73
+ * `git-unavailable` / `git-error` lines worthless.
74
+ *
75
+ * Expressed as a SUBTRACTION from `isQueryFailure` rather than as a hard-coded
76
+ * warn-list, so a reason added to the frozen set upstream is warned by default
77
+ * (fail-toward-visible) instead of silently inheriting the benign path.
78
+ *
79
+ * @type {ReadonlySet<string>}
80
+ */
81
+ const SILENT_QUERY_FAILURES = new Set(['not-a-git-repo']);
82
+
83
+ /**
84
+ * Classify a rejected ASYNC `execFile` into a `REMOTE_RESOLUTION_REASONS`
85
+ * member.
86
+ *
87
+ * This cannot reuse `vcs-repo-spec.mjs`'s own (unexported) `classifyGitFailure`
88
+ * because that one reads an `execFileSync` result, where the spawn errno lands
89
+ * in `err.code` and the child's exit status in `err.status`. Async `execFile`
90
+ * folds BOTH into `err.code`, discriminated only by TYPE: the string `'ENOENT'`
91
+ * when the binary is not on PATH, the number `128` when git ran and rejected
92
+ * the path as a work tree. Same taxonomy, different carrier.
93
+ *
94
+ * @param {unknown} err
95
+ * @returns {'git-unavailable'|'not-a-git-repo'|'git-error'}
96
+ */
97
+ function classifyGitProbeFailure(err) {
98
+ const code = err && typeof err === 'object' ? /** @type {any} */ (err).code : undefined;
99
+ if (code === 'ENOENT') return 'git-unavailable';
100
+ // `git remote -v` exits 128 for "not a git repository" — and, unlike the
101
+ // `remote get-url origin` call this replaced, NOT for "no such remote":
102
+ // a repo with zero remotes exits 0 with empty stdout. That is exactly the
103
+ // split that makes `no-remotes` (benign absence) reportable at all.
104
+ if (code === 128) return 'not-a-git-repo';
105
+ return 'git-error';
106
+ }
107
+
108
+ /**
109
+ * Detect the repo's VCS family from its git remotes.
110
+ *
111
+ * **Never throws.** Returns a discriminated result carrying a reason from
112
+ * `vcs-repo-spec.mjs`'s frozen `REMOTE_RESOLUTION_REASONS` (plus
113
+ * {@link PROBE_TIMEOUT}), so the caller can separate "I could not ask"
114
+ * (`isQueryFailure` → WARN) from "I asked; this repo has no VCS remote"
115
+ * (benign, silent). This is the `mirror-issues-banner.mjs` shape — a third
116
+ * state beyond `value | null` — applied to the last unmigrated path in this
117
+ * module (#1039).
118
+ *
119
+ * What it replaces, and why both halves were defects:
120
+ * 1. `git remote get-url origin` — a hard-coded remote NAME. In a repo whose
121
+ * remotes are `gitlab` + `github` (no `origin`) the call failed, the
122
+ * caller swallowed it to `null`, and the banner was structurally dark:
123
+ * it could never report, not even on a red pipeline.
124
+ * 2. `remoteUrl.includes('github.com') ? 'github' : 'gitlab'` — a substring
125
+ * test that classifies GitHub Enterprise (`git@github.example.com:o/r`)
126
+ * as gitlab and then drives `glab` at a GitHub instance.
127
+ * `detectVcsFamily` fixes both: preference-ordered remote selection, and
128
+ * host-prefix classification (`github.*` / `gitlab.*`) ahead of remote-name.
129
+ *
130
+ * The `git remote -v` spawn stays HERE rather than inside `detectVcsFamily`
131
+ * because this module owes every subprocess a timeout race — the shared core
132
+ * is synchronous by design. So the output is captured once and handed to the
133
+ * shared classifier through its `gitRun` seam: one spawn, one taxonomy, no
134
+ * second copy of the parsing or the credential strip.
43
135
  *
44
136
  * @param {string} repoRoot
45
137
  * @param {{ execFile?: Function, timeoutMs?: number }} deps
46
- * @returns {Promise<'github'|'gitlab'>}
138
+ * @returns {Promise<{ ok: true, vcs: 'github'|'gitlab' }
139
+ * | { ok: false, reason: string, stderr?: string }>}
47
140
  */
48
141
  async function detectVcs(repoRoot, deps = {}) {
49
142
  // Use the smaller of 2000ms or the caller-supplied timeout so that a short
50
143
  // test-level timeout is still respected here.
51
144
  const gitTimeout = Math.min(2000, deps.timeoutMs ?? 2000);
52
- const result = await execWithTimeout(
53
- 'git',
54
- ['remote', 'get-url', 'origin'],
55
- { cwd: repoRoot, timeoutMs: gitTimeout, execFile: deps.execFile },
56
- );
57
- const remoteUrl = result.stdout.trim();
58
- return remoteUrl.includes('github.com') ? 'github' : 'gitlab';
145
+
146
+ let stdout;
147
+ try {
148
+ const result = await execWithTimeout(
149
+ 'git',
150
+ ['remote', '-v'],
151
+ { cwd: repoRoot, timeoutMs: gitTimeout, execFile: deps.execFile },
152
+ );
153
+ stdout = String(result?.stdout ?? '');
154
+ } catch (err) {
155
+ const msg = err instanceof Error ? err.message : String(err ?? '');
156
+ if (msg === 'timeout') return { ok: false, reason: PROBE_TIMEOUT };
157
+ const stderr = err && typeof err === 'object' ? String(/** @type {any} */ (err).stderr ?? '') : '';
158
+ return { ok: false, reason: classifyGitProbeFailure(err), stderr };
159
+ }
160
+
161
+ // Replay the captured output through the shared core's `gitRun` seam. The
162
+ // arg guard is insurance, not decoration: `detectVcsFamily` asks only for
163
+ // `remote -v` today, and if it ever grows a second query the guard turns a
164
+ // silently-wrong answer (built from the WRONG command's output) into a
165
+ // reported `git-error`.
166
+ const replayGitRun = (args) =>
167
+ Array.isArray(args) && args.at(-2) === 'remote' && args.at(-1) === '-v'
168
+ ? { ok: true, stdout, stderr: '', status: 0 }
169
+ : { ok: false, stdout: '', stderr: `unsupported git query in ci-status probe: ${String(args)}`, status: 1 };
170
+
171
+ const family = detectVcsFamily({ repoRoot, gitRun: replayGitRun });
172
+ if (!family.ok) return { ok: false, reason: family.reason, stderr: family.stderr };
173
+ return { ok: true, vcs: family.vcs };
59
174
  }
60
175
 
61
176
  /**
@@ -308,9 +423,25 @@ async function checkGitlab(repoRoot, now, deps = {}) {
308
423
  /**
309
424
  * GitHub CI status check (v1 — red/green only; lastGreen not implemented).
310
425
  *
311
- * #872: pins the `gh repo view` lookup to `deps.repoSpec` via `-R` when
312
- * resolved (host-pinning — `gh repo view` otherwise falls back to the
313
- * ambient `GH_HOST`).
426
+ * #872/#1022: pins the `gh repo view` lookup to `deps.repoSpec` when resolved
427
+ * (host-pinning — `gh repo view` otherwise falls back to the ambient
428
+ * `GH_HOST`). The spec is passed POSITIONALLY, not via `-R`: `gh repo view`
429
+ * takes `[<repository>]` as a positional argument and has no `-R`/`--repo`
430
+ * flag at all, so `-R` made gh exit 1 with `unknown shorthand flag: 'R'` —
431
+ * an error `checkCiStatus`'s outer catch swallowed to `null`, leaving the
432
+ * Phase 4 banner silently dead on EVERY GitHub repo (#1022). The
433
+ * `[HOST/]OWNER/REPO` shape `resolveRepoSpec({ vcs: 'github' })` returns is
434
+ * exactly the positional's documented input format.
435
+ *
436
+ * The asymmetry with `getGlabProjectId` is real and deliberate: `glab repo
437
+ * view` DOES accept `-R`, and `gh api`/`glab api` accept neither `-R` nor a
438
+ * positional — only `--hostname`. Do not unify these three call sites.
439
+ *
440
+ * `nameWithOwner` is NOT derivable from `deps.repoSpec`, so this lookup
441
+ * cannot be dropped: the spec carries a HOST prefix the `repos/<owner>/<repo>`
442
+ * API path must not contain, it is `undefined` whenever no remote resolves
443
+ * (cross-family guard, unsafe-argv guard), and `normalizeGithubSpec` falls
444
+ * back to the raw URL on an unrecognised remote shape.
314
445
  *
315
446
  * @param {string} repoRoot
316
447
  * @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string, repoHost?: string }} deps
@@ -318,8 +449,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
318
449
  */
319
450
  async function checkGithub(repoRoot, deps = {}) {
320
451
  // Resolve owner/repo from gh to keep the API path generic.
321
- const repoViewArgs = ['repo', 'view', '--json', 'nameWithOwner'];
322
- if (deps.repoSpec) repoViewArgs.push('-R', deps.repoSpec);
452
+ const repoViewArgs = ['repo', 'view'];
453
+ if (deps.repoSpec) repoViewArgs.push(deps.repoSpec);
454
+ repoViewArgs.push('--json', 'nameWithOwner');
323
455
  const repoViewResult = await execWithTimeout(
324
456
  'gh',
325
457
  repoViewArgs,
@@ -387,11 +519,17 @@ async function checkGithub(repoRoot, deps = {}) {
387
519
  * Checks CI status for the current HEAD commit.
388
520
  *
389
521
  * Returns `null` (silent no-op) when:
390
- * - Not in a VCS repo (no git origin)
522
+ * - The repo has no usable VCS remote (not a git repo, no remotes at all,
523
+ * or >= 2 remotes with no preference match) — a benign, measured absence
391
524
  * - Required CLI (glab / gh) not in PATH
392
525
  * - Any CLI invocation times out
393
526
  * - JSON parse failure on CLI output
394
527
  *
528
+ * Also returns `null`, but with a `console.warn` trace, when the VCS-detection
529
+ * QUERY ITSELF failed (`git` not on PATH, `git remote -v` erroring) or when a
530
+ * present CLI rejected its invocation. `null` alone cannot express "could not
531
+ * read" — see the outer catch and Step 1 for why the warn channel carries it.
532
+ *
395
533
  * @param {{
396
534
  * repoRoot?: string,
397
535
  * vcs?: 'gitlab'|'github',
@@ -441,12 +579,30 @@ export async function checkCiStatus(opts = {}, deps = {}) {
441
579
  // Step 1: detect VCS (or use forced value).
442
580
  let vcs = forcedVcs;
443
581
  if (!vcs) {
444
- try {
445
- vcs = await detectVcs(repoRoot, depsWithExec);
446
- } catch {
447
- // No git remote → not in a VCS repo silent no-op.
582
+ const detected = await detectVcs(repoRoot, depsWithExec);
583
+ if (!detected.ok) {
584
+ // The two-state `null` return is fixed by 13 sibling banners, so the
585
+ // third state lives in the WARN channel: an ABSENCE (`no-remotes`,
586
+ // `no-matching-remote`, `unsafe-value`) is a real, benign answer and
587
+ // stays silent — warning there would train operators to ignore this
588
+ // line, which is the more expensive error. A QUERY FAILURE
589
+ // (`git-unavailable`, `git-error`) means the question could not be
590
+ // asked at all, and that is precisely the state that was
591
+ // indistinguishable from "nothing to report" before #1039.
592
+ // `not-a-git-repo` is the one query failure this banner reads as an
593
+ // absence — see {@link SILENT_QUERY_FAILURES}.
594
+ if (isQueryFailure(detected.reason) && !SILENT_QUERY_FAILURES.has(detected.reason)) {
595
+ const detail = detected.stderr
596
+ ? ` — ${redactUrlCredentials(detected.stderr).trim()}`
597
+ : '';
598
+ console.warn(
599
+ `WARN ci-status-banner: VCS detection failed (${detected.reason}), banner suppressed — ` +
600
+ `CI state is UNKNOWN, not "green".${detail}`,
601
+ );
602
+ }
448
603
  return null;
449
604
  }
605
+ vcs = detected.vcs;
450
606
  }
451
607
 
452
608
  // Step 1b (#872): resolve the -R/--hostname host-pinning spec ONCE per
@@ -482,7 +638,35 @@ export async function checkCiStatus(opts = {}, deps = {}) {
482
638
  return null;
483
639
  }
484
640
 
485
- // Unexpected errors also silent null to keep the banner non-blocking.
641
+ // Everything else means the CLI was PRESENT but the invocation failed —
642
+ // non-zero exit, rejected flag, unparseable output. Returning a bare null
643
+ // here makes that state indistinguishable from "CLI not installed", which
644
+ // is exactly how #1022 (`gh repo view -R` → `unknown shorthand flag: 'R'`)
645
+ // stayed invisible on every GitHub repo. THIS module keeps the two-state
646
+ // contract (null ⇒ silent no-op) for backwards compatibility with the
647
+ // Phase-4 callers already written against it, so leave a stderr trace
648
+ // instead: non-blocking, but the next defect of this class is no longer
649
+ // silent. Credentials redacted defense-in-depth (#907) — the message can
650
+ // quote the failed argv, which carries the repo spec.
651
+ //
652
+ // DIRECTION FOR NEW BANNERS — do not copy this shape. The
653
+ // absence-preserving form is `scripts/lib/mirror-issues-banner.mjs`: a
654
+ // THIRD return state `{ severity, message, degraded }` whose `degraded`
655
+ // is a member of the closed `DEGRADED_REASONS` enum exported there, so
656
+ // "could not read" stays distinguishable from "read, and clean".
657
+ //
658
+ // The REMOTE probe (`detectVcs`) has since been pulled onto that shape
659
+ // (#1039): it returns a reason from the frozen `REMOTE_RESOLUTION_REASONS`
660
+ // set and Step 1 branches on `isQueryFailure` — absence silent, query
661
+ // failure warned. This outer catch is what remains unmigrated: it still
662
+ // collapses every CLI-side failure onto `null`, which a caller reads as
663
+ // all-clear, and it cannot be widened without changing a return contract
664
+ // 13 sibling banners share. Same verdict, stated caller-side, in
665
+ // `skills/session-start/SKILL.md` § Phase 4 (the mirror-issues
666
+ // paragraph): "Do not reproduce it."
667
+ console.warn(
668
+ `WARN ci-status-banner: CI status check failed, banner suppressed — ${redactUrlCredentials(msg)}`,
669
+ );
486
670
  return null;
487
671
  }
488
672
  }