@humanbased/crosscheck 1.2.0 → 1.3.0-beta.82

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 (331) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +182 -375
  3. package/README.zh.md +1 -1
  4. package/assets/icon-256.png +0 -0
  5. package/assets/linear-comment.svg +18 -0
  6. package/assets/linear-onboard.svg +30 -0
  7. package/assets/linear-status.svg +23 -0
  8. package/assets/linear-test.svg +34 -0
  9. package/assets/skills/code-review/.crosscheck-skill.json +9 -0
  10. package/assets/skills/code-review/LICENSE +21 -0
  11. package/assets/skills/code-review/SKILL.md +89 -0
  12. package/assets/skills/code-review/agents/openai.yaml +3 -0
  13. package/assets/skills/code-review-skill/.crosscheck-skill.json +9 -0
  14. package/assets/skills/code-review-skill/LICENSE +21 -0
  15. package/assets/skills/code-review-skill/SKILL.md +231 -0
  16. package/assets/skills/code-review-skill/assets/pr-review-template.md +137 -0
  17. package/assets/skills/code-review-skill/assets/review-checklist.md +123 -0
  18. package/assets/skills/code-review-skill/reference/angular.md +768 -0
  19. package/assets/skills/code-review-skill/reference/architecture-review-guide.md +472 -0
  20. package/assets/skills/code-review-skill/reference/c.md +890 -0
  21. package/assets/skills/code-review-skill/reference/code-quality-universal.md +488 -0
  22. package/assets/skills/code-review-skill/reference/code-review-best-practices.md +136 -0
  23. package/assets/skills/code-review-skill/reference/common-bugs-checklist.md +286 -0
  24. package/assets/skills/code-review-skill/reference/cpp.md +893 -0
  25. package/assets/skills/code-review-skill/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  26. package/assets/skills/code-review-skill/reference/cross-cutting/error-handling-principles.md +492 -0
  27. package/assets/skills/code-review-skill/reference/cross-cutting/n-plus-one-queries.md +309 -0
  28. package/assets/skills/code-review-skill/reference/cross-cutting/sql-injection-prevention.md +308 -0
  29. package/assets/skills/code-review-skill/reference/cross-cutting/xss-prevention.md +264 -0
  30. package/assets/skills/code-review-skill/reference/csharp.md +525 -0
  31. package/assets/skills/code-review-skill/reference/css-less-sass.md +661 -0
  32. package/assets/skills/code-review-skill/reference/django.md +985 -0
  33. package/assets/skills/code-review-skill/reference/fastapi.md +580 -0
  34. package/assets/skills/code-review-skill/reference/go.md +993 -0
  35. package/assets/skills/code-review-skill/reference/java.md +409 -0
  36. package/assets/skills/code-review-skill/reference/java8.md +586 -0
  37. package/assets/skills/code-review-skill/reference/kotlin.md +1018 -0
  38. package/assets/skills/code-review-skill/reference/nestjs.md +593 -0
  39. package/assets/skills/code-review-skill/reference/performance-review-guide.md +816 -0
  40. package/assets/skills/code-review-skill/reference/php.md +684 -0
  41. package/assets/skills/code-review-skill/reference/python.md +1073 -0
  42. package/assets/skills/code-review-skill/reference/qt.md +757 -0
  43. package/assets/skills/code-review-skill/reference/react.md +871 -0
  44. package/assets/skills/code-review-skill/reference/ruby.md +964 -0
  45. package/assets/skills/code-review-skill/reference/rust.md +846 -0
  46. package/assets/skills/code-review-skill/reference/security-review-guide.md +494 -0
  47. package/assets/skills/code-review-skill/reference/svelte.md +1064 -0
  48. package/assets/skills/code-review-skill/reference/swift.md +936 -0
  49. package/assets/skills/code-review-skill/reference/typescript.md +1016 -0
  50. package/assets/skills/code-review-skill/reference/vue.md +924 -0
  51. package/assets/skills/code-review-skill/reference/zig.md +440 -0
  52. package/assets/skills/code-review-skill/scripts/pr-analyzer.py +435 -0
  53. package/assets/skills/code-review-skill/scripts/test_pr_analyzer.py +380 -0
  54. package/assets/skills/codebase-design/.crosscheck-skill.json +9 -0
  55. package/assets/skills/codebase-design/DEEPENING.md +37 -0
  56. package/assets/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  57. package/assets/skills/codebase-design/LICENSE +21 -0
  58. package/assets/skills/codebase-design/SKILL.md +114 -0
  59. package/assets/skills/codebase-design/agents/openai.yaml +3 -0
  60. package/assets/skills/diagnosing-bugs/.crosscheck-skill.json +9 -0
  61. package/assets/skills/diagnosing-bugs/LICENSE +21 -0
  62. package/assets/skills/diagnosing-bugs/SKILL.md +134 -0
  63. package/assets/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  64. package/assets/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  65. package/crosscheck.config.example.yml +101 -9
  66. package/dist/__tests__/board.test.js +11 -0
  67. package/dist/__tests__/board.test.js.map +1 -1
  68. package/dist/__tests__/can-write-verdict.test.d.ts +2 -0
  69. package/dist/__tests__/can-write-verdict.test.d.ts.map +1 -0
  70. package/dist/__tests__/can-write-verdict.test.js +31 -0
  71. package/dist/__tests__/can-write-verdict.test.js.map +1 -0
  72. package/dist/__tests__/codex.test.js +14 -27
  73. package/dist/__tests__/codex.test.js.map +1 -1
  74. package/dist/__tests__/comment-bodies.test.js +49 -1
  75. package/dist/__tests__/comment-bodies.test.js.map +1 -1
  76. package/dist/__tests__/conflict-resolve.test.js +44 -1
  77. package/dist/__tests__/conflict-resolve.test.js.map +1 -1
  78. package/dist/__tests__/fix.test.js +33 -0
  79. package/dist/__tests__/fix.test.js.map +1 -1
  80. package/dist/__tests__/linear-branding.test.d.ts +2 -0
  81. package/dist/__tests__/linear-branding.test.d.ts.map +1 -0
  82. package/dist/__tests__/linear-branding.test.js +156 -0
  83. package/dist/__tests__/linear-branding.test.js.map +1 -0
  84. package/dist/__tests__/linear-client.test.d.ts +2 -0
  85. package/dist/__tests__/linear-client.test.d.ts.map +1 -0
  86. package/dist/__tests__/linear-client.test.js +120 -0
  87. package/dist/__tests__/linear-client.test.js.map +1 -0
  88. package/dist/__tests__/linear-comment.test.d.ts +2 -0
  89. package/dist/__tests__/linear-comment.test.d.ts.map +1 -0
  90. package/dist/__tests__/linear-comment.test.js +151 -0
  91. package/dist/__tests__/linear-comment.test.js.map +1 -0
  92. package/dist/__tests__/linear-identity.test.d.ts +2 -0
  93. package/dist/__tests__/linear-identity.test.d.ts.map +1 -0
  94. package/dist/__tests__/linear-identity.test.js +253 -0
  95. package/dist/__tests__/linear-identity.test.js.map +1 -0
  96. package/dist/__tests__/linear-notify.test.d.ts +2 -0
  97. package/dist/__tests__/linear-notify.test.d.ts.map +1 -0
  98. package/dist/__tests__/linear-notify.test.js +144 -0
  99. package/dist/__tests__/linear-notify.test.js.map +1 -0
  100. package/dist/__tests__/linear-ref.test.d.ts +2 -0
  101. package/dist/__tests__/linear-ref.test.d.ts.map +1 -0
  102. package/dist/__tests__/linear-ref.test.js +261 -0
  103. package/dist/__tests__/linear-ref.test.js.map +1 -0
  104. package/dist/__tests__/linear-test-ref.test.d.ts +2 -0
  105. package/dist/__tests__/linear-test-ref.test.d.ts.map +1 -0
  106. package/dist/__tests__/linear-test-ref.test.js +81 -0
  107. package/dist/__tests__/linear-test-ref.test.js.map +1 -0
  108. package/dist/__tests__/linear-verify.test.d.ts +2 -0
  109. package/dist/__tests__/linear-verify.test.d.ts.map +1 -0
  110. package/dist/__tests__/linear-verify.test.js +132 -0
  111. package/dist/__tests__/linear-verify.test.js.map +1 -0
  112. package/dist/__tests__/linear-worker.test.d.ts +2 -0
  113. package/dist/__tests__/linear-worker.test.d.ts.map +1 -0
  114. package/dist/__tests__/linear-worker.test.js +83 -0
  115. package/dist/__tests__/linear-worker.test.js.map +1 -0
  116. package/dist/__tests__/linear-write-possible.test.d.ts +2 -0
  117. package/dist/__tests__/linear-write-possible.test.d.ts.map +1 -0
  118. package/dist/__tests__/linear-write-possible.test.js +30 -0
  119. package/dist/__tests__/linear-write-possible.test.js.map +1 -0
  120. package/dist/__tests__/onboard-preservation.test.js +59 -3
  121. package/dist/__tests__/onboard-preservation.test.js.map +1 -1
  122. package/dist/__tests__/optimize.test.js +2 -0
  123. package/dist/__tests__/optimize.test.js.map +1 -1
  124. package/dist/__tests__/pr-status.test.js +163 -2
  125. package/dist/__tests__/pr-status.test.js.map +1 -1
  126. package/dist/__tests__/pr-workflow-state.test.js +102 -1
  127. package/dist/__tests__/pr-workflow-state.test.js.map +1 -1
  128. package/dist/__tests__/repo-picker.test.js +7 -1
  129. package/dist/__tests__/repo-picker.test.js.map +1 -1
  130. package/dist/__tests__/repository-guidance.test.d.ts +2 -0
  131. package/dist/__tests__/repository-guidance.test.d.ts.map +1 -0
  132. package/dist/__tests__/repository-guidance.test.js +107 -0
  133. package/dist/__tests__/repository-guidance.test.js.map +1 -0
  134. package/dist/__tests__/review-comment-body.test.js +35 -0
  135. package/dist/__tests__/review-comment-body.test.js.map +1 -1
  136. package/dist/__tests__/review-models.test.js +19 -3
  137. package/dist/__tests__/review-models.test.js.map +1 -1
  138. package/dist/__tests__/review-strategy.test.d.ts +2 -0
  139. package/dist/__tests__/review-strategy.test.d.ts.map +1 -0
  140. package/dist/__tests__/review-strategy.test.js +397 -0
  141. package/dist/__tests__/review-strategy.test.js.map +1 -0
  142. package/dist/__tests__/runner.test.js +29 -1
  143. package/dist/__tests__/runner.test.js.map +1 -1
  144. package/dist/__tests__/skill-attribution.test.d.ts +2 -0
  145. package/dist/__tests__/skill-attribution.test.d.ts.map +1 -0
  146. package/dist/__tests__/skill-attribution.test.js +53 -0
  147. package/dist/__tests__/skill-attribution.test.js.map +1 -0
  148. package/dist/__tests__/skill-broker.test.d.ts +2 -0
  149. package/dist/__tests__/skill-broker.test.d.ts.map +1 -0
  150. package/dist/__tests__/skill-broker.test.js +144 -0
  151. package/dist/__tests__/skill-broker.test.js.map +1 -0
  152. package/dist/__tests__/skill-catalog.test.d.ts +2 -0
  153. package/dist/__tests__/skill-catalog.test.d.ts.map +1 -0
  154. package/dist/__tests__/skill-catalog.test.js +40 -0
  155. package/dist/__tests__/skill-catalog.test.js.map +1 -0
  156. package/dist/__tests__/skill-installer.test.d.ts +2 -0
  157. package/dist/__tests__/skill-installer.test.d.ts.map +1 -0
  158. package/dist/__tests__/skill-installer.test.js +96 -0
  159. package/dist/__tests__/skill-installer.test.js.map +1 -0
  160. package/dist/__tests__/skills-config.test.d.ts +2 -0
  161. package/dist/__tests__/skills-config.test.d.ts.map +1 -0
  162. package/dist/__tests__/skills-config.test.js +12 -0
  163. package/dist/__tests__/skills-config.test.js.map +1 -0
  164. package/dist/cli.js +29 -0
  165. package/dist/cli.js.map +1 -1
  166. package/dist/commands/detect-step.d.ts.map +1 -1
  167. package/dist/commands/detect-step.js +4 -0
  168. package/dist/commands/detect-step.js.map +1 -1
  169. package/dist/commands/kickass.d.ts.map +1 -1
  170. package/dist/commands/kickass.js +3 -2
  171. package/dist/commands/kickass.js.map +1 -1
  172. package/dist/commands/linear-test.d.ts +18 -0
  173. package/dist/commands/linear-test.d.ts.map +1 -0
  174. package/dist/commands/linear-test.js +130 -0
  175. package/dist/commands/linear-test.js.map +1 -0
  176. package/dist/commands/onboard.d.ts +36 -3
  177. package/dist/commands/onboard.d.ts.map +1 -1
  178. package/dist/commands/onboard.js +233 -42
  179. package/dist/commands/onboard.js.map +1 -1
  180. package/dist/commands/review.d.ts.map +1 -1
  181. package/dist/commands/review.js +65 -6
  182. package/dist/commands/review.js.map +1 -1
  183. package/dist/commands/run.d.ts.map +1 -1
  184. package/dist/commands/run.js +51 -7
  185. package/dist/commands/run.js.map +1 -1
  186. package/dist/commands/skill.d.ts +2 -0
  187. package/dist/commands/skill.d.ts.map +1 -0
  188. package/dist/commands/skill.js +16 -0
  189. package/dist/commands/skill.js.map +1 -0
  190. package/dist/commands/status.d.ts.map +1 -1
  191. package/dist/commands/status.js +53 -1
  192. package/dist/commands/status.js.map +1 -1
  193. package/dist/commands/watch.d.ts.map +1 -1
  194. package/dist/commands/watch.js +169 -64
  195. package/dist/commands/watch.js.map +1 -1
  196. package/dist/config/loader.d.ts +3 -1
  197. package/dist/config/loader.d.ts.map +1 -1
  198. package/dist/config/loader.js +13 -0
  199. package/dist/config/loader.js.map +1 -1
  200. package/dist/config/review-model-tiers.json +3 -3
  201. package/dist/config/review-strategy.json +204 -0
  202. package/dist/config/schema.d.ts +261 -15
  203. package/dist/config/schema.d.ts.map +1 -1
  204. package/dist/config/schema.js +90 -8
  205. package/dist/config/schema.js.map +1 -1
  206. package/dist/github/client.d.ts +21 -1
  207. package/dist/github/client.d.ts.map +1 -1
  208. package/dist/github/client.js +46 -7
  209. package/dist/github/client.js.map +1 -1
  210. package/dist/github/webhook.d.ts +4 -0
  211. package/dist/github/webhook.d.ts.map +1 -1
  212. package/dist/github/webhook.js.map +1 -1
  213. package/dist/issues/ticket-ref.d.ts.map +1 -1
  214. package/dist/issues/ticket-ref.js +6 -5
  215. package/dist/issues/ticket-ref.js.map +1 -1
  216. package/dist/lib/annotation.d.ts +7 -0
  217. package/dist/lib/annotation.d.ts.map +1 -1
  218. package/dist/lib/annotation.js +11 -1
  219. package/dist/lib/annotation.js.map +1 -1
  220. package/dist/lib/board.d.ts +3 -0
  221. package/dist/lib/board.d.ts.map +1 -1
  222. package/dist/lib/board.js +4 -2
  223. package/dist/lib/board.js.map +1 -1
  224. package/dist/lib/clone.d.ts +1 -0
  225. package/dist/lib/clone.d.ts.map +1 -1
  226. package/dist/lib/clone.js +32 -10
  227. package/dist/lib/clone.js.map +1 -1
  228. package/dist/lib/comment-bodies.d.ts +37 -0
  229. package/dist/lib/comment-bodies.d.ts.map +1 -1
  230. package/dist/lib/comment-bodies.js +47 -9
  231. package/dist/lib/comment-bodies.js.map +1 -1
  232. package/dist/lib/pr-status.d.ts.map +1 -1
  233. package/dist/lib/pr-status.js +36 -2
  234. package/dist/lib/pr-status.js.map +1 -1
  235. package/dist/lib/pr-workflow-state.d.ts +5 -0
  236. package/dist/lib/pr-workflow-state.d.ts.map +1 -1
  237. package/dist/lib/pr-workflow-state.js +36 -1
  238. package/dist/lib/pr-workflow-state.js.map +1 -1
  239. package/dist/lib/repo-picker.d.ts +3 -0
  240. package/dist/lib/repo-picker.d.ts.map +1 -1
  241. package/dist/lib/repo-picker.js +45 -9
  242. package/dist/lib/repo-picker.js.map +1 -1
  243. package/dist/lib/repository-guidance.d.ts +2 -0
  244. package/dist/lib/repository-guidance.d.ts.map +1 -0
  245. package/dist/lib/repository-guidance.js +55 -0
  246. package/dist/lib/repository-guidance.js.map +1 -0
  247. package/dist/lib/review-models.d.ts +15 -2
  248. package/dist/lib/review-models.d.ts.map +1 -1
  249. package/dist/lib/review-models.js +26 -6
  250. package/dist/lib/review-models.js.map +1 -1
  251. package/dist/lib/review-strategy.d.ts +92 -0
  252. package/dist/lib/review-strategy.d.ts.map +1 -0
  253. package/dist/lib/review-strategy.js +282 -0
  254. package/dist/lib/review-strategy.js.map +1 -0
  255. package/dist/lib/runner.d.ts +92 -0
  256. package/dist/lib/runner.d.ts.map +1 -1
  257. package/dist/lib/runner.js +470 -54
  258. package/dist/lib/runner.js.map +1 -1
  259. package/dist/lib/workflow.d.ts +9 -0
  260. package/dist/lib/workflow.d.ts.map +1 -1
  261. package/dist/lib/workflow.js +20 -0
  262. package/dist/lib/workflow.js.map +1 -1
  263. package/dist/linear/client.d.ts +18 -0
  264. package/dist/linear/client.d.ts.map +1 -0
  265. package/dist/linear/client.js +67 -0
  266. package/dist/linear/client.js.map +1 -0
  267. package/dist/linear/comment.d.ts +20 -0
  268. package/dist/linear/comment.d.ts.map +1 -0
  269. package/dist/linear/comment.js +57 -0
  270. package/dist/linear/comment.js.map +1 -0
  271. package/dist/linear/identity.d.ts +59 -0
  272. package/dist/linear/identity.d.ts.map +1 -0
  273. package/dist/linear/identity.js +187 -0
  274. package/dist/linear/identity.js.map +1 -0
  275. package/dist/linear/notify.d.ts +35 -0
  276. package/dist/linear/notify.d.ts.map +1 -0
  277. package/dist/linear/notify.js +76 -0
  278. package/dist/linear/notify.js.map +1 -0
  279. package/dist/linear/ref.d.ts +13 -0
  280. package/dist/linear/ref.d.ts.map +1 -0
  281. package/dist/linear/ref.js +90 -0
  282. package/dist/linear/ref.js.map +1 -0
  283. package/dist/linear/verify.d.ts +26 -0
  284. package/dist/linear/verify.d.ts.map +1 -0
  285. package/dist/linear/verify.js +67 -0
  286. package/dist/linear/verify.js.map +1 -0
  287. package/dist/reviewers/claude.d.ts +4 -1
  288. package/dist/reviewers/claude.d.ts.map +1 -1
  289. package/dist/reviewers/claude.js +39 -7
  290. package/dist/reviewers/claude.js.map +1 -1
  291. package/dist/reviewers/codex.d.ts +3 -1
  292. package/dist/reviewers/codex.d.ts.map +1 -1
  293. package/dist/reviewers/codex.js +76 -70
  294. package/dist/reviewers/codex.js.map +1 -1
  295. package/dist/reviewers/conflict-resolve.d.ts +3 -1
  296. package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
  297. package/dist/reviewers/conflict-resolve.js +21 -6
  298. package/dist/reviewers/conflict-resolve.js.map +1 -1
  299. package/dist/reviewers/fix.d.ts +5 -2
  300. package/dist/reviewers/fix.d.ts.map +1 -1
  301. package/dist/reviewers/fix.js +26 -10
  302. package/dist/reviewers/fix.js.map +1 -1
  303. package/dist/skills/attribution.d.ts +4 -0
  304. package/dist/skills/attribution.d.ts.map +1 -0
  305. package/dist/skills/attribution.js +14 -0
  306. package/dist/skills/attribution.js.map +1 -0
  307. package/dist/skills/broker-server.d.ts +2 -0
  308. package/dist/skills/broker-server.d.ts.map +1 -0
  309. package/dist/skills/broker-server.js +17 -0
  310. package/dist/skills/broker-server.js.map +1 -0
  311. package/dist/skills/broker.d.ts +42 -0
  312. package/dist/skills/broker.d.ts.map +1 -0
  313. package/dist/skills/broker.js +285 -0
  314. package/dist/skills/broker.js.map +1 -0
  315. package/dist/skills/catalog.d.ts +28 -0
  316. package/dist/skills/catalog.d.ts.map +1 -0
  317. package/dist/skills/catalog.js +104 -0
  318. package/dist/skills/catalog.js.map +1 -0
  319. package/dist/skills/installer.d.ts +10 -0
  320. package/dist/skills/installer.d.ts.map +1 -0
  321. package/dist/skills/installer.js +138 -0
  322. package/dist/skills/installer.js.map +1 -0
  323. package/dist/skills/integrity.d.ts +4 -0
  324. package/dist/skills/integrity.d.ts.map +1 -0
  325. package/dist/skills/integrity.js +36 -0
  326. package/dist/skills/integrity.js.map +1 -0
  327. package/docs/dynamic-thoroughness.md +738 -0
  328. package/docs/linear-identity-contract.md +139 -0
  329. package/docs/linear-identity.md +293 -0
  330. package/get-started.md +223 -11
  331. package/package.json +4 -3
