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
@@ -315,14 +315,14 @@ export async function teardownWorktree(context, result, opts = {}) {
315
315
  try {
316
316
  // #987 defense-in-depth: pass the in-memory genesis proof (captured by
317
317
  // runStoryPipeline right after acquire — same process, no file I/O)
318
- // so the delete is double-gated at the fs layer. Spread-guarded:
319
- // release()'s proof gate triggers on `proof !== undefined`, so a
320
- // null/absent proof MUST be omitted entirely or EVERY release would
321
- // be refused with 'proof-mismatch'.
318
+ // so the delete is double-gated at the fs layer. `_lockOwnerProof` is
319
+ // null/undefined when the lock could not yield a full proof; release()
320
+ // gates on `proof != null` (#989) and degrades to the session_id-only
321
+ // path for that case, so no call-site guard is needed.
322
322
  releaseResult = release({
323
323
  sessionId: result._lockSessionId,
324
324
  repoRoot: result.worktreePath,
325
- ...(result._lockOwnerProof ? { proof: result._lockOwnerProof } : {}),
325
+ proof: result._lockOwnerProof,
326
326
  });
327
327
  } catch (lockErr) {
328
328
  console.error(
@@ -10,39 +10,96 @@
10
10
  * - Module-level cache keyed on (vcs, limit) — one VCS round-trip per session.
11
11
  * - Graceful degradation: missing CLI / non-zero exit / parse failure → null.
12
12
  * Never throws to the caller.
13
+ * - No silent caps: the scan reads at most `limit` issues, so every count is a
14
+ * LOWER BOUND when the window fills. `truncated` says so in the return value
15
+ * and a WARNING says so on stderr — a bound that is applied is announced.
13
16
  *
14
17
  * Stale threshold: 30 days since `updated_at`.
15
18
  *
16
19
  * Dependencies:
17
- * - VCS detection follows `skills/gitlab-ops/SKILL.md` (origin URL contains "github.com" → gh, else glab).
20
+ * - VCS detection delegates to `vcs-repo-spec.mjs::detectVcsFamily` (#1039):
21
+ * remote-host family classification over ALL remotes, not a hard-coded
22
+ * `origin` lookup with a `url.includes('github.com')` test.
18
23
  */
19
24
 
20
25
  import { spawnSync } from 'node:child_process';
21
26
 
27
+ import { warn } from './common.mjs';
22
28
  import { normalizeLabel } from './label-scope.mjs';
23
- import { resolveRepoSpec } from './vcs-repo-spec.mjs';
29
+ import { detectVcsFamily, isQueryFailure, redactUrlCredentials, resolveRepoSpec } from './vcs-repo-spec.mjs';
24
30
 
25
31
  export const STALE_THRESHOLD_DAYS = 30;
26
32
 
33
+ /**
34
+ * Default scan window, and the SINGLE source for that number — every caller
35
+ * either omits `limit` or imports this constant. A second hand-written copy is
36
+ * what made the old default wrong in three places at once.
37
+ *
38
+ * Ceiling: 100 is the largest single-request page BOTH CLIs serve reliably
39
+ * (the GitLab API clamps `per_page` at 100), so it is the widest exact window
40
+ * available without paginating. Above it the scan truncates — `truncated: true`
41
+ * plus a stderr WARNING announce that, and every count becomes a lower bound.
42
+ * Revisit with a `--page` loop if a repo's OPEN backlog routinely exceeds 100.
43
+ */
44
+ export const DEFAULT_BACKLOG_LIMIT = 100;
45
+
27
46
  /** Module-level cache. Keyed by JSON.stringify({vcs, limit}). */
28
47
  const _cache = new Map();
29
48
 
30
49
  /**
31
- * Detect the VCS for the current working directory by inspecting the origin URL.
50
+ * Detect the VCS family of a repo from its git remotes.
32
51
  * Returns 'github' | 'gitlab' | null. Never throws.
33
52
  *
53
+ * This probe cannot take `vcs` as a parameter and cannot use
54
+ * `resolveRepoSpec({vcs})`: it runs in order to DETERMINE `vcs`. So it uses the
55
+ * vcs-less projection {@link detectVcsFamily}, which classifies every remote by
56
+ * URL host (then by remote name) instead of pinning `origin`.
57
+ *
58
+ * Two #1039 defects this replaces:
59
+ * 1. `git remote get-url origin` returned non-zero on a repo whose remotes are
60
+ * named `gitlab`/`github` (no `origin`) — a perfectly scannable backlog
61
+ * read as "no VCS".
62
+ * 2. `url.includes('github.com')` classified GitHub Enterprise
63
+ * (`git@github.example.com:o/r.git`) as gitlab, pointing `glab` at a GitHub
64
+ * instance. `detectVcsFamily`'s host rule (`github.*`) covers it.
65
+ *
66
+ * `null` still means "no backlog signal" to the caller, but the two states it
67
+ * used to fold are now distinguishable on stderr: a QUERY FAILURE (git missing,
68
+ * not a git repo) emits exactly one WARNING, because a 40-issue backlog reading
69
+ * as empty is a degraded measurement the operator must see. An ABSENCE
70
+ * (`no-remotes`, `no-matching-remote`) stays SILENT — it is a legitimate repo
71
+ * state, and warning on it would train operators to ignore the warning that
72
+ * matters.
73
+ *
74
+ * @param {{ repoRoot?: string, gitRun?: (args: string[]) => { ok: boolean, stdout?: string, stderr?: string, status?: number, code?: string } }} [opts]
75
+ * `gitRun` is the injectable git seam of `detectVcsFamily` (tests stub it
76
+ * instead of shelling out); `repoRoot` defaults to `process.cwd()` there.
34
77
  * @returns {'github'|'gitlab'|null}
35
78
  */
36
- export function detectVcs() {
79
+ export function detectVcs({ repoRoot, gitRun } = {}) {
80
+ let detected;
37
81
  try {
38
- const r = spawnSync('git', ['remote', 'get-url', 'origin'], { encoding: 'utf8' });
39
- if (r.status !== 0) return null;
40
- const url = String(r.stdout || '').trim();
41
- if (!url) return null;
42
- return url.includes('github.com') ? 'github' : 'gitlab';
82
+ detected = detectVcsFamily({ repoRoot, gitRun });
43
83
  } catch {
44
84
  return null;
45
85
  }
86
+
87
+ if (detected.ok) return detected.vcs;
88
+
89
+ if (isQueryFailure(detected.reason)) {
90
+ // Redact before logging: git stderr can echo a remote URL carrying
91
+ // userinfo credentials (#907, CWE-214). First line only — a git fatal is
92
+ // one line, and the rest is noise in a session banner.
93
+ const detail = redactUrlCredentials(String(detected.stderr || ''))
94
+ .split('\n')[0]
95
+ .trim();
96
+ warn(
97
+ `backlog scan could not determine the VCS family (${detected.reason})` +
98
+ `${detail ? `: ${detail}` : ''} — backlog signal degraded to null (contributes 0 delta to mode selection).`
99
+ );
100
+ }
101
+
102
+ return null;
46
103
  }
47
104
 
48
105
  /**
@@ -91,6 +148,10 @@ function ageDays(iso, nowMs) {
91
148
  * - `labels`: array of strings OR array of {name: string} objects
92
149
  * - `updated_at` (glab) or `updatedAt` (gh): ISO-8601 timestamp
93
150
  *
151
+ * `total` is the number of records AGGREGATED, never the number of records that
152
+ * exist in the tracker — the caller decides the window, so only the caller
153
+ * (or `scanBacklog`'s `truncated` flag) can tell the two apart.
154
+ *
94
155
  * @param {Array<object>} issues
95
156
  * @param {number} nowMs — injected for tests
96
157
  * @returns {{criticalCount: number, highCount: number, staleCount: number, byLabel: Record<string, number>, total: number}}
@@ -140,12 +201,21 @@ export function summarizeIssues(issues, nowMs = Date.now()) {
140
201
  * (vcs, limit) pair never collide on a shared cache entry.
141
202
  *
142
203
  * Returns null on any of:
143
- * - VCS cannot be detected (no git origin)
204
+ * - VCS family cannot be detected ({@link detectVcs} — no remotes, or git
205
+ * itself unavailable; the latter also emits one WARNING)
144
206
  * - CLI binary missing (`glab` for gitlab, `gh` for github)
145
207
  * - CLI exits non-zero or produces unparsable output
146
208
  *
147
209
  * Never throws.
148
210
  *
211
+ * The scan reads at most `limit` records (default `DEFAULT_BACKLOG_LIMIT`), so
212
+ * every count is a LOWER BOUND once the window fills. `truncated` reports that:
213
+ * `true` means the CLI returned a full window, so records — and the critical /
214
+ * high / stale issues among them — may lie beyond it. It is deliberately
215
+ * conservative: a backlog of exactly `limit` issues reports `truncated: true`
216
+ * even though nothing was missed. Over-reporting "you may have missed some" is
217
+ * safe; under-reporting it is the bug this flag exists to prevent.
218
+ *
149
219
  * @param {{
150
220
  * limit?: number,
151
221
  * vcs?: 'github'|'gitlab'|null,
@@ -160,19 +230,23 @@ export function summarizeIssues(issues, nowMs = Date.now()) {
160
230
  * get-url`); tests inject a stub instead of shelling out. `runJsonFn` is
161
231
  * the injectable seam for the CLI runner — defaults to the real `runJson`
162
232
  * (shells out to `glab`/`gh`).
163
- * @returns {Promise<null | {criticalCount: number, highCount: number, staleCount: number, byLabel: Record<string, number>, total: number, vcs: string, limit: number}>}
233
+ * @returns {Promise<null | {criticalCount: number, highCount: number, staleCount: number, byLabel: Record<string, number>, total: number, vcs: string, limit: number, truncated: boolean}>}
164
234
  */
165
235
  export async function scanBacklog(opts = {}) {
166
- const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : 50;
236
+ const limit =
237
+ Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : DEFAULT_BACKLOG_LIMIT;
238
+ const repoRoot = typeof opts.repoRoot === 'string' ? opts.repoRoot : process.cwd();
167
239
  // Distinguish "user did not pass vcs" (auto-detect) from "user explicitly passed
168
240
  // null" (degrade). 'vcs' in opts catches the explicit-null path so callers can
169
241
  // force the no-vcs branch in tests without monkey-patching detectVcs.
170
- const vcs = 'vcs' in opts ? opts.vcs : detectVcs();
242
+ // `repoRoot` is passed through so detection and `-R` spec resolution below
243
+ // answer about the SAME repo — they used to disagree whenever a caller passed
244
+ // `repoRoot` (detection silently read `process.cwd()` instead).
245
+ const vcs = 'vcs' in opts ? opts.vcs : detectVcs({ repoRoot });
171
246
  const nowMs = typeof opts.nowMs === 'number' ? opts.nowMs : Date.now();
172
247
 
173
248
  if (vcs !== 'github' && vcs !== 'gitlab') return null;
174
249
 
175
- const repoRoot = typeof opts.repoRoot === 'string' ? opts.repoRoot : process.cwd();
176
250
  const resolveRepoSpecFn =
177
251
  typeof opts.resolveRepoSpecFn === 'function' ? opts.resolveRepoSpecFn : resolveRepoSpec;
178
252
  const runJsonFn = typeof opts.runJsonFn === 'function' ? opts.runJsonFn : runJson;
@@ -197,8 +271,25 @@ export async function scanBacklog(opts = {}) {
197
271
  return null;
198
272
  }
199
273
 
274
+ // A full window means records may lie beyond it, so every count is a lower
275
+ // bound. `>=` (not `> `) because the CLIs cap silently: the GitLab API clamps
276
+ // `per_page` at 100, so an over-fetch of `limit + 1` would come back capped
277
+ // and read as "not truncated" — under-approximating exactly the way the
278
+ // window itself did. A full window is the only signal that survives clamping.
279
+ const truncated = issues.length >= limit;
280
+
200
281
  const summary = summarizeIssues(issues, nowMs);
201
- const result = { ...summary, vcs, limit };
282
+ const result = { ...summary, vcs, limit, truncated };
283
+
284
+ // Announce the bound (never a silent cap). Emitted once per cache key — a
285
+ // cache hit returns before this point, so a per-session scan warns once.
286
+ if (truncated) {
287
+ warn(
288
+ `backlog scan filled its ${limit}-issue window (${bin}): criticalCount/highCount/staleCount are LOWER BOUNDS. ` +
289
+ `Pass a larger limit for exact counts.`
290
+ );
291
+ }
292
+
202
293
  _cache.set(cacheKey, result);
203
294
  return result;
204
295
  }
@@ -18,7 +18,7 @@ import { resolve } from 'node:path';
18
18
  import { parseStateMd, parseRecommendations } from './state-md.mjs';
19
19
  import { normalizeSession, tailRealSessions } from './session-schema.mjs';
20
20
  import { parseBootstrapLock } from './bootstrap-lock-freshness.mjs';
21
- import { scanBacklog } from './backlog-scan.mjs';
21
+ import { scanBacklog, DEFAULT_BACKLOG_LIMIT } from './backlog-scan.mjs';
22
22
 
23
23
  // ---------------------------------------------------------------------------
24
24
  // Public API
@@ -36,7 +36,11 @@ import { scanBacklog } from './backlog-scan.mjs';
36
36
  * @param {string} [opts.sessionsPath] — defaults to '.orchestrator/metrics/sessions.jsonl'
37
37
  * @param {string} [opts.lockPath] — defaults to '.orchestrator/bootstrap.lock'
38
38
  * @param {Array} [opts.learnings] — pre-surfaced top-N learnings; defaults to []
39
- * @param {number} [opts.backlogLimit] — passed to scanBacklog; defaults to 50
39
+ * @param {number} [opts.backlogLimit] — passed to scanBacklog; defaults to
40
+ * `DEFAULT_BACKLOG_LIMIT` from backlog-scan.mjs (never a local copy of that
41
+ * number). A window smaller than the repo's open backlog makes
42
+ * `backlog.criticalCount`/`highCount`/`staleCount` lower bounds —
43
+ * `backlog.truncated` is the flag that says so.
40
44
  * @param {number} [opts.sessionTailN] — defaults to 10 (last N sessions)
41
45
  * @param {Function} [opts._scanBacklog] — injectable seam for tests (defaults to scanBacklog)
42
46
  * @returns {Promise<import('./mode-selector.mjs').Signals>}
@@ -56,7 +60,7 @@ export async function buildLiveSignals(opts = {}) {
56
60
  const learnings = Array.isArray(opts.learnings) ? opts.learnings : [];
57
61
  const backlogLimit = typeof opts.backlogLimit === 'number' && opts.backlogLimit > 0
58
62
  ? opts.backlogLimit
59
- : 50;
63
+ : DEFAULT_BACKLOG_LIMIT;
60
64
  const sessionTailN = typeof opts.sessionTailN === 'number' && opts.sessionTailN > 0
61
65
  ? opts.sessionTailN
62
66
  : 10;