@humanbased/crosscheck 1.2.0-beta.81 → 1.2.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.
- package/LICENSE +1 -1
- package/README.md +375 -182
- package/README.zh.md +1 -1
- package/crosscheck.config.example.yml +9 -101
- package/dist/__tests__/board.test.js +0 -11
- package/dist/__tests__/board.test.js.map +1 -1
- package/dist/__tests__/codex.test.js +27 -14
- package/dist/__tests__/codex.test.js.map +1 -1
- package/dist/__tests__/comment-bodies.test.js +1 -49
- package/dist/__tests__/comment-bodies.test.js.map +1 -1
- package/dist/__tests__/conflict-resolve.test.js +1 -44
- package/dist/__tests__/conflict-resolve.test.js.map +1 -1
- package/dist/__tests__/fix.test.js +0 -33
- package/dist/__tests__/fix.test.js.map +1 -1
- package/dist/__tests__/onboard-preservation.test.js +3 -59
- package/dist/__tests__/onboard-preservation.test.js.map +1 -1
- package/dist/__tests__/optimize.test.js +0 -2
- package/dist/__tests__/optimize.test.js.map +1 -1
- package/dist/__tests__/repo-picker.test.js +1 -7
- package/dist/__tests__/repo-picker.test.js.map +1 -1
- package/dist/__tests__/review-comment-body.test.js +0 -35
- package/dist/__tests__/review-comment-body.test.js.map +1 -1
- package/dist/__tests__/review-models.test.js +3 -19
- package/dist/__tests__/review-models.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +1 -29
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/cli.js +0 -29
- package/dist/cli.js.map +1 -1
- package/dist/commands/onboard.d.ts +3 -36
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +42 -233
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +6 -65
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -45
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +1 -53
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +12 -90
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.d.ts +1 -3
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +0 -13
- package/dist/config/loader.js.map +1 -1
- package/dist/config/review-model-tiers.json +3 -3
- package/dist/config/schema.d.ts +15 -261
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +8 -90
- package/dist/config/schema.js.map +1 -1
- package/dist/github/client.d.ts +1 -21
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +7 -46
- package/dist/github/client.js.map +1 -1
- package/dist/github/webhook.d.ts +0 -4
- package/dist/github/webhook.d.ts.map +1 -1
- package/dist/github/webhook.js.map +1 -1
- package/dist/issues/ticket-ref.d.ts.map +1 -1
- package/dist/issues/ticket-ref.js +5 -6
- package/dist/issues/ticket-ref.js.map +1 -1
- package/dist/lib/annotation.d.ts +0 -7
- package/dist/lib/annotation.d.ts.map +1 -1
- package/dist/lib/annotation.js +1 -11
- package/dist/lib/annotation.js.map +1 -1
- package/dist/lib/board.d.ts +0 -3
- package/dist/lib/board.d.ts.map +1 -1
- package/dist/lib/board.js +2 -4
- package/dist/lib/board.js.map +1 -1
- package/dist/lib/clone.d.ts +0 -1
- package/dist/lib/clone.d.ts.map +1 -1
- package/dist/lib/clone.js +10 -32
- package/dist/lib/clone.js.map +1 -1
- package/dist/lib/comment-bodies.d.ts +0 -37
- package/dist/lib/comment-bodies.d.ts.map +1 -1
- package/dist/lib/comment-bodies.js +9 -47
- package/dist/lib/comment-bodies.js.map +1 -1
- package/dist/lib/repo-picker.d.ts +0 -3
- package/dist/lib/repo-picker.d.ts.map +1 -1
- package/dist/lib/repo-picker.js +9 -45
- package/dist/lib/repo-picker.js.map +1 -1
- package/dist/lib/review-models.d.ts +2 -15
- package/dist/lib/review-models.d.ts.map +1 -1
- package/dist/lib/review-models.js +6 -26
- package/dist/lib/review-models.js.map +1 -1
- package/dist/lib/runner.d.ts +0 -92
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +54 -470
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts +0 -9
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js +0 -20
- package/dist/lib/workflow.js.map +1 -1
- package/dist/reviewers/claude.d.ts +1 -4
- package/dist/reviewers/claude.d.ts.map +1 -1
- package/dist/reviewers/claude.js +7 -39
- package/dist/reviewers/claude.js.map +1 -1
- package/dist/reviewers/codex.d.ts +1 -3
- package/dist/reviewers/codex.d.ts.map +1 -1
- package/dist/reviewers/codex.js +70 -76
- package/dist/reviewers/codex.js.map +1 -1
- package/dist/reviewers/conflict-resolve.d.ts +1 -3
- package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
- package/dist/reviewers/conflict-resolve.js +6 -21
- package/dist/reviewers/conflict-resolve.js.map +1 -1
- package/dist/reviewers/fix.d.ts +2 -5
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +10 -26
- package/dist/reviewers/fix.js.map +1 -1
- package/get-started.md +11 -207
- package/package.json +3 -4
- package/assets/icon-256.png +0 -0
- package/assets/linear-comment.svg +0 -18
- package/assets/linear-onboard.svg +0 -30
- package/assets/linear-status.svg +0 -23
- package/assets/linear-test.svg +0 -34
- package/assets/skills/code-review/.crosscheck-skill.json +0 -9
- package/assets/skills/code-review/LICENSE +0 -21
- package/assets/skills/code-review/SKILL.md +0 -89
- package/assets/skills/code-review/agents/openai.yaml +0 -3
- package/assets/skills/code-review-skill/.crosscheck-skill.json +0 -9
- package/assets/skills/code-review-skill/LICENSE +0 -21
- package/assets/skills/code-review-skill/SKILL.md +0 -231
- package/assets/skills/code-review-skill/assets/pr-review-template.md +0 -137
- package/assets/skills/code-review-skill/assets/review-checklist.md +0 -123
- package/assets/skills/code-review-skill/reference/angular.md +0 -768
- package/assets/skills/code-review-skill/reference/architecture-review-guide.md +0 -472
- package/assets/skills/code-review-skill/reference/c.md +0 -890
- package/assets/skills/code-review-skill/reference/code-quality-universal.md +0 -488
- package/assets/skills/code-review-skill/reference/code-review-best-practices.md +0 -136
- package/assets/skills/code-review-skill/reference/common-bugs-checklist.md +0 -286
- package/assets/skills/code-review-skill/reference/cpp.md +0 -893
- package/assets/skills/code-review-skill/reference/cross-cutting/async-concurrency-patterns.md +0 -515
- package/assets/skills/code-review-skill/reference/cross-cutting/error-handling-principles.md +0 -492
- package/assets/skills/code-review-skill/reference/cross-cutting/n-plus-one-queries.md +0 -309
- package/assets/skills/code-review-skill/reference/cross-cutting/sql-injection-prevention.md +0 -308
- package/assets/skills/code-review-skill/reference/cross-cutting/xss-prevention.md +0 -264
- package/assets/skills/code-review-skill/reference/csharp.md +0 -525
- package/assets/skills/code-review-skill/reference/css-less-sass.md +0 -661
- package/assets/skills/code-review-skill/reference/django.md +0 -985
- package/assets/skills/code-review-skill/reference/fastapi.md +0 -580
- package/assets/skills/code-review-skill/reference/go.md +0 -993
- package/assets/skills/code-review-skill/reference/java.md +0 -409
- package/assets/skills/code-review-skill/reference/java8.md +0 -586
- package/assets/skills/code-review-skill/reference/kotlin.md +0 -1018
- package/assets/skills/code-review-skill/reference/nestjs.md +0 -593
- package/assets/skills/code-review-skill/reference/performance-review-guide.md +0 -816
- package/assets/skills/code-review-skill/reference/php.md +0 -684
- package/assets/skills/code-review-skill/reference/python.md +0 -1073
- package/assets/skills/code-review-skill/reference/qt.md +0 -757
- package/assets/skills/code-review-skill/reference/react.md +0 -871
- package/assets/skills/code-review-skill/reference/ruby.md +0 -964
- package/assets/skills/code-review-skill/reference/rust.md +0 -846
- package/assets/skills/code-review-skill/reference/security-review-guide.md +0 -494
- package/assets/skills/code-review-skill/reference/svelte.md +0 -1064
- package/assets/skills/code-review-skill/reference/swift.md +0 -936
- package/assets/skills/code-review-skill/reference/typescript.md +0 -1016
- package/assets/skills/code-review-skill/reference/vue.md +0 -924
- package/assets/skills/code-review-skill/reference/zig.md +0 -440
- package/assets/skills/code-review-skill/scripts/pr-analyzer.py +0 -435
- package/assets/skills/code-review-skill/scripts/test_pr_analyzer.py +0 -380
- package/assets/skills/codebase-design/.crosscheck-skill.json +0 -9
- package/assets/skills/codebase-design/DEEPENING.md +0 -37
- package/assets/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/assets/skills/codebase-design/LICENSE +0 -21
- package/assets/skills/codebase-design/SKILL.md +0 -114
- package/assets/skills/codebase-design/agents/openai.yaml +0 -3
- package/assets/skills/diagnosing-bugs/.crosscheck-skill.json +0 -9
- package/assets/skills/diagnosing-bugs/LICENSE +0 -21
- package/assets/skills/diagnosing-bugs/SKILL.md +0 -134
- package/assets/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/assets/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -41
- package/dist/__tests__/can-write-verdict.test.d.ts +0 -2
- package/dist/__tests__/can-write-verdict.test.d.ts.map +0 -1
- package/dist/__tests__/can-write-verdict.test.js +0 -31
- package/dist/__tests__/can-write-verdict.test.js.map +0 -1
- package/dist/__tests__/linear-branding.test.d.ts +0 -2
- package/dist/__tests__/linear-branding.test.d.ts.map +0 -1
- package/dist/__tests__/linear-branding.test.js +0 -156
- package/dist/__tests__/linear-branding.test.js.map +0 -1
- package/dist/__tests__/linear-client.test.d.ts +0 -2
- package/dist/__tests__/linear-client.test.d.ts.map +0 -1
- package/dist/__tests__/linear-client.test.js +0 -120
- package/dist/__tests__/linear-client.test.js.map +0 -1
- package/dist/__tests__/linear-comment.test.d.ts +0 -2
- package/dist/__tests__/linear-comment.test.d.ts.map +0 -1
- package/dist/__tests__/linear-comment.test.js +0 -151
- package/dist/__tests__/linear-comment.test.js.map +0 -1
- package/dist/__tests__/linear-identity.test.d.ts +0 -2
- package/dist/__tests__/linear-identity.test.d.ts.map +0 -1
- package/dist/__tests__/linear-identity.test.js +0 -253
- package/dist/__tests__/linear-identity.test.js.map +0 -1
- package/dist/__tests__/linear-notify.test.d.ts +0 -2
- package/dist/__tests__/linear-notify.test.d.ts.map +0 -1
- package/dist/__tests__/linear-notify.test.js +0 -144
- package/dist/__tests__/linear-notify.test.js.map +0 -1
- package/dist/__tests__/linear-ref.test.d.ts +0 -2
- package/dist/__tests__/linear-ref.test.d.ts.map +0 -1
- package/dist/__tests__/linear-ref.test.js +0 -261
- package/dist/__tests__/linear-ref.test.js.map +0 -1
- package/dist/__tests__/linear-test-ref.test.d.ts +0 -2
- package/dist/__tests__/linear-test-ref.test.d.ts.map +0 -1
- package/dist/__tests__/linear-test-ref.test.js +0 -81
- package/dist/__tests__/linear-test-ref.test.js.map +0 -1
- package/dist/__tests__/linear-verify.test.d.ts +0 -2
- package/dist/__tests__/linear-verify.test.d.ts.map +0 -1
- package/dist/__tests__/linear-verify.test.js +0 -132
- package/dist/__tests__/linear-verify.test.js.map +0 -1
- package/dist/__tests__/linear-worker.test.d.ts +0 -2
- package/dist/__tests__/linear-worker.test.d.ts.map +0 -1
- package/dist/__tests__/linear-worker.test.js +0 -83
- package/dist/__tests__/linear-worker.test.js.map +0 -1
- package/dist/__tests__/linear-write-possible.test.d.ts +0 -2
- package/dist/__tests__/linear-write-possible.test.d.ts.map +0 -1
- package/dist/__tests__/linear-write-possible.test.js +0 -30
- package/dist/__tests__/linear-write-possible.test.js.map +0 -1
- package/dist/__tests__/repository-guidance.test.d.ts +0 -2
- package/dist/__tests__/repository-guidance.test.d.ts.map +0 -1
- package/dist/__tests__/repository-guidance.test.js +0 -107
- package/dist/__tests__/repository-guidance.test.js.map +0 -1
- package/dist/__tests__/review-strategy.test.d.ts +0 -2
- package/dist/__tests__/review-strategy.test.d.ts.map +0 -1
- package/dist/__tests__/review-strategy.test.js +0 -397
- package/dist/__tests__/review-strategy.test.js.map +0 -1
- package/dist/__tests__/skill-attribution.test.d.ts +0 -2
- package/dist/__tests__/skill-attribution.test.d.ts.map +0 -1
- package/dist/__tests__/skill-attribution.test.js +0 -53
- package/dist/__tests__/skill-attribution.test.js.map +0 -1
- package/dist/__tests__/skill-broker.test.d.ts +0 -2
- package/dist/__tests__/skill-broker.test.d.ts.map +0 -1
- package/dist/__tests__/skill-broker.test.js +0 -144
- package/dist/__tests__/skill-broker.test.js.map +0 -1
- package/dist/__tests__/skill-catalog.test.d.ts +0 -2
- package/dist/__tests__/skill-catalog.test.d.ts.map +0 -1
- package/dist/__tests__/skill-catalog.test.js +0 -40
- package/dist/__tests__/skill-catalog.test.js.map +0 -1
- package/dist/__tests__/skill-installer.test.d.ts +0 -2
- package/dist/__tests__/skill-installer.test.d.ts.map +0 -1
- package/dist/__tests__/skill-installer.test.js +0 -96
- package/dist/__tests__/skill-installer.test.js.map +0 -1
- package/dist/__tests__/skills-config.test.d.ts +0 -2
- package/dist/__tests__/skills-config.test.d.ts.map +0 -1
- package/dist/__tests__/skills-config.test.js +0 -12
- package/dist/__tests__/skills-config.test.js.map +0 -1
- package/dist/commands/linear-test.d.ts +0 -18
- package/dist/commands/linear-test.d.ts.map +0 -1
- package/dist/commands/linear-test.js +0 -130
- package/dist/commands/linear-test.js.map +0 -1
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.d.ts.map +0 -1
- package/dist/commands/skill.js +0 -16
- package/dist/commands/skill.js.map +0 -1
- package/dist/config/review-strategy.json +0 -204
- package/dist/lib/repository-guidance.d.ts +0 -2
- package/dist/lib/repository-guidance.d.ts.map +0 -1
- package/dist/lib/repository-guidance.js +0 -55
- package/dist/lib/repository-guidance.js.map +0 -1
- package/dist/lib/review-strategy.d.ts +0 -92
- package/dist/lib/review-strategy.d.ts.map +0 -1
- package/dist/lib/review-strategy.js +0 -282
- package/dist/lib/review-strategy.js.map +0 -1
- package/dist/linear/client.d.ts +0 -18
- package/dist/linear/client.d.ts.map +0 -1
- package/dist/linear/client.js +0 -67
- package/dist/linear/client.js.map +0 -1
- package/dist/linear/comment.d.ts +0 -20
- package/dist/linear/comment.d.ts.map +0 -1
- package/dist/linear/comment.js +0 -57
- package/dist/linear/comment.js.map +0 -1
- package/dist/linear/identity.d.ts +0 -59
- package/dist/linear/identity.d.ts.map +0 -1
- package/dist/linear/identity.js +0 -187
- package/dist/linear/identity.js.map +0 -1
- package/dist/linear/notify.d.ts +0 -35
- package/dist/linear/notify.d.ts.map +0 -1
- package/dist/linear/notify.js +0 -76
- package/dist/linear/notify.js.map +0 -1
- package/dist/linear/ref.d.ts +0 -13
- package/dist/linear/ref.d.ts.map +0 -1
- package/dist/linear/ref.js +0 -90
- package/dist/linear/ref.js.map +0 -1
- package/dist/linear/verify.d.ts +0 -26
- package/dist/linear/verify.d.ts.map +0 -1
- package/dist/linear/verify.js +0 -67
- package/dist/linear/verify.js.map +0 -1
- package/dist/skills/attribution.d.ts +0 -4
- package/dist/skills/attribution.d.ts.map +0 -1
- package/dist/skills/attribution.js +0 -14
- package/dist/skills/attribution.js.map +0 -1
- package/dist/skills/broker-server.d.ts +0 -2
- package/dist/skills/broker-server.d.ts.map +0 -1
- package/dist/skills/broker-server.js +0 -17
- package/dist/skills/broker-server.js.map +0 -1
- package/dist/skills/broker.d.ts +0 -42
- package/dist/skills/broker.d.ts.map +0 -1
- package/dist/skills/broker.js +0 -285
- package/dist/skills/broker.js.map +0 -1
- package/dist/skills/catalog.d.ts +0 -28
- package/dist/skills/catalog.d.ts.map +0 -1
- package/dist/skills/catalog.js +0 -104
- package/dist/skills/catalog.js.map +0 -1
- package/dist/skills/installer.d.ts +0 -10
- package/dist/skills/installer.d.ts.map +0 -1
- package/dist/skills/installer.js +0 -138
- package/dist/skills/installer.js.map +0 -1
- package/dist/skills/integrity.d.ts +0 -4
- package/dist/skills/integrity.d.ts.map +0 -1
- package/dist/skills/integrity.js +0 -36
- package/dist/skills/integrity.js.map +0 -1
- package/docs/dynamic-thoroughness.md +0 -738
- package/docs/linear-identity-contract.md +0 -139
- package/docs/linear-identity.md +0 -293
package/dist/lib/runner.js
CHANGED
|
@@ -3,31 +3,21 @@ import { randomUUID } from 'crypto';
|
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { filterStepsByTypes } from './repo-workflow.js';
|
|
7
6
|
import { runCodexReview } from '../reviewers/codex.js';
|
|
8
7
|
import { runClaudeReview } from '../reviewers/claude.js';
|
|
9
8
|
import { runFixStep, runCodexFixStep } from '../reviewers/fix.js';
|
|
10
9
|
import { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
|
|
11
10
|
import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE } from '../lib/verdict.js';
|
|
12
11
|
import { createGithubClient, postReviewComment, getLastCrossCheckCommentId, getLastCrossCheckReviewComment } from '../github/client.js';
|
|
13
|
-
import { resolveLinearAuth, withWorker } from '../linear/identity.js';
|
|
14
|
-
import { notifyLinear } from '../linear/notify.js';
|
|
15
|
-
import { shouldPostToLinear } from '../linear/comment.js';
|
|
16
|
-
import { getLinearCredentials } from '../config/loader.js';
|
|
17
12
|
import { acquireRemoteLock, releaseRemoteLock } from '../github/review-status.js';
|
|
18
13
|
import { log as fileLog, logError, classifyError } from '../lib/logger.js';
|
|
19
14
|
import { buildCommitTrailers } from '../lib/annotation.js';
|
|
20
15
|
import { resolveClaudeModel, resolveCodexModel } from '../lib/review-models.js';
|
|
21
|
-
import { resolveReviewStrategy, escalate, clampToLevels } from './review-strategy.js';
|
|
22
|
-
import { CLAUDE_EFFORT_LEVELS, CODEX_EFFORT_LEVELS } from '../config/schema.js';
|
|
23
16
|
import { buildStepIdentityFields } from '../lib/event-fields.js';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
17
|
+
import { buildFixAppliedCommentBody, buildConflictResolvedCommentBody, buildRetriedReviewBanner } from '../lib/comment-bodies.js';
|
|
18
|
+
import { loadWorkflow, loadHarnessSection, evaluateWhen } from '../lib/workflow.js';
|
|
26
19
|
import { isSubscriptionLimitError, isVendorUnavailableError } from '../lib/smart-switch.js';
|
|
27
20
|
import { tierTimeoutMs } from '../reviewers/tier-timeouts.js';
|
|
28
|
-
import { loadSkillCatalog } from '../skills/catalog.js';
|
|
29
|
-
import { createSkillActivationSession } from '../skills/broker.js';
|
|
30
|
-
import { formatSkillAttribution } from '../skills/attribution.js';
|
|
31
21
|
const MAX_CROSSCHECK_COMMITS = 5;
|
|
32
22
|
const FIX_RETRY_DELAY_MS = 2 * 60 * 1000;
|
|
33
23
|
const REVIEW_RETRY_DELAY_MS = 2 * 60 * 1000;
|
|
@@ -59,27 +49,33 @@ function isTransientApiError(err) {
|
|
|
59
49
|
export function getEffectiveStepType(stepType, isRecheckRun) {
|
|
60
50
|
return stepType === 'review' && isRecheckRun ? 'recheck' : stepType;
|
|
61
51
|
}
|
|
62
|
-
|
|
52
|
+
// Counts crosscheck-authored commits unique to this PR (ahead of base) rather
|
|
53
|
+
// than the branch's full history. Long-lived integration branches like
|
|
54
|
+
// `staging` accumulate [crosscheck] commits from many merged PRs — counting
|
|
55
|
+
// those would trip the per-PR fix-loop guard immediately and skip fix/recheck.
|
|
56
|
+
//
|
|
57
|
+
// Fails closed: when `origin/<base>` isn't available (e.g. clone fetched the
|
|
58
|
+
// base ref with `base_branch_fetch_skipped`), fall back to the full-history
|
|
59
|
+
// count rather than returning 0. Over-counting can stop fix early; returning 0
|
|
60
|
+
// would silently disable the cap and let runaway fix loops keep pushing.
|
|
61
|
+
export function countCrosscheckCommitsForPR(tmpDir, baseRef) {
|
|
63
62
|
const runLog = (args) => execFileSync('git', ['log', '--oneline', ...args], { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
64
63
|
const count = (out) => out.split('\n').filter(l => l.includes('[crosscheck]')).length;
|
|
65
64
|
try {
|
|
66
|
-
return
|
|
65
|
+
return count(runLog([`origin/${baseRef}..HEAD`]));
|
|
67
66
|
}
|
|
68
67
|
catch {
|
|
69
68
|
// Scoped range unavailable — fall back to full history so the cap still
|
|
70
|
-
// applies.
|
|
71
|
-
// but that's preferable to bypassing the safety guard.
|
|
69
|
+
// applies. May over-count when the branch has prior merged crosscheck
|
|
70
|
+
// commits, but that's preferable to bypassing the safety guard.
|
|
72
71
|
try {
|
|
73
|
-
return
|
|
72
|
+
return count(runLog([]));
|
|
74
73
|
}
|
|
75
74
|
catch {
|
|
76
|
-
return
|
|
75
|
+
return 0;
|
|
77
76
|
}
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
|
-
export function countCrosscheckCommitsForPR(tmpDir, baseRef) {
|
|
81
|
-
return countCrosscheckCommitsForPRDetailed(tmpDir, baseRef).count;
|
|
82
|
-
}
|
|
83
79
|
export function buildWorkflowCompleteEvent(inputs) {
|
|
84
80
|
const stepValues = Object.values(inputs.results);
|
|
85
81
|
const lastVerdict = stepValues.reverse().find(r => r.verdict !== undefined)?.verdict ?? null;
|
|
@@ -242,173 +238,8 @@ function diffBucket(totalLines) {
|
|
|
242
238
|
return 'large';
|
|
243
239
|
return 'xlarge';
|
|
244
240
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
* working copy rather than the API — the runner has the repo on disk, so this
|
|
248
|
-
* costs one `git diff` instead of a round trip.
|
|
249
|
-
*
|
|
250
|
-
* Returns null when the diff can't be read. Callers then fall back to the
|
|
251
|
-
* configured tier, which is why `quality.tier` stays meaningful under smart mode.
|
|
252
|
-
*/
|
|
253
|
-
export function buildPRContext(ctx) {
|
|
254
|
-
const { tmpDir, pr } = ctx;
|
|
255
|
-
try {
|
|
256
|
-
// execFileSync, not execSync: a git ref may legally contain `;`, `$( )` and
|
|
257
|
-
// backticks, and this value drives routing rather than best-effort logging.
|
|
258
|
-
const raw = execFileSync('git', ['diff', '--numstat', `origin/${pr.base.ref}...HEAD`], { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
259
|
-
if (!raw)
|
|
260
|
-
return null;
|
|
261
|
-
const files = [];
|
|
262
|
-
let additions = 0;
|
|
263
|
-
let deletions = 0;
|
|
264
|
-
for (const line of raw.split('\n')) {
|
|
265
|
-
// numstat: <added>\t<deleted>\t<path>. Binary files report '-' for both.
|
|
266
|
-
const [add, del, ...rest] = line.split('\t');
|
|
267
|
-
const path = rest.join('\t').trim();
|
|
268
|
-
if (!path)
|
|
269
|
-
continue;
|
|
270
|
-
files.push(path);
|
|
271
|
-
additions += parseInt(add, 10) || 0;
|
|
272
|
-
deletions += parseInt(del, 10) || 0;
|
|
273
|
-
}
|
|
274
|
-
if (files.length === 0)
|
|
275
|
-
return null;
|
|
276
|
-
return {
|
|
277
|
-
files,
|
|
278
|
-
additions,
|
|
279
|
-
deletions,
|
|
280
|
-
labels: pr.labels?.map(l => l.name) ?? [],
|
|
281
|
-
title: pr.title,
|
|
282
|
-
baseRef: pr.base.ref,
|
|
283
|
-
...(pr.base.repo.default_branch !== undefined && { defaultBranch: pr.base.repo.default_branch }),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Folds a resolved strategy into the quality config the reviewers receive, so
|
|
292
|
-
* every downstream `quality.tier` read picks up the per-PR decision without
|
|
293
|
-
* threading a new parameter through each vendor signature.
|
|
294
|
-
*
|
|
295
|
-
* A null strategy — fixed mode, or an unreadable diff — returns the config
|
|
296
|
-
* untouched, which is why `quality.tier` remains the documented fallback.
|
|
297
|
-
*/
|
|
298
|
-
export function strategyQuality(quality, strategy) {
|
|
299
|
-
if (!strategy?.tier)
|
|
300
|
-
return quality;
|
|
301
|
-
return { ...quality, tier: strategy.tier };
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Applies the class's effort alongside its tier. Without this the strategy's
|
|
305
|
-
* effort was resolved and logged but never sent, so the run line named a level
|
|
306
|
-
* the CLI was never given.
|
|
307
|
-
*
|
|
308
|
-
* `accepted` is the vendor CLI's vocabulary, which is narrower than the model's:
|
|
309
|
-
* the strategy escalates to `xhigh` on round 3 and claude-opus-5 reasons at that
|
|
310
|
-
* level, but the claude CLI has no flag for it, so claudeEffort() mapped the
|
|
311
|
-
* unknown value to `medium` — a round 3 weaker than round 2. Clamp here, where
|
|
312
|
-
* the strategy meets the config, rather than at each CLI.
|
|
313
|
-
*/
|
|
314
|
-
export function strategyVendor(vendor, strategy, accepted) {
|
|
315
|
-
if (!strategy?.effort)
|
|
316
|
-
return vendor;
|
|
317
|
-
const effort = clampToLevels(strategy.effort, accepted);
|
|
318
|
-
if (effort === null)
|
|
319
|
-
return vendor;
|
|
320
|
-
return { ...vendor, effort };
|
|
321
|
-
}
|
|
322
|
-
// NOTE on the asymmetry with `model`: an explicit vendors.*.model is honored
|
|
323
|
-
// over the strategy, but vendors.*.effort is not. That is deliberate rather than
|
|
324
|
-
// an oversight — `effort` carries a schema default, so a parsed config cannot
|
|
325
|
-
// distinguish "the user chose medium" from "nobody set it", and treating the
|
|
326
|
-
// default as a user choice would disable effort escalation for everyone. The
|
|
327
|
-
// override is documented in crosscheck.config.example.yml next to the model
|
|
328
|
-
// note; set `quality.mode: fixed` to keep a hand-set effort on every call.
|
|
329
|
-
/**
|
|
330
|
-
* True when the strategy actually determined the model that ran.
|
|
331
|
-
*
|
|
332
|
-
* Judged from the resolved model rather than from config shape, because two
|
|
333
|
-
* different configs defeat the tier map:
|
|
334
|
-
* - an explicit `vendors.*.model` outranks it, and
|
|
335
|
-
* - codex under subscription auth with no `model`/`model_tiers` resolves every
|
|
336
|
-
* tier to the CLI's own `default`, so fast/balanced/thorough are the same run.
|
|
337
|
-
* In both cases the strategy's tier is not what happened, and citing it would
|
|
338
|
-
* assert a routing decision that never took place — the exact auditability
|
|
339
|
-
* property this feature exists to provide.
|
|
340
|
-
*/
|
|
341
|
-
export function strategyDeterminedModel(vendor, strategy, resolvedModel) {
|
|
342
|
-
if (strategy === null || vendor.model)
|
|
343
|
-
return false;
|
|
344
|
-
// 'default' means the vendor CLI chose, not us.
|
|
345
|
-
return resolvedModel !== 'default';
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Classifies the PR and resolves the strategy, or returns null under
|
|
349
|
-
* `quality.mode: fixed` so the single configured tier applies unchanged.
|
|
350
|
-
*/
|
|
351
|
-
export function resolveStrategyForPR(ctx) {
|
|
352
|
-
if (ctx.config.quality.mode !== 'smart')
|
|
353
|
-
return null;
|
|
354
|
-
const prContext = buildPRContext(ctx);
|
|
355
|
-
if (!prContext)
|
|
356
|
-
return null;
|
|
357
|
-
return resolveReviewStrategy(prContext);
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* The tier, effort, and vendor configs every step of one round runs under.
|
|
361
|
-
*
|
|
362
|
-
* One function rather than a fold at each use site: the review step ran the
|
|
363
|
-
* escalated strategy while the fix step re-folded the base class, so a promoted
|
|
364
|
-
* round reviewed with the stronger model and then fixed with the weaker one —
|
|
365
|
-
* and took the weaker tier's subprocess timeout with it.
|
|
366
|
-
*
|
|
367
|
-
* Rounds beyond the first escalate: the class tier was already tried and did not
|
|
368
|
-
* resolve the PR, so difficulty is now measured rather than predicted. escalate()
|
|
369
|
-
* raises effort where the model supports it and promotes a tier where it does
|
|
370
|
-
* not, and never weakens the model.
|
|
371
|
-
*/
|
|
372
|
-
export function resolveRoundExecution(config, strategy, round) {
|
|
373
|
-
if (!strategy) {
|
|
374
|
-
return {
|
|
375
|
-
strategy: null,
|
|
376
|
-
quality: config.quality,
|
|
377
|
-
claudeVendor: config.vendors.claude,
|
|
378
|
-
codexVendor: config.vendors.codex,
|
|
379
|
-
roundConfig: config,
|
|
380
|
-
escalated: false,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
// The vendors that may actually run this round, each with the model it would
|
|
384
|
-
// use and the vocabulary its CLI accepts. Keyed to the enabled vendors rather
|
|
385
|
-
// than to claude alone: on a codex-only install the claude tier model is never
|
|
386
|
-
// called, so judging escalation by its effort ladder promoted a tier every
|
|
387
|
-
// round while codex sat at the effort it started on.
|
|
388
|
-
const baseQuality = strategyQuality(config.quality, strategy);
|
|
389
|
-
const lanes = [];
|
|
390
|
-
if (config.vendors.claude.enabled) {
|
|
391
|
-
lanes.push({ model: resolveClaudeModel(baseQuality, config.vendors.claude), accepted: CLAUDE_EFFORT_LEVELS });
|
|
392
|
-
}
|
|
393
|
-
if (config.vendors.codex.enabled) {
|
|
394
|
-
lanes.push({ model: resolveCodexModel(baseQuality, config.vendors.codex), accepted: CODEX_EFFORT_LEVELS });
|
|
395
|
-
}
|
|
396
|
-
const escalated = escalate({ tier: strategy.tier ?? config.quality.tier, effort: strategy.effort }, round, lanes);
|
|
397
|
-
const roundStrategy = { ...strategy, tier: escalated.tier, effort: escalated.effort };
|
|
398
|
-
const quality = strategyQuality(config.quality, roundStrategy);
|
|
399
|
-
const claudeVendor = strategyVendor(config.vendors.claude, roundStrategy, CLAUDE_EFFORT_LEVELS);
|
|
400
|
-
const codexVendor = strategyVendor(config.vendors.codex, roundStrategy, CODEX_EFFORT_LEVELS);
|
|
401
|
-
return {
|
|
402
|
-
strategy: roundStrategy,
|
|
403
|
-
quality,
|
|
404
|
-
claudeVendor,
|
|
405
|
-
codexVendor,
|
|
406
|
-
roundConfig: { ...config, quality, vendors: { ...config.vendors, claude: claudeVendor, codex: codexVendor } },
|
|
407
|
-
escalated: escalated.tier !== strategy.tier || escalated.effort !== strategy.effort,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
function emitPRComplexity(ctx, triggerField, effectiveTierForRun) {
|
|
411
|
-
const { owner, repoName, prNumber, tmpDir, pr } = ctx;
|
|
241
|
+
function emitPRComplexity(ctx, triggerField) {
|
|
242
|
+
const { owner, repoName, prNumber, tmpDir, pr, config } = ctx;
|
|
412
243
|
try {
|
|
413
244
|
const raw = execSync(`git diff --stat origin/${pr.base.ref}...HEAD`, { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
414
245
|
if (!raw)
|
|
@@ -439,7 +270,7 @@ function emitPRComplexity(ctx, triggerField, effectiveTierForRun) {
|
|
|
439
270
|
diff_bucket: diffBucket(insertions + deletions),
|
|
440
271
|
file_mix: mix,
|
|
441
272
|
languages: [...langSet],
|
|
442
|
-
quality_tier:
|
|
273
|
+
quality_tier: config.quality.tier,
|
|
443
274
|
...triggerField,
|
|
444
275
|
});
|
|
445
276
|
}
|
|
@@ -508,68 +339,12 @@ async function pushWithNonFastForwardHandling(params) {
|
|
|
508
339
|
export async function runWorkflow(ctx) {
|
|
509
340
|
const { owner, repoName, prNumber, pr, tmpDir, token, config, origin, log, onPhaseChange, trigger } = ctx;
|
|
510
341
|
const triggerField = trigger !== undefined ? { trigger } : {};
|
|
511
|
-
const
|
|
342
|
+
const steps = (ctx.steps ?? loadWorkflow(process.cwd())).map(step => {
|
|
512
343
|
if (!step.harness || step.instructions)
|
|
513
344
|
return step;
|
|
514
345
|
const resolved = loadHarnessSection(step.harness, process.cwd());
|
|
515
346
|
return resolved ? { ...step, instructions: resolved } : step;
|
|
516
347
|
});
|
|
517
|
-
// Resolved once per runWorkflow call: the fix step pushes commits, so
|
|
518
|
-
// re-classifying per step could yield a different class and make the review
|
|
519
|
-
// and recheck comments cite different tiers for the same PR.
|
|
520
|
-
//
|
|
521
|
-
// Not once per PR: --crazy/--halfcrazy re-enter runWorkflow per round, and by
|
|
522
|
-
// then the diff includes crosscheck's own fix commits, so a later round can
|
|
523
|
-
// legitimately classify differently. Each comment cites the class that
|
|
524
|
-
// produced it, so the record stays accurate either way.
|
|
525
|
-
const strategy = resolveStrategyForPR(ctx);
|
|
526
|
-
if (config.quality.mode === 'smart' && !strategy) {
|
|
527
|
-
// A smart-mode install quietly behaving as fixed is otherwise invisible.
|
|
528
|
-
fileLog({ level: 'warn', event: 'strategy_unresolved', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'pr_context_unavailable', fallback_tier: config.quality.tier });
|
|
529
|
-
}
|
|
530
|
-
else if (strategy) {
|
|
531
|
-
// A config written before `mode` existed parses as smart on upgrade, so a
|
|
532
|
-
// hand-set `quality.tier` can be silently overridden. onboard preserves the
|
|
533
|
-
// old tier by reading raw yaml, but that only helps users who re-run it —
|
|
534
|
-
// so record it here for everyone else.
|
|
535
|
-
//
|
|
536
|
-
// info, not warn: `config.quality.tier` carries a schema default of
|
|
537
|
-
// `balanced` on every install, so the parsed config cannot tell a hand-set
|
|
538
|
-
// tier from an unset one. Five of the eight classes resolve to something
|
|
539
|
-
// other than balanced, which made this fire on the majority of PRs — and
|
|
540
|
-
// recommend a `mode: fixed` opt-out to users who never chose a tier at all.
|
|
541
|
-
// Only the raw yaml can draw that distinction (thoroughnessDefaults), and it
|
|
542
|
-
// is not available on this path.
|
|
543
|
-
if (strategy.tier && strategy.tier !== config.quality.tier) {
|
|
544
|
-
fileLog({ level: 'info', event: 'strategy_overrode_configured_tier', repo: `${owner}/${repoName}`, pr: prNumber, configured_tier: config.quality.tier, applied_tier: strategy.tier, pr_class: strategy.classId });
|
|
545
|
-
}
|
|
546
|
-
fileLog({ level: 'info', event: 'strategy_resolved', repo: `${owner}/${repoName}`, pr: prNumber, strategy_version: strategy.version, pr_class: strategy.classId, tier: strategy.tier, effort: strategy.effort, steps: strategy.steps, domain: strategy.domain });
|
|
547
|
-
}
|
|
548
|
-
// The class's step set NARROWS the configured pipeline; it never widens it.
|
|
549
|
-
// A repo set to review-only stays review-only whatever the class says, which
|
|
550
|
-
// matches how per-repo `crosscheck alter` overrides compose. Reuses
|
|
551
|
-
// filterStepsByTypes so the conflict-resolve rule (orthogonal to the depth
|
|
552
|
-
// ladder, kept only when the depth permits code modification) stays in one
|
|
553
|
-
// place rather than being re-derived here.
|
|
554
|
-
const steps = (() => {
|
|
555
|
-
if (!strategy || strategy.steps.length === 0)
|
|
556
|
-
return configuredSteps;
|
|
557
|
-
const classTypes = strategy.steps.filter((t) => t === 'review' || t === 'fix' || t === 'recheck');
|
|
558
|
-
if (classTypes.length === 0)
|
|
559
|
-
return configuredSteps;
|
|
560
|
-
const narrowed = filterStepsByTypes(configuredSteps, classTypes);
|
|
561
|
-
const dropped = configuredSteps.length - narrowed.length;
|
|
562
|
-
if (dropped > 0) {
|
|
563
|
-
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} → ${classTypes.join(', ')} (${dropped} step${dropped === 1 ? '' : 's'} dropped)`));
|
|
564
|
-
fileLog({ level: 'info', event: 'strategy_steps_narrowed', repo: `${owner}/${repoName}`, pr: prNumber, pr_class: strategy.classId, configured: configuredSteps.map((x) => x.type), applied: narrowed.map((x) => x.type), strategy_version: strategy.version });
|
|
565
|
-
}
|
|
566
|
-
return narrowed;
|
|
567
|
-
})();
|
|
568
|
-
if (strategy && strategy.tier === null) {
|
|
569
|
-
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} → skipped (${strategy.reason})`));
|
|
570
|
-
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'strategy_class_skip', pr_class: strategy.classId, strategy_version: strategy.version });
|
|
571
|
-
return { verdict: null, strategySkipped: strategy.classId };
|
|
572
|
-
}
|
|
573
348
|
const results = {};
|
|
574
349
|
// SHAs the workflow pushed AND set a `crosscheck/review` pending status on.
|
|
575
350
|
// Each one must be released in the finally below — otherwise the pending
|
|
@@ -581,47 +356,6 @@ export async function runWorkflow(ctx) {
|
|
|
581
356
|
// can iterate the same list and release these shas if SIGINT/SIGTERM fires
|
|
582
357
|
// mid-workflow (process.exit there bypasses our finally below).
|
|
583
358
|
const pushedShasNeedingRelease = ctx.pushedShas ?? [];
|
|
584
|
-
const skillCatalog = config.skills.enabled.length > 0 ? loadSkillCatalog() : [];
|
|
585
|
-
const skillSessions = new Map();
|
|
586
|
-
const skillSessionFor = (stepName, stepType) => {
|
|
587
|
-
if (skillCatalog.length === 0)
|
|
588
|
-
return undefined;
|
|
589
|
-
const existing = skillSessions.get(stepName);
|
|
590
|
-
if (existing)
|
|
591
|
-
return existing;
|
|
592
|
-
const session = createSkillActivationSession(stepType, config.skills.enabled, skillCatalog);
|
|
593
|
-
skillSessions.set(stepName, session);
|
|
594
|
-
return session;
|
|
595
|
-
};
|
|
596
|
-
// Skills were offered and the agent took none. Silent before, which is how a
|
|
597
|
-
// prompt that never triggered activation ran unnoticed for 336 steps.
|
|
598
|
-
// `skills_activated: []` on the *_complete events says something similar but
|
|
599
|
-
// quietly; what is new here is `enabled` (what was actually on offer) and the
|
|
600
|
-
// warn level, which makes a step that activates nothing greppable on its own.
|
|
601
|
-
const logSkillsNoneActivated = (session, identity) => {
|
|
602
|
-
// skillSessionFor hands back a session whenever the catalog is non-empty,
|
|
603
|
-
// but createSkillActivationSession filters that catalog down to
|
|
604
|
-
// config.skills.enabled — so a configured name that never resolved (typo,
|
|
605
|
-
// skill not installed) leaves enabledSkills empty, and then
|
|
606
|
-
// renderSkillBrokerInstructions renders nothing at all. Nothing was offered,
|
|
607
|
-
// so nothing was refused: that is broken config, not agent non-compliance,
|
|
608
|
-
// and logging it here would conflate the two causes the event exists to
|
|
609
|
-
// tell apart.
|
|
610
|
-
if (session.enabledSkills.length === 0)
|
|
611
|
-
return;
|
|
612
|
-
fileLog({ level: 'warn', event: 'skills_none_activated', repo: `${owner}/${repoName}`, pr: prNumber, ...identity, enabled: session.enabledSkills.map(skill => skill.name) });
|
|
613
|
-
};
|
|
614
|
-
// Linear write-back identity. Resolved up front — before any expensive step —
|
|
615
|
-
// and allowed to throw. The contract is that a configured-but-failing
|
|
616
|
-
// client_credentials mint ABORTS rather than degrading, because silently
|
|
617
|
-
// continuing would either drop the write or re-attribute it to a human. This
|
|
618
|
-
// matches commands/review.ts; the two paths must not disagree.
|
|
619
|
-
// The contract is one token per command run. runWorkflow is re-entered for every
|
|
620
|
-
// fix/recheck round under --crazy and max_rounds, so minting here would mint per
|
|
621
|
-
// round and let a late transient failure abort work already done. The caller
|
|
622
|
-
// resolves once and passes it in; resolving here is the single-round fallback.
|
|
623
|
-
// A dry run posts nothing, so it never mints.
|
|
624
|
-
let linearAuth = ctx.linearAuth ?? null;
|
|
625
359
|
let workflowFailed = false;
|
|
626
360
|
let workflowError = undefined;
|
|
627
361
|
let failedStep = undefined;
|
|
@@ -650,42 +384,8 @@ export async function runWorkflow(ctx) {
|
|
|
650
384
|
const workflowStart = Date.now();
|
|
651
385
|
const stepsRun = [];
|
|
652
386
|
let currentStepName;
|
|
653
|
-
|
|
654
|
-
// Every step of this round reads from here — review, fix, and recheck alike —
|
|
655
|
-
// so a promoted round cannot review with one model and fix with another.
|
|
656
|
-
//
|
|
657
|
-
// Above the try, and above emitPRComplexity, because both the complexity event
|
|
658
|
-
// and workflow_complete report the tier that ran: an escalated round reporting
|
|
659
|
-
// the base class tier is the same defect as a comment citing one.
|
|
660
|
-
const { strategy: roundStrategy, quality, claudeVendor, codexVendor, roundConfig, escalated } = resolveRoundExecution(config, strategy, ctx.round ?? 1);
|
|
661
|
-
emitPRComplexity(ctx, triggerField, quality.tier);
|
|
387
|
+
emitPRComplexity(ctx, triggerField);
|
|
662
388
|
try {
|
|
663
|
-
// Inside the try so a preflight failure still reaches the completion handler in
|
|
664
|
-
// the finally — resolving above it meant a failed mint skipped workflow_complete
|
|
665
|
-
// entirely and left no record of the run.
|
|
666
|
-
// canWriteVerdict here too: the caller passes null deliberately when the selected
|
|
667
|
-
// steps cannot write a verdict, and this fallback previously read that as
|
|
668
|
-
// "unresolved" and resolved anyway — defeating the gate one line upstream.
|
|
669
|
-
if (!ctx.dryRun && !linearAuth && linearWritePossible(config.linear, steps)) {
|
|
670
|
-
linearAuth = await resolveLinearAuth(config.linear, getLinearCredentials(config.linear.auth));
|
|
671
|
-
fileLog({ level: 'info', event: 'linear_auth_resolved', repo: `${owner}/${repoName}`, pr: prNumber, mode: linearAuth.mode, actor: linearAuth.actor });
|
|
672
|
-
}
|
|
673
|
-
// Logged once per run, not per step: nothing here depends on `step`, and
|
|
674
|
-
// recomputing inside the loop printed the same line for review and recheck.
|
|
675
|
-
if (strategy && roundStrategy) {
|
|
676
|
-
// Report the effort each vendor was actually GIVEN, not the level the round
|
|
677
|
-
// asked for. The two CLI vocabularies differ, so one round can send codex
|
|
678
|
-
// `xhigh` and claude `high`; printing the request names a level nobody ran.
|
|
679
|
-
const appliedEffort = [...new Set([
|
|
680
|
-
...(config.vendors.claude.enabled ? [claudeVendor.effort] : []),
|
|
681
|
-
...(config.vendors.codex.enabled ? [codexVendor.effort] : []),
|
|
682
|
-
])].join('/');
|
|
683
|
-
const escalatedNote = escalated ? ` · round ${ctx.round} escalated` : '';
|
|
684
|
-
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} → ${roundStrategy.tier ?? 'skip'} tier${appliedEffort ? ` (${appliedEffort})` : ''}${escalatedNote}`));
|
|
685
|
-
if (escalatedNote) {
|
|
686
|
-
fileLog({ level: 'info', event: 'strategy_escalated', repo: `${owner}/${repoName}`, pr: prNumber, round: ctx.round, from_tier: strategy.tier, to_tier: roundStrategy.tier, from_effort: strategy.effort, to_effort: roundStrategy.effort, applied_effort_claude: config.vendors.claude.enabled ? claudeVendor.effort : null, applied_effort_codex: config.vendors.codex.enabled ? codexVendor.effort : null, strategy_version: strategy.version });
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
389
|
for (const step of steps) {
|
|
690
390
|
currentStepName = step.name;
|
|
691
391
|
stepsRun.push(step.name);
|
|
@@ -748,21 +448,17 @@ export async function runWorkflow(ctx) {
|
|
|
748
448
|
let inputTokens;
|
|
749
449
|
let outputTokens;
|
|
750
450
|
let model = 'default';
|
|
751
|
-
let effort;
|
|
752
451
|
let retried;
|
|
753
|
-
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
754
|
-
// Under `quality.mode: smart` the PR's class picks the tier; under fixed
|
|
755
|
-
// this is config.quality untouched.
|
|
756
452
|
const runReviewWithVendor = async (candidate) => {
|
|
757
453
|
if (candidate === 'codex') {
|
|
758
454
|
;
|
|
759
|
-
({ review: rawReview, tokensUsed, model,
|
|
455
|
+
({ review: rawReview, tokensUsed, model, retried } = await runCodexReview(tmpDir, pr.base.ref, pr.title, config.quality, config.vendors.codex, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec), log, ctx.issueContext));
|
|
760
456
|
inputTokens = undefined;
|
|
761
457
|
outputTokens = undefined;
|
|
762
458
|
}
|
|
763
459
|
else {
|
|
764
460
|
;
|
|
765
|
-
({ review: rawReview, tokensUsed, inputTokens, outputTokens, model,
|
|
461
|
+
({ review: rawReview, tokensUsed, inputTokens, outputTokens, model, retried } = await runClaudeReview(tmpDir, pr.base.ref, pr.title, config.quality, config.vendors.claude, config.budget.per_review_usd, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), !!ctx.roundMode, log, ctx.issueContext));
|
|
766
462
|
}
|
|
767
463
|
};
|
|
768
464
|
try {
|
|
@@ -819,11 +515,6 @@ export async function runWorkflow(ctx) {
|
|
|
819
515
|
}
|
|
820
516
|
}
|
|
821
517
|
}
|
|
822
|
-
const activatedSkills = skillSession?.activations() ?? [];
|
|
823
|
-
if (activatedSkills.length > 0)
|
|
824
|
-
log(chalk.dim(` skills: ${formatSkillAttribution(activatedSkills)}`));
|
|
825
|
-
else if (skillSession)
|
|
826
|
-
logSkillsNoneActivated(skillSession, stepIdentity);
|
|
827
518
|
// First attempt timed out but the delayed retry succeeded — surface a
|
|
828
519
|
// soft notice on the review comment so the author knows it was a transient blip.
|
|
829
520
|
if (retried) {
|
|
@@ -845,17 +536,15 @@ export async function runWorkflow(ctx) {
|
|
|
845
536
|
const baseBody = verdict === null
|
|
846
537
|
? `${NULL_VERDICT_WARNING}\n\n${clean}`
|
|
847
538
|
: prependVerdictToComment(gate.downgraded ? `${SEVERITY_GATE_NOTE}\n\n${clean}` : clean, verdict);
|
|
848
|
-
// Skills are not folded into the body — postReviewComment renders the
|
|
849
|
-
// receipt beneath the attribution footer.
|
|
850
539
|
const commentBody = retried
|
|
851
540
|
? `${buildRetriedReviewBanner(retried.timeoutMs, retried.delayMs)}\n\n${baseBody}`
|
|
852
541
|
: baseBody;
|
|
853
542
|
const commentCount = countComments(rawReview);
|
|
854
|
-
fileLog({ level: 'info', event: 'review_complete', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, verdict, duration_ms: Date.now() - stepStart, tokens_used: tokensUsed,
|
|
543
|
+
fileLog({ level: 'info', event: 'review_complete', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, verdict, duration_ms: Date.now() - stepStart, tokens_used: tokensUsed, ...(inputTokens !== undefined && { input_tokens: inputTokens }), ...(outputTokens !== undefined && { output_tokens: outputTokens }), ...(ctx.round !== undefined && { round: ctx.round }), ...(ctx.roundMode && { mode: ctx.roundMode }), ...triggerField });
|
|
855
544
|
// Recheck verdict is stored separately to preserve the original review's commentCount on the board
|
|
856
545
|
const phaseUpdate = isRecheck
|
|
857
|
-
? { recheckVerdict: verdict, phase: donePhase, recheckTokens: tokensUsed, recheckReviewer: reviewer, qualityTier: quality.tier }
|
|
858
|
-
: { verdict, commentCount, phase: donePhase, crTokens: tokensUsed, crReviewer: reviewer, qualityTier: quality.tier };
|
|
546
|
+
? { recheckVerdict: verdict, phase: donePhase, recheckTokens: tokensUsed, recheckReviewer: reviewer, qualityTier: config.quality.tier }
|
|
547
|
+
: { verdict, commentCount, phase: donePhase, crTokens: tokensUsed, crReviewer: reviewer, qualityTier: config.quality.tier };
|
|
859
548
|
if (ctx.dryRun) {
|
|
860
549
|
onPhaseChange('dry-run — comment not posted', phaseUpdate);
|
|
861
550
|
log(chalk.dim(`\n--- dry-run: comment that would be posted ---\n${commentBody}\n--- end ---`));
|
|
@@ -890,47 +579,9 @@ export async function runWorkflow(ctx) {
|
|
|
890
579
|
annotationSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
|
|
891
580
|
}
|
|
892
581
|
catch { /* fall back to pr.head.sha if git is unavailable */ }
|
|
893
|
-
const commentId = await postReviewComment(octokit, owner, repoName, prNumber, commentBody, reviewer, config.brand, origin, verdict ?? undefined, priorReviewId, isRecheck, model, effectiveType, ctx.round ?? 1, annotationSha, nextStepAnnotation, ctx.trigger === 'kickass' ? 'kickass' : undefined
|
|
894
|
-
// Withheld when an explicit vendors.*.model overrode the tier map:
|
|
895
|
-
// citing a tier the run did not use would assert a routing decision
|
|
896
|
-
// that never happened.
|
|
897
|
-
strategyDeterminedModel(reviewer === 'codex' ? config.vendors.codex : config.vendors.claude, roundStrategy, model) && roundStrategy?.tier
|
|
898
|
-
? { version: roundStrategy.version, classId: roundStrategy.classId, tier: roundStrategy.tier, reason: roundStrategy.reason }
|
|
899
|
-
: undefined);
|
|
582
|
+
const commentId = await postReviewComment(octokit, owner, repoName, prNumber, commentBody, reviewer, config.brand, origin, verdict ?? undefined, priorReviewId, isRecheck, model, effectiveType, ctx.round ?? 1, annotationSha, nextStepAnnotation, ctx.trigger === 'kickass' ? 'kickass' : undefined);
|
|
900
583
|
const commentUrl = `github.com/${owner}/${repoName}/pull/${prNumber}`;
|
|
901
584
|
fileLog({ level: 'info', event: 'comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, url: `https://${commentUrl}` });
|
|
902
|
-
// Mirror the verdict onto the PR's Linear issue. `run` and `watch` both
|
|
903
|
-
// land here, so this is the path that matters — reviews posted from
|
|
904
|
-
// commands/review.ts are the exception, not the rule.
|
|
905
|
-
if (linearAuth && shouldPostToLinear(verdict ?? null, config.linear.comment_on)) {
|
|
906
|
-
{
|
|
907
|
-
// Attribute to crosscheck/review, /fix, /recheck rather than a flat actor.
|
|
908
|
-
const stepAuth = config.linear.identity.per_step_actor ? withWorker(linearAuth, effectiveType) : linearAuth;
|
|
909
|
-
const linearResult = await notifyLinear({
|
|
910
|
-
auth: stepAuth,
|
|
911
|
-
config: config.linear,
|
|
912
|
-
pr: { branch: pr.head.ref, title: pr.title, body: pr.body ?? '', url: `https://${commentUrl}`, sha: annotationSha },
|
|
913
|
-
verdict: verdict ?? null,
|
|
914
|
-
reviewer,
|
|
915
|
-
origin,
|
|
916
|
-
model,
|
|
917
|
-
stepType: effectiveType,
|
|
918
|
-
round: ctx.round ?? 1,
|
|
919
|
-
service: config.brand.service_name,
|
|
920
|
-
});
|
|
921
|
-
fileLog({
|
|
922
|
-
level: linearResult.status === 'failed' ? 'warn' : 'info',
|
|
923
|
-
event: 'linear_comment', repo: `${owner}/${repoName}`, pr: prNumber,
|
|
924
|
-
status: linearResult.status, reason: linearResult.reason, issue: linearResult.identifier,
|
|
925
|
-
});
|
|
926
|
-
if (linearResult.status === 'posted') {
|
|
927
|
-
log(chalk.dim(` linear: commented on ${linearResult.identifier}`));
|
|
928
|
-
}
|
|
929
|
-
else if (linearResult.status === 'failed') {
|
|
930
|
-
log(chalk.yellow(` linear: write failed — ${linearResult.reason}`));
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
585
|
results[step.name] = { verdict, commentBody, commentUrl, commentId, tokens_used: tokensUsed, input_tokens: inputTokens, output_tokens: outputTokens, vendor: reviewer, model };
|
|
935
586
|
}
|
|
936
587
|
}
|
|
@@ -990,28 +641,17 @@ export async function runWorkflow(ctx) {
|
|
|
990
641
|
skipFix('no_vendor');
|
|
991
642
|
continue;
|
|
992
643
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
// models handle well, but a cheap fixer that introduces a regression costs
|
|
996
|
-
// a whole extra round. Recheck does not step down either — it decides
|
|
997
|
-
// whether to spend another round, and a weak judge there is how loops run
|
|
998
|
-
// away.
|
|
999
|
-
const claudeFixModel = resolveClaudeModel(quality, claudeVendor);
|
|
1000
|
-
const codexFixModel = resolveCodexModel(quality, codexVendor);
|
|
644
|
+
const claudeFixModel = resolveClaudeModel(config.quality, config.vendors.claude);
|
|
645
|
+
const codexFixModel = resolveCodexModel(config.quality, config.vendors.codex);
|
|
1001
646
|
// Guard: don't push more than MAX_CROSSCHECK_COMMITS per PR.
|
|
1002
647
|
// Scope to commits ahead of base so long-lived branches (e.g. staging)
|
|
1003
648
|
// don't count [crosscheck] commits from previously merged PRs.
|
|
1004
649
|
// Crazy/halfcrazy mode doubles the cap since it deliberately loops.
|
|
1005
|
-
const
|
|
650
|
+
const existingCount = countCrosscheckCommitsForPR(tmpDir, pr.base.ref);
|
|
1006
651
|
const effectiveCommitLimit = ctx.roundMode ? MAX_CROSSCHECK_COMMITS * 2 : MAX_CROSSCHECK_COMMITS;
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// than a real cap hit — say so, or the next person debugs the wrong thing.
|
|
1011
|
-
log(commitCount.scoped
|
|
1012
|
-
? chalk.yellow(`⚠ PR #${prNumber}: ${commitCount.count}/${effectiveCommitLimit} [crosscheck] commits already — stopping auto-fix`)
|
|
1013
|
-
: chalk.yellow(`⚠ PR #${prNumber}: cannot scope [crosscheck] commit count (origin/${pr.base.ref} missing; ${commitCount.count} across all history) — stopping auto-fix`));
|
|
1014
|
-
skipFix(commitCount.scoped ? 'commit_limit_reached' : 'commit_count_unscoped');
|
|
652
|
+
if (existingCount >= effectiveCommitLimit) {
|
|
653
|
+
log(chalk.yellow(`⚠ PR #${prNumber}: ${effectiveCommitLimit} [crosscheck] commits already — stopping auto-fix`));
|
|
654
|
+
skipFix('commit_limit_reached');
|
|
1015
655
|
continue;
|
|
1016
656
|
}
|
|
1017
657
|
onPhaseChange(`${vendor} fixing...`, { phase: 'fixing' });
|
|
@@ -1019,24 +659,18 @@ export async function runWorkflow(ctx) {
|
|
|
1019
659
|
let appliedCount = 0;
|
|
1020
660
|
let fixChangedFiles = [];
|
|
1021
661
|
let fixTokensUsed;
|
|
1022
|
-
let fixEffort;
|
|
1023
662
|
let fixErr = undefined;
|
|
1024
663
|
let activeVendor = vendor;
|
|
1025
|
-
|
|
1026
|
-
// model here, and the balanced 600s budget would cut it off.
|
|
1027
|
-
const tierMs = tierTimeoutMs(quality.tier);
|
|
1028
|
-
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
664
|
+
const tierMs = tierTimeoutMs(config.quality.tier);
|
|
1029
665
|
const runFix = async (v) => {
|
|
1030
666
|
if (v === 'codex') {
|
|
1031
|
-
return runCodexFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', codexFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec) ?? tierMs
|
|
667
|
+
return runCodexFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', codexFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec) ?? tierMs);
|
|
1032
668
|
}
|
|
1033
|
-
|
|
1034
|
-
// quality.tier out of it, and both must be this round's values.
|
|
1035
|
-
return runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', roundConfig, claudeFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec) ?? tierMs, skillSession);
|
|
669
|
+
return runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', config, claudeFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec) ?? tierMs);
|
|
1036
670
|
};
|
|
1037
671
|
try {
|
|
1038
672
|
;
|
|
1039
|
-
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed
|
|
673
|
+
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed } = await runFix(vendor));
|
|
1040
674
|
}
|
|
1041
675
|
catch (err) {
|
|
1042
676
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 1, vendor }, err);
|
|
@@ -1050,7 +684,7 @@ export async function runWorkflow(ctx) {
|
|
|
1050
684
|
fileLog({ level: 'warn', event: 'fix_vendor_fallback', repo: `${owner}/${repoName}`, pr: prNumber, from: vendor, to: fallbackVendor, ...(isSubscriptionLimitError(err) && { reason: 'vendor_limit' }) });
|
|
1051
685
|
try {
|
|
1052
686
|
;
|
|
1053
|
-
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed
|
|
687
|
+
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed } = await runFix(fallbackVendor));
|
|
1054
688
|
activeVendor = fallbackVendor;
|
|
1055
689
|
}
|
|
1056
690
|
catch (fallbackErr) {
|
|
@@ -1071,7 +705,7 @@ export async function runWorkflow(ctx) {
|
|
|
1071
705
|
onPhaseChange(`${activeVendor} fixing (retry)...`, { phase: 'fixing' });
|
|
1072
706
|
try {
|
|
1073
707
|
;
|
|
1074
|
-
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed
|
|
708
|
+
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed } = await runFix(activeVendor));
|
|
1075
709
|
fileLog({ level: 'info', event: 'fix_retry_succeeded', repo: `${owner}/${repoName}`, pr: prNumber });
|
|
1076
710
|
fixErr = undefined;
|
|
1077
711
|
}
|
|
@@ -1080,20 +714,6 @@ export async function runWorkflow(ctx) {
|
|
|
1080
714
|
fixErr = retryErr;
|
|
1081
715
|
}
|
|
1082
716
|
}
|
|
1083
|
-
const activatedSkills = skillSession?.activations() ?? [];
|
|
1084
|
-
if (activatedSkills.length > 0)
|
|
1085
|
-
log(chalk.dim(` skills: ${formatSkillAttribution(activatedSkills)}`));
|
|
1086
|
-
else if (skillSession)
|
|
1087
|
-
logSkillsNoneActivated(skillSession, { step_type: 'fix', step_name: step.name });
|
|
1088
|
-
// Every delivery mode (commit card, fix PR, suggested-diff comment) closes
|
|
1089
|
-
// with the same footer as a review comment.
|
|
1090
|
-
const fixAttributionFooter = () => buildAttributionFooter({
|
|
1091
|
-
action: 'Fixed',
|
|
1092
|
-
vendor: activeVendor,
|
|
1093
|
-
model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
|
|
1094
|
-
effort: fixEffort,
|
|
1095
|
-
skills: activatedSkills,
|
|
1096
|
-
});
|
|
1097
717
|
if (fixErr !== undefined) {
|
|
1098
718
|
skipFix(isSubscriptionLimitError(fixErr) ? 'vendor_limit' : 'fix_error');
|
|
1099
719
|
// Only notify for transient failures — auth errors are operator issues, not PR author issues
|
|
@@ -1102,13 +722,7 @@ export async function runWorkflow(ctx) {
|
|
|
1102
722
|
const octokit = createGithubClient(token);
|
|
1103
723
|
await octokit.rest.issues.createComment({
|
|
1104
724
|
owner, repo: repoName, issue_number: prNumber,
|
|
1105
|
-
body:
|
|
1106
|
-
prUrl: pr.html_url,
|
|
1107
|
-
vendor: activeVendor,
|
|
1108
|
-
model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
|
|
1109
|
-
effort: fixEffort,
|
|
1110
|
-
skills: activatedSkills,
|
|
1111
|
-
}),
|
|
725
|
+
body: `⚠️ **Auto-fix failed**\n\nThe fix step timed out after retrying. Push a new commit or run \`crosscheck run ${pr.html_url}\` to retry manually.\n\n<!-- crosscheck: fix_failed -->`,
|
|
1112
726
|
});
|
|
1113
727
|
fileLog({ level: 'info', event: 'fix_failed_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber });
|
|
1114
728
|
}
|
|
@@ -1127,7 +741,7 @@ export async function runWorkflow(ctx) {
|
|
|
1127
741
|
// event — NOT `fix_complete` — so status consumers (WORKFLOW_ACTIVITY_EVENTS,
|
|
1128
742
|
// the legacy status fold) don't misread a no-op as real workflow progress and
|
|
1129
743
|
// mark the PR NEEDS_RECHECK or hide it from `scan --tidy` with no fix applied.
|
|
1130
|
-
fileLog({ level: 'info', event: 'fix_noop', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: 0, no_changes: true, tokens_used: fixTokensUsed,
|
|
744
|
+
fileLog({ level: 'info', event: 'fix_noop', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: 0, no_changes: true, tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
1131
745
|
results[step.name] = { applied_count: 0, ...(fixTokensUsed !== undefined && { tokens_used: fixTokensUsed }), vendor };
|
|
1132
746
|
continue;
|
|
1133
747
|
}
|
|
@@ -1179,7 +793,7 @@ export async function runWorkflow(ctx) {
|
|
|
1179
793
|
}
|
|
1180
794
|
}
|
|
1181
795
|
onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
|
|
1182
|
-
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'commit', tokens_used: fixTokensUsed,
|
|
796
|
+
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'commit', tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
1183
797
|
// Post a summary comment so the silent commit push is visible on the timeline
|
|
1184
798
|
// as a comment card. Best-effort — a failure here must not fail the run.
|
|
1185
799
|
try {
|
|
@@ -1190,9 +804,6 @@ export async function runWorkflow(ctx) {
|
|
|
1190
804
|
changedFiles: fixChangedFiles,
|
|
1191
805
|
vendor: activeVendor,
|
|
1192
806
|
reviewCommentBody,
|
|
1193
|
-
model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
|
|
1194
|
-
effort: fixEffort,
|
|
1195
|
-
skills: activatedSkills,
|
|
1196
807
|
});
|
|
1197
808
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
1198
809
|
fileLog({ level: 'info', event: 'fix_applied_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
|
|
@@ -1229,13 +840,7 @@ export async function runWorkflow(ctx) {
|
|
|
1229
840
|
head: fixBranch,
|
|
1230
841
|
base: pr.head.ref,
|
|
1231
842
|
title: fixPrTitle,
|
|
1232
|
-
body:
|
|
1233
|
-
`Auto-fix by crosscheck for CR issues found in #${prNumber}.`,
|
|
1234
|
-
'',
|
|
1235
|
-
`Review: https://github.com/${owner}/${repoName}/pull/${prNumber}`,
|
|
1236
|
-
'',
|
|
1237
|
-
fixAttributionFooter(),
|
|
1238
|
-
].join('\n'),
|
|
843
|
+
body: `Auto-fix by crosscheck for CR issues found in #${prNumber}.\n\nReview: https://github.com/${owner}/${repoName}/pull/${prNumber}`,
|
|
1239
844
|
});
|
|
1240
845
|
if (config.post_review.auto_fix.delivery.label) {
|
|
1241
846
|
try {
|
|
@@ -1246,7 +851,7 @@ export async function runWorkflow(ctx) {
|
|
|
1246
851
|
catch { /* label may not exist in this repo — skip */ }
|
|
1247
852
|
}
|
|
1248
853
|
onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
|
|
1249
|
-
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'pull_request', fix_pr: fixPr.number, tokens_used: fixTokensUsed,
|
|
854
|
+
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'pull_request', fix_pr: fixPr.number, tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
1250
855
|
results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
|
|
1251
856
|
}
|
|
1252
857
|
else {
|
|
@@ -1258,11 +863,11 @@ export async function runWorkflow(ctx) {
|
|
|
1258
863
|
catch { /* ignore */ }
|
|
1259
864
|
if (patch) {
|
|
1260
865
|
const octokit = createGithubClient(token);
|
|
1261
|
-
const body = `### Suggested fixes (crosscheck auto-fix)\n\n\`\`\`diff\n${patch.slice(0, 16000)}\n
|
|
866
|
+
const body = `### Suggested fixes (crosscheck auto-fix)\n\n\`\`\`diff\n${patch.slice(0, 16000)}\n\`\`\``;
|
|
1262
867
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
1263
868
|
}
|
|
1264
869
|
onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
|
|
1265
|
-
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, delivery: 'comment', tokens_used: fixTokensUsed,
|
|
870
|
+
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, delivery: 'comment', tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
1266
871
|
results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
|
|
1267
872
|
}
|
|
1268
873
|
}
|
|
@@ -1335,9 +940,7 @@ export async function runWorkflow(ctx) {
|
|
|
1335
940
|
skipConflictResolve('codex_conflict_resolve_unsupported');
|
|
1336
941
|
continue;
|
|
1337
942
|
}
|
|
1338
|
-
|
|
1339
|
-
// measured at 37s against ~643s for a review — so it always runs fast.
|
|
1340
|
-
const conflictResolveModel = resolveClaudeModel({ ...config.quality, tier: config.quality.mode === 'smart' ? 'fast' : config.quality.tier }, config.vendors.claude);
|
|
943
|
+
const conflictResolveModel = resolveClaudeModel(config.quality, config.vendors.claude);
|
|
1341
944
|
const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
|
|
1342
945
|
if (isFork) {
|
|
1343
946
|
try {
|
|
@@ -1348,15 +951,13 @@ export async function runWorkflow(ctx) {
|
|
|
1348
951
|
skipConflictResolve('fork_pr');
|
|
1349
952
|
continue;
|
|
1350
953
|
}
|
|
1351
|
-
const
|
|
1352
|
-
if (
|
|
954
|
+
const existingCount = countCrosscheckCommitsForPR(tmpDir, pr.base.ref);
|
|
955
|
+
if (existingCount >= MAX_CROSSCHECK_COMMITS) {
|
|
1353
956
|
try {
|
|
1354
957
|
execSync('git merge --abort', { cwd: tmpDir });
|
|
1355
958
|
}
|
|
1356
959
|
catch { /* ignore */ }
|
|
1357
|
-
log(
|
|
1358
|
-
? chalk.yellow(`⚠ PR #${prNumber}: ${crCommitCount.count}/${MAX_CROSSCHECK_COMMITS} [crosscheck] commits already — stopping conflict-resolve`)
|
|
1359
|
-
: chalk.yellow(`⚠ PR #${prNumber}: cannot scope [crosscheck] commit count (origin/${pr.base.ref} missing) — stopping conflict-resolve`));
|
|
960
|
+
log(chalk.yellow(`⚠ PR #${prNumber}: ${MAX_CROSSCHECK_COMMITS} [crosscheck] commits already — stopping conflict-resolve`));
|
|
1360
961
|
skipConflictResolve('commit_limit_reached');
|
|
1361
962
|
continue;
|
|
1362
963
|
}
|
|
@@ -1366,11 +967,9 @@ export async function runWorkflow(ctx) {
|
|
|
1366
967
|
let appliedCount = 0;
|
|
1367
968
|
let resolvedPaths = [];
|
|
1368
969
|
let resolveTokensUsed;
|
|
1369
|
-
let resolveEffort;
|
|
1370
|
-
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
1371
970
|
try {
|
|
1372
971
|
;
|
|
1373
|
-
({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed
|
|
972
|
+
({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed } = await runConflictResolveStep(tmpDir, pr.title, step.instructions ?? '', conflictResolveModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec)));
|
|
1374
973
|
}
|
|
1375
974
|
catch (err) {
|
|
1376
975
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'conflict-resolve', attempt: 1 }, err);
|
|
@@ -1381,11 +980,6 @@ export async function runWorkflow(ctx) {
|
|
|
1381
980
|
skipConflictResolve(isSubscriptionLimitError(err) ? 'vendor_limit' : 'resolve_error');
|
|
1382
981
|
continue;
|
|
1383
982
|
}
|
|
1384
|
-
const activatedSkills = skillSession?.activations() ?? [];
|
|
1385
|
-
if (activatedSkills.length > 0)
|
|
1386
|
-
log(chalk.dim(` skills: ${formatSkillAttribution(activatedSkills)}`));
|
|
1387
|
-
else if (skillSession)
|
|
1388
|
-
logSkillsNoneActivated(skillSession, { step_type: 'conflict-resolve', step_name: step.name });
|
|
1389
983
|
if (appliedCount === 0) {
|
|
1390
984
|
try {
|
|
1391
985
|
execSync('git merge --abort', { cwd: tmpDir });
|
|
@@ -1488,7 +1082,7 @@ export async function runWorkflow(ctx) {
|
|
|
1488
1082
|
fileLog({ level: 'warn', event: 'remote_lock_refresh_failed', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha, error: err instanceof Error ? err.message : String(err) });
|
|
1489
1083
|
}
|
|
1490
1084
|
onPhaseChange('conflicts resolved ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: resolveTokensUsed });
|
|
1491
|
-
fileLog({ level: 'info', event: 'conflict_resolve_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, conflicts_resolved: conflictedFiles.length, sha: newSha, tokens_used: resolveTokensUsed,
|
|
1085
|
+
fileLog({ level: 'info', event: 'conflict_resolve_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, conflicts_resolved: conflictedFiles.length, sha: newSha, tokens_used: resolveTokensUsed, duration_ms: Date.now() - conflictResolveStepStart, ...triggerField });
|
|
1492
1086
|
// Post a summary comment so the silent merge-commit push is visible on the
|
|
1493
1087
|
// timeline as a comment card. Best-effort — a failure here must not fail the run.
|
|
1494
1088
|
// Prefer the resolver's actual rewrite set; fall back to the originally-conflicted
|
|
@@ -1499,9 +1093,6 @@ export async function runWorkflow(ctx) {
|
|
|
1499
1093
|
owner, repo: repoName, sha: newSha,
|
|
1500
1094
|
conflictCount: conflictedFiles.length,
|
|
1501
1095
|
files: resolvedPaths.length > 0 ? resolvedPaths : conflictedFiles,
|
|
1502
|
-
model: conflictResolveModel,
|
|
1503
|
-
effort: resolveEffort,
|
|
1504
|
-
skills: activatedSkills,
|
|
1505
1096
|
});
|
|
1506
1097
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
1507
1098
|
fileLog({ level: 'info', event: 'conflict_resolved_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
|
|
@@ -1538,9 +1129,6 @@ export async function runWorkflow(ctx) {
|
|
|
1538
1129
|
throw err;
|
|
1539
1130
|
}
|
|
1540
1131
|
finally {
|
|
1541
|
-
for (const session of skillSessions.values())
|
|
1542
|
-
session.close();
|
|
1543
|
-
skillSessions.clear();
|
|
1544
1132
|
if (pushedShasNeedingRelease.length > 0 || fixPushedShaRequiresRecheck !== null) {
|
|
1545
1133
|
const lockOctokit = createGithubClient(token);
|
|
1546
1134
|
const outcome = workflowFailed ? 'failure' : 'success';
|
|
@@ -1592,11 +1180,7 @@ export async function runWorkflow(ctx) {
|
|
|
1592
1180
|
failedStep,
|
|
1593
1181
|
round: ctx.round,
|
|
1594
1182
|
trigger: ctx.trigger,
|
|
1595
|
-
|
|
1596
|
-
// class tier — under smart mode all three differ, and telemetry naming the
|
|
1597
|
-
// wrong one is the same class of problem as a comment citing a tier that
|
|
1598
|
-
// never reached the vendor.
|
|
1599
|
-
qualityTier: quality.tier,
|
|
1183
|
+
qualityTier: config.quality.tier,
|
|
1600
1184
|
}));
|
|
1601
1185
|
}
|
|
1602
1186
|
}
|