@@ -1,18 +1,37 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { existsSync, statSync } from 'fs';
3
3
  import chalk from 'chalk';
4
- import { loadConfig, getGithubTokenSource, getWebhookSecretPath, resolveConfigPath } from '../config/loader.js';
4
+ import { loadConfig, getGithubTokenSource, getWebhookSecretPath, resolveConfigPath, getLinearCredentials } from '../config/loader.js';
5
+ import { verifyLinearIdentity } from '../linear/verify.js';
5
6
  import { checkCodexAuth } from '../reviewers/codex.js';
6
7
  import { checkClaudeAuth } from '../reviewers/claude.js';
7
8
  import { getLogDir, getTodayLogPath } from '../lib/logger.js';
8
9
  import { buildImpactReport } from './impact.js';
10
+ import { formatSkillIdentity, loadSkillCatalog } from '../skills/catalog.js';
9
11
  function row(label, value, ok) {
10
12
  const indicator = ok === undefined ? ' ' : ok ? chalk.green('✓') : chalk.red('✗');
11
13
  console.log(` ${indicator} ${chalk.bold(label.padEnd(22))} ${value}`);
12
14
  }
15
+ const LINEAR_PROBE_TIMEOUT_MS = 10_000;
13
16
  export async function runStatus(configPath) {
14
17
  const config = loadConfig(configPath);
15
18
  const activeConfigPath = resolveConfigPath(configPath);
19
+ // Start the Linear probe before the CLI auth checks so the two network round
20
+ // trips overlap, and bound it — status must stay responsive even if Linear is
21
+ // slow or unreachable. verifyLinearIdentity never throws; the timeout is the
22
+ // only way this can reject, so it resolves to a report either way.
23
+ const linearProbe = config.linear.enabled
24
+ ? Promise.race([
25
+ verifyLinearIdentity(config.linear, getLinearCredentials(config.linear.auth)),
26
+ new Promise(resolve => setTimeout(() => resolve({
27
+ ok: false,
28
+ mode: config.linear.auth.mode,
29
+ actor: config.linear.identity.actor,
30
+ attribution: config.linear.auth.mode === 'client_credentials' ? 'app' : 'user',
31
+ error: `timed out after ${LINEAR_PROBE_TIMEOUT_MS / 1000}s`,
32
+ }), LINEAR_PROBE_TIMEOUT_MS).unref?.()),
33
+ ])
34
+ : null;
16
35
  console.log(chalk.bold('\ncrosscheck status\n'));
17
36
  // Auth
18
37
  console.log(chalk.dim(' Auth'));
@@ -38,6 +57,15 @@ export async function runStatus(configPath) {
38
57
  row('config path', activeConfigPath ?? 'defaults only');
39
58
  row('mode', config.mode);
40
59
  row('quality tier', config.quality.tier);
60
+ const skillCatalog = loadSkillCatalog();
61
+ const bundledSkills = new Map(skillCatalog.map(skill => [skill.name, skill]));
62
+ row('installed skills', skillCatalog.map(formatSkillIdentity).join(', '));
63
+ row('enabled skills', config.skills.enabled.length > 0
64
+ ? config.skills.enabled.map(name => {
65
+ const skill = bundledSkills.get(name);
66
+ return skill ? formatSkillIdentity(skill) : `${name} (not installed or failed integrity check)`;
67
+ }).join(', ')
68
+ : 'none');
41
69
  row('codex auth', config.vendors.codex.auth);
42
70
  row('codex model', config.vendors.codex.model ?? 'auto (by tier)');
43
71
  row('claude model', config.vendors.claude.model ?? 'default');
@@ -50,6 +78,30 @@ export async function runStatus(configPath) {
50
78
  if (config.quality.focus.length > 0) {
51
79
  row('focus', config.quality.focus.join(', '));
52
80
  }
81
+ // Linear identity — only when the operator has opted in.
82
+ if (linearProbe) {
83
+ console.log();
84
+ console.log(chalk.dim(' Linear'));
85
+ const report = await linearProbe;
86
+ row('auth mode', report.mode, report.ok);
87
+ if (!report.ok) {
88
+ row('identity', report.error ?? 'verification failed', false);
89
+ }
90
+ else {
91
+ row('organization', report.organization ?? 'unknown');
92
+ if (report.attribution === 'app') {
93
+ const actor = config.linear.identity.per_step_actor ? `${report.actor}/<step>` : report.actor;
94
+ row('writes as', `${actor} ${chalk.dim('(crosscheck itself)')}`, true);
95
+ }
96
+ else {
97
+ // Deliberately not a failed check. Attributing to a person is the wrong
98
+ // state for a shared workspace, but it is a perfectly reasonable choice for
99
+ // a solo user with no attribution problem — so this informs, it does not nag.
100
+ row('writes as', `${report.attributesTo ?? 'your Linear account'} ${chalk.dim('(api key)')}`);
101
+ console.log(chalk.dim(' to post as crosscheck itself, see docs/linear-identity.md'));
102
+ }
103
+ }
104
+ }
53
105
  // Logs
54
106
  console.log();
55
107
  console.log(chalk.dim(' Logs'));
@@ -1 +1 @@
1
- {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/commands/watch.ts"],"names":[],"mappings":"AAyKA,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,SAAc,iBAqvClD"}
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/commands/watch.ts"],"names":[],"mappings":"AA2KA,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAuBD,wBAAsB,QAAQ,CAAC,IAAI,GAAE,SAAc,iBAo1ClD"}
@@ -3,18 +3,19 @@ import chalk from 'chalk';
3
3
  import { createWebhookServer } from '../github/webhook.js';
4
4
  import { createGithubClient, getCommitMessage, registerOrgWebhook, deleteOrgWebhook, registerRepoWebhook, deleteRepoWebhook, patchOrgWebhookEvents, patchRepoWebhookEvents, findOrgWebhook, findRepoWebhook, listUserRepos, checkRepoAccessible, } from '../github/client.js';
5
5
  import { detectOriginFull, assignReviewer } from '../github/detector.js';
6
- import { loadConfig, getGithubToken, getWebhookSecret, resolveConfigPath, promptDeploymentMode, detectScopesForDeployment, patchDeploymentConfig, detectGitHubLogin, } from '../config/loader.js';
6
+ import { loadConfig, getGithubToken, getWebhookSecret, resolveConfigPath, promptDeploymentMode, detectScopesForDeployment, patchDeploymentConfig, detectGitHubLogin, getLinearCredentials, } from '../config/loader.js';
7
7
  import { randomFortune } from '../lib/fortune.js';
8
8
  import { scanUnreviewedPRs } from '../lib/backtrace.js';
9
9
  import { initLogger, log as fileLog, logError, logUncaught } from '../lib/logger.js';
10
10
  import { isAuthorAllowed } from '../lib/filter.js';
11
11
  import { runWorkflow } from '../lib/runner.js';
12
- import { loadWorkflow, DEFAULT_RECHECK_INSTRUCTIONS } from '../lib/workflow.js';
13
- import { filterStepsByTypes, formatRepoWorkflowSteps, isRecheckWithoutFix, isReviewOnlyWorkflow, readRepoWorkflowStepTypes, resolveRepoWorkflowSteps, workflowHasStep } from '../lib/repo-workflow.js';
12
+ import { loadWorkflow, linearWritePossible, DEFAULT_RECHECK_INSTRUCTIONS } from '../lib/workflow.js';
13
+ import { filterStepsByTypes, formatRepoWorkflowSteps, isReviewOnlyWorkflow, readRepoWorkflowStepTypes, resolveRepoWorkflowSteps, workflowHasStep } from '../lib/repo-workflow.js';
14
14
  import { fetchStepHistory, identifyNextWorkflowStep, decideReviewOnly } from '../lib/pr-workflow-state.js';
15
15
  import { parseAnnotation } from '../lib/annotation.js';
16
16
  import { PRBoard, fmtTime, FMT_TIME_WIDTH } from '../lib/board.js';
17
17
  import { clonePRForReview } from '../lib/clone.js';
18
+ import { resolveLinearAuth, isLinearConfigError } from '../linear/identity.js';
18
19
  import { getSmartSwitch, isSubscriptionLimitError, detectFailedVendor, triggerSwitch, notifyReviewSuccess, stopSmartSwitch, } from '../lib/smart-switch.js';
19
20
  import { buildReviewFailedCommentBody } from '../lib/comment-bodies.js';
20
21
  import { classifyReviewerError } from '../lib/reviewer-error.js';
@@ -128,6 +129,21 @@ function openTunnel(localPort) {
128
129
  });
129
130
  });
130
131
  }
