session-orchestrator 3.23.0 → 4.0.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 (393) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -37,6 +37,7 @@ import { emitEvent } from '../events.mjs';
37
37
  import { main as gcMain } from '../../gc-stale-worktrees.mjs';
38
38
  import { SEMANTIC_ID_RE } from '../session-id.mjs';
39
39
  import { repoPathHash } from '../session-registry.mjs';
40
+ import { leaveSourceRoot } from '../session-transition.mjs';
40
41
  // Marker location is owned by its READER (session-end Phase 4a) — importing the
41
42
  // constant from there keeps writer and reader on one string. The dependency runs
42
43
  // heavy→lean (this pipeline → the dependency-free cleanup helper), never back.
@@ -731,6 +732,21 @@ function writePromotionMarker({ wtPath, sourceRoot, sessionId, branch }) {
731
732
  * @param {string} params.sessionId - Semantic session-ID matching SEMANTIC_ID_RE.
732
733
  * @param {string} params.branch - Branch name (existing or new) matching ENTER_WORKTREE_BRANCH_RE.
733
734
  * @param {string} params.repoRoot - Path to the source git repository (passed explicitly to avoid CWD drift per #219).
735
+ * @param {string} [params.rawSessionId] - The RAW (physical) session id that owns
736
+ * the SOURCE root's `session.lock` and registry entry. Optional. When it is a
737
+ * non-empty string, the source root is LEFT mechanically (#1170): after the
738
+ * destination worktree provably exists, `leaveSourceRoot()` runs and its
739
+ * result is reported as `left` in the return value. OMITTING it (strictly
740
+ * `undefined`) preserves the pre-#1170 behaviour exactly — no teardown, no
741
+ * `left` key, no WARN. Passing something UNUSABLE (`null`, `''`, a non-string —
742
+ * `readLock({ repoRoot }).session_id` returns `null` on a missing/unreadable
743
+ * lock, which is how this happens in practice) is NOT the same case: it WARNs
744
+ * and reports `left: { ok: false, reason: 'raw-session-id-unusable' }`, because
745
+ * a silent skip there is the phantom-owner state of #1069. NOT the semantic
746
+ * `sessionId` above, which is a naming key, never an ownership key.
747
+ * @param {string} [params.reason='worktree-promotion'] - Departure reason
748
+ * recorded verbatim in the `orchestrator.session.root_left` payload. Only used
749
+ * when `rawSessionId` is given.
734
750
  * @param {object} [opts]
735
751
  * @param {Function} [opts.$] - zx-like template-tag executor (DI seam); falls back to lazy `await import('zx')`.
736
752
  * Every freshly created worktree also gets a `.orchestrator/promoted-from.json`
@@ -739,18 +755,27 @@ function writePromotionMarker({ wtPath, sourceRoot, sessionId, branch }) {
739
755
  * session with a different id. Marker writing is best-effort and never fails
740
756
  * the promotion.
741
757
  *
742
- * @returns {Promise<{ wtPath: string, reused: boolean, branch?: string, promotedFrom?: string, reusedBranch?: true }>}
758
+ * @returns {Promise<{ wtPath: string, reused: boolean, branch?: string, promotedFrom?: string, reusedBranch?: true, left?: { ok: boolean, steps: object, reason?: string } }>}
743
759
  * `branch` is the branch the new worktree actually landed on — equal to the
744
760
  * `branch` param except in case 1 above, where it is `so/<sessionId>` and
745
761
  * `promotedFrom` carries the requested source branch. `reusedBranch` is
746
762
  * present (and `true`) only when an existing `so/<sessionId>` was checked out
747
763
  * rather than created. All three are absent on the `reused: true` path (no
748
- * branch was chosen — the worktree pre-existed).
764
+ * branch was chosen — the worktree pre-existed). `left` is present on BOTH
765
+ * success exits — and ONLY — when `rawSessionId` was supplied (including when
766
+ * it was supplied unusable, where it reports the refusal).
749
767
  * @throws {TypeError} when any required param is missing or fails validation.
750
768
  * @throws {WorktreeBoundaryError} when the computed worktree path escapes `basePath`.
751
769
  * @throws {WorktreePromotionBranchError} when `so/<sessionId>` is checked out elsewhere.
752
770
  */
753
- export async function enterWorktree({ basePath, sessionId, branch, repoRoot } = {}, opts = {}) {
771
+ export async function enterWorktree({
772
+ basePath,
773
+ sessionId,
774
+ branch,
775
+ repoRoot,
776
+ rawSessionId,
777
+ reason = 'worktree-promotion',
778
+ } = {}, opts = {}) {
754
779
  // -------------------------------------------------------------------------
755
780
  // Step 1: Input validation (TypeError on any malformed param).
756
781
  // -------------------------------------------------------------------------
@@ -839,11 +864,62 @@ export async function enterWorktree({ basePath, sessionId, branch, repoRoot } =
839
864
  );
840
865
  }
841
866
 
867
+ // -------------------------------------------------------------------------
868
+ // Step 3b: The source-root departure (#1170), as a MECHANICAL step.
869
+ //
870
+ // Before #1170 this was skill PROSE at four sites: `enterWorktree()` created
871
+ // the destination and the coordinator was trusted to call `leaveSourceRoot()`
872
+ // afterwards. A step that only exists in prose is a step that is sometimes
873
+ // skipped — and skipping it is exactly the phantom-peer / double-live-lock
874
+ // state #1069 was filed about.
875
+ //
876
+ // Ordering is load-bearing and only runs on a proven-existing destination:
877
+ // every throw above leaves the source root untouched, because a departure
878
+ // from a root whose successor does not exist strands the session with no
879
+ // live root at all. `leaveSourceRoot()` never throws, so a failed teardown
880
+ // WARNs and is reported in `left` — it never fails the promotion.
881
+ // -------------------------------------------------------------------------
882
+ const departSourceRoot = async (result) => {
883
+ // NOT PASSED and PASSED-BUT-UNUSABLE are different callers with different
884
+ // bugs, and collapsing them re-opened #1069 silently. `undefined` is the
885
+ // documented opt-out (the autopilot callers, which own no source session):
886
+ // byte-identical pre-#1170 behaviour, no `left`, no WARN.
887
+ if (rawSessionId === undefined) return result;
888
+ // Anything else falsy or non-string means a caller INTENDED to depart and
889
+ // handed us something unusable. The documented derivation is
890
+ // `readLock({ repoRoot }).session_id`, which is `null` whenever the lock is
891
+ // missing or unreadable — so the realistic failure lands here, and before
892
+ // this branch it returned early with nothing on stderr: promotion succeeds,
893
+ // source root never deregistered, phantom owner (#1069).
894
+ if (typeof rawSessionId !== 'string' || rawSessionId.length === 0) {
895
+ const shown = rawSessionId === null
896
+ ? 'null'
897
+ : typeof rawSessionId === 'string' ? 'empty string' : typeof rawSessionId;
898
+ console.warn(
899
+ `enterWorktree: rawSessionId unusable (${shown}) — source root NOT departed; ` +
900
+ 'read it via readLock({ repoRoot }).session_id',
901
+ );
902
+ result.left = { ok: false, reason: 'raw-session-id-unusable' };
903
+ return result;
904
+ }
905
+ const left = await leaveSourceRoot({
906
+ repoRoot,
907
+ sessionId: rawSessionId,
908
+ semanticSessionId: sessionId,
909
+ reason,
910
+ });
911
+ if (left.ok !== true) {
912
+ console.warn(`enterWorktree: leaveSourceRoot: ${left.reason ?? 'unknown'}`);
913
+ }
914
+ result.left = left;
915
+ return result;
916
+ };
917
+
842
918
  // -------------------------------------------------------------------------
843
919
  // Step 4: Idempotency — reuse if worktree already exists with .git.
844
920
  // -------------------------------------------------------------------------
845
921
  if (fs.existsSync(wtPath) && fs.existsSync(path.join(wtPath, '.git'))) {
846
- return { wtPath, reused: true };
922
+ return await departSourceRoot({ wtPath, reused: true });
847
923
  }
848
924
 
849
925
  // -------------------------------------------------------------------------
@@ -931,7 +1007,7 @@ export async function enterWorktree({ basePath, sessionId, branch, repoRoot } =
931
1007
  // Present only when it happened, so the common shape stays byte-identical
932
1008
  // for every existing consumer and strict-equality pin.
933
1009
  if (reusedBranch) result.reusedBranch = true;
934
- return result;
1010
+ return await departSourceRoot(result);
935
1011
  }
936
- return { wtPath, reused: false, branch };
1012
+ return await departSourceRoot({ wtPath, reused: false, branch });
937
1013
  }
@@ -20,6 +20,7 @@ import { parseStateMd, parseRecommendations } from './state-md.mjs';
20
20
  import { normalizeSession, tailRealSessions } from './session-schema.mjs';
21
21
  import { parseBootstrapLock } from './bootstrap-lock-freshness.mjs';
22
22
  import { scanBacklog, DEFAULT_BACKLOG_LIMIT } from './backlog-scan.mjs';
23
+ import { readCanonicalSessions } from './sessions-canonical.mjs';
23
24
 
24
25
  // ---------------------------------------------------------------------------
25
26
  // Public API
@@ -113,29 +114,31 @@ export async function buildLiveSignals(opts = {}) {
113
114
  let recentSessions = [];
114
115
 
115
116
  try {
116
- if (existsSync(sessionsPath)) {
117
- const raw = readFileSync(sessionsPath, 'utf8');
118
- const lines = raw
119
- .split('\n')
120
- .map((l) => l.trim())
121
- .filter((l) => l.length > 0);
122
- // #834: parse ALL lines (not just the naive last-N) before windowing —
123
- // `status: 'abandoned'` phantom stubs must be filtered out BEFORE the
124
- // tail is taken, or sessionTailN silently means "last N LINES" instead
125
- // of "last N REAL sessions". tailRealSessions() does the filter + tail.
126
- const parsed = [];
127
- for (const line of lines) {
128
- try {
129
- const obj = JSON.parse(line);
130
- parsed.push(normalizeSession(obj));
131
- } catch {
132
- // Branch 4: skip malformed lines silently
133
- }
134
- }
135
- recentSessions = tailRealSessions(parsed, sessionTailN);
136
- }
117
+ // #1186: readCanonicalSessions applies the #1167 newest-wins-per-`session_id`
118
+ // / attestable-`supersedes` collapse BEFORE the tail is taken. The raw read
119
+ // this replaced parsed every line unconditionally (malformed lines skipped
120
+ // on JSON.parse failure only), so a duplicated `session_id` — a
121
+ // crash-recovery re-append, or the #1068 abandoned-stub/supersede pair —
122
+ // counted as TWO entries toward `sessionTailN`, silently narrowing the
123
+ // REAL window by however many duplicates sat in the file's tail.
124
+ //
125
+ // Behavioural change: a well-formed record with NO `session_id` (never
126
+ // legitimate per REQUIRED_FIELDS, but possible on a hand-edited or
127
+ // pre-schema legacy line) is now DROPPED rather than counted — it cannot
128
+ // be deduplicated by identity, so `canonicalizeSessions` excludes it (see
129
+ // sessions-canonical.mjs's own contract). Missing-file / unreadable-file
130
+ // handling (ENOENT vs EACCES/EISDIR, #1188) is delegated to the shared
131
+ // reader; both still degrade to `[]` here, matching Branch 3's contract.
132
+ const canonical = readCanonicalSessions({ filePath: sessionsPath });
133
+ const parsed = canonical.map((obj) => normalizeSession(obj));
134
+ // #834: `status: 'abandoned'` phantom stubs must still be filtered out
135
+ // BEFORE the tail is taken, or sessionTailN silently means "last N LINES"
136
+ // instead of "last N REAL sessions". tailRealSessions() does the filter +
137
+ // tail.
138
+ recentSessions = tailRealSessions(parsed, sessionTailN);
137
139
  } catch {
138
- // Branch 3: file unreadable — recentSessions stays []
140
+ // Branch 3: file unreadable — recentSessions stays [] (readCanonicalSessions
141
+ // itself never throws, but the graceful-null contract is kept as a backstop).
139
142
  }
140
143
 
141
144
  // --- Branch 5: bootstrap.lock ---
@@ -3,8 +3,22 @@
3
3
  * Checks CI status for the current HEAD commit and returns a structured
4
4
  * result for session-start Phase 4 banner rendering.
5
5
  *
6
- * Plain-JS — no Zod dependency. Never throws. Returns null on any
7
- * no-op condition (no VCS, CLI missing, timeout, parse failure).
6
+ * Plain-JS — no Zod dependency. Never throws.
7
+ *
8
+ * THREE return states (#1031), not two:
9
+ * - `null` — read, and there is nothing to report (a
10
+ * benign ABSENCE: no VCS remote, no pipeline
11
+ * worth a banner, green with no soft failures)
12
+ * - `{status, ok, details, …}` — a real CI reading (`green` | `red` |
13
+ * `unknown`)
14
+ * - `{severity, message, degraded, ok:false}` — the state could NOT be read.
15
+ * `degraded` is a member of the frozen {@link DEGRADED_REASONS} enum
16
+ * exported below. This must NEVER be read as "CI is green".
17
+ *
18
+ * Until #1031 every unreadable state collapsed onto `null`, which in the banner
19
+ * contract reads as all-clear — the exact confusion #1022 (`gh repo view -R` →
20
+ * `unknown shorthand flag`) hid behind on every GitHub repo. The shape is the
21
+ * one `scripts/lib/mirror-issues-banner.mjs` established.
8
22
  *
9
23
  * Supports GitLab (via glab) and GitHub (via gh).
10
24
  * VCS is auto-detected from the repo's git remotes via
@@ -29,6 +43,78 @@ const execFileAsync = promisify(_execFile);
29
43
  /** Default timeout in milliseconds for CLI invocations. */
30
44
  export const DEFAULT_TIMEOUT_MS = 8000;
31
45
 
46
+ /**
47
+ * Closed set of `degraded` reasons (#1031). A degraded result means "the CI
48
+ * state was NOT successfully read" — never "CI is green".
49
+ *
50
+ * Deliberately THIS module's own enum rather than a merged one shared with
51
+ * `mirror-issues-banner.mjs` / `git-config-drift.mjs`: those two already carry
52
+ * DIFFERENT member sets for their own failure surfaces, and a merged superset
53
+ * would hand every consumer members its probe can never emit — an enum whose
54
+ * members are not exhaustively reachable cannot be switched on exhaustively,
55
+ * which is the only reason to freeze one. The members here are exactly what
56
+ * this probe can fail at:
57
+ *
58
+ * - `cli-missing` a required binary (`git`, `glab`, `gh`) is not on PATH.
59
+ * - `timeout` a subprocess outlived its budget. Not silent since #1031:
60
+ * a hang is not actionable, but "state unknown" IS the
61
+ * finding, and silence renders identically to green.
62
+ * - `parse-error` a CLI answered with output this module could not read
63
+ * (unparseable JSON, or valid JSON of the wrong shape).
64
+ * - `query-failed` residual bucket — a present CLI ran and rejected the
65
+ * invocation, or a resolvable remote whose API target could
66
+ * not be derived. Folding these into `parse-error` would
67
+ * mislabel a rejected flag as malformed output.
68
+ * - `git-error` the VCS probe itself failed (`git remote -v` non-zero for
69
+ * a reason other than "not a work tree"). Distinct from
70
+ * `query-failed` because it means the module never got as
71
+ * far as choosing a CI platform.
72
+ *
73
+ * @type {readonly ['cli-missing','timeout','parse-error','query-failed','git-error']}
74
+ */
75
+ export const DEGRADED_REASONS = Object.freeze([
76
+ 'cli-missing',
77
+ 'timeout',
78
+ 'parse-error',
79
+ 'query-failed',
80
+ 'git-error',
81
+ ]);
82
+
83
+ /**
84
+ * Build the third return state. Distinct from `null` on purpose: `null` in the
85
+ * banner contract reads as "all clear", which a failed read has NOT
86
+ * established.
87
+ *
88
+ * The `detail` is appended bounded (BV-004: 200 chars — long enough for a
89
+ * `Command failed: <argv>` first line, short enough not to bury the
90
+ * session-start banner it prints beside; revisit if a CLI starts emitting a
91
+ * diagnostic that needs more). Callers pass ALREADY-redacted text — every
92
+ * source here is either a fixed string or a `redactUrlCredentials` result.
93
+ *
94
+ * @param {'cli-missing'|'timeout'|'parse-error'|'query-failed'|'git-error'} reason
95
+ * @param {string} [detail] Optional, already-redacted diagnostic tail
96
+ * @returns {{ severity: 'warn', ok: false, message: string, degraded: string }}
97
+ */
98
+ function degradedResult(reason, detail) {
99
+ // Redaction (the callers' job) and control-byte escaping are DIFFERENT
100
+ // protections: `redactUrlCredentials` removes secrets, it does not neutralise
101
+ // ANSI/CR. `parseCliJson` already escapes the text it embeds (see the call at
102
+ // the JSON.parse catch), but the degraded tail reaches the operator's terminal
103
+ // by a second route — a CLI stderr quoted into `err.message` — and skipped it.
104
+ // Escape here so BOTH routes are covered at their common exit. Slice first,
105
+ // escape after: the budget is 200 chars of DETAIL, and escaping cannot then
106
+ // leave a cut mid-`\uXXXX`.
107
+ const tail = detail ? ` — ${escapeControlBytes(String(detail).trim().slice(0, 200))}` : '';
108
+ return {
109
+ severity: 'warn',
110
+ ok: false,
111
+ message:
112
+ `⚠ ci-status: CI status for HEAD could not be determined (${reason}) — ` +
113
+ `state UNKNOWN, not "green".${tail}`,
114
+ degraded: reason,
115
+ };
116
+ }
117
+
32
118
  /**
33
119
  * Wraps execFile with a per-call timeout race.
34
120
  *
@@ -148,10 +234,10 @@ function parseCliJson(stdout, label, expect) {
148
234
  // `cause` preserves the original for a debugger; the reason is ALSO
149
235
  // inlined into the message because the outer catch reads `err.message`
150
236
  // only — a cause-only wrapper would lose it on the operator-facing line.
151
- throw new Error(
237
+ throw tagParseError(new Error(
152
238
  `${label} returned unparseable JSON (${reason}) — ${shown}`,
153
239
  { cause: err },
154
- );
240
+ ));
155
241
  }
156
242
 
157
243
  // A shape mismatch reports the JSON TYPE, never the payload. The parse
@@ -161,25 +247,47 @@ function parseCliJson(stdout, label, expect) {
161
247
  // its contents echoed anywhere. A type name carries no body content.
162
248
  const actual = jsonTypeOf(parsed);
163
249
  if (expect === 'array' && actual !== 'array') {
164
- throw new Error(`${label} returned JSON of an unexpected shape — expected an array, got ${actual}`);
250
+ throw tagParseError(new Error(`${label} returned JSON of an unexpected shape — expected an array, got ${actual}`));
165
251
  }
166
252
  if (expect === 'object' && actual !== 'object') {
167
- throw new Error(`${label} returned JSON of an unexpected shape — expected an object, got ${actual}`);
253
+ throw tagParseError(new Error(`${label} returned JSON of an unexpected shape — expected an object, got ${actual}`));
168
254
  }
169
255
  return parsed;
170
256
  }
171
257
 
258
+ /**
259
+ * Stamp the `parse-error` degraded reason onto an error raised by
260
+ * {@link parseCliJson}.
261
+ *
262
+ * Carried on the error object rather than re-derived from its message text in
263
+ * the outer catch: a message-substring test would silently reclassify the
264
+ * moment a wording changes, and the two failures (`unparseable JSON`, wrong
265
+ * shape) are one class only because THIS function says so.
266
+ *
267
+ * @param {Error} err
268
+ * @returns {Error}
269
+ */
270
+ function tagParseError(err) {
271
+ return Object.assign(err, { degradedReason: 'parse-error' });
272
+ }
273
+
172
274
  /**
173
275
  * The one failure this probe can produce that the frozen
174
276
  * `REMOTE_RESOLUTION_REASONS` set has no member for: the async timeout race
175
277
  * every subprocess here runs under has no counterpart in the SYNCHRONOUS
176
278
  * `vcs-repo-spec.mjs` core (it uses `execFileSync`, which cannot time out).
177
279
  *
178
- * Deliberately NOT a query-failure for warning purposes — a timeout stays
179
- * SILENT, matching how `checkCiStatus`'s outer catch has always treated one
180
- * (`msg === 'timeout'` → silent `null`). Warning on a hung `git` but not on a
181
- * hung `glab` would be an inconsistency inside a single banner, and a hang is
182
- * not a fact an operator can act on the way "git is not installed" is.
280
+ * Deliberately NOT a query-failure for WARNING purposes — a timeout emits no
281
+ * `console.warn`, matching how `checkCiStatus`'s outer catch has always
282
+ * treated one, and warning on a hung `git` but not on a hung `glab` would be
283
+ * an inconsistency inside a single banner.
284
+ *
285
+ * It is no longer SILENT, though (#1031). The old argument — "a hang is not a
286
+ * fact an operator can act on the way 'git is not installed' is" — is
287
+ * superseded: what the operator acts on is not the hang, it is that CI state
288
+ * is UNKNOWN, and returning `null` rendered that identically to green. A
289
+ * timeout therefore yields `degradedResult('timeout')` on both this path and
290
+ * the outer catch.
183
291
  */
184
292
  const PROBE_TIMEOUT = 'probe-timeout';
185
293
 
@@ -203,6 +311,32 @@ const PROBE_TIMEOUT = 'probe-timeout';
203
311
  */
204
312
  const SILENT_QUERY_FAILURES = new Set(['not-a-git-repo']);
205
313
 
314
+ /**
315
+ * Project a `REMOTE_RESOLUTION_REASONS` query failure onto this module's own
316
+ * {@link DEGRADED_REASONS} member.
317
+ *
318
+ * Written as explicit branches rather than a lookup table so that EVERY reason
319
+ * literal this module can emit sits inside a literal call to the builder
320
+ * call — which is what makes the enum-membership census in
321
+ * `tests/lib/ci-status-banner.test.mjs` a regex over the source instead of a
322
+ * hand-typed list that drifts.
323
+ *
324
+ * The fallthrough is `query-failed`, not a throw: a reason added to the frozen
325
+ * upstream set must degrade visibly (fail-toward-visible), never crash the
326
+ * probe or slip back onto the silent path.
327
+ *
328
+ * @param {string} reason
329
+ * @param {string} [detail] Already-redacted git stderr
330
+ * @returns {{ severity: 'warn', ok: false, message: string, degraded: string }}
331
+ */
332
+ function degradedForVcsReason(reason, detail) {
333
+ // git itself is a CLI, and "not on PATH" is the same fact for it as for
334
+ // glab/gh — one member, not two spellings of it.
335
+ if (reason === 'git-unavailable') return degradedResult('cli-missing', detail);
336
+ if (reason === 'git-error') return degradedResult('git-error', detail);
337
+ return degradedResult('query-failed', detail);
338
+ }
339
+
206
340
  /**
207
341
  * Classify a rejected ASYNC `execFile` into a `REMOTE_RESOLUTION_REASONS`
208
342
  * member.
@@ -651,24 +785,22 @@ async function checkGithub(repoRoot, deps = {}) {
651
785
  /**
652
786
  * Checks CI status for the current HEAD commit.
653
787
  *
654
- * Returns `null` (silent no-op) when:
655
- * - The repo has no usable VCS remote (not a git repo, no remotes at all,
656
- * or >= 2 remotes with no preference match) — a benign, measured absence
657
- * - Required CLI (glab / gh) not in PATH
658
- * - Any CLI invocation times out
659
- *
660
- * Also returns `null`, but with a `console.warn` trace, when the VCS-detection
661
- * QUERY ITSELF failed (`git` not on PATH, `git remote -v` erroring), when a
662
- * present CLI rejected its invocation, or when a CLI returned output this
663
- * module could not parse (see {@link parseCliJson}). `null` alone cannot
664
- * express "could not read" — see the outer catch and Step 1 for why the warn
665
- * channel carries it.
666
- *
667
- * The unparseable-output case was listed above as SILENT until 2026-08-28.
668
- * That was never the behaviour — it has always fallen through to the outer
669
- * catch's warn branch (measured at 30940cb). The drift survived because the
670
- * one test covering it asserted only the `null` and let the file-wide
671
- * `console.warn` spy swallow the rest.
788
+ * Returns `null` (silent no-op) ONLY for a measured ABSENCE: the repo has no
789
+ * usable VCS remote (not a git repo, no remotes at all, or >= 2 remotes with no
790
+ * preference match). That is a complete answer — there is no CI here.
791
+ *
792
+ * Returns a DEGRADED result (`{severity:'warn', ok:false, message, degraded}`,
793
+ * see {@link DEGRADED_REASONS}) when the state could not be read: the
794
+ * VCS-detection query itself failed (`git` not on PATH, `git remote -v`
795
+ * erroring), a required CLI is missing, an invocation timed out, a present CLI
796
+ * rejected its invocation, a GitLab API target could not be derived, or a CLI
797
+ * returned output this module could not parse (see {@link parseCliJson}).
798
+ * Callers MUST treat that as "state unknown", never as clean — the generic
799
+ * degraded path in `scripts/lib/session-start-probes.mjs` does exactly that.
800
+ *
801
+ * Missing-CLI and timeout additionally stay SILENT on the `console.warn`
802
+ * channel (they are normal states on a CLI-less machine); every other degraded
803
+ * reason also emits a warn.
672
804
  *
673
805
  * @param {{
674
806
  * repoRoot?: string,
@@ -686,6 +818,11 @@ async function checkGithub(repoRoot, deps = {}) {
686
818
  * `resolveGitlabProjectTarget`, which proves host and project path from one
687
819
  * sanitized remote; GitHub retains the #872 spec/host resolvers.
688
820
  * @returns {Promise<null | {
821
+ * severity: 'warn',
822
+ * ok: false,
823
+ * message: string,
824
+ * degraded: 'cli-missing'|'timeout'|'parse-error'|'query-failed'|'git-error',
825
+ * } | {
689
826
  * status: 'green'|'red'|'unknown',
690
827
  * ok: boolean,
691
828
  * lastGreen?: { sha: string, pipelineId: number, ageCommits: number, ageDays: number|null },
@@ -723,24 +860,31 @@ export async function checkCiStatus(opts = {}, deps = {}) {
723
860
  if (!vcs) {
724
861
  const detected = await detectVcs(repoRoot, depsWithExec);
725
862
  if (!detected.ok) {
726
- // The two-state `null` return is fixed by 13 sibling banners, so the
727
- // third state lives in the WARN channel: an ABSENCE (`no-remotes`,
728
- // `no-matching-remote`, `unsafe-value`) is a real, benign answer and
729
- // stays silent — warning there would train operators to ignore this
730
- // line, which is the more expensive error. A QUERY FAILURE
731
- // (`git-unavailable`, `git-error`) means the question could not be
732
- // asked at all, and that is precisely the state that was
733
- // indistinguishable from "nothing to report" before #1039.
734
- // `not-a-git-repo` is the one query failure this banner reads as an
735
- // absence — see {@link SILENT_QUERY_FAILURES}.
863
+ // An ABSENCE (`no-remotes`, `no-matching-remote`, `unsafe-value`) is a
864
+ // real, benign answer and stays `null` AND silent — degrading there
865
+ // would print a banner on every session-start in a remote-less repo and
866
+ // train operators to ignore this line, which is the more expensive
867
+ // error. A QUERY FAILURE (`git-unavailable`, `git-error`) means the
868
+ // question could not be asked at all: since #1031 that is the third
869
+ // state, no longer a warn-only side channel on top of an all-clear
870
+ // `null`. `not-a-git-repo` is the one query failure this banner reads
871
+ // as an absence — see {@link SILENT_QUERY_FAILURES}.
872
+ if (detected.reason === PROBE_TIMEOUT) {
873
+ return degradedResult('timeout');
874
+ }
736
875
  if (isQueryFailure(detected.reason) && !SILENT_QUERY_FAILURES.has(detected.reason)) {
737
- const detail = detected.stderr
738
- ? ` — ${redactUrlCredentials(detected.stderr).trim()}`
876
+ const stderr = detected.stderr
877
+ ? redactUrlCredentials(detected.stderr).trim()
739
878
  : '';
879
+ const detail = stderr ? ` — ${stderr}` : '';
740
880
  console.warn(
741
881
  `WARN ci-status-banner: VCS detection failed (${detected.reason}), banner suppressed — ` +
742
882
  `CI state is UNKNOWN, not "green".${detail}`,
743
883
  );
884
+ return degradedForVcsReason(
885
+ detected.reason,
886
+ stderr ? `${detected.reason}: ${stderr}` : detected.reason,
887
+ );
744
888
  }
745
889
  return null;
746
890
  }
@@ -765,7 +909,10 @@ export async function checkCiStatus(opts = {}, deps = {}) {
765
909
  'WARN ci-status-banner: a GitLab remote was detected but its host/project path ' +
766
910
  'could not be derived, banner suppressed — CI state is UNKNOWN, not "green".',
767
911
  );
768
- return null;
912
+ return degradedResult(
913
+ 'query-failed',
914
+ 'a GitLab remote was detected but its host/project path could not be derived',
915
+ );
769
916
  }
770
917
  return await checkGitlab(repoRoot, now, { ...depsWithExec, gitlabProject });
771
918
  }
@@ -779,47 +926,45 @@ export async function checkCiStatus(opts = {}, deps = {}) {
779
926
  // Unknown VCS value — silent no-op.
780
927
  return null;
781
928
  } catch (err) {
782
- // Swallow all errors: ENOENT (CLI missing), timeout, parse failures.
783
- // These are all no-op conditions per the spec.
929
+ // Never throws: every failure below leaves as the third return state.
784
930
  const msg = err instanceof Error ? err.message : String(err);
785
931
 
786
- // Timeout and ENOENT (missing CLI) → silent null.
787
- if (
788
- msg === 'timeout' ||
789
- (err && err.code === 'ENOENT')
790
- ) {
791
- return null;
932
+ // Timeout and ENOENT (missing CLI) stay SILENT on the warn channel — a
933
+ // machine without `glab` is a normal state, and a warn on every
934
+ // session-start there drowns the #1022 signal this channel carries. Since
935
+ // #1031 they are no longer silent on the RETURN channel: neither state
936
+ // established that CI is green, so both degrade.
937
+ if (msg === 'timeout') return degradedResult('timeout');
938
+ if (err && err.code === 'ENOENT') {
939
+ return degradedResult('cli-missing', redactUrlCredentials(msg));
792
940
  }
793
941
 
794
942
  // Everything else means the CLI was PRESENT but the invocation failed —
795
- // non-zero exit, rejected flag, unparseable output. Returning a bare null
796
- // here makes that state indistinguishable from "CLI not installed", which
797
- // is exactly how #1022 (`gh repo view -R` → `unknown shorthand flag: 'R'`)
798
- // stayed invisible on every GitHub repo. THIS module keeps the two-state
799
- // contract (null ⇒ silent no-op) for backwards compatibility with the
800
- // Phase-4 callers already written against it, so leave a stderr trace
801
- // instead: non-blocking, but the next defect of this class is no longer
802
- // silent. Credentials redacted defense-in-depth (#907) — the message can
803
- // quote the failed argv, which carries the repo spec.
943
+ // non-zero exit, rejected flag, unparseable output. A bare `null` here made
944
+ // that state indistinguishable from "CLI not installed" AND from "CI is
945
+ // green", which is exactly how #1022 (`gh repo view -R` → `unknown
946
+ // shorthand flag: 'R'`) stayed invisible on every GitHub repo.
804
947
  //
805
- // DIRECTION FOR NEW BANNERS — do not copy this shape. The
806
- // absence-preserving form is `scripts/lib/mirror-issues-banner.mjs`: a
807
- // THIRD return state `{ severity, message, degraded }` whose `degraded`
808
- // is a member of the closed `DEGRADED_REASONS` enum exported there, so
809
- // "could not read" stays distinguishable from "read, and clean".
948
+ // #1031 finished the migration this comment used to describe as pending:
949
+ // the third state `{ severity, message, degraded }` — the shape
950
+ // `scripts/lib/mirror-issues-banner.mjs` established — now leaves here too,
951
+ // alongside (not instead of) the stderr trace. `parse-error` is carried on
952
+ // the error by `tagParseError`; everything else is the residual
953
+ // `query-failed`.
810
954
  //
811
- // The REMOTE probe (`detectVcs`) has since been pulled onto that shape
812
- // (#1039): it returns a reason from the frozen `REMOTE_RESOLUTION_REASONS`
813
- // set and Step 1 branches on `isQueryFailure` — absence silent, query
814
- // failure warned. This outer catch is what remains unmigrated: it still
815
- // collapses every CLI-side failure onto `null`, which a caller reads as
816
- // all-clear, and it cannot be widened without changing a return contract
817
- // 13 sibling banners share. Same verdict, stated caller-side, in
818
- // `skills/session-start/SKILL.md` § Phase 4 (the mirror-issues
819
- // paragraph): "Do not reproduce it."
955
+ // Credentials redacted defense-in-depth (#907) — the message can quote the
956
+ // failed argv, which carries the repo spec — and the redacted text is what
957
+ // reaches BOTH the warn and the operator-facing `message`.
958
+ const redacted = redactUrlCredentials(msg);
820
959
  console.warn(
821
- `WARN ci-status-banner: CI status check failed, banner suppressed — ${redactUrlCredentials(msg)}`,
960
+ `WARN ci-status-banner: CI status check failed, banner suppressed — ${redacted}`,
822
961
  );
823
- return null;
962
+ // Literal reasons on BOTH branches, never `degradedResult(reason)` with a
963
+ // computed variable: the enum census in the test file is a regex over this
964
+ // source, and a variable hides the member from it.
965
+ if (err && typeof err === 'object' && err.degradedReason === 'parse-error') {
966
+ return degradedResult('parse-error', redacted);
967
+ }
968
+ return degradedResult('query-failed', redacted);
824
969
  }
825
970
  }