@humanbased/crosscheck 0.14.0 → 0.15.0-beta.145

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 (176) hide show
  1. package/ISSUE.md +1 -1
  2. package/LICENSE +1 -1
  3. package/README.md +191 -8
  4. package/README.zh.md +1 -1
  5. package/crosscheck.config.example.yml +4 -2
  6. package/dist/__tests__/backtrace.test.js +1 -1
  7. package/dist/__tests__/backtrace.test.js.map +1 -1
  8. package/dist/__tests__/diagnose.test.js +36 -0
  9. package/dist/__tests__/diagnose.test.js.map +1 -1
  10. package/dist/__tests__/durations.test.js +5 -1
  11. package/dist/__tests__/durations.test.js.map +1 -1
  12. package/dist/__tests__/error-classification.test.d.ts +2 -0
  13. package/dist/__tests__/error-classification.test.d.ts.map +1 -0
  14. package/dist/__tests__/error-classification.test.js +36 -0
  15. package/dist/__tests__/error-classification.test.js.map +1 -0
  16. package/dist/__tests__/fix.test.js +48 -1
  17. package/dist/__tests__/fix.test.js.map +1 -1
  18. package/dist/__tests__/issue.test.js +2 -2
  19. package/dist/__tests__/issue.test.js.map +1 -1
  20. package/dist/__tests__/kickass.test.js +362 -69
  21. package/dist/__tests__/kickass.test.js.map +1 -1
  22. package/dist/__tests__/optimize.test.js +3 -3
  23. package/dist/__tests__/optimize.test.js.map +1 -1
  24. package/dist/__tests__/pr-picker.test.js +8 -7
  25. package/dist/__tests__/pr-picker.test.js.map +1 -1
  26. package/dist/__tests__/pr-status.test.js +41 -20
  27. package/dist/__tests__/pr-status.test.js.map +1 -1
  28. package/dist/__tests__/pr-workflow-state.test.d.ts +2 -0
  29. package/dist/__tests__/pr-workflow-state.test.d.ts.map +1 -0
  30. package/dist/__tests__/pr-workflow-state.test.js +184 -0
  31. package/dist/__tests__/pr-workflow-state.test.js.map +1 -0
  32. package/dist/__tests__/review-models.test.js +18 -0
  33. package/dist/__tests__/review-models.test.js.map +1 -1
  34. package/dist/__tests__/run.test.d.ts +2 -0
  35. package/dist/__tests__/run.test.d.ts.map +1 -0
  36. package/dist/__tests__/run.test.js +81 -0
  37. package/dist/__tests__/run.test.js.map +1 -0
  38. package/dist/__tests__/runner.test.js +117 -1
  39. package/dist/__tests__/runner.test.js.map +1 -1
  40. package/dist/__tests__/scopes.test.js +11 -11
  41. package/dist/__tests__/scopes.test.js.map +1 -1
  42. package/dist/__tests__/smart-switch.test.js +1 -1
  43. package/dist/__tests__/smart-switch.test.js.map +1 -1
  44. package/dist/__tests__/tier-timeouts.test.d.ts +2 -0
  45. package/dist/__tests__/tier-timeouts.test.d.ts.map +1 -0
  46. package/dist/__tests__/tier-timeouts.test.js +23 -0
  47. package/dist/__tests__/tier-timeouts.test.js.map +1 -0
  48. package/dist/__tests__/webhook.test.d.ts +2 -0
  49. package/dist/__tests__/webhook.test.d.ts.map +1 -0
  50. package/dist/__tests__/webhook.test.js +197 -0
  51. package/dist/__tests__/webhook.test.js.map +1 -0
  52. package/dist/cli.js +38 -5
  53. package/dist/cli.js.map +1 -1
  54. package/dist/commands/detect-step.d.ts +5 -0
  55. package/dist/commands/detect-step.d.ts.map +1 -0
  56. package/dist/commands/detect-step.js +124 -0
  57. package/dist/commands/detect-step.js.map +1 -0
  58. package/dist/commands/diagnose.d.ts +1 -1
  59. package/dist/commands/diagnose.d.ts.map +1 -1
  60. package/dist/commands/diagnose.js +30 -1
  61. package/dist/commands/diagnose.js.map +1 -1
  62. package/dist/commands/kickass.d.ts +28 -10
  63. package/dist/commands/kickass.d.ts.map +1 -1
  64. package/dist/commands/kickass.js +295 -68
  65. package/dist/commands/kickass.js.map +1 -1
  66. package/dist/commands/review.d.ts.map +1 -1
  67. package/dist/commands/review.js +14 -5
  68. package/dist/commands/review.js.map +1 -1
  69. package/dist/commands/run.d.ts +16 -1
  70. package/dist/commands/run.d.ts.map +1 -1
  71. package/dist/commands/run.js +347 -44
  72. package/dist/commands/run.js.map +1 -1
  73. package/dist/commands/serve.d.ts.map +1 -1
  74. package/dist/commands/serve.js +41 -3
  75. package/dist/commands/serve.js.map +1 -1
  76. package/dist/commands/status.d.ts.map +1 -1
  77. package/dist/commands/status.js +10 -2
  78. package/dist/commands/status.js.map +1 -1
  79. package/dist/commands/watch.d.ts.map +1 -1
  80. package/dist/commands/watch.js +200 -6
  81. package/dist/commands/watch.js.map +1 -1
  82. package/dist/config/schema.d.ts +52 -0
  83. package/dist/config/schema.d.ts.map +1 -1
  84. package/dist/config/schema.js +24 -1
  85. package/dist/config/schema.js.map +1 -1
  86. package/dist/github/client.d.ts +40 -1
  87. package/dist/github/client.d.ts.map +1 -1
  88. package/dist/github/client.js +69 -9
  89. package/dist/github/client.js.map +1 -1
  90. package/dist/github/review-status.d.ts.map +1 -1
  91. package/dist/github/review-status.js +7 -4
  92. package/dist/github/review-status.js.map +1 -1
  93. package/dist/github/webhook.d.ts +25 -1
  94. package/dist/github/webhook.d.ts.map +1 -1
  95. package/dist/github/webhook.js +37 -1
  96. package/dist/github/webhook.js.map +1 -1
  97. package/dist/lib/annotation.d.ts +4 -0
  98. package/dist/lib/annotation.d.ts.map +1 -1
  99. package/dist/lib/annotation.js +5 -1
  100. package/dist/lib/annotation.js.map +1 -1
  101. package/dist/lib/board.d.ts.map +1 -1
  102. package/dist/lib/board.js +7 -5
  103. package/dist/lib/board.js.map +1 -1
  104. package/dist/lib/comment-bodies.d.ts.map +1 -1
  105. package/dist/lib/comment-bodies.js +3 -2
  106. package/dist/lib/comment-bodies.js.map +1 -1
  107. package/dist/lib/durations.d.ts.map +1 -1
  108. package/dist/lib/durations.js +5 -3
  109. package/dist/lib/durations.js.map +1 -1
  110. package/dist/lib/logger.d.ts +4 -1
  111. package/dist/lib/logger.d.ts.map +1 -1
  112. package/dist/lib/logger.js +41 -5
  113. package/dist/lib/logger.js.map +1 -1
  114. package/dist/lib/pr-picker.d.ts.map +1 -1
  115. package/dist/lib/pr-picker.js +5 -1
  116. package/dist/lib/pr-picker.js.map +1 -1
  117. package/dist/lib/pr-status.d.ts +4 -3
  118. package/dist/lib/pr-status.d.ts.map +1 -1
  119. package/dist/lib/pr-status.js +19 -13
  120. package/dist/lib/pr-status.js.map +1 -1
  121. package/dist/lib/pr-workflow-state.d.ts +68 -0
  122. package/dist/lib/pr-workflow-state.d.ts.map +1 -0
  123. package/dist/lib/pr-workflow-state.js +328 -0
  124. package/dist/lib/pr-workflow-state.js.map +1 -0
  125. package/dist/lib/product.d.ts +3 -0
  126. package/dist/lib/product.d.ts.map +1 -0
  127. package/dist/lib/product.js +5 -0
  128. package/dist/lib/product.js.map +1 -0
  129. package/dist/lib/repo-picker.d.ts +1 -0
  130. package/dist/lib/repo-picker.d.ts.map +1 -1
  131. package/dist/lib/repo-picker.js +50 -33
  132. package/dist/lib/repo-picker.js.map +1 -1
  133. package/dist/lib/review-models.d.ts +2 -2
  134. package/dist/lib/review-models.d.ts.map +1 -1
  135. package/dist/lib/review-models.js +6 -1
  136. package/dist/lib/review-models.js.map +1 -1
  137. package/dist/lib/runner.d.ts +19 -1
  138. package/dist/lib/runner.d.ts.map +1 -1
  139. package/dist/lib/runner.js +338 -55
  140. package/dist/lib/runner.js.map +1 -1
  141. package/dist/lib/scopes.js +1 -1
  142. package/dist/lib/scopes.js.map +1 -1
  143. package/dist/lib/smart-switch.js +1 -1
  144. package/dist/lib/smart-switch.js.map +1 -1
  145. package/dist/lib/vendor.d.ts +4 -0
  146. package/dist/lib/vendor.d.ts.map +1 -0
  147. package/dist/lib/vendor.js +14 -0
  148. package/dist/lib/vendor.js.map +1 -0
  149. package/dist/lib/workflow.d.ts +5 -0
  150. package/dist/lib/workflow.d.ts.map +1 -1
  151. package/dist/lib/workflow.js.map +1 -1
  152. package/dist/reviewers/claude.d.ts +3 -1
  153. package/dist/reviewers/claude.d.ts.map +1 -1
  154. package/dist/reviewers/claude.js +15 -10
  155. package/dist/reviewers/claude.js.map +1 -1
  156. package/dist/reviewers/codex.d.ts +1 -1
  157. package/dist/reviewers/codex.d.ts.map +1 -1
  158. package/dist/reviewers/codex.js +7 -10
  159. package/dist/reviewers/codex.js.map +1 -1
  160. package/dist/reviewers/conflict-resolve.d.ts +1 -1
  161. package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
  162. package/dist/reviewers/conflict-resolve.js +3 -2
  163. package/dist/reviewers/conflict-resolve.js.map +1 -1
  164. package/dist/reviewers/fix.d.ts +5 -1
  165. package/dist/reviewers/fix.d.ts.map +1 -1
  166. package/dist/reviewers/fix.js +68 -2
  167. package/dist/reviewers/fix.js.map +1 -1
  168. package/dist/reviewers/tier-timeouts.d.ts +5 -0
  169. package/dist/reviewers/tier-timeouts.d.ts.map +1 -0
  170. package/dist/reviewers/tier-timeouts.js +14 -0
  171. package/dist/reviewers/tier-timeouts.js.map +1 -0
  172. package/docs/fixture-pr.md +112 -0
  173. package/docs/proof-demo.md +102 -0
  174. package/get-started.md +128 -31
  175. package/get-started.zh.md +7 -1
  176. package/package.json +4 -2