132
+ // PR history is what decides whether a step runs at all — the approval stop, the
133
+ // per-SHA dedup, the resume point. Acting without it means posting a duplicate review
134
+ // or modifying an already-approved commit, so callers fail closed rather than assume an
135
+ // empty history. One short retry first: the usual cause is a transient GitHub blip or a
136
+ // rate-limit tick, and deferring the event strands the PR until something else happens.
137
+ const HISTORY_RETRY_DELAY_MS = 3_000;
138
+ async function fetchStepHistoryWithRetry(owner, repo, prNumber, token) {
139
+ try {
140
+ return await fetchStepHistory(owner, repo, prNumber, token);
141
+ }
142
+ catch {
143
+ await new Promise(resolve => setTimeout(resolve, HISTORY_RETRY_DELAY_MS));
144
+ return await fetchStepHistory(owner, repo, prNumber, token);
145
+ }
146
+ }
131
147
  export async function runWatch(opts = {}) {
132
148
  const configPath = opts.config;
133
149
  let config = loadConfig(configPath);
@@ -156,6 +172,25 @@ export async function runWatch(opts = {}) {
156
172
  console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
157
173
  process.exit(1);
158
174
  }
175
+ // Preflight Linear identity at startup. watch is a long-running daemon: a bad
176
+ // credential discovered per-event would either drop events silently or spam the
177
+ // board. Fail loudly at boot instead, the way the run path fails before its
178
+ // clone. The token is discarded — per-event resolution mints a fresh one, since
179
+ // app tokens outlive neither a long daemon run nor a rotation.
180
+ // Same gate as the run and per-event paths: a daemon whose configured workflow
181
+ // is fix/conflict-resolve only can never call notifyLinear, so a missing
182
+ // credential must not stop it booting.
183
+ if (linearWritePossible(config.linear, loadWorkflow(process.cwd()))) {
184
+ try {
185
+ const preflight = await resolveLinearAuth(config.linear, getLinearCredentials(config.linear.auth));
186
+ console.log(chalk.dim(` linear: ${preflight.mode} — writes as ${preflight.actor}`));
187
+ }
188
+ catch (err) {
189
+ logError({ command: 'watch', phase: 'linear-auth' }, err);
190
+ console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
191
+ process.exit(isLinearConfigError(err) ? 1 : 2);
192
+ }
193
+ }
159
194
  fileLog({ level: 'info', event: 'session_start', command: 'watch' });
160
195
  // Fetch the authenticated user so the onComment handler can reject annotation
161
196
  // injection from non-token accounts. If this fails, the handler is disabled
@@ -191,12 +226,16 @@ export async function runWatch(opts = {}) {
191
226
  // Last-reviewed diff hash per PR — skip reviews when a new SHA has identical diff vs base
192
227
  // (force-push, amend, no-op rebase). Persisted so restarts don't re-review unchanged content.
193
228
  const diffHashes = new PersistentDiffHashMap();
194
- // PRs reviewed at least once this session synchronize events on these run as recheck rounds
195
- const reviewedPRKeys = new Set();
229
+ // Completed round count per PRthe auto-loop's round counter, which history cannot
230
+ // supply (it returns the last recheck's round unchanged across iterations).
196
231
  const prRoundCounts = new Map();
197
- // PR+sha pairs completed by this watch session used to suppress issue_comment
198
- // re-entries for reviews that watch posted itself (as opposed to kickass).
199
- const reviewedPRShaKeys = new Set();
232
+ // Comment IDs the issue_comment bridge has already handled, so a webhook redelivery
233
+ // of the same comment does not dispatch twice. There is no need to guess at which
234
+ // comments are "ours": webhook.ts forwards only `trigger=kickass` annotations, and
235
+ // this watcher's own reviews carry trigger=watch/comment/backtrace, so they never
236
+ // reach the bridge. Every comment that does reach it is a kickass dispatch whose
237
+ // verdict this watcher is meant to act on — history detection decides how.
238
+ const handledCommentIds = new Set();
200
239
  // Idle tracking — reset on any PR activity; used by the idle-issue timer.
201
240
  let lastActivityAt = Date.now();
202
241
  let idleAnalysisRunning = false;
@@ -215,6 +254,7 @@ export async function runWatch(opts = {}) {
215
254
  fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'author_not_allowed', author: params.author });
216
255
  return;
217
256
  }
