session-orchestrator 3.20.0 → 3.22.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 (202) 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/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/030-wave-execution.mdc +10 -8
  6. package/.cursor/rules/040-discovery.mdc +6 -6
  7. package/.cursor/rules/050-plan.mdc +8 -8
  8. package/CHANGELOG.md +515 -0
  9. package/README.md +16 -11
  10. package/agents/analyst.md +1 -1
  11. package/agents/architect-reviewer.md +1 -1
  12. package/agents/code-implementer.md +4 -2
  13. package/agents/db-specialist.md +1 -1
  14. package/agents/dialectic-deriver.md +1 -1
  15. package/agents/docs-writer.md +1 -1
  16. package/agents/memory-proposal-collector.md +7 -5
  17. package/agents/qa-strategist.md +1 -1
  18. package/agents/security-reviewer.md +1 -1
  19. package/agents/session-reviewer.md +42 -1
  20. package/agents/skill-applied-judge.md +1 -1
  21. package/agents/test-writer.md +1 -1
  22. package/agents/ui-developer.md +1 -1
  23. package/agents/ux-evaluator.md +1 -1
  24. package/commands/eli5.md +33 -0
  25. package/commands/release.md +62 -0
  26. package/commands/test.md +2 -2
  27. package/docs/components.md +6 -5
  28. package/docs/migration-v3.md +9 -6
  29. package/docs/persona-panel.md +3 -1
  30. package/docs/scope-collision-guard.md +167 -0
  31. package/docs/session-config-reference.md +31 -8
  32. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  33. package/hooks/enforce-scope.mjs +103 -3
  34. package/hooks/hooks-codex.json +1 -1
  35. package/hooks/hooks.json +21 -1
  36. package/hooks/on-session-end.mjs +76 -97
  37. package/hooks/on-session-start.mjs +195 -104
  38. package/hooks/on-stop.mjs +127 -12
  39. package/hooks/post-bash-write-verify.mjs +8 -32
  40. package/hooks/pre-auq-clarity.mjs +787 -0
  41. package/hooks/pre-bash-issue-budget.mjs +17 -18
  42. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  43. package/package.json +3 -1
  44. package/pi/prompts/eli5.md +12 -0
  45. package/pi/prompts/release.md +12 -0
  46. package/scripts/auq-audit.mjs +825 -0
  47. package/scripts/autopilot.mjs +10 -9
  48. package/scripts/emit-session.mjs +42 -0
  49. package/scripts/export-hw-learnings.mjs +61 -2
  50. package/scripts/lib/auq/clarity.mjs +1314 -0
  51. package/scripts/lib/auq/parse.mjs +1006 -0
  52. package/scripts/lib/auq/schema.mjs +1457 -0
  53. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  54. package/scripts/lib/backlog-scan.mjs +106 -15
  55. package/scripts/lib/build-live-signals.mjs +7 -3
  56. package/scripts/lib/ci-status-banner.mjs +267 -77
  57. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  58. package/scripts/lib/config/vault-integration.mjs +12 -1
  59. package/scripts/lib/dispatcher/rank.mjs +4 -7
  60. package/scripts/lib/gates/gate-full.mjs +3 -3
  61. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  62. package/scripts/lib/git-config-drift.mjs +471 -0
  63. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  64. package/scripts/lib/io.mjs +432 -7
  65. package/scripts/lib/issue-budget.mjs +63 -9
  66. package/scripts/lib/learnings/select.mjs +157 -3
  67. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  68. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  69. package/scripts/lib/named-vault-resolver.mjs +105 -16
  70. package/scripts/lib/owner-interview.mjs +78 -32
  71. package/scripts/lib/peer-cards/schema.mjs +6 -2
  72. package/scripts/lib/peer-discovery.mjs +73 -22
  73. package/scripts/lib/project-hygiene.mjs +64 -4
  74. package/scripts/lib/reconcile/renderer.mjs +17 -4
  75. package/scripts/lib/reconcile/writer.mjs +69 -30
  76. package/scripts/lib/redact-spans.mjs +89 -0
  77. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  78. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  79. package/scripts/lib/resource-probe.mjs +18 -2
  80. package/scripts/lib/scope-baseline.mjs +77 -17
  81. package/scripts/lib/scope-gate.mjs +658 -0
  82. package/scripts/lib/secret-masker.mjs +262 -0
  83. package/scripts/lib/session-lock.mjs +34 -10
  84. package/scripts/lib/session-registry.mjs +9 -1
  85. package/scripts/lib/spiral-carryover.mjs +23 -2
  86. package/scripts/lib/state-md/mission-status.mjs +164 -58
  87. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  88. package/scripts/lib/validate/check-agents.mjs +77 -5
  89. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  90. package/scripts/lib/validate/check-commands.mjs +2 -20
  91. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
  93. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  94. package/scripts/lib/validate/check-rules.mjs +153 -9
  95. package/scripts/lib/validate/check-skills.mjs +191 -0
  96. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  97. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  98. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  99. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  100. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  101. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  102. package/scripts/lib/validate/tier-inference.mjs +46 -8
  103. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  104. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  105. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  106. package/scripts/lib/vault-mirror/process.mjs +264 -31
  107. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  108. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  109. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  110. package/scripts/lib/vcs-repo-spec.mjs +680 -30
  111. package/scripts/lib/wave-resource-gate.mjs +67 -73
  112. package/scripts/materialize-wave-scope.mjs +281 -0
  113. package/scripts/print-learnings-index.mjs +30 -3
  114. package/scripts/release.mjs +983 -107
  115. package/scripts/run-quality-gate.mjs +14 -0
  116. package/scripts/site-numbers.mjs +1049 -0
  117. package/scripts/validate-plugin.mjs +64 -0
  118. package/scripts/validate-wave-scope.mjs +286 -12
  119. package/scripts/vault-backfill.mjs +32 -5
  120. package/scripts/vault-mirror.mjs +26 -1
  121. package/skills/_shared/monitor-patterns.md +24 -4
  122. package/skills/_shared/parallel-aware-auq.md +30 -24
  123. package/skills/_shared/parallel-aware-preamble.md +31 -2
  124. package/skills/_shared/state-ownership.md +49 -6
  125. package/skills/bootstrap/SKILL.md +2 -1
  126. package/skills/brainstorm/SKILL.md +18 -18
  127. package/skills/brainstorm/soul.md +12 -0
  128. package/skills/claude-md-drift-check/SKILL.md +9 -1
  129. package/skills/debug/SKILL.md +4 -1
  130. package/skills/discovery/SKILL.md +28 -24
  131. package/skills/discovery/issue-templates.md +4 -4
  132. package/skills/discovery/probes-code.md +2 -2
  133. package/skills/discovery/probes-feature.md +6 -6
  134. package/skills/discovery/probes-infra.md +2 -2
  135. package/skills/discovery/probes-session.md +5 -5
  136. package/skills/dispatcher/SKILL.md +10 -1
  137. package/skills/eli5/SKILL.md +43 -0
  138. package/skills/evolve/SKILL.md +8 -9
  139. package/skills/frontmatter-guard/SKILL.md +9 -1
  140. package/skills/gitlab-ops/SKILL.md +73 -59
  141. package/skills/gitlab-portfolio/SKILL.md +10 -1
  142. package/skills/grill/SKILL.md +6 -6
  143. package/skills/grill/soul.md +16 -0
  144. package/skills/memory-cleanup/SKILL.md +20 -7
  145. package/skills/npm-publish/SKILL.md +23 -51
  146. package/skills/peekaboo-driver/SKILL.md +3 -3
  147. package/skills/persona-panel/SKILL.md +3 -1
  148. package/skills/plan/SKILL.md +18 -16
  149. package/skills/plan/mode-feature.md +1 -1
  150. package/skills/plan/mode-new.md +42 -12
  151. package/skills/plan/soul.md +12 -0
  152. package/skills/reconcile/SKILL.md +3 -3
  153. package/skills/repo-audit/SKILL.md +10 -1
  154. package/skills/session-end/SKILL.md +97 -22
  155. package/skills/session-end/metrics-collection.md +1 -1
  156. package/skills/session-end/phase-3-6-tail.md +37 -2
  157. package/skills/session-end/session-metrics-write.md +4 -10
  158. package/skills/session-plan/SKILL.md +2 -2
  159. package/skills/session-plan/wave-template.md +1 -1
  160. package/skills/session-start/SKILL.md +82 -36
  161. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  162. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  163. package/skills/session-start/soul.md +110 -0
  164. package/skills/spinout/SKILL.md +5 -1
  165. package/skills/sunset-review/SKILL.md +11 -1
  166. package/skills/test-runner/SKILL.md +2 -2
  167. package/skills/tmux-layout/SKILL.md +7 -2
  168. package/skills/using-orchestrator/SKILL.md +1 -1
  169. package/skills/vault-mirror/SKILL.md +10 -1
  170. package/skills/vault-sync/SKILL.md +10 -1
  171. package/skills/vault-sync/validator.mjs +55 -6
  172. package/skills/wave-executor/wave-loop.md +64 -12
  173. package/skills/write-executable-plan/SKILL.md +6 -6
  174. package/scripts/lib/mission-status-schema.mjs +0 -114
  175. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  176. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  177. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  178. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  179. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  180. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  181. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  182. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  183. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  184. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  185. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  186. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  187. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  188. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  189. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  190. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  191. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  192. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  193. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  194. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  195. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  196. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  197. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  198. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  199. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  200. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  201. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  202. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -7,12 +7,22 @@
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
+ resolveGitlabProjectTarget,
22
+ redactUrlCredentials,
23
+ detectVcsFamily,
24
+ isQueryFailure,
25
+ } from './vcs-repo-spec.mjs';
16
26
 