@@ -1,12 +1,19 @@
1
+ import type { Config } from '../config/schema.js';
2
+ import type { ErrorCategory } from '../lib/logger.js';
1
3
  import type { ScanPRStatus as PRStatus, ScanResult } from '../lib/pr-status.js';
2
4
  export interface KickassOpts {
3
5
  force?: boolean;
4
6
  staleAfter?: string;
5
7
  dryRun?: boolean;
8
+ roundMode?: 'crazy' | 'halfcrazy';
9
+ timeout?: string;
10
+ concurrent?: number;
11
+ staggerMs?: number;
6
12
  }
7
- export type KickassAction = 'review' | 'fix' | 'recheck' | 'merge' | 'skip';
13
+ export type KickassAction = 'review' | 'fix' | 'recheck' | 'skip';
8
14
  export type KickassSkipReason = 'fork_pr' | 'stale_signature';
9
- export type KickassFailureReason = 'error';
15
+ export type KickassFailureReason = ErrorCategory;
16
+ export type FixDeliveryMode = Config['post_review']['auto_fix']['delivery']['mode'];
10
17
  export interface PreflightItem {
11
18
  pr: PRStatus;
12
19
  action: KickassAction;
@@ -14,6 +21,7 @@ export interface PreflightItem {
14
21
  details: string[];
15
22
  explanation?: string;
16
23
  skipReason?: KickassSkipReason;
24
+ chainRecheck?: boolean;
17
25
  }
18
26
  export interface KickassExecutionResult {
19
27
  pr: PRStatus;
@@ -22,8 +30,15 @@ export interface KickassExecutionResult {
22
30
  }
23
31
  export interface ExecuteKickassDeps {
24
32
  getCurrentHeadSha: (item: PreflightItem) => Promise<string>;
25
- dispatchRun: (item: PreflightItem) => Promise<void>;
26
- dispatchMerge: (item: PreflightItem) => Promise<void>;
33
+ dispatchRun: (item: PreflightItem) => Promise<string | void>;
34
+ /** Route status messages through a custom sink (e.g. PRBoard scrollback). Defaults to console.log. */
35
+ log?: (msg: string) => void;
36
+ /** Called just before dispatchRun — use to add a board slot. */
37
+ onDispatchStart?: (item: PreflightItem, key: string, startedAt: number) => void;
38
+ /** Called after a successful dispatchRun — use to complete a board slot. */
39
+ onDispatchEnd?: (item: PreflightItem, key: string, startedAt: number) => void;
40
+ /** Called when dispatchRun throws — use to mark a board slot as failed. */
41
+ onDispatchFail?: (item: PreflightItem, key: string, error: unknown) => void;
27
42
  }
28
43
  export interface KickassDeps {
29
44
  loadScanResult: (options: {
@@ -32,18 +47,21 @@ export interface KickassDeps {
32
47
  }) => Promise<ScanResult>;
33
48
  pickPRs: (prs: PRStatus[]) => Promise<PRStatus[]>;
34
49
  confirm: (message: string) => Promise<boolean>;
50
+ getFixDeliveryMode?: () => FixDeliveryMode | Promise<FixDeliveryMode>;
35
51
  getCurrentHeadSha: (item: PreflightItem) => Promise<string>;
36
- dispatchRun: (item: PreflightItem) => Promise<void>;
37
- dispatchMerge: (item: PreflightItem) => Promise<void>;
52
+ onBeforeExecute?: () => void;
53
+ onAfterExecute?: () => void;
54
+ dispatchRun: (item: PreflightItem) => Promise<string | void>;
38
55
  }
39
56
  export declare function runKickass(opts?: KickassOpts): Promise<void>;
40
57
  export declare function runKickassWithDeps(opts: KickassOpts | undefined, deps: KickassDeps): Promise<void>;
41
- export declare function buildPreflightPlan(prs: PRStatus[]): PreflightItem[];
42
- export declare function executeKickassPlan(plan: PreflightItem[], deps: ExecuteKickassDeps): Promise<KickassExecutionResult[]>;
43
- export declare function printPreflight(plan: PreflightItem[]): void;
58
+ export declare function buildPreflightPlan(prs: PRStatus[], roundMode?: 'crazy' | 'halfcrazy', fixDeliveryMode?: FixDeliveryMode): PreflightItem[];
59
+ export declare function executeKickassPlan(plan: PreflightItem[], deps: ExecuteKickassDeps, concurrency?: number, staggerMs?: number): Promise<KickassExecutionResult[]>;
60
+ export declare function printPreflight(plan: PreflightItem[], mergeReady?: PRStatus[]): void;
61
+ export declare function printMergeReady(prs: PRStatus[]): void;
44
62
  export declare function summarizeExecutionResults(results: KickassExecutionResult[]): string;
45
63
  export declare function printExecutionSummary(results: KickassExecutionResult[]): void;
46
- export declare function buildKickassRunArgs(itemOrPR: PreflightItem | PRStatus): string[];
64
+ export declare function buildKickassRunArgs(itemOrPR: PreflightItem | PRStatus, roundMode?: 'crazy' | 'halfcrazy', timeout?: string): string[];
47
65
  export interface CliInvocation {
48
66
  command: string;
49
67
  args: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"kickass.d.ts","sourceRoot":"","sources":["../../src/commands/kickass.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"kickass.d.ts","sourceRoot":"","sources":["../../src/commands/kickass.ts"],"names":[],"mappings":"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"}
@@ -5,14 +5,19 @@ import { createInterface } from 'readline/promises';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execa } from 'execa';
7
7
  import { createGithubClient } from '../github/client.js';
8
- import { mergePullRequest } from '../github/merge.js';
9
- import { getGithubToken } from '../config/loader.js';
8
+ import { getGithubToken, loadConfig } from '../config/loader.js';
10
9
  import { parseDuration } from '../lib/durations.js';
11
- import { logError } from '../lib/logger.js';
10
+ import { classifyError, logError } from '../lib/logger.js';
12
11
  import { pickPRs } from '../lib/pr-picker.js';
13
12
  import { handleScanError, loadScanResult } from './scan.js';
13
+ import { PRBoard } from '../lib/board.js';
14
+ import { loadWorkflow } from '../lib/workflow.js';
14
15
  export async function runKickass(opts = {}) {
15
- await runKickassWithDeps(opts, defaultKickassDeps());
16
+ const config = loadConfig();
17
+ const workflow = loadWorkflow(process.cwd());
18
+ const board = new PRBoard();
19
+ board.setConfig(config, workflow);
20
+ await runKickassWithDeps(opts, defaultKickassDeps(opts, board));
16
21
  }
17
22
  export async function runKickassWithDeps(opts = {}, deps) {
18
23
  let staleAfterMs;
@@ -23,11 +28,38 @@ export async function runKickassWithDeps(opts = {}, deps) {
23
28
  console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
24
29
  process.exit(1);
25
30
  }
31
+ if (opts.timeout) {
32
+ try {
33
+ parseDuration(opts.timeout);
34
+ }
35
+ catch {
36
+ console.error(chalk.red(`✗ Invalid --timeout value "${opts.timeout}". Use a duration like 300s or 10m.`));
37
+ process.exit(1);
38
+ }
39
+ }
40
+ if (opts.concurrent !== undefined && (opts.concurrent < 0 || !Number.isInteger(opts.concurrent))) {
41
+ console.error(chalk.red('✗ --concurrent must be a non-negative integer (0 = one agent per selected PR)'));
42
+ process.exit(1);
43
+ }
26
44
  try {
27
45
  const scan = await deps.loadScanResult({ force: opts.force, staleAfterMs });
28
- const queue = scan.prs.filter(pr => pr.freshness === 'stale' && pr.nextAction !== null);
46
+ // Actionable = nextAction is set and is not merge (merge not dispatched in v1).
47
+ // Stale PRs shown first; not-stale actionable PRs follow.
48
+ const queue = scan.prs
49
+ .filter(pr => pr.nextAction !== null && pr.nextAction !== 'merge')
50
+ .sort((a, b) => {
51
+ if (a.freshness !== b.freshness)
52
+ return a.freshness === 'stale' ? -1 : 1;
53
+ return 0;
54
+ });
55
+ const mergeReady = scan.prs.filter(pr => pr.nextAction === 'merge');
56
+ if (queue.length === 0 && mergeReady.length === 0) {
57
+ printNoActionablePRsWarning(scan.cached);
58
+ return;
59
+ }
29
60
  if (queue.length === 0) {
30
- console.log(chalk.dim('No stale PRs need attention.'));
61
+ printMergeReady(mergeReady);
62
+ console.log(chalk.dim('\nNo PRs need review, fix, or recheck — all actionable work is merge-ready (manual).'));
31
63
  return;
32
64
  }
33
65
  const selected = await deps.pickPRs(queue);
@@ -35,8 +67,9 @@ export async function runKickassWithDeps(opts = {}, deps) {
35
67
  console.log(chalk.dim('No PRs selected.'));
36
68
  return;
37
69
  }
38
- const plan = buildPreflightPlan(selected);
39
- printPreflight(plan);
70
+ const fixDeliveryMode = deps.getFixDeliveryMode ? await deps.getFixDeliveryMode() : 'pull_request';
71
+ const plan = buildPreflightPlan(selected, opts.roundMode, fixDeliveryMode);
72
+ printPreflight(plan, mergeReady);
40
73
  if (opts.dryRun) {
41
74
  console.log(chalk.dim('\ndry-run: no mutations executed'));
42
75
  return;
@@ -46,7 +79,17 @@ export async function runKickassWithDeps(opts = {}, deps) {
46
79
  console.log(chalk.dim('Canceled.'));
47
80
  return;
48
81
  }
49
- const results = await executeKickassPlan(plan, deps);
82
+ // 0 = one agent per selected PR; undefined/1 = sequential
83
+ const resolvedConcurrency = opts.concurrent === 0
84
+ ? selected.length
85
+ : Math.max(1, opts.concurrent ?? 1);
86
+ const resolvedStagger = resolvedConcurrency > 1 ? (opts.staggerMs ?? 2_000) : 0;
87
+ if (resolvedConcurrency > 1) {
88
+ console.log(chalk.dim(`\n running ${resolvedConcurrency} agents in parallel (${resolvedStagger}ms stagger)`));
89
+ }
90
+ deps.onBeforeExecute?.();
91
+ const results = await executeKickassPlan(plan, deps, resolvedConcurrency, resolvedStagger);
92
+ deps.onAfterExecute?.();
50
93
  printExecutionSummary(results);
51
94
  if (results.some(result => result.status === 'failed')) {
52
95
  process.exitCode = 2;
@@ -56,7 +99,9 @@ export async function runKickassWithDeps(opts = {}, deps) {
56
99
  handleScanError('kickass', err);
57
100
  }
58
101
  }
59
- export function buildPreflightPlan(prs) {
102
+ export function buildPreflightPlan(prs, roundMode, fixDeliveryMode = 'pull_request') {
103
+ const modeTag = roundMode ? ` [${roundMode}]` : '';
104
+ const chainRecheck = fixDeliveryMode === 'commit';
60
105
  return prs.map((pr) => {
61
106
  const fork = isForkPR(pr);
62
107
  if (pr.nextAction === 'fix' && fork) {
@@ -89,59 +134,177 @@ export function buildPreflightPlan(prs) {
89
134
  return {
90
135
  pr,
91
136
  action: 'fix',
92
- transition: `${pr.reviewState} -> Fix`,
93
- details: [`fixer ${fixerLabel(pr)}`, 'delivery commit'],
94
- };
95
- }
96
- if (pr.nextAction === 'recheck') {
97
- return {
98
- pr,
99
- action: 'recheck',
100
- transition: `${pr.reviewState} -> Recheck`,
101
- details: ['links latest review'],
137
+ transition: chainRecheck
138
+ ? `${pr.reviewState} -> fix→recheck${modeTag}`
139
+ : `${pr.reviewState} -> fix`,
140
+ details: [
141
+ `fixer ${fixerLabel(pr)}`,
142
+ `delivery ${fixDeliveryMode}`,
143
+ ...(chainRecheck ? [] : ['recheck deferred']),
144
+ ],
145
+ chainRecheck,
102
146
  };
103
147
  }
148
+ // nextAction === 'recheck' — fix was applied externally; close the loop with one recheck
104
149
  return {
105
150
  pr,
106
- action: 'merge',
107
- transition: 'APPROVE -> Merge',
108
- details: ['method squash', `checks ${checksLabel(pr)}`],
151
+ action: 'recheck',
152
+ transition: `${pr.reviewState} -> Recheck`,
153
+ details: ['links latest review'],
109
154
  };
110
155
  });
111
156
  }
112
- export async function executeKickassPlan(plan, deps) {
113
- const results = [];
114
- for (const item of plan) {
157
+ function printCapturedOutput(label, output, log = console.log) {
158
+ const lines = output.trimEnd().split('\n');
159
+ log(chalk.dim(`\n── ${label} ${'─'.repeat(Math.max(0, 48 - label.length))}`));
160
+ for (const line of lines)
161
+ log(` ${line}`);
162
+ }
163
+ function printNoActionablePRsWarning(fromCache) {
164
+ console.log(chalk.dim('No actionable PRs found.'));
165
+ if (fromCache) {
166
+ console.log(chalk.yellow(`⚠ This result came from the scan cache. Rerun with --force to refresh the queue.`));
167
+ }
168
+ }
169
+ export async function executeKickassPlan(plan, deps, concurrency = 1, staggerMs = 0) {
170
+ const results = new Array(plan.length);
171
+ const log = deps.log ?? console.log;
172
+ const executeItem = async (item, index, attempt = 1) => {
115
173
  if (item.action === 'skip') {
116
- console.log(chalk.yellow(`↷ skip ${formatPRSignature(item.pr)} ${item.skipReason ?? 'skipped'}`));
117
- results.push({ pr: item.pr, status: 'skipped', reason: item.skipReason });
118
- continue;
174
+ log(chalk.yellow(`↷ skip ${formatPRSignature(item.pr)} ${item.skipReason ?? 'skipped'}`));
175
+ results[index] = { pr: item.pr, status: 'skipped', reason: item.skipReason };
176
+ return;
119
177
  }
120
178
  try {
121
179
  const currentHeadSha = await deps.getCurrentHeadSha(item);
122
180
  if (currentHeadSha !== item.pr.headSha) {
123
- console.log(chalk.yellow(`↷ skip ${formatPRSignature(item.pr)} stale_signature`));
124
- results.push({ pr: item.pr, status: 'skipped', reason: 'stale_signature' });
125
- continue;
181
+ log(chalk.yellow(`↷ skip ${formatPRSignature(item.pr)} stale_signature`));
182
+ results[index] = { pr: item.pr, status: 'skipped', reason: 'stale_signature' };
183
+ return;
126
184
  }
127
- console.log(chalk.cyan(`\n→ ${item.transition} ${formatPRSignature(item.pr)}`));
128
- if (item.action === 'merge') {
129
- await deps.dispatchMerge(item);
185
+ const key = `${item.pr.owner}/${item.pr.repo}#${item.pr.number}@${item.pr.headSha}`;
186
+ const startedAt = Date.now();
187
+ const attemptLabel = attempt > 1 ? ` (retry ${attempt - 1})` : '';
188
+ log(chalk.cyan(`\n→ ${item.transition} ${formatPRSignature(item.pr)}${attemptLabel}`));
189
+ deps.onDispatchStart?.(item, key, startedAt);
190
+ const output = await deps.dispatchRun(item);
191
+ deps.onDispatchEnd?.(item, key, startedAt);
192
+ if (typeof output === 'string' && output)
193
+ printCapturedOutput(formatPRSignature(item.pr), output, log);
194
+ if (item.action === 'fix' && item.chainRecheck === true) {
195
+ const fixedHeadSha = await deps.getCurrentHeadSha(item);
196
+ if (fixedHeadSha !== item.pr.headSha) {
197
+ const recheckItem = buildPostFixRecheckItem(item, fixedHeadSha);
198
+ const recheckKey = `${recheckItem.pr.owner}/${recheckItem.pr.repo}#${recheckItem.pr.number}@${recheckItem.pr.headSha}`;
199
+ const recheckStart = Date.now();
200
+ log(chalk.cyan(`\n→ ${recheckItem.transition} ${formatPRSignature(recheckItem.pr)}`));
201
+ deps.onDispatchStart?.(recheckItem, recheckKey, recheckStart);
202
+ const recheckOutput = await deps.dispatchRun(recheckItem);
203
+ deps.onDispatchEnd?.(recheckItem, recheckKey, recheckStart);
204
+ if (typeof recheckOutput === 'string' && recheckOutput)
205
+ printCapturedOutput(formatPRSignature(recheckItem.pr), recheckOutput, log);
206
+ }
207
+ else {
208
+ log(chalk.dim(` head SHA unchanged after fix — recheck deferred`));
209
+ }
210
+ }
211
+ results[index] = { pr: item.pr, status: 'executed' };
212
+ }
213
+ catch (err) {
214
+ logError({ event: 'kickass_pr_failed', owner: item.pr.owner, repo: item.pr.repo, pr: item.pr.number, ...(attempt > 1 && { attempt }) }, err);
215
+ log(chalk.red(`✗ failed ${formatPRSignature(item.pr)}`));
216
+ const failKey = `${item.pr.owner}/${item.pr.repo}#${item.pr.number}@${item.pr.headSha}`;
217
+ deps.onDispatchFail?.(item, failKey, err);
218
+ // Classify execa errors using structured fields, not the raw message.
219
+ // The raw message includes the full CLI invocation (e.g. "Command failed with exit
220
+ // code 1: node crosscheck run --timeout 300s --no-timeout"), so a text match against
221
+ // `message` would misclassify ordinary subprocess failures as 'timeout' whenever the
222
+ // command contains a --timeout flag.
223
+ const maybeExeca = err;
224
+ let msgForClassify;
225
+ if (maybeExeca.timedOut === true) {
226
+ // execa's structured timeout flag — reliable; bypass message matching entirely.
227
+ msgForClassify = 'timed out';
228
+ }
229
+ else if (typeof maybeExeca.exitCode === 'number') {
230
+ // Subprocess failure: prefer stderr (actual error output) over the message which
231
+ // includes the full command string. Strip the command suffix when stderr is absent.
232
+ const stderr = typeof maybeExeca.stderr === 'string' ? maybeExeca.stderr.trim() : '';
233
+ msgForClassify = stderr || (err instanceof Error ? err.message.replace(/:\s*\S.*$/, '') : String(err));
130
234
  }
131
235
  else {
132
- await deps.dispatchRun(item);
236
+ msgForClassify = err instanceof Error ? err.message : String(err);
133
237
  }
134
- results.push({ pr: item.pr, status: 'executed' });
238
+ const category = classifyError(msgForClassify);
239
+ results[index] = { pr: item.pr, status: 'failed', reason: category };
135
240
  }
136
- catch (err) {
137
- logError({ event: 'kickass_pr_failed', owner: item.pr.owner, repo: item.pr.repo, pr: item.pr.number }, err);
138
- console.error(chalk.red(`✗ failed ${formatPRSignature(item.pr)}`));
139
- results.push({ pr: item.pr, status: 'failed', reason: 'error' });
241
+ };
242
+ if (concurrency <= 1) {
243
+ for (let i = 0; i < plan.length; i++)
244
+ await executeItem(plan[i], i);
245
+ }
246
+ else {
247
+ // Worker-pool: up to `concurrency` PRs run in parallel.
248
+ // staggerMs > 0 delays each worker's start by (workerIdx * staggerMs) to spread
249
+ // concurrent subprocess startup API calls over time rather than hitting GitHub simultaneously.
250
+ let ptr = 0;
251
+ const makeWorker = (workerIdx) => async () => {
252
+ if (staggerMs > 0 && workerIdx > 0) {
253
+ await new Promise(resolve => setTimeout(resolve, workerIdx * staggerMs));
254
+ }
255
+ while (ptr < plan.length) {
256
+ const i = ptr++;
257
+ await executeItem(plan[i], i);
258
+ }
259
+ };
260
+ await Promise.all(Array.from({ length: Math.min(concurrency, plan.length) }, (_, idx) => makeWorker(idx)()));
261
+ }
262
+ // Retry transient failures up to 4 times with escalating delays.
263
+ // Auth and permission failures are operator issues that won't self-heal.
264
+ const RETRYABLE = new Set(['network', 'timeout']);
265
+ const RETRY_DELAYS_MS = [60_000, 120_000, 300_000, 600_000];
266
+ for (let attempt = 2; attempt <= RETRY_DELAYS_MS.length + 1; attempt++) {
267
+ const delayMs = RETRY_DELAYS_MS[attempt - 2];
268
+ const retryItems = results
269
+ .map((r, i) => ({ r, i }))
270
+ .filter(({ r }) => r.status === 'failed' && RETRYABLE.has(r.reason));
271
+ if (retryItems.length === 0)
272
+ break;
273
+ const delaySec = delayMs / 1000;
274
+ const delayLabel = delaySec >= 60 ? `${delaySec / 60}m` : `${delaySec}s`;
275
+ log(chalk.dim(`\n ${retryItems.length} transient failure(s) — retry ${attempt - 1}/${RETRY_DELAYS_MS.length} in ${delayLabel}...`));
276
+ await new Promise(resolve => setTimeout(resolve, delayMs));
277
+ for (const { i } of retryItems) {
278
+ const priorResult = results[i];
279
+ await executeItem(plan[i], i, attempt);
280
+ // Stale-signature means the fix already committed in a prior attempt but the
281
+ // chained recheck failed transiently. Instead of reporting that failure as
282
+ // final, fetch the current head and run a bare recheck to actually retry it.
283
+ if (results[i].status === 'skipped' && results[i].reason === 'stale_signature'
284
+ && plan[i].action === 'fix' && plan[i].chainRecheck === true) {
285
+ try {
286
+ const currentHead = await deps.getCurrentHeadSha(plan[i]);
287
+ const recheckItem = buildPostFixRecheckItem(plan[i], currentHead);
288
+ await executeItem(recheckItem, i, attempt);
289
+ }
290
+ catch {
291
+ // If we cannot fetch the head (network failure), preserve the original failure.
292
+ results[i] = priorResult;
293
+ }
294
+ }
140
295
  }
141
296
  }
142
297
  return results;
143
298
  }
144
- export function printPreflight(plan) {
299
+ function buildPostFixRecheckItem(item, headSha) {
300
+ return {
301
+ pr: { ...item.pr, headSha, nextAction: 'recheck', reviewState: 'NEEDS_RECHECK' },
302
+ action: 'recheck',
303
+ transition: 'fix -> Recheck',
304
+ details: ['links latest review', `head ${headSha.slice(0, 7)}`],
305
+ };
306
+ }
307
+ export function printPreflight(plan, mergeReady = []) {
145
308
  console.log('\nPreflight');
146
309
  const grouped = groupPreflight(plan);
147
310
  for (const [transition, items] of grouped) {
@@ -151,6 +314,14 @@ export function printPreflight(plan) {
151
314
  console.log(` ${formatPRSignature(item.pr)} ${item.details.join(' ')}${explanation}`);
152
315
  }
153
316
  }
317
+ if (mergeReady.length > 0)
318
+ printMergeReady(mergeReady);
319
+ }
320
+ export function printMergeReady(prs) {
321
+ console.log(chalk.dim('\nneeds merge (manual — not selected)'));
322
+ for (const pr of prs) {
323
+ console.log(chalk.dim(` ${formatPRSignature(pr)} APPROVE`));
324
+ }
154
325
  }
155
326
  export function summarizeExecutionResults(results) {
156
327
  const executed = results.filter(result => result.status === 'executed').length;
@@ -161,18 +332,36 @@ export function summarizeExecutionResults(results) {
161
332
  export function printExecutionSummary(results) {
162
333
  console.log(chalk.dim(`\n${summarizeExecutionResults(results)}`));
163
334
  }
164
- export function buildKickassRunArgs(itemOrPR) {
335
+ export function buildKickassRunArgs(itemOrPR, roundMode, timeout) {
165
336
  const item = 'action' in itemOrPR ? itemOrPR : buildPreflightPlan([itemOrPR])[0];
166
- if (item.action === 'merge' || item.action === 'skip')
337
+ if (item.action === 'skip')
167
338
  return [];
168
- return [
169
- 'run',
170
- item.pr.url,
171
- '--steps',
172
- stepForAction(item.action),
173
- '--expected-head-sha',
174
- item.pr.headSha,
175
- ];
339
+ const args = ['run', item.pr.url];
340
+ // No --steps for normal review/recheck/fix actions: run.ts calls
341
+ // identifyNextWorkflowStep against live PR history to determine the correct
342
+ // next step. Exception: when kickass demoted a fix action to review because the
343
+ // latest annotation covers an older SHA (no_usable_review_comment), we must
344
+ // force --steps review so run.ts doesn't re-detect from live history and choose
345
+ // the stale review's fix step, applying fixes to the unreviewed new diff.
346
+ if (item.action === 'review' && item.explanation === 'no_usable_review_comment') {
347
+ args.push('--steps', 'review');
348
+ }
349
+ args.push('--expected-head-sha', item.pr.headSha);
350
+ if (item.action !== 'fix') {
351
+ if (roundMode === 'crazy')
352
+ args.push('--crazy');
353
+ else if (roundMode === 'halfcrazy')
354
+ args.push('--half-crazy');
355
+ }
356
+ else if (roundMode) {
357
+ // fix legs don't loop, but still need the no-timeout constraint lifted
358
+ args.push('--no-timeout');
359
+ }
360
+ // forward user-specified --timeout for runs that aren't already in a round mode
361
+ if (timeout && !roundMode)
362
+ args.push('--timeout', timeout);
363
+ args.push('--trigger', 'kickass');
364
+ return args;
176
365
  }
177
366
  export function resolveCliInvocation(options = {}) {
178
367
  const exists = options.exists ?? existsSync;
@@ -191,16 +380,48 @@ export function resolveCliInvocation(options = {}) {
191
380
  return invocationForEntry(sourceCli, execPath, localTsx, exists);
192
381
  throw new Error('Cannot resolve crosscheck CLI entrypoint. Run npm run build before kickass, or run from a source checkout with dev dependencies installed.');
193
382
  }
194
- function defaultKickassDeps() {
383
+ function defaultKickassDeps(opts = {}, board) {
195
384
  let cli;
196
385
  const getCli = () => {
197
386
  cli ??= resolveCliInvocation();
198
387
  return cli;
199
388
  };
389
+ const dispatchRun = async (item) => {
390
+ const invocation = getCli();
391
+ const args = [...invocation.args, ...buildKickassRunArgs(item, opts.roundMode, opts.timeout)];
392
+ // When board is active always pipe so output routes through board.log scrollback.
393
+ // Without board, pipe only for explicit concurrent mode; sequential streams inline.
394
+ if (board || opts.concurrent !== undefined) {
395
+ try {
396
+ const result = await execa(invocation.command, args, { stdio: 'pipe', all: true });
397
+ return result.all ?? '';
398
+ }
399
+ catch (err) {
400
+ // Surface captured output before re-throwing so the board log includes
401
+ // the child's stdout/stderr (auth errors, model failures, etc.) that were
402
+ // previously visible via inherited stdio.
403
+ const e = err;
404
+ const captured = typeof e.all === 'string' ? e.all.trim()
405
+ : typeof e.stderr === 'string' ? e.stderr.trim() : '';
406
+ if (captured) {
407
+ if (board)
408
+ board.log(captured);
409
+ else
410
+ console.error(captured);
411
+ }
412
+ throw err;
413
+ }
414
+ }
415
+ await execa(invocation.command, args, { stdio: 'inherit' });
416
+ };
417
+ const actionPhase = (action) => action === 'fix' ? 'fixing' : action === 'recheck' ? 'rechecking' : 'reviewing';
418
+ const actionLabel = (action) => action === 'fix' ? 'applying fix...' : action === 'recheck' ? 'rechecking...' : 'reviewing...';
419
+ const donePhase = (action) => action === 'fix' ? 'fixed' : action === 'recheck' ? 'rechecked' : 'reviewed';
200
420
  return {
201
421
  loadScanResult,
202
422
  pickPRs,
203
423
  confirm: confirmMutation,
424
+ getFixDeliveryMode: () => loadConfig().post_review.auto_fix.delivery.mode,
204
425
  getCurrentHeadSha: async (item) => {
205
426
  const token = getGithubToken();
206
427
  const octokit = createGithubClient(token);
@@ -211,18 +432,24 @@ function defaultKickassDeps() {
211
432
  });
212
433
  return data.head.sha;
213
434
  },
214
- dispatchRun: async (item) => {
215
- const invocation = getCli();
216
- await execa(invocation.command, [...invocation.args, ...buildKickassRunArgs(item)], { stdio: 'inherit' });
217
- },
218
- dispatchMerge: async (item) => {
219
- const token = getGithubToken();
220
- const octokit = createGithubClient(token);
221
- await mergePullRequest(octokit, item.pr.owner, item.pr.repo, item.pr.number, {
222
- method: 'squash',
223
- expectedHeadSha: item.pr.headSha,
224
- });
225
- },
435
+ dispatchRun,
436
+ ...(board && {
437
+ log: (msg) => board.log(msg),
438
+ onDispatchStart: (item, key, _startedAt) => {
439
+ board.addPR(key, item.pr.number, `${item.pr.owner}/${item.pr.repo}`, item.pr.headRef);
440
+ board.updatePR(key, { phase: actionPhase(item.action), label: actionLabel(item.action) });
441
+ },
442
+ onDispatchEnd: (item, key, startedAt) => {
443
+ board.updatePR(key, { phase: donePhase(item.action) });
444
+ board.completePR(key, { elapsedMs: Date.now() - startedAt, url: item.pr.url });
445
+ },
446
+ onDispatchFail: (_item, key, err) => {
447
+ const msg = err instanceof Error ? err.message : String(err);
448
+ board.failPR(key, msg);
449
+ },
450
+ onBeforeExecute: () => board.start(),
451
+ onAfterExecute: () => board.stop(),
452
+ }),
226
453
  };
227
454
  }
228
455
  async function confirmMutation(message) {
@@ -268,10 +495,10 @@ function checksLabel(pr) {
268
495
  return 'green';
269
496
  return pr.merge.mergeStateStatus ?? 'unknown';
270
497
  }
271
- function stepForAction(action) {
272
- if (action === 'review')
498
+ function stepsForItem(item) {
499
+ if (item.action === 'review')
273
500
  return 'review';
274
- if (action === 'fix')
501
+ if (item.action === 'fix')
275
502
  return 'fix';
276
503
  return 'recheck';
277
504
  }