257
+ const prKey = `${owner}/${repoName}#${prNumber}`;
218
258
  const { origin, method: originMethod } = await detectOriginFull(params.body ?? '', params.headRef, owner, repoName, prNumber, config, token, params.author);
219
259
  // Smart-switch: when cross-vendor is degraded, override to single-vendor with the healthy vendor
220
260
  const ss = getSmartSwitch();
@@ -235,9 +275,16 @@ export async function runWatch(opts = {}) {
235
275
  title: params.title,
236
276
  body: params.body ?? '',
237
277
  head: { ref: params.headRef, sha: params.headSha, repo: params.headRepo ? { full_name: params.headRepo } : null },
238
- base: { ref: params.baseRef, repo: { full_name: `${owner}/${repoName}` } },
278
+ base: {
279
+ ref: params.baseRef,
280
+ repo: {
281
+ full_name: `${owner}/${repoName}`,
282
+ ...(params.defaultBranch !== undefined && { default_branch: params.defaultBranch }),
283
+ },
284
+ },
239
285
  html_url: `https://github.com/${owner}/${repoName}/pull/${prNumber}`,
240
286
  user: { login: params.author },
287
+ ...(params.labels !== undefined && { labels: params.labels.map(name => ({ name })) }),
241
288
  };
242
289
  if (!acquirePRLock(owner, repoName, prNumber, params.headSha)) {
243
290
  fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'in_progress_local' });