17
27
  const execFileAsync = promisify(_execFile);
18
28
 
@@ -38,24 +48,130 @@ async function execWithTimeout(cmd, args, opts = {}) {
38
48
  }
39
49
 
40
50
  /**
41
- * Detect VCS from git remote URL.
42
- * Returns 'github' | 'gitlab'. Throws if git is unavailable or no origin.
51
+ * The one failure this probe can produce that the frozen
52
+ * `REMOTE_RESOLUTION_REASONS` set has no member for: the async timeout race
53
+ * every subprocess here runs under has no counterpart in the SYNCHRONOUS
54
+ * `vcs-repo-spec.mjs` core (it uses `execFileSync`, which cannot time out).
55
+ *
56
+ * Deliberately NOT a query-failure for warning purposes — a timeout stays
57
+ * SILENT, matching how `checkCiStatus`'s outer catch has always treated one
58
+ * (`msg === 'timeout'` → silent `null`). Warning on a hung `git` but not on a
59
+ * hung `glab` would be an inconsistency inside a single banner, and a hang is
60
+ * not a fact an operator can act on the way "git is not installed" is.
61
+ */
62
+ const PROBE_TIMEOUT = 'probe-timeout';
63
+
64
+ /**
65
+ * The ONE `isQueryFailure` member this banner deliberately keeps silent.
66
+ *
67
+ * `vcs-repo-spec.mjs` classifies `not-a-git-repo` as a query failure, and that
68
+ * is correct IN ITS DOMAIN: a remote-resolution helper genuinely could not
69
+ * enumerate remotes. For THIS consumer the same fact is a benign absence —
70
+ * session-start Phase 4 asking a non-repo directory about its CI has a complete
71
+ * and unsurprising answer ("there is no CI here"), and an operator cannot act on
72
+ * being told their directory is not a git repo. Warning would print on every
73
+ * such session-start and train exactly the reflex that makes the
74
+ * `git-unavailable` / `git-error` lines worthless.
75
+ *
76
+ * Expressed as a SUBTRACTION from `isQueryFailure` rather than as a hard-coded
77
+ * warn-list, so a reason added to the frozen set upstream is warned by default
78
+ * (fail-toward-visible) instead of silently inheriting the benign path.
79
+ *
80
+ * @type {ReadonlySet<string>}
81
+ */
82
+ const SILENT_QUERY_FAILURES = new Set(['not-a-git-repo']);
83
+
84
+ /**
85
+ * Classify a rejected ASYNC `execFile` into a `REMOTE_RESOLUTION_REASONS`
86
+ * member.
87
+ *
88
+ * This cannot reuse `vcs-repo-spec.mjs`'s own (unexported) `classifyGitFailure`
89
+ * because that one reads an `execFileSync` result, where the spawn errno lands
90
+ * in `err.code` and the child's exit status in `err.status`. Async `execFile`
91
+ * folds BOTH into `err.code`, discriminated only by TYPE: the string `'ENOENT'`
92
+ * when the binary is not on PATH, the number `128` when git ran and rejected
93
+ * the path as a work tree. Same taxonomy, different carrier.
94
+ *
95
+ * @param {unknown} err
96
+ * @returns {'git-unavailable'|'not-a-git-repo'|'git-error'}
97
+ */
98
+ function classifyGitProbeFailure(err) {
99
+ const code = err && typeof err === 'object' ? /** @type {any} */ (err).code : undefined;
100
+ if (code === 'ENOENT') return 'git-unavailable';
101
+ // `git remote -v` exits 128 for "not a git repository" — and, unlike the
102
+ // `remote get-url origin` call this replaced, NOT for "no such remote":
103
+ // a repo with zero remotes exits 0 with empty stdout. That is exactly the
104
+ // split that makes `no-remotes` (benign absence) reportable at all.
105
+ if (code === 128) return 'not-a-git-repo';
106
+ return 'git-error';
107
+ }
108
+
109
+ /**
110
+ * Detect the repo's VCS family from its git remotes.
111
+ *
112
+ * **Never throws.** Returns a discriminated result carrying a reason from
113
+ * `vcs-repo-spec.mjs`'s frozen `REMOTE_RESOLUTION_REASONS` (plus
114
+ * {@link PROBE_TIMEOUT}), so the caller can separate "I could not ask"
115
+ * (`isQueryFailure` → WARN) from "I asked; this repo has no VCS remote"
116
+ * (benign, silent). This is the `mirror-issues-banner.mjs` shape — a third
117
+ * state beyond `value | null` — applied to the last unmigrated path in this
118
+ * module (#1039).
119
+ *
120
+ * What it replaces, and why both halves were defects:
121
+ * 1. `git remote get-url origin` — a hard-coded remote NAME. In a repo whose
122
+ * remotes are `gitlab` + `github` (no `origin`) the call failed, the
123
+ * caller swallowed it to `null`, and the banner was structurally dark:
124
+ * it could never report, not even on a red pipeline.
125
+ * 2. `remoteUrl.includes('github.com') ? 'github' : 'gitlab'` — a substring
126
+ * test that classifies GitHub Enterprise (`git@github.example.com:o/r`)
127
+ * as gitlab and then drives `glab` at a GitHub instance.
128
+ * `detectVcsFamily` fixes both: preference-ordered remote selection, and
129
+ * host-prefix classification (`github.*` / `gitlab.*`) ahead of remote-name.
130
+ *
131
+ * The `git remote -v` spawn stays HERE rather than inside `detectVcsFamily`
132
+ * because this module owes every subprocess a timeout race — the shared core
133
+ * is synchronous by design. So the output is captured once and handed to the
134
+ * shared classifier through its `gitRun` seam: one spawn, one taxonomy, no
135
+ * second copy of the parsing or the credential strip.
43
136
  *
44
137
  * @param {string} repoRoot
45
138
  * @param {{ execFile?: Function, timeoutMs?: number }} deps
46
- * @returns {Promise<'github'|'gitlab'>}
139
+ * @returns {Promise<{ ok: true, vcs: 'github'|'gitlab' }
140
+ * | { ok: false, reason: string, stderr?: string }>}
47
141
  */