@@ -257,21 +304,26 @@ export async function runWatch(opts = {}) {
257
304
  logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'lock' }, err);
258
305
  return;
259
306
  }
260
- const prKey = `${owner}/${repoName}#${prNumber}`;
261
- // Determine the correct starting step from PR comment history so watch
262
- // behaves correctly after a restart (reviewedPRKeys is in-memory only).
263
- // Fast-path: if the PR was reviewed in this session, skip the API call.
264
- // Exception: comment-triggered runs must always do live detection
265
- // reviewedPRKeys is SHA-agnostic and would misroute a kickass review
266
- // annotation for a new SHA as a recheck run instead of routing to fix.
267
- // auto_loop runs also skip the session fast-path so history detection
268
- // picks the correct starting step (fix, not recheck); round is instead
269
- // incremented from prRoundCounts to match the completed round count.
270
- let isRecheckRun = params.action === 'comment'
271
- ? reviewedPRShaKeys.has(key) // SHA-specific: already processed this exact sha
272
- : params.action === 'auto_loop'
273
- ? false // always use history detection for auto-loop rounds
274
- : reviewedPRKeys.has(prKey); // session fast-path for webhook/backtrace events
307
+ // Determine the correct starting step from PR comment history. Webhook, backtrace,
308
+ // and auto_loop events always ask there is no PR-level session fast-path.
309
+ //
310
+ // There used to be one (`reviewedPRKeys`): reviewed-in-this-session meant "run the
311
+ // next event as a recheck". It was wrong by construction on two axes. It is
312
+ // SHA-agnostic, so a push it had never seen inherited the previous SHA's routing;
313
+ // and it only knows what THIS process did, so an approval, a forced re-review, or a
314
+ // fix pushed by `kickass`, `crosscheck run`, or a second watcher was invisible to
315
+ // it. Each case where it guessed wrong grew its own guard — recheck-no-fix depth,
316
+ // repo overrides, approvals and every one of those guards did the same thing:
317
+ // undo the guess and ask history. So ask history. It costs one call per event, and
318
+ // it is the only thing that sees the PR as it actually is.
319
+ //
320
+ // Comment triggers ask too. The SHA-specific fast path they used to take ran an
321
+ // externally-posted NEEDS_WORK as a recheck instead of routing to fix, because it
322
+ // skipped detection whenever this session had already touched the SHA.
323
+ let isRecheckRun = false;
324
+ // auto_loop's round is pre-computed from the completed round count rather than
325
+ // from history, which returns the last recheck's round unchanged across iterations
326
+ // and would defeat the max_rounds cap.
275
327
  let round = params.action === 'auto_loop'
276
328
  ? (prRoundCounts.get(prKey) ?? 1) + 1
277
329
  : isRecheckRun ? (prRoundCounts.get(prKey) ?? 1) + 1 : 1;
@@ -285,17 +337,6 @@ export async function runWatch(opts = {}) {
285
337
  isRecheckRun = false;
286
338
  round = 1;
287
339
  }
288
- // A recheck-no-fix depth (`review,recheck`) decides review-vs-recheck per SHA from
289
- // the last verdict: an unresolved review gets a recheck, an APPROVE gets a fresh
290
- // review (identifyNextWorkflowStep). The session fast-path above is PR-level and
291
- // SHA-agnostic, so it would coerce every later push in this process into a recheck
292
- // and never consult that decision. Force history detection instead. Comment-
293
- // triggered runs keep their SHA-specific fast path — a kickass re-review targets a
294
- // SHA history detection would report as already complete.
295
- if (isRecheckWithoutFix(repoStepOverride) && isRecheckRun && params.action !== 'comment') {
296
- isRecheckRun = false;
297
- round = 1;
298
- }
299
340
  if (reviewOnlyForRepo) {
300
341
  // Review-only comes from a per-repo override (crosscheck alter --review-only).
301
342
  // Restrict the workflow to review step(s) and dedup on the exact head sha so
@@ -312,7 +353,7 @@ export async function runWatch(opts = {}) {
312
353
  round = 1;
313
354
  resolvedSteps = reviewSteps;
314
355
  try {
315
- const history = await fetchStepHistory(owner, repoName, prNumber, token);
356
+ const history = await fetchStepHistoryWithRetry(owner, repoName, prNumber, token);
316
357
  const decision = decideReviewOnly(history, params.headSha);
317
358
  if (decision.alreadyReviewed) {
318
359
  await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'success');
@@ -322,7 +363,18 @@ export async function runWatch(opts = {}) {
322
363
  }
323
364
  round = decision.round;
324
365
  }
325
- catch { /* best-effort — fall back to a fresh round-1 review */ }
366
+ catch (err) {
367
+ // The dedup this mode relies on lives in that history. Reviewing without it
368
+ // would post a second review on a SHA already covered.
369
+ // Released as `failure`, not `success`: this SHA was not reviewed, and
370
+ // `crosscheck/review` is a commit status a repo can require for merge — a
371
+ // green one here would let an unreviewed HEAD satisfy branch protection
372
+ // during a GitHub outage.
373
+ await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'failure');
374
+ releasePRLock(owner, repoName, prNumber, params.headSha);
375
+ fileLog({ level: 'warn', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'history_unavailable', sha: params.headSha, error: err instanceof Error ? err.message : String(err) });
376
+ return;
377
+ }
326
378
  }