48
142
  async function detectVcs(repoRoot, deps = {}) {
49
143
  // Use the smaller of 2000ms or the caller-supplied timeout so that a short
50
144
  // test-level timeout is still respected here.
51
145
  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';
146
+
147
+ let stdout;
148
+ try {
149
+ const result = await execWithTimeout(
150
+ 'git',
151
+ ['remote', '-v'],
152
+ { cwd: repoRoot, timeoutMs: gitTimeout, execFile: deps.execFile },
153
+ );
154
+ stdout = String(result?.stdout ?? '');
155
+ } catch (err) {
156
+ const msg = err instanceof Error ? err.message : String(err ?? '');
157
+ if (msg === 'timeout') return { ok: false, reason: PROBE_TIMEOUT };
158
+ const stderr = err && typeof err === 'object' ? String(/** @type {any} */ (err).stderr ?? '') : '';
159
+ return { ok: false, reason: classifyGitProbeFailure(err), stderr };
160
+ }
161
+
162
+ // Replay the captured output through the shared core's `gitRun` seam. The
163
+ // arg guard is insurance, not decoration: `detectVcsFamily` asks only for
164
+ // `remote -v` today, and if it ever grows a second query the guard turns a
165
+ // silently-wrong answer (built from the WRONG command's output) into a
166
+ // reported `git-error`.
167
+ const replayGitRun = (args) =>
168
+ Array.isArray(args) && args.at(-2) === 'remote' && args.at(-1) === '-v'
169
+ ? { ok: true, stdout, stderr: '', status: 0 }
170
+ : { ok: false, stdout: '', stderr: `unsupported git query in ci-status probe: ${String(args)}`, status: 1 };
171
+
172
+ const family = detectVcsFamily({ repoRoot, gitRun: replayGitRun });
173
+ if (!family.ok) return { ok: false, reason: family.reason, stderr: family.stderr };
174
+ return { ok: true, vcs: family.vcs };
59
175
  }