327
379
  else if (!isRecheckRun) {
328
380
  try {
@@ -331,14 +383,14 @@ export async function runWatch(opts = {}) {
331
383
  // fresh so live edits to workflow.yml apply across events.
332
384
  const globalWorkflow = loadWorkflow(process.cwd());
333
385
  const allSteps = repoStepOverride ? filterStepsByTypes(globalWorkflow, repoStepOverride) : globalWorkflow;
334
- const history = await fetchStepHistory(owner, repoName, prNumber, token);
386
+ const history = await fetchStepHistoryWithRetry(owner, repoName, prNumber, token);
335
387
  const nextResult = identifyNextWorkflowStep(history, allSteps, params.headSha);
336
388
  if (nextResult.step === null) {
337
389
  // Workflow already complete for this HEAD sha — release lock and skip.
338
390
  // Happens when a synchronize event fires after all steps are done.
339
391
  await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'success');
340
392
  releasePRLock(owner, repoName, prNumber, params.headSha);
341
- fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'workflow_complete', sha: params.headSha });
393
+ fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: nextResult.stopReason ?? 'workflow_complete', sha: params.headSha });
342
394
  return;
343
395
  }
344
396
  if (nextResult.hasExistingReview) {
@@ -391,15 +443,26 @@ export async function runWatch(opts = {}) {
391
443
  }
392
444
  }
393
445
  }
394
- catch {
395
- // best-effort fall back to session-based detection.
396
- // For auto_loop, force isRecheckRun=true so a transient history-fetch
397
- // failure does not restart the workflow from the review step.
398
- // A recheck-no-fix repo has no session state left to fall back to (the guard
399
- // above cleared it), so it re-reviews the new SHA the conservative direction,
400
- // since a fresh review never assumes findings that may already be resolved.
401
- if (params.action === 'auto_loop')
402
- isRecheckRun = true;
446
+ catch (err) {
447
+ // History still unreadable after the retry. Everything this function decides —
448
+ // whether HEAD is already approved, already reviewed, or mid-loop lives in
449
+ // that history, so running anyway risks a second review on an approved commit
450
+ // or a fix re-applied to code that already has one. Defer instead: release the
451
+ // locks and let the next event, `kickass`, or `crosscheck run` pick the PR up.
452
+ //
453
+ // auto_loop is the exception. It is a continuation of a run this process just
454
+ // completed, so the state is known from the session rather than from history,
455
+ // and deferring would strand the loop — nothing else re-triggers it.
456
+ if (params.action !== 'auto_loop') {
457
+ // `failure`, not `success` — see the review-only branch above: a green
458
+ // `crosscheck/review` on a HEAD nobody reviewed can satisfy branch
459
+ // protection.
460
+ await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'failure');
461
+ releasePRLock(owner, repoName, prNumber, params.headSha);
462
+ fileLog({ level: 'warn', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'history_unavailable', sha: params.headSha, error: err instanceof Error ? err.message : String(err) });
463
+ return;
464
+ }
465
+ isRecheckRun = true;
403
466
  }
404
467
  }
405
468
  const reviewStart = Date.now();
@@ -407,6 +470,32 @@ export async function runWatch(opts = {}) {
407
470
  let stopHeartbeat = () => { };
408
471
  let boardAdded = false;
409
472
  try {
473
+ // Board first, so any failure below is visible rather than swallowed by the
474
+ // enclosing catch (which only calls logError — a no-op when file logging is
475
+ // off). Then auth, then the clone: during an outage a long-running watcher
476
+ // would otherwise pay for a full clone on every event before rejecting it.
477
+ // Computed here rather than at the runWorkflow call so the auth gate below
478
+ // sees the workflow that will actually run. resolvedSteps alone is
479
+ // undefined for a plain event, and canWriteVerdict fails open on undefined
480
+ // — which let a fix-only daemon resolve credentials on every event.
481
+ const workflowStepsForRun = resolvedSteps
482
+ ?? (repoStepOverride ? filterStepsByTypes(loadWorkflow(process.cwd()), repoStepOverride) : loadWorkflow(process.cwd()));
483
+ board.addPR(key, prNumber, `${owner}/${repoName}`, params.headRef, round);
484
+ boardAdded = true;
485
+ // Only workflows that can write a verdict need an identity — the fix and
486
+ // resolve paths never call notifyLinear.
487
+ let linearAuth = null;
488
+ if (linearWritePossible(effectiveConfig.linear, workflowStepsForRun)) {
489
+ try {
490
+ linearAuth = await resolveLinearAuth(effectiveConfig.linear, getLinearCredentials(effectiveConfig.linear.auth));
491
+ }
492
+ catch (err) {
493
+ const message = err instanceof Error ? err.message : String(err);
494
+ board.failPR(key, `linear identity unavailable: ${message}`);
495
+ fileLog({ level: 'error', event: 'linear_auth_failed', repo: `${owner}/${repoName}`, pr: prNumber, reason: message });
496
+ throw err;
497
+ }
498
+ }
410
499
  await clonePRForReview({
411
500
  owner, repo: repoName, prNumber, baseRef: params.baseRef,
412
501
  tmpDir, token, protocol: config.clone_protocol,
@@ -442,18 +531,18 @@ export async function runWatch(opts = {}) {
442
531
  logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'no_diff_comment' }, err);
443
532
  }
444
533
  await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'success');
534
+ // The PR is on the board from before the clone, so close its slot rather
535
+ // than leaving a row that never resolves.
536
+ board.completePR(key, { elapsedMs: Date.now() - reviewStart, url: `https://github.com/${owner}/${repoName}/pull/${prNumber}` });
445
537
  return;
446
538
  }
447
- board.addPR(key, prNumber, `${owner}/${repoName}`, params.headRef, round);
448
- boardAdded = true;
449
539
  const prLoc = computePRLoc(tmpDir, params.baseRef);
450
540
  board.updatePR(key, { prLoc });
451
541
  stopHeartbeat = startRemoteLockHeartbeat(lockOctokit, owner, repoName, params.headSha);
452
- const workflowStepsForRun = resolvedSteps
453
- ?? (repoStepOverride ? filterStepsByTypes(loadWorkflow(process.cwd()), repoStepOverride) : undefined);
454
- const { verdict, fixAppliedCount } = await runWorkflow({
542
+ const { verdict, fixAppliedCount, strategySkipped } = await runWorkflow({
455
543
  owner, repoName, prNumber, pr,
456
544
  tmpDir, token, config: effectiveConfig, origin,
545
+ linearAuth,
457
546
  reviewStart,
458
547
  log: (msg) => bLog(`${chalk.dim(fmtTime())} ${msg}`),
459
548
  onPhaseChange: (label, data) => board.updatePR(key, { label, ...data }),
@@ -470,10 +559,11 @@ export async function runWatch(opts = {}) {
470
559
  round,
471
560
  trigger: params.action === 'backtrace' ? 'backtrace' : params.action === 'comment' ? 'comment' : 'watch',
472
561
  });
473
- void verdict;
474
- reviewedPRKeys.add(prKey);
475
- reviewedPRShaKeys.add(key); // key = "owner/repo#pr@sha"
476
- prRoundCounts.set(prKey, round);
562
+ // A strategy-skipped PR never ran a review, so it must not advance the
563
+ // auto-loop's round counter — that run posted nothing to count.
564
+ if (strategySkipped === undefined) {
565
+ prRoundCounts.set(prKey, round);
566
+ }
477
567
  // Recompute the diff hash AFTER runWorkflow — workflow steps such as
478
568
  // `conflict-resolve` or `fix` followed by `recheck` can mutate the checkout,
479
569
  // so the pre-workflow hash may not represent the content that was actually
@@ -493,7 +583,10 @@ export async function runWatch(opts = {}) {
493
583
  }
494
584
  catch { /* fall back — skip auto-loop this cycle */ }
495
585
  }
496
- if (newDiffHash) {
586
+ // Same gate as the session caches above: caching the diff of a PR that was
587
+ // never reviewed would make the first event that *should* review it get
588
+ // skipped as `no_diff_change`.
589
+ if (newDiffHash && strategySkipped === undefined) {
497
590
  // For non-commit delivery with an applied fix the checkout may be on a fix
498
591
  // branch; computeDiffHash would return the post-fix diff, not the original PR
499
592
  // diff. Caching that under prKey corrupts future no_diff_change checks for the
@@ -514,9 +607,15 @@ export async function runWatch(opts = {}) {
514
607
  }
515
608
  }
516
609
  }
610
+ // A class-skipped PR (e.g. lockfile-only) never ran a review, so say so
611
+ // rather than reporting it as a completed one.
612
+ if (strategySkipped) {
613
+ bLog(`${chalk.dim(fmtTime())} ${chalk.dim(`PR #${prNumber} skipped — ${strategySkipped} class, nothing to review`)}`);
614
+ }
517
615
  board.completePR(key, {
518
616
  elapsedMs: Date.now() - reviewStart,
519
617
  url: `github.com/${owner}/${repoName}/pull/${prNumber}`,
618
+ ...(strategySkipped !== undefined && { label: `skipped · ${strategySkipped}` }),
520
619
  });
521
620
  // Smart-switch recovery confirmation: if a restore attempt is pending and
522
621
  // this reviewer matches the previously-degraded vendor, announce full restoration.
@@ -649,6 +748,8 @@ export async function runWatch(opts = {}) {
649
748
  title: pr.title, body: pr.body, author: pr.user.login,
650
749
  headSha: pr.head.sha, headRef: pr.head.ref, headRepo: pr.head.repo?.full_name ?? null,
651
750
  baseRef: pr.base.ref, action: event.action,
751
+ ...(pr.labels !== undefined && { labels: pr.labels.map(l => l.name) }),
752
+ ...(pr.base.repo.default_branch !== undefined && { defaultBranch: pr.base.repo.default_branch }),
652
753
  });
653
754
  }, (msg) => bLog(chalk.dim(fmtTime()) + ' ' + msg), fileLog, async (event) => {
654
755
  const owner = event.repository.owner.login;
@@ -698,10 +799,11 @@ export async function runWatch(opts = {}) {
698
799
  fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_stale_sha', sha: prData.head.sha, annotation_sha: annotationSha });
699
800
  return;
700
801
  }
701
- if (reviewedPRShaKeys.has(`${owner}/${repoName}#${prNumber}@${prData.head.sha}`)) {
702
- fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_self_trigger', sha: prData.head.sha });
802
+ if (handledCommentIds.has(event.comment.id)) {
803
+ fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_already_handled', comment_id: event.comment.id });
703
804
  return;
704
805
  }
806
+ handledCommentIds.add(event.comment.id);
705
807
  // GitHub can deliver issue_comment before ck run's finally block releases
706
808
  // the remote lock. Retry with backoff; re-fetch the PR head on each attempt
707
809
  // so we don't dispatch against a sha that advanced while we were waiting.
@@ -725,10 +827,6 @@ export async function runWatch(opts = {}) {
725
827
  break;
726
828
  }
727
829
  }
728
- if (reviewedPRShaKeys.has(`${owner}/${repoName}#${prNumber}@${prData.head.sha}`)) {
729
- fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_self_trigger', sha: prData.head.sha });
730
- break;
731
- }
732
830
  if (await checkRemoteLock(octokit, owner, repoName, prData.head.sha).catch(() => false)) {
733
831
  fileLog({ level: 'info', event: 'comment_trigger_deferred', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'in_progress_remote', attempt });
734
832
  continue;
@@ -743,6 +841,11 @@ export async function runWatch(opts = {}) {
743
841
  headRepo: prData.head.repo?.full_name ?? null,
744
842
  baseRef: prData.base.ref,
745
843
  action: 'comment',
844
+ // Same fields the webhook path forwards: without them the risky
845
+ // class's label and hotfix rules classify this PR differently
846
+ // depending on which trigger ran it.
847
+ ...(prData.labels !== undefined && { labels: prData.labels.map((l) => l.name) }),
848
+ ...(prData.base.repo?.default_branch !== undefined && { defaultBranch: prData.base.repo.default_branch }),
746
849
  });
747
850
  break;
748
851
  }
@@ -1029,6 +1132,8 @@ export async function runWatch(opts = {}) {
1029
1132
  title: pr.title, body: pr.body, author: pr.author,
1030
1133
  headSha: pr.headSha, headRef: pr.headRef, headRepo: pr.headRepo,
1031
1134
  baseRef: pr.baseRef, action: 'backtrace',
1135
+ ...(pr.labels !== undefined && { labels: pr.labels }),
1136
+ ...(pr.defaultBranch !== undefined && { defaultBranch: pr.defaultBranch }),
1032
1137
  })));
1033
1138
  }
1034
1139
  catch (err) {