60
176
 
61
177
  /**
@@ -76,42 +192,19 @@ async function getHeadSha(repoRoot, deps = {}) {
76
192
  }
77
193
 
78
194
  /**
79
- * Get GitLab project ID via glab.
80
- *
81
- * #872: pins to `deps.repoSpec` via `-R` when resolved (host-pinning —
82
- * `glab repo view` otherwise falls back to the ambient `GITLAB_HOST`).
83
- *
84
- * @param {string} repoRoot
85
- * @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string }} deps
86
- * @returns {Promise<number>}
87
- */
88
- async function getGlabProjectId(repoRoot, deps = {}) {
89
- const args = ['repo', 'view', '--output', 'json'];
90
- if (deps.repoSpec) args.push('-R', deps.repoSpec);
91
- const result = await execWithTimeout(
92
- 'glab',
93
- args,
94
- { cwd: repoRoot, timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS, execFile: deps.execFile },
95
- );
96
- const parsed = JSON.parse(result.stdout);
97
- return parsed.id;
98
- }
99
-
100
- /**
101
- * Run `glab api <path>` and return parsed JSON.
195
+ * Run a host-pinned `glab api <path>` request and return parsed JSON.
102
196
  *
103
- * #872: `glab api` has no repo/`-R` concept it accepts only `--hostname`
104
- * to pin which GitLab instance the request targets. Pinned via
105
- * `deps.repoHost` when resolved.
197
+ * GitLab's API accepts neither a remote URL nor `-R`; callers provide the host
198
+ * proven by `resolveGitlabProjectTarget` so this helper cannot fall back to
199
+ * ambient `GITLAB_HOST` configuration.
106
200
  *
107
201
  * @param {string} apiPath
108
202
  * @param {string} repoRoot
109
- * @param {{ execFile?: Function, timeoutMs?: number, repoHost?: string }} deps
203
+ * @param {{ execFile?: Function, timeoutMs?: number, repoHost: string }} deps
110
204
  * @returns {Promise<unknown>}
111
205
  */
112
206
  async function glabApi(apiPath, repoRoot, deps = {}) {
113
- const args = ['api', apiPath];
114
- if (deps.repoHost) args.push('--hostname', deps.repoHost);
207
+ const args = ['api', apiPath, '--hostname', deps.repoHost];
115
208
  const result = await execWithTimeout(
116
209
  'glab',
117
210
  args,
@@ -161,17 +254,32 @@ function ageDaysFrom(isoDate, now) {
161
254
  *
162
255
  * @param {string} repoRoot
163
256
  * @param {number} now
164
- * @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string, repoHost?: string }} deps
257
+ * @param {{
258
+ * execFile?: Function,
259
+ * timeoutMs?: number,
260
+ * gitlabProject?: { host: string, encodedProjectPath: string },
261
+ * }} deps
165
262
  * @returns {Promise<object|null>}
166
263
  */
167
264
  async function checkGitlab(repoRoot, now, deps = {}) {
168
- const projectId = await getGlabProjectId(repoRoot, deps);
169
- const currentSha = await getHeadSha(repoRoot, deps);
265
+ const project = deps.gitlabProject;
266
+ if (
267
+ !project ||
268
+ typeof project.host !== 'string' ||
269
+ typeof project.encodedProjectPath !== 'string' ||
270
+ project.host === '' ||
271
+ project.encodedProjectPath === ''
272
+ ) {
273
+ return null;
274
+ }
170
275
 
276
+ const currentSha = await getHeadSha(repoRoot, deps);
277
+ const apiDeps = { ...deps, repoHost: project.host };
278
+ const projectPath = `projects/${project.encodedProjectPath}`;
171
279
  const pipelines = await glabApi(
172
- `projects/${projectId}/pipelines?order_by=updated_at&sort=desc&per_page=15`,
280
+ `${projectPath}/pipelines?order_by=updated_at&sort=desc&per_page=15`,
173
281
  repoRoot,
174
- deps,
282
+ apiDeps,
175
283
  );
176
284
 
177
285
  if (!Array.isArray(pipelines)) return null;
@@ -201,9 +309,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
201
309
  let allowFailureJobs;
202
310
  try {
203
311
  const jobs = await glabApi(
204
- `projects/${projectId}/pipelines/${currentPipeline.id}/jobs`,
312
+ `${projectPath}/pipelines/${currentPipeline.id}/jobs`,
205
313
  repoRoot,
206
- deps,
314
+ apiDeps,
207
315
  );
208
316
  if (Array.isArray(jobs)) {
209
317
  const softFailed = jobs
@@ -268,9 +376,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
268
376
  let failingJobName;
269
377
  try {
270
378
  const jobs = await glabApi(
271
- `projects/${projectId}/pipelines/${currentPipeline.id}/jobs`,
379
+ `${projectPath}/pipelines/${currentPipeline.id}/jobs`,
272
380
  repoRoot,
273
- deps,
381
+ apiDeps,
274
382
  );
275
383
  if (Array.isArray(jobs)) {
276
384
  const failedJob = jobs.find((j) => j.status === 'failed');
@@ -308,9 +416,25 @@ async function checkGitlab(repoRoot, now, deps = {}) {
308
416
  /**
309
417
  * GitHub CI status check (v1 — red/green only; lastGreen not implemented).
310
418
  *
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`).
419
+ * #872/#1022: pins the `gh repo view` lookup to `deps.repoSpec` when resolved
420
+ * (host-pinning — `gh repo view` otherwise falls back to the ambient
421
+ * `GH_HOST`). The spec is passed POSITIONALLY, not via `-R`: `gh repo view`
422
+ * takes `[<repository>]` as a positional argument and has no `-R`/`--repo`
423
+ * flag at all, so `-R` made gh exit 1 with `unknown shorthand flag: 'R'` —
424
+ * an error `checkCiStatus`'s outer catch swallowed to `null`, leaving the
425
+ * Phase 4 banner silently dead on EVERY GitHub repo (#1022). The
426
+ * `[HOST/]OWNER/REPO` shape `resolveRepoSpec({ vcs: 'github' })` returns is
427
+ * exactly the positional's documented input format.
428
+ *
429
+ * GitLab CI differs deliberately: it derives a host-pinned API target from
430
+ * the selected remote, while GitHub still needs this lookup because its API
431
+ * path requires `nameWithOwner`. Do not unify these call sites.
432
+ *
433
+ * `nameWithOwner` is NOT derivable from `deps.repoSpec`, so this lookup
434
+ * cannot be dropped: the spec carries a HOST prefix the `repos/<owner>/<repo>`
435
+ * API path must not contain, it is `undefined` whenever no remote resolves
436
+ * (cross-family guard, unsafe-argv guard), and `normalizeGithubSpec` falls
437
+ * back to the raw URL on an unrecognised remote shape.
314
438
  *
315
439
  * @param {string} repoRoot
316
440
  * @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string, repoHost?: string }} deps
@@ -318,8 +442,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
318
442
  */
319
443
  async function checkGithub(repoRoot, deps = {}) {
320
444
  // 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);
445
+ const repoViewArgs = ['repo', 'view'];
446
+ if (deps.repoSpec) repoViewArgs.push(deps.repoSpec);
447
+ repoViewArgs.push('--json', 'nameWithOwner');
323
448
  const repoViewResult = await execWithTimeout(
324
449
  'gh',
325
450
  repoViewArgs,
@@ -387,11 +512,17 @@ async function checkGithub(repoRoot, deps = {}) {
387
512
  * Checks CI status for the current HEAD commit.
388
513
  *
389
514
  * Returns `null` (silent no-op) when:
390
- * - Not in a VCS repo (no git origin)
515
+ * - The repo has no usable VCS remote (not a git repo, no remotes at all,
516
+ * or >= 2 remotes with no preference match) — a benign, measured absence
391
517
  * - Required CLI (glab / gh) not in PATH
392
518
  * - Any CLI invocation times out
393
519
  * - JSON parse failure on CLI output
394
520
  *
521
+ * Also returns `null`, but with a `console.warn` trace, when the VCS-detection
522
+ * QUERY ITSELF failed (`git` not on PATH, `git remote -v` erroring) or when a
523
+ * present CLI rejected its invocation. `null` alone cannot express "could not
524
+ * read" — see the outer catch and Step 1 for why the warn channel carries it.
525
+ *
395
526
  * @param {{
396
527
  * repoRoot?: string,
397
528
  * vcs?: 'gitlab'|'github',
@@ -402,10 +533,11 @@ async function checkGithub(repoRoot, deps = {}) {
402
533
  * execFile?: Function,
403
534
  * resolveRepoSpec?: (opts: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined,
404
535
  * resolveRepoHost?: (opts: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined,
405
- * }} deps Dependency-injection seam for testing. `resolveRepoSpec`/
406
- * `resolveRepoHost` default to the real `vcs-repo-spec.mjs` exports
407
- * (#872 host-pinning see that module for the `-R` vs `--hostname`
408
- * contract).
536
+ * resolveGitlabProjectTarget?: (opts: { repoRoot: string }) =>
537
+ * { host: string, encodedProjectPath: string }|undefined,
538
+ * }} deps Dependency-injection seam for testing. GitLab defaults to
539
+ * `resolveGitlabProjectTarget`, which proves host and project path from one
540
+ * sanitized remote; GitHub retains the #872 spec/host resolvers.
409
541
  * @returns {Promise<null | {
410
542
  * status: 'green'|'red'|'unknown',
411
543
  * ok: boolean,
@@ -434,6 +566,7 @@ export async function checkCiStatus(opts = {}, deps = {}) {
434
566
 
435
567
  const resolveRepoSpecDep = deps.resolveRepoSpec ?? resolveRepoSpec;
436
568
  const resolveRepoHostDep = deps.resolveRepoHost ?? resolveRepoHost;
569
+ const resolveGitlabProjectTargetDep = deps.resolveGitlabProjectTarget ?? resolveGitlabProjectTarget;
437
570
 
438
571
  const depsWithExec = { execFile: execFileDep, timeoutMs };
439
572
 
@@ -441,30 +574,59 @@ export async function checkCiStatus(opts = {}, deps = {}) {
441
574
  // Step 1: detect VCS (or use forced value).
442
575
  let vcs = forcedVcs;
443
576
  if (!vcs) {
444
- try {
445
- vcs = await detectVcs(repoRoot, depsWithExec);
446
- } catch {
447
- // No git remote → not in a VCS repo silent no-op.
577
+ const detected = await detectVcs(repoRoot, depsWithExec);
578
+ if (!detected.ok) {
579
+ // The two-state `null` return is fixed by 13 sibling banners, so the
580
+ // third state lives in the WARN channel: an ABSENCE (`no-remotes`,
581
+ // `no-matching-remote`, `unsafe-value`) is a real, benign answer and
582
+ // stays silent — warning there would train operators to ignore this
583
+ // line, which is the more expensive error. A QUERY FAILURE
584
+ // (`git-unavailable`, `git-error`) means the question could not be
585
+ // asked at all, and that is precisely the state that was
586
+ // indistinguishable from "nothing to report" before #1039.
587
+ // `not-a-git-repo` is the one query failure this banner reads as an
588
+ // absence — see {@link SILENT_QUERY_FAILURES}.
589
+ if (isQueryFailure(detected.reason) && !SILENT_QUERY_FAILURES.has(detected.reason)) {
590
+ const detail = detected.stderr
591
+ ? ` — ${redactUrlCredentials(detected.stderr).trim()}`
592
+ : '';
593
+ console.warn(
594
+ `WARN ci-status-banner: VCS detection failed (${detected.reason}), banner suppressed — ` +
595
+ `CI state is UNKNOWN, not "green".${detail}`,
596
+ );
597
+ }
448
598
  return null;
449
599
  }
600
+ vcs = detected.vcs;
450
601
  }
451
602
 
452
- // Step 1b (#872): resolve the -R/--hostname host-pinning spec ONCE per
453
- // checkCiStatus call a bare glab/gh spawn falls back to the ambient
454
- // GITLAB_HOST/GH_HOST env var, which can silently target the wrong
455
- // instance on a multi-instance host. `cwd: repoRoot` alone does not fix
456
- // this (ambient env still wins over cwd).
457
- const repoSpec = resolveRepoSpecDep({ repoRoot, vcs });
458
- const repoHost = resolveRepoHostDep({ repoRoot, vcs });
459
- const depsWithPinning = { ...depsWithExec, repoSpec, repoHost };
460
-
461
- // Step 2: dispatch to VCS-specific implementation.
603
+ // Step 1b: GitLab's API target must be proven before its first glab spawn.
604
+ // A missing target is never permission to fall back to ambient
605
+ // GITLAB_HOST/repository configuration. GitHub retains its distinct
606
+ // repository lookup because its API path requires `nameWithOwner`.
607
+ //
608
+ // It is also not an ABSENCE: detectVcs just proved a GitLab remote exists,
609
+ // so `!gitlabProject` means "remote present, its form was rejected" — a
610
+ // QUERY FAILURE by the same rule the block above states. Returning null
611
+ // silently put this repo back in the pre-#1039 state where "CI green" and
612
+ // "could not ask" look identical. Reachable inputs measured 2026-08-21:
613
+ // `git://` scheme, a doubled slash in the path, a query string.
462
614
  if (vcs === 'gitlab') {
463
- return await checkGitlab(repoRoot, now, depsWithPinning);
615
+ const gitlabProject = resolveGitlabProjectTargetDep({ repoRoot });
616
+ if (!gitlabProject) {
617
+ console.warn(
618
+ 'WARN ci-status-banner: a GitLab remote was detected but its host/project path ' +
619
+ 'could not be derived, banner suppressed — CI state is UNKNOWN, not "green".',
620
+ );
621
+ return null;
622
+ }
623
+ return await checkGitlab(repoRoot, now, { ...depsWithExec, gitlabProject });
464
624
  }
465
625
 
466
626
  if (vcs === 'github') {
467
- return await checkGithub(repoRoot, depsWithPinning);
627
+ const repoSpec = resolveRepoSpecDep({ repoRoot, vcs });
628
+ const repoHost = resolveRepoHostDep({ repoRoot, vcs });
629
+ return await checkGithub(repoRoot, { ...depsWithExec, repoSpec, repoHost });
468
630
  }
469
631
 
470
632
  // Unknown VCS value — silent no-op.
@@ -482,7 +644,35 @@ export async function checkCiStatus(opts = {}, deps = {}) {
482
644
  return null;
483
645
  }
484
646
 
485
- // Unexpected errors also silent null to keep the banner non-blocking.
647
+ // Everything else means the CLI was PRESENT but the invocation failed —
648
+ // non-zero exit, rejected flag, unparseable output. Returning a bare null
649
+ // here makes that state indistinguishable from "CLI not installed", which
650
+ // is exactly how #1022 (`gh repo view -R` → `unknown shorthand flag: 'R'`)
651
+ // stayed invisible on every GitHub repo. THIS module keeps the two-state
652
+ // contract (null ⇒ silent no-op) for backwards compatibility with the
653
+ // Phase-4 callers already written against it, so leave a stderr trace
654
+ // instead: non-blocking, but the next defect of this class is no longer
655
+ // silent. Credentials redacted defense-in-depth (#907) — the message can
656
+ // quote the failed argv, which carries the repo spec.
657
+ //
658
+ // DIRECTION FOR NEW BANNERS — do not copy this shape. The
659
+ // absence-preserving form is `scripts/lib/mirror-issues-banner.mjs`: a
660
+ // THIRD return state `{ severity, message, degraded }` whose `degraded`
661
+ // is a member of the closed `DEGRADED_REASONS` enum exported there, so
662
+ // "could not read" stays distinguishable from "read, and clean".
663
+ //
664
+ // The REMOTE probe (`detectVcs`) has since been pulled onto that shape
665
+ // (#1039): it returns a reason from the frozen `REMOTE_RESOLUTION_REASONS`
666
+ // set and Step 1 branches on `isQueryFailure` — absence silent, query
667
+ // failure warned. This outer catch is what remains unmigrated: it still
668
+ // collapses every CLI-side failure onto `null`, which a caller reads as
669
+ // all-clear, and it cannot be widened without changing a return contract
670
+ // 13 sibling banners share. Same verdict, stated caller-side, in
671
+ // `skills/session-start/SKILL.md` § Phase 4 (the mirror-issues
672
+ // paragraph): "Do not reproduce it."
673
+ console.warn(
674
+ `WARN ci-status-banner: CI status check failed, banner suppressed — ${redactUrlCredentials(msg)}`,
675
+ );
486
676
  return null;
487
677
  }
488
678
  }
@@ -43,13 +43,36 @@ import { hasBlockHeader } from './block-header.mjs';
43
43
  const ALLOWED_AUTONOMY = ['off', 'advisory', 'autonomous-gated'];
44
44
  const DEFAULT_CONFIDENCE_FLOOR = 0.5;
45
45
 
46
+ /**
47
+ * The display-only tail of an option LABEL, stripped before the enum match.
48
+ *
49
+ * The label and the stored value were the SAME string, which made the label
50
+ * unchangeable: both consumers (`skills/session-start/SKILL.md` Phase 1.1 and
51
+ * `skills/bootstrap/SKILL.md` Phase 3.5.1) hand the SELECTED LABEL straight to
52
+ * writeDispatcherAutonomyBlock(), and coerceAutonomy() falls back to 'off' on
53
+ * any value outside the enum. Putting AUQ-003's `(Recommended)` marker into the
54
+ * label would therefore have written `autonomy: off` for an operator who picked
55
+ * `advisory` — a silent downgrade, fail-closed and therefore invisible.
56
+ *
57
+ * Measured before this split (renderDispatcherAutonomyBlock, 2026-08-22):
58
+ * 'advisory' => autonomy: advisory
59
+ * 'Advisory (surface only)' => autonomy: off ← the downgrade
60
+ *
61
+ * Anchored at the end, so it can only ever strip a suffix. The enum values
62
+ * themselves are untouched and stay greppable.
63
+ */
64
+ const RECOMMENDED_SUFFIX = /\s*\((?:Recommended|Empfohlen|Default)\)\s*$/u;
65
+
46
66
  /**
47
67
  * Coordinator-facing AUQ question definition for the one-time capture.
48
68
  * Mirrors the shape of `getInterviewQuestions()` entries in owner-interview.mjs:
49
69
  * { question, header, options: [{ label, description }], multiSelect }
50
70
  *
51
- * Option labels match the ALLOWED_AUTONOMY enum so the selected label maps
52
- * directly to the `autonomy` value passed to writeDispatcherAutonomyBlock().
71
+ * Each label BEGINS with its ALLOWED_AUTONOMY value; option 1 additionally
72
+ * carries the `(Recommended)` marker AUQ-003 puts in the label. `coerceAutonomy()`
73
+ * strips that marker, so the selected label still maps to the enum value written
74
+ * by writeDispatcherAutonomyBlock() — see the comment on RECOMMENDED_SUFFIX for
75
+ * why the two halves must not be the same string.
53
76
  * Option 1 (`off`) is the recommended, fail-closed default.
54
77
  *
55
78
  * @returns {{ question: string, header: string, options: Array<{ label: string, description: string }>, multiSelect: boolean }}
@@ -57,23 +80,23 @@ const DEFAULT_CONFIDENCE_FLOOR = 0.5;
57
80
  export function getDispatcherAutonomyQuestion() {
58
81
  return {
59
82
  question:
60
- 'Cross-repo dispatcher autonomy: how should this repo participate when the free-repo dispatcher routes work to it?',
61
- header: 'Dispatcher Autonomy (one-time)',
83
+ 'When /dispatcher looks across your repos for the next one to work on, how may it treat this repo?',
84
+ header: 'Dispatcher',
62
85
  options: [
63
86
  {
64
- label: 'off',
87
+ label: 'off (Recommended)',
65
88
  description:
66
- '(Recommended) Fail-closed. The dispatcher never routes work to this repo automatically. No behaviour change.',
89
+ 'Nothing changes: this repo is never offered and never started for you. Safe default — you keep picking the work.',
67
90
  },
68
91
  {
69
92
  label: 'advisory',
70
93
  description:
71
- 'The dispatcher surfaces ranked free-repo candidates for operator review no automated dispatch.',
94
+ 'This repo may appear in the ranked suggestions, so you can compare it against the others. You still start each session.',
72
95
  },
73
96
  {
74
97
  label: 'autonomous-gated',
75
98
  description:
76
- 'A deterministic confidence gate is checked first; only dispatches that clear the confidence-floor route automatically.',
99
+ 'Work may start here without asking, but only when the ranking beats the confidence floor written next to this setting.',
77
100
  },
78
101
  ],
79
102
  multiSelect: false,
@@ -128,7 +151,7 @@ export function isDispatcherAutonomyBlockPresent(claudeMdContent) {
128
151
  */
129
152
  function coerceAutonomy(value) {
130
153
  if (value === null || value === undefined) return 'off';
131
- const normalized = String(value).toLowerCase().trim();
154
+ const normalized = String(value).toLowerCase().replace(RECOMMENDED_SUFFIX, '').trim();
132
155
  return ALLOWED_AUTONOMY.includes(normalized) ? normalized : 'off';
133
156
  }
134
157
 
@@ -192,9 +192,20 @@ function _parseInlineObject(raw) {
192
192
  */
193
193
  export function _parseResourceThresholds(kv) {
194
194
  return {
195
+ // Memory thresholds are denominated in the signal `evaluate()` actually
196
+ // judges on — memory_pressure > ram_available > ram_free, in that order
197
+ // (#1089). They are NOT compared against Darwin's `os.freemem()` unless
198
+ // nothing better is published, which on Darwin never happens.
195
199
  'ram-free-min-gb': _coerceInteger(kv, 'ram-free-min-gb', 4),
196
200
  'ram-free-critical-gb': _coerceInteger(kv, 'ram-free-critical-gb', 2),
197
- 'cpu-load-max-pct': _coerceInteger(kv, 'cpu-load-max-pct', 80),
201
+ // 80 → 90 (#1089). At 80 this fired on 15.6% of 1477 measured session
202
+ // starts, largely on the decaying tail of the coordinator's own gate run;
203
+ // at 90 it fires on 12.8%, and under the two-signal rule it no longer caps
204
+ // anything on its own. Both numbers are measurements, not preferences.
205
+ 'cpu-load-max-pct': _coerceInteger(kv, 'cpu-load-max-pct', 90),
206
+ // Unchanged at 5 — but now compared against LIVE PEER SESSIONS rather than
207
+ // the Claude process count. Same number, different denominator: measured
208
+ // firing rate drops from 93.6% to 4.2% (median processes:sessions = 6.0).
198
209
  'concurrent-sessions-warn': _coerceInteger(kv, 'concurrent-sessions-warn', 5),
199
210
  'ssh-no-docker': _coerceBoolean(kv, 'ssh-no-docker', true),
200
211
  };
@@ -34,7 +34,7 @@ import path from 'node:path';
34
34
 
35
35
  import { scanBacklog } from '../backlog-scan.mjs';
36
36
  import { checkCiStatus as realCheckCiStatus } from '../ci-status-banner.mjs';
37
- import { probe as realProbe, evaluate as realEvaluate } from '../resource-probe.mjs';
37
+ import { probe as realProbe, evaluate as realEvaluate, DEFAULT_RESOURCE_THRESHOLDS as CANONICAL_RESOURCE_THRESHOLDS } from '../resource-probe.mjs';
38
38
  import { isRealSession } from '../session-schema/filters.mjs';
39
39
 
40
40
  /** Staleness cap (days). Beyond this, additional age does not raise the score. */
@@ -49,12 +49,9 @@ const MS_PER_DAY = 86_400_000;
49
49
  * Used by the default `resourceVerdict` dep so ranking works standalone
50
50
  * without a parsed Session Config in hand.
51
51
  */
52
- const DEFAULT_RESOURCE_THRESHOLDS = {
53
- 'ram-free-min-gb': 4,
54
- 'ram-free-critical-gb': 2,
55
- 'cpu-load-max-pct': 80,
56
- 'concurrent-sessions-warn': 5,
57
- };
52
+ // #1089: re-exported from the single canonical definition rather than a third
53
+ // hand-maintained copy — the three copies had already drifted apart.
54
+ const DEFAULT_RESOURCE_THRESHOLDS = { ...CANONICAL_RESOURCE_THRESHOLDS };
58
55
 
59
56
  // ---------------------------------------------------------------------------
60
57
  // PURE core
@@ -36,7 +36,7 @@ const startTime = Date.now();
36
36
  const tcResult = runCheck(typecheckCmd);
37
37
  const tcErrorCount =
38
38
  tcResult.status === 'fail'
39
- ? extractCount(tcResult.output, /error TS\d+/)
39
+ ? extractCount(tcResult.fullOutput ?? tcResult.output, /error TS\d+/)
40
40
  : 0;
41
41
 
42
42
  // --- Test ---
@@ -46,14 +46,14 @@ const tcErrorCount =
46
46
  const testResult = runCheck(testCmd);
47
47
  const { passed: testPassed, failed: testFailed, total: testTotal } =
48
48
  testResult.status !== 'skip'
49
- ? extractTestCounts(testResult.output)
49
+ ? extractTestCounts(testResult.fullOutput ?? testResult.output)
50
50
  : { passed: 0, failed: 0, total: 0 };
51
51
 
52
52
  // --- Lint ---
53
53
  const lintResult = runCheck(lintCmd);
54
54
  const lintWarnings =
55
55
  lintResult.status !== 'skip'
56
- ? extractCount(lintResult.output, /warning/i)
56
+ ? extractCount(lintResult.fullOutput ?? lintResult.output, /warning/i)
57
57
  : 0;
58
58
 
59
59
  // --- Debug artifacts ---