@humanbased/crosscheck 1.2.0 → 1.3.0-beta.81
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 +182 -375
- package/README.zh.md +1 -1
- package/assets/icon-256.png +0 -0
- package/assets/linear-comment.svg +18 -0
- package/assets/linear-onboard.svg +30 -0
- package/assets/linear-status.svg +23 -0
- package/assets/linear-test.svg +34 -0
- package/assets/skills/code-review/.crosscheck-skill.json +9 -0
- package/assets/skills/code-review/LICENSE +21 -0
- package/assets/skills/code-review/SKILL.md +89 -0
- package/assets/skills/code-review/agents/openai.yaml +3 -0
- package/assets/skills/code-review-skill/.crosscheck-skill.json +9 -0
- package/assets/skills/code-review-skill/LICENSE +21 -0
- package/assets/skills/code-review-skill/SKILL.md +231 -0
- package/assets/skills/code-review-skill/assets/pr-review-template.md +137 -0
- package/assets/skills/code-review-skill/assets/review-checklist.md +123 -0
- package/assets/skills/code-review-skill/reference/angular.md +768 -0
- package/assets/skills/code-review-skill/reference/architecture-review-guide.md +472 -0
- package/assets/skills/code-review-skill/reference/c.md +890 -0
- package/assets/skills/code-review-skill/reference/code-quality-universal.md +488 -0
- package/assets/skills/code-review-skill/reference/code-review-best-practices.md +136 -0
- package/assets/skills/code-review-skill/reference/common-bugs-checklist.md +286 -0
- package/assets/skills/code-review-skill/reference/cpp.md +893 -0
- package/assets/skills/code-review-skill/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/assets/skills/code-review-skill/reference/cross-cutting/error-handling-principles.md +492 -0
- package/assets/skills/code-review-skill/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/assets/skills/code-review-skill/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/assets/skills/code-review-skill/reference/cross-cutting/xss-prevention.md +264 -0
- package/assets/skills/code-review-skill/reference/csharp.md +525 -0
- package/assets/skills/code-review-skill/reference/css-less-sass.md +661 -0
- package/assets/skills/code-review-skill/reference/django.md +985 -0
- package/assets/skills/code-review-skill/reference/fastapi.md +580 -0
- package/assets/skills/code-review-skill/reference/go.md +993 -0
- package/assets/skills/code-review-skill/reference/java.md +409 -0
- package/assets/skills/code-review-skill/reference/java8.md +586 -0
- package/assets/skills/code-review-skill/reference/kotlin.md +1018 -0
- package/assets/skills/code-review-skill/reference/nestjs.md +593 -0
- package/assets/skills/code-review-skill/reference/performance-review-guide.md +816 -0
- package/assets/skills/code-review-skill/reference/php.md +684 -0
- package/assets/skills/code-review-skill/reference/python.md +1073 -0
- package/assets/skills/code-review-skill/reference/qt.md +757 -0
- package/assets/skills/code-review-skill/reference/react.md +871 -0
- package/assets/skills/code-review-skill/reference/ruby.md +964 -0
- package/assets/skills/code-review-skill/reference/rust.md +846 -0
- package/assets/skills/code-review-skill/reference/security-review-guide.md +494 -0
- package/assets/skills/code-review-skill/reference/svelte.md +1064 -0
- package/assets/skills/code-review-skill/reference/swift.md +936 -0
- package/assets/skills/code-review-skill/reference/typescript.md +1016 -0
- package/assets/skills/code-review-skill/reference/vue.md +924 -0
- package/assets/skills/code-review-skill/reference/zig.md +440 -0
- package/assets/skills/code-review-skill/scripts/pr-analyzer.py +435 -0
- package/assets/skills/code-review-skill/scripts/test_pr_analyzer.py +380 -0
- package/assets/skills/codebase-design/.crosscheck-skill.json +9 -0
- package/assets/skills/codebase-design/DEEPENING.md +37 -0
- package/assets/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/assets/skills/codebase-design/LICENSE +21 -0
- package/assets/skills/codebase-design/SKILL.md +114 -0
- package/assets/skills/codebase-design/agents/openai.yaml +3 -0
- package/assets/skills/diagnosing-bugs/.crosscheck-skill.json +9 -0
- package/assets/skills/diagnosing-bugs/LICENSE +21 -0
- package/assets/skills/diagnosing-bugs/SKILL.md +134 -0
- package/assets/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/assets/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/crosscheck.config.example.yml +101 -9
- package/dist/__tests__/board.test.js +11 -0
- package/dist/__tests__/board.test.js.map +1 -1
- package/dist/__tests__/can-write-verdict.test.d.ts +2 -0
- package/dist/__tests__/can-write-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/can-write-verdict.test.js +31 -0
- package/dist/__tests__/can-write-verdict.test.js.map +1 -0
- package/dist/__tests__/codex.test.js +14 -27
- package/dist/__tests__/codex.test.js.map +1 -1
- package/dist/__tests__/comment-bodies.test.js +49 -1
- package/dist/__tests__/comment-bodies.test.js.map +1 -1
- package/dist/__tests__/conflict-resolve.test.js +44 -1
- package/dist/__tests__/conflict-resolve.test.js.map +1 -1
- package/dist/__tests__/fix.test.js +33 -0
- package/dist/__tests__/fix.test.js.map +1 -1
- package/dist/__tests__/linear-branding.test.d.ts +2 -0
- package/dist/__tests__/linear-branding.test.d.ts.map +1 -0
- package/dist/__tests__/linear-branding.test.js +156 -0
- package/dist/__tests__/linear-branding.test.js.map +1 -0
- package/dist/__tests__/linear-client.test.d.ts +2 -0
- package/dist/__tests__/linear-client.test.d.ts.map +1 -0
- package/dist/__tests__/linear-client.test.js +120 -0
- package/dist/__tests__/linear-client.test.js.map +1 -0
- package/dist/__tests__/linear-comment.test.d.ts +2 -0
- package/dist/__tests__/linear-comment.test.d.ts.map +1 -0
- package/dist/__tests__/linear-comment.test.js +151 -0
- package/dist/__tests__/linear-comment.test.js.map +1 -0
- package/dist/__tests__/linear-identity.test.d.ts +2 -0
- package/dist/__tests__/linear-identity.test.d.ts.map +1 -0
- package/dist/__tests__/linear-identity.test.js +253 -0
- package/dist/__tests__/linear-identity.test.js.map +1 -0
- package/dist/__tests__/linear-notify.test.d.ts +2 -0
- package/dist/__tests__/linear-notify.test.d.ts.map +1 -0
- package/dist/__tests__/linear-notify.test.js +144 -0
- package/dist/__tests__/linear-notify.test.js.map +1 -0
- package/dist/__tests__/linear-ref.test.d.ts +2 -0
- package/dist/__tests__/linear-ref.test.d.ts.map +1 -0
- package/dist/__tests__/linear-ref.test.js +261 -0
- package/dist/__tests__/linear-ref.test.js.map +1 -0
- package/dist/__tests__/linear-test-ref.test.d.ts +2 -0
- package/dist/__tests__/linear-test-ref.test.d.ts.map +1 -0
- package/dist/__tests__/linear-test-ref.test.js +81 -0
- package/dist/__tests__/linear-test-ref.test.js.map +1 -0
- package/dist/__tests__/linear-verify.test.d.ts +2 -0
- package/dist/__tests__/linear-verify.test.d.ts.map +1 -0
- package/dist/__tests__/linear-verify.test.js +132 -0
- package/dist/__tests__/linear-verify.test.js.map +1 -0
- package/dist/__tests__/linear-worker.test.d.ts +2 -0
- package/dist/__tests__/linear-worker.test.d.ts.map +1 -0
- package/dist/__tests__/linear-worker.test.js +83 -0
- package/dist/__tests__/linear-worker.test.js.map +1 -0
- package/dist/__tests__/linear-write-possible.test.d.ts +2 -0
- package/dist/__tests__/linear-write-possible.test.d.ts.map +1 -0
- package/dist/__tests__/linear-write-possible.test.js +30 -0
- package/dist/__tests__/linear-write-possible.test.js.map +1 -0
- package/dist/__tests__/onboard-preservation.test.js +59 -3
- package/dist/__tests__/onboard-preservation.test.js.map +1 -1
- package/dist/__tests__/optimize.test.js +2 -0
- package/dist/__tests__/optimize.test.js.map +1 -1
- package/dist/__tests__/repo-picker.test.js +7 -1
- package/dist/__tests__/repo-picker.test.js.map +1 -1
- package/dist/__tests__/repository-guidance.test.d.ts +2 -0
- package/dist/__tests__/repository-guidance.test.d.ts.map +1 -0
- package/dist/__tests__/repository-guidance.test.js +107 -0
- package/dist/__tests__/repository-guidance.test.js.map +1 -0
- package/dist/__tests__/review-comment-body.test.js +35 -0
- package/dist/__tests__/review-comment-body.test.js.map +1 -1
- package/dist/__tests__/review-models.test.js +19 -3
- package/dist/__tests__/review-models.test.js.map +1 -1
- package/dist/__tests__/review-strategy.test.d.ts +2 -0
- package/dist/__tests__/review-strategy.test.d.ts.map +1 -0
- package/dist/__tests__/review-strategy.test.js +397 -0
- package/dist/__tests__/review-strategy.test.js.map +1 -0
- package/dist/__tests__/runner.test.js +29 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/skill-attribution.test.d.ts +2 -0
- package/dist/__tests__/skill-attribution.test.d.ts.map +1 -0
- package/dist/__tests__/skill-attribution.test.js +53 -0
- package/dist/__tests__/skill-attribution.test.js.map +1 -0
- package/dist/__tests__/skill-broker.test.d.ts +2 -0
- package/dist/__tests__/skill-broker.test.d.ts.map +1 -0
- package/dist/__tests__/skill-broker.test.js +144 -0
- package/dist/__tests__/skill-broker.test.js.map +1 -0
- package/dist/__tests__/skill-catalog.test.d.ts +2 -0
- package/dist/__tests__/skill-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/skill-catalog.test.js +40 -0
- package/dist/__tests__/skill-catalog.test.js.map +1 -0
- package/dist/__tests__/skill-installer.test.d.ts +2 -0
- package/dist/__tests__/skill-installer.test.d.ts.map +1 -0
- package/dist/__tests__/skill-installer.test.js +96 -0
- package/dist/__tests__/skill-installer.test.js.map +1 -0
- package/dist/__tests__/skills-config.test.d.ts +2 -0
- package/dist/__tests__/skills-config.test.d.ts.map +1 -0
- package/dist/__tests__/skills-config.test.js +12 -0
- package/dist/__tests__/skills-config.test.js.map +1 -0
- package/dist/cli.js +29 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/linear-test.d.ts +18 -0
- package/dist/commands/linear-test.d.ts.map +1 -0
- package/dist/commands/linear-test.js +130 -0
- package/dist/commands/linear-test.js.map +1 -0
- package/dist/commands/onboard.d.ts +36 -3
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +233 -42
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +65 -6
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +45 -7
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +16 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +53 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +90 -12
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.d.ts +3 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +13 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/review-model-tiers.json +3 -3
- package/dist/config/review-strategy.json +204 -0
- package/dist/config/schema.d.ts +261 -15
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +90 -8
- package/dist/config/schema.js.map +1 -1
- package/dist/github/client.d.ts +21 -1
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +46 -7
- package/dist/github/client.js.map +1 -1
- package/dist/github/webhook.d.ts +4 -0
- 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 +6 -5
- package/dist/issues/ticket-ref.js.map +1 -1
- package/dist/lib/annotation.d.ts +7 -0
- package/dist/lib/annotation.d.ts.map +1 -1
- package/dist/lib/annotation.js +11 -1
- package/dist/lib/annotation.js.map +1 -1
- package/dist/lib/board.d.ts +3 -0
- package/dist/lib/board.d.ts.map +1 -1
- package/dist/lib/board.js +4 -2
- package/dist/lib/board.js.map +1 -1
- package/dist/lib/clone.d.ts +1 -0
- package/dist/lib/clone.d.ts.map +1 -1
- package/dist/lib/clone.js +32 -10
- package/dist/lib/clone.js.map +1 -1
- package/dist/lib/comment-bodies.d.ts +37 -0
- package/dist/lib/comment-bodies.d.ts.map +1 -1
- package/dist/lib/comment-bodies.js +47 -9
- package/dist/lib/comment-bodies.js.map +1 -1
- package/dist/lib/repo-picker.d.ts +3 -0
- package/dist/lib/repo-picker.d.ts.map +1 -1
- package/dist/lib/repo-picker.js +45 -9
- package/dist/lib/repo-picker.js.map +1 -1
- package/dist/lib/repository-guidance.d.ts +2 -0
- package/dist/lib/repository-guidance.d.ts.map +1 -0
- package/dist/lib/repository-guidance.js +55 -0
- package/dist/lib/repository-guidance.js.map +1 -0
- package/dist/lib/review-models.d.ts +15 -2
- package/dist/lib/review-models.d.ts.map +1 -1
- package/dist/lib/review-models.js +26 -6
- package/dist/lib/review-models.js.map +1 -1
- package/dist/lib/review-strategy.d.ts +92 -0
- package/dist/lib/review-strategy.d.ts.map +1 -0
- package/dist/lib/review-strategy.js +282 -0
- package/dist/lib/review-strategy.js.map +1 -0
- package/dist/lib/runner.d.ts +92 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +470 -54
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts +9 -0
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js +20 -0
- package/dist/lib/workflow.js.map +1 -1
- package/dist/linear/client.d.ts +18 -0
- package/dist/linear/client.d.ts.map +1 -0
- package/dist/linear/client.js +67 -0
- package/dist/linear/client.js.map +1 -0
- package/dist/linear/comment.d.ts +20 -0
- package/dist/linear/comment.d.ts.map +1 -0
- package/dist/linear/comment.js +57 -0
- package/dist/linear/comment.js.map +1 -0
- package/dist/linear/identity.d.ts +59 -0
- package/dist/linear/identity.d.ts.map +1 -0
- package/dist/linear/identity.js +187 -0
- package/dist/linear/identity.js.map +1 -0
- package/dist/linear/notify.d.ts +35 -0
- package/dist/linear/notify.d.ts.map +1 -0
- package/dist/linear/notify.js +76 -0
- package/dist/linear/notify.js.map +1 -0
- package/dist/linear/ref.d.ts +13 -0
- package/dist/linear/ref.d.ts.map +1 -0
- package/dist/linear/ref.js +90 -0
- package/dist/linear/ref.js.map +1 -0
- package/dist/linear/verify.d.ts +26 -0
- package/dist/linear/verify.d.ts.map +1 -0
- package/dist/linear/verify.js +67 -0
- package/dist/linear/verify.js.map +1 -0
- package/dist/reviewers/claude.d.ts +4 -1
- package/dist/reviewers/claude.d.ts.map +1 -1
- package/dist/reviewers/claude.js +39 -7
- package/dist/reviewers/claude.js.map +1 -1
- package/dist/reviewers/codex.d.ts +3 -1
- package/dist/reviewers/codex.d.ts.map +1 -1
- package/dist/reviewers/codex.js +76 -70
- package/dist/reviewers/codex.js.map +1 -1
- package/dist/reviewers/conflict-resolve.d.ts +3 -1
- package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
- package/dist/reviewers/conflict-resolve.js +21 -6
- package/dist/reviewers/conflict-resolve.js.map +1 -1
- package/dist/reviewers/fix.d.ts +5 -2
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +26 -10
- package/dist/reviewers/fix.js.map +1 -1
- package/dist/skills/attribution.d.ts +4 -0
- package/dist/skills/attribution.d.ts.map +1 -0
- package/dist/skills/attribution.js +14 -0
- package/dist/skills/attribution.js.map +1 -0
- package/dist/skills/broker-server.d.ts +2 -0
- package/dist/skills/broker-server.d.ts.map +1 -0
- package/dist/skills/broker-server.js +17 -0
- package/dist/skills/broker-server.js.map +1 -0
- package/dist/skills/broker.d.ts +42 -0
- package/dist/skills/broker.d.ts.map +1 -0
- package/dist/skills/broker.js +285 -0
- package/dist/skills/broker.js.map +1 -0
- package/dist/skills/catalog.d.ts +28 -0
- package/dist/skills/catalog.d.ts.map +1 -0
- package/dist/skills/catalog.js +104 -0
- package/dist/skills/catalog.js.map +1 -0
- package/dist/skills/installer.d.ts +10 -0
- package/dist/skills/installer.d.ts.map +1 -0
- package/dist/skills/installer.js +138 -0
- package/dist/skills/installer.js.map +1 -0
- package/dist/skills/integrity.d.ts +4 -0
- package/dist/skills/integrity.d.ts.map +1 -0
- package/dist/skills/integrity.js +36 -0
- package/dist/skills/integrity.js.map +1 -0
- package/docs/dynamic-thoroughness.md +738 -0
- package/docs/linear-identity-contract.md +139 -0
- package/docs/linear-identity.md +293 -0
- package/get-started.md +207 -11
- package/package.json +4 -3
package/dist/lib/runner.js
CHANGED
|
@@ -3,21 +3,31 @@ 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';
|
|
6
7
|
import { runCodexReview } from '../reviewers/codex.js';
|
|
7
8
|
import { runClaudeReview } from '../reviewers/claude.js';
|
|
8
9
|
import { runFixStep, runCodexFixStep } from '../reviewers/fix.js';
|
|
9
10
|
import { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
|
|
10
11
|
import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE } from '../lib/verdict.js';
|
|
11
12
|
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';
|
|
12
17
|
import { acquireRemoteLock, releaseRemoteLock } from '../github/review-status.js';
|
|
13
18
|
import { log as fileLog, logError, classifyError } from '../lib/logger.js';
|
|
14
19
|
import { buildCommitTrailers } from '../lib/annotation.js';
|
|
15
20
|
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';
|
|
16
23
|
import { buildStepIdentityFields } from '../lib/event-fields.js';
|
|
17
|
-
import { buildFixAppliedCommentBody, buildConflictResolvedCommentBody, buildRetriedReviewBanner } from '../lib/comment-bodies.js';
|
|
18
|
-
import { loadWorkflow, loadHarnessSection, evaluateWhen } from '../lib/workflow.js';
|
|
24
|
+
import { buildAttributionFooter, buildFixAppliedCommentBody, buildFixFailedCommentBody, buildConflictResolvedCommentBody, buildRetriedReviewBanner } from '../lib/comment-bodies.js';
|
|
25
|
+
import { linearWritePossible, loadWorkflow, loadHarnessSection, evaluateWhen } from '../lib/workflow.js';
|
|
19
26
|
import { isSubscriptionLimitError, isVendorUnavailableError } from '../lib/smart-switch.js';
|
|
20
27
|
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';
|
|
21
31
|
const MAX_CROSSCHECK_COMMITS = 5;
|
|
22
32
|
const FIX_RETRY_DELAY_MS = 2 * 60 * 1000;
|
|
23
33
|
const REVIEW_RETRY_DELAY_MS = 2 * 60 * 1000;
|
|
@@ -49,33 +59,27 @@ function isTransientApiError(err) {
|
|
|
49
59
|
export function getEffectiveStepType(stepType, isRecheckRun) {
|
|
50
60
|
return stepType === 'review' && isRecheckRun ? 'recheck' : stepType;
|
|
51
61
|
}
|
|
52
|
-
|
|
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) {
|
|
62
|
+
export function countCrosscheckCommitsForPRDetailed(tmpDir, baseRef) {
|
|
62
63
|
const runLog = (args) => execFileSync('git', ['log', '--oneline', ...args], { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
63
64
|
const count = (out) => out.split('\n').filter(l => l.includes('[crosscheck]')).length;
|
|
64
65
|
try {
|
|
65
|
-
return count(runLog([`origin/${baseRef}..HEAD`]));
|
|
66
|
+
return { count: count(runLog([`origin/${baseRef}..HEAD`])), scoped: true };
|
|
66
67
|
}
|
|
67
68
|
catch {
|
|
68
69
|
// Scoped range unavailable — fall back to full history so the cap still
|
|
69
|
-
// applies.
|
|
70
|
-
//
|
|
70
|
+
// applies. Over-counts when the branch has prior merged crosscheck commits,
|
|
71
|
+
// but that's preferable to bypassing the safety guard.
|
|
71
72
|
try {
|
|
72
|
-
return count(runLog([]));
|
|
73
|
+
return { count: count(runLog([])), scoped: false };
|
|
73
74
|
}
|
|
74
75
|
catch {
|
|
75
|
-
return 0;
|
|
76
|
+
return { count: 0, scoped: false };
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
export function countCrosscheckCommitsForPR(tmpDir, baseRef) {
|
|
81
|
+
return countCrosscheckCommitsForPRDetailed(tmpDir, baseRef).count;
|
|
82
|
+
}
|
|
79
83
|
export function buildWorkflowCompleteEvent(inputs) {
|
|
80
84
|
const stepValues = Object.values(inputs.results);
|
|
81
85
|
const lastVerdict = stepValues.reverse().find(r => r.verdict !== undefined)?.verdict ?? null;
|
|
@@ -238,8 +242,173 @@ function diffBucket(totalLines) {
|
|
|
238
242
|
return 'large';
|
|
239
243
|
return 'xlarge';
|
|
240
244
|
}
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Builds the input the review strategy classifies on, from the already-cloned
|
|
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;
|
|
243
412
|
try {
|
|
244
413
|
const raw = execSync(`git diff --stat origin/${pr.base.ref}...HEAD`, { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
245
414
|
if (!raw)
|
|
@@ -270,7 +439,7 @@ function emitPRComplexity(ctx, triggerField) {
|
|
|
270
439
|
diff_bucket: diffBucket(insertions + deletions),
|
|
271
440
|
file_mix: mix,
|
|
272
441
|
languages: [...langSet],
|
|
273
|
-
quality_tier:
|
|
442
|
+
quality_tier: effectiveTierForRun,
|
|
274
443
|
...triggerField,
|
|
275
444
|
});
|
|
276
445
|
}
|
|
@@ -339,12 +508,68 @@ async function pushWithNonFastForwardHandling(params) {
|
|
|
339
508
|
export async function runWorkflow(ctx) {
|
|
340
509
|
const { owner, repoName, prNumber, pr, tmpDir, token, config, origin, log, onPhaseChange, trigger } = ctx;
|
|
341
510
|
const triggerField = trigger !== undefined ? { trigger } : {};
|
|
342
|
-
const
|
|
511
|
+
const configuredSteps = (ctx.steps ?? loadWorkflow(process.cwd())).map(step => {
|
|
343
512
|
if (!step.harness || step.instructions)
|
|
344
513
|
return step;
|
|
345
514
|
const resolved = loadHarnessSection(step.harness, process.cwd());
|
|
346
515
|
return resolved ? { ...step, instructions: resolved } : step;
|
|
347
516
|
});
|
|
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
|
+
}
|
|
348
573
|
const results = {};
|
|
349
574
|
// SHAs the workflow pushed AND set a `crosscheck/review` pending status on.
|
|
350
575
|
// Each one must be released in the finally below — otherwise the pending
|
|
@@ -356,6 +581,47 @@ export async function runWorkflow(ctx) {
|
|
|
356
581
|
// can iterate the same list and release these shas if SIGINT/SIGTERM fires
|
|
357
582
|
// mid-workflow (process.exit there bypasses our finally below).
|
|
358
583
|
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;
|
|
359
625
|
let workflowFailed = false;
|
|
360
626
|
let workflowError = undefined;
|
|
361
627
|
let failedStep = undefined;
|
|
@@ -384,8 +650,42 @@ export async function runWorkflow(ctx) {
|
|
|
384
650
|
const workflowStart = Date.now();
|
|
385
651
|
const stepsRun = [];
|
|
386
652
|
let currentStepName;
|
|
387
|
-
|
|
653
|
+
// Class picks tier AND effort under smart; untouched config under fixed.
|
|
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);
|
|
388
662
|
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
|
+
}
|
|
389
689
|
for (const step of steps) {
|
|
390
690
|
currentStepName = step.name;
|
|
391
691
|
stepsRun.push(step.name);
|
|
@@ -448,17 +748,21 @@ export async function runWorkflow(ctx) {
|
|
|
448
748
|
let inputTokens;
|
|
449
749
|
let outputTokens;
|
|
450
750
|
let model = 'default';
|
|
751
|
+
let effort;
|
|
451
752
|
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.
|
|
452
756
|
const runReviewWithVendor = async (candidate) => {
|
|
453
757
|
if (candidate === 'codex') {
|
|
454
758
|
;
|
|
455
|
-
({ review: rawReview, tokensUsed, model, retried } = await runCodexReview(tmpDir, pr.base.ref, pr.title,
|
|
759
|
+
({ review: rawReview, tokensUsed, model, effort, retried } = await runCodexReview(tmpDir, pr.base.ref, pr.title, quality, codexVendor, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec), log, ctx.issueContext, skillSession));
|
|
456
760
|
inputTokens = undefined;
|
|
457
761
|
outputTokens = undefined;
|
|
458
762
|
}
|
|
459
763
|
else {
|
|
460
764
|
;
|
|
461
|
-
({ review: rawReview, tokensUsed, inputTokens, outputTokens, model, retried } = await runClaudeReview(tmpDir, pr.base.ref, pr.title,
|
|
765
|
+
({ review: rawReview, tokensUsed, inputTokens, outputTokens, model, effort, retried } = await runClaudeReview(tmpDir, pr.base.ref, pr.title, quality, claudeVendor, config.budget.per_review_usd, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), !!ctx.roundMode, log, ctx.issueContext, skillSession));
|
|
462
766
|
}
|
|
463
767
|
};
|
|
464
768
|
try {
|
|
@@ -515,6 +819,11 @@ export async function runWorkflow(ctx) {
|
|
|
515
819
|
}
|
|
516
820
|
}
|
|
517
821
|
}
|
|
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);
|
|
518
827
|
// First attempt timed out but the delayed retry succeeded — surface a
|
|
519
828
|
// soft notice on the review comment so the author knows it was a transient blip.
|
|
520
829
|
if (retried) {
|
|
@@ -536,15 +845,17 @@ export async function runWorkflow(ctx) {
|
|
|
536
845
|
const baseBody = verdict === null
|
|
537
846
|
? `${NULL_VERDICT_WARNING}\n\n${clean}`
|
|
538
847
|
: 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.
|
|
539
850
|
const commentBody = retried
|
|
540
851
|
? `${buildRetriedReviewBanner(retried.timeoutMs, retried.delayMs)}\n\n${baseBody}`
|
|
541
852
|
: baseBody;
|
|
542
853
|
const commentCount = countComments(rawReview);
|
|
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 });
|
|
854
|
+
fileLog({ level: 'info', event: 'review_complete', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, verdict, duration_ms: Date.now() - stepStart, tokens_used: tokensUsed, skills_activated: activatedSkills.map(skill => skill.name), ...(inputTokens !== undefined && { input_tokens: inputTokens }), ...(outputTokens !== undefined && { output_tokens: outputTokens }), ...(ctx.round !== undefined && { round: ctx.round }), ...(ctx.roundMode && { mode: ctx.roundMode }), ...triggerField });
|
|
544
855
|
// Recheck verdict is stored separately to preserve the original review's commentCount on the board
|
|
545
856
|
const phaseUpdate = isRecheck
|
|
546
|
-
? { recheckVerdict: verdict, phase: donePhase, recheckTokens: tokensUsed, recheckReviewer: reviewer, qualityTier:
|
|
547
|
-
: { verdict, commentCount, phase: donePhase, crTokens: tokensUsed, crReviewer: reviewer, qualityTier:
|
|
857
|
+
? { recheckVerdict: verdict, phase: donePhase, recheckTokens: tokensUsed, recheckReviewer: reviewer, qualityTier: quality.tier }
|
|
858
|
+
: { verdict, commentCount, phase: donePhase, crTokens: tokensUsed, crReviewer: reviewer, qualityTier: quality.tier };
|
|
548
859
|
if (ctx.dryRun) {
|
|
549
860
|
onPhaseChange('dry-run — comment not posted', phaseUpdate);
|
|
550
861
|
log(chalk.dim(`\n--- dry-run: comment that would be posted ---\n${commentBody}\n--- end ---`));
|
|
@@ -579,9 +890,47 @@ export async function runWorkflow(ctx) {
|
|
|
579
890
|
annotationSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
|
|
580
891
|
}
|
|
581
892
|
catch { /* fall back to pr.head.sha if git is unavailable */ }
|
|
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
|
|
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, activatedSkills, effort,
|
|
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);
|
|
583
900
|
const commentUrl = `github.com/${owner}/${repoName}/pull/${prNumber}`;
|
|
584
901
|
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
|
+
}
|
|
585
934
|
results[step.name] = { verdict, commentBody, commentUrl, commentId, tokens_used: tokensUsed, input_tokens: inputTokens, output_tokens: outputTokens, vendor: reviewer, model };
|
|
586
935
|
}
|
|
587
936
|
}
|
|
@@ -641,17 +990,28 @@ export async function runWorkflow(ctx) {
|
|
|
641
990
|
skipFix('no_vendor');
|
|
642
991
|
continue;
|
|
643
992
|
}
|
|
644
|
-
|
|
645
|
-
|
|
993
|
+
// The fix step holds the review's tier — this round's, escalation
|
|
994
|
+
// included: it is generation against an explicit findings list, which
|
|
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);
|
|
646
1001
|
// Guard: don't push more than MAX_CROSSCHECK_COMMITS per PR.
|
|
647
1002
|
// Scope to commits ahead of base so long-lived branches (e.g. staging)
|
|
648
1003
|
// don't count [crosscheck] commits from previously merged PRs.
|
|
649
1004
|
// Crazy/halfcrazy mode doubles the cap since it deliberately loops.
|
|
650
|
-
const
|
|
1005
|
+
const commitCount = countCrosscheckCommitsForPRDetailed(tmpDir, pr.base.ref);
|
|
651
1006
|
const effectiveCommitLimit = ctx.roundMode ? MAX_CROSSCHECK_COMMITS * 2 : MAX_CROSSCHECK_COMMITS;
|
|
652
|
-
if (
|
|
653
|
-
|
|
654
|
-
|
|
1007
|
+
if (commitCount.count >= effectiveCommitLimit) {
|
|
1008
|
+
// Report the count, not the limit. An unscoped count means origin/<base>
|
|
1009
|
+
// was missing, so this is an over-count from whole-repo history rather
|
|
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');
|
|
655
1015
|
continue;
|
|
656
1016
|
}
|
|
657
1017
|
onPhaseChange(`${vendor} fixing...`, { phase: 'fixing' });
|
|
@@ -659,18 +1019,24 @@ export async function runWorkflow(ctx) {
|
|
|
659
1019
|
let appliedCount = 0;
|
|
660
1020
|
let fixChangedFiles = [];
|
|
661
1021
|
let fixTokensUsed;
|
|
1022
|
+
let fixEffort;
|
|
662
1023
|
let fixErr = undefined;
|
|
663
1024
|
let activeVendor = vendor;
|
|
664
|
-
|
|
1025
|
+
// The strategy tier, not the configured one: a risky PR runs the thorough
|
|
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);
|
|
665
1029
|
const runFix = async (v) => {
|
|
666
1030
|
if (v === 'codex') {
|
|
667
|
-
return runCodexFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', codexFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec) ?? tierMs);
|
|
1031
|
+
return runCodexFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', codexFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec) ?? tierMs, skillSession, codexVendor.effort);
|
|
668
1032
|
}
|
|
669
|
-
|
|
1033
|
+
// roundConfig, not config: runFixStep reads vendors.claude.effort and
|
|
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);
|
|
670
1036
|
};
|
|
671
1037
|
try {
|
|
672
1038
|
;
|
|
673
|
-
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed } = await runFix(vendor));
|
|
1039
|
+
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed, effort: fixEffort } = await runFix(vendor));
|
|
674
1040
|
}
|
|
675
1041
|
catch (err) {
|
|
676
1042
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 1, vendor }, err);
|
|
@@ -684,7 +1050,7 @@ export async function runWorkflow(ctx) {
|
|
|
684
1050
|
fileLog({ level: 'warn', event: 'fix_vendor_fallback', repo: `${owner}/${repoName}`, pr: prNumber, from: vendor, to: fallbackVendor, ...(isSubscriptionLimitError(err) && { reason: 'vendor_limit' }) });
|
|
685
1051
|
try {
|
|
686
1052
|
;
|
|
687
|
-
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed } = await runFix(fallbackVendor));
|
|
1053
|
+
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed, effort: fixEffort } = await runFix(fallbackVendor));
|
|
688
1054
|
activeVendor = fallbackVendor;
|
|
689
1055
|
}
|
|
690
1056
|
catch (fallbackErr) {
|
|
@@ -705,7 +1071,7 @@ export async function runWorkflow(ctx) {
|
|
|
705
1071
|
onPhaseChange(`${activeVendor} fixing (retry)...`, { phase: 'fixing' });
|
|
706
1072
|
try {
|
|
707
1073
|
;
|
|
708
|
-
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed } = await runFix(activeVendor));
|
|
1074
|
+
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed, effort: fixEffort } = await runFix(activeVendor));
|
|
709
1075
|
fileLog({ level: 'info', event: 'fix_retry_succeeded', repo: `${owner}/${repoName}`, pr: prNumber });
|
|
710
1076
|
fixErr = undefined;
|
|
711
1077
|
}
|
|
@@ -714,6 +1080,20 @@ export async function runWorkflow(ctx) {
|
|
|
714
1080
|
fixErr = retryErr;
|
|
715
1081
|
}
|
|
716
1082
|
}
|
|
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
|
+
});
|
|
717
1097
|
if (fixErr !== undefined) {
|
|
718
1098
|
skipFix(isSubscriptionLimitError(fixErr) ? 'vendor_limit' : 'fix_error');
|
|
719
1099
|
// Only notify for transient failures — auth errors are operator issues, not PR author issues
|
|
@@ -722,7 +1102,13 @@ export async function runWorkflow(ctx) {
|
|
|
722
1102
|
const octokit = createGithubClient(token);
|
|
723
1103
|
await octokit.rest.issues.createComment({
|
|
724
1104
|
owner, repo: repoName, issue_number: prNumber,
|
|
725
|
-
body:
|
|
1105
|
+
body: buildFixFailedCommentBody({
|
|
1106
|
+
prUrl: pr.html_url,
|
|
1107
|
+
vendor: activeVendor,
|
|
1108
|
+
model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
|
|
1109
|
+
effort: fixEffort,
|
|
1110
|
+
skills: activatedSkills,
|
|
1111
|
+
}),
|
|
726
1112
|
});
|
|
727
1113
|
fileLog({ level: 'info', event: 'fix_failed_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber });
|
|
728
1114
|
}
|
|
@@ -741,7 +1127,7 @@ export async function runWorkflow(ctx) {
|
|
|
741
1127
|
// event — NOT `fix_complete` — so status consumers (WORKFLOW_ACTIVITY_EVENTS,
|
|
742
1128
|
// the legacy status fold) don't misread a no-op as real workflow progress and
|
|
743
1129
|
// mark the PR NEEDS_RECHECK or hide it from `scan --tidy` with no fix applied.
|
|
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 });
|
|
1130
|
+
fileLog({ level: 'info', event: 'fix_noop', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: 0, no_changes: true, tokens_used: fixTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
745
1131
|
results[step.name] = { applied_count: 0, ...(fixTokensUsed !== undefined && { tokens_used: fixTokensUsed }), vendor };
|
|
746
1132
|
continue;
|
|
747
1133
|
}
|
|
@@ -793,7 +1179,7 @@ export async function runWorkflow(ctx) {
|
|
|
793
1179
|
}
|
|
794
1180
|
}
|
|
795
1181
|
onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: 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 });
|
|
1182
|
+
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'commit', tokens_used: fixTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
797
1183
|
// Post a summary comment so the silent commit push is visible on the timeline
|
|
798
1184
|
// as a comment card. Best-effort — a failure here must not fail the run.
|
|
799
1185
|
try {
|
|
@@ -804,6 +1190,9 @@ export async function runWorkflow(ctx) {
|
|
|
804
1190
|
changedFiles: fixChangedFiles,
|
|
805
1191
|
vendor: activeVendor,
|
|
806
1192
|
reviewCommentBody,
|
|
1193
|
+
model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
|
|
1194
|
+
effort: fixEffort,
|
|
1195
|
+
skills: activatedSkills,
|
|
807
1196
|
});
|
|
808
1197
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
809
1198
|
fileLog({ level: 'info', event: 'fix_applied_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
|
|
@@ -840,7 +1229,13 @@ export async function runWorkflow(ctx) {
|
|
|
840
1229
|
head: fixBranch,
|
|
841
1230
|
base: pr.head.ref,
|
|
842
1231
|
title: fixPrTitle,
|
|
843
|
-
body:
|
|
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'),
|
|
844
1239
|
});
|
|
845
1240
|
if (config.post_review.auto_fix.delivery.label) {
|
|
846
1241
|
try {
|
|
@@ -851,7 +1246,7 @@ export async function runWorkflow(ctx) {
|
|
|
851
1246
|
catch { /* label may not exist in this repo — skip */ }
|
|
852
1247
|
}
|
|
853
1248
|
onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: 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 });
|
|
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, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
855
1250
|
results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
|
|
856
1251
|
}
|
|
857
1252
|
else {
|
|
@@ -863,11 +1258,11 @@ export async function runWorkflow(ctx) {
|
|
|
863
1258
|
catch { /* ignore */ }
|
|
864
1259
|
if (patch) {
|
|
865
1260
|
const octokit = createGithubClient(token);
|
|
866
|
-
const body = `### Suggested fixes (crosscheck auto-fix)\n\n\`\`\`diff\n${patch.slice(0, 16000)}\n
|
|
1261
|
+
const body = `### Suggested fixes (crosscheck auto-fix)\n\n\`\`\`diff\n${patch.slice(0, 16000)}\n\`\`\`\n\n${fixAttributionFooter()}`;
|
|
867
1262
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
868
1263
|
}
|
|
869
1264
|
onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: 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 });
|
|
1265
|
+
fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, delivery: 'comment', tokens_used: fixTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
|
|
871
1266
|
results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
|
|
872
1267
|
}
|
|
873
1268
|
}
|
|
@@ -940,7 +1335,9 @@ export async function runWorkflow(ctx) {
|
|
|
940
1335
|
skipConflictResolve('codex_conflict_resolve_unsupported');
|
|
941
1336
|
continue;
|
|
942
1337
|
}
|
|
943
|
-
|
|
1338
|
+
// Conflict-resolve is mechanical text surgery bounded by the markers —
|
|
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);
|
|
944
1341
|
const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
|
|
945
1342
|
if (isFork) {
|
|
946
1343
|
try {
|
|
@@ -951,13 +1348,15 @@ export async function runWorkflow(ctx) {
|
|
|
951
1348
|
skipConflictResolve('fork_pr');
|
|
952
1349
|
continue;
|
|
953
1350
|
}
|
|
954
|
-
const
|
|
955
|
-
if (
|
|
1351
|
+
const crCommitCount = countCrosscheckCommitsForPRDetailed(tmpDir, pr.base.ref);
|
|
1352
|
+
if (crCommitCount.count >= MAX_CROSSCHECK_COMMITS) {
|
|
956
1353
|
try {
|
|
957
1354
|
execSync('git merge --abort', { cwd: tmpDir });
|
|
958
1355
|
}
|
|
959
1356
|
catch { /* ignore */ }
|
|
960
|
-
log(
|
|
1357
|
+
log(crCommitCount.scoped
|
|
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`));
|
|
961
1360
|
skipConflictResolve('commit_limit_reached');
|
|
962
1361
|
continue;
|
|
963
1362
|
}
|
|
@@ -967,9 +1366,11 @@ export async function runWorkflow(ctx) {
|
|
|
967
1366
|
let appliedCount = 0;
|
|
968
1367
|
let resolvedPaths = [];
|
|
969
1368
|
let resolveTokensUsed;
|
|
1369
|
+
let resolveEffort;
|
|
1370
|
+
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
970
1371
|
try {
|
|
971
1372
|
;
|
|
972
|
-
({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed } = await runConflictResolveStep(tmpDir, pr.title, step.instructions ?? '', conflictResolveModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec)));
|
|
1373
|
+
({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed, effort: resolveEffort } = await runConflictResolveStep(tmpDir, pr.title, step.instructions ?? '', conflictResolveModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), skillSession, config.vendors.claude.effort));
|
|
973
1374
|
}
|
|
974
1375
|
catch (err) {
|
|
975
1376
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'conflict-resolve', attempt: 1 }, err);
|
|
@@ -980,6 +1381,11 @@ export async function runWorkflow(ctx) {
|
|
|
980
1381
|
skipConflictResolve(isSubscriptionLimitError(err) ? 'vendor_limit' : 'resolve_error');
|
|
981
1382
|
continue;
|
|
982
1383
|
}
|
|
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 });
|
|
983
1389
|
if (appliedCount === 0) {
|
|
984
1390
|
try {
|
|
985
1391
|
execSync('git merge --abort', { cwd: tmpDir });
|
|
@@ -1082,7 +1488,7 @@ export async function runWorkflow(ctx) {
|
|
|
1082
1488
|
fileLog({ level: 'warn', event: 'remote_lock_refresh_failed', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha, error: err instanceof Error ? err.message : String(err) });
|
|
1083
1489
|
}
|
|
1084
1490
|
onPhaseChange('conflicts resolved ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: 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 });
|
|
1491
|
+
fileLog({ level: 'info', event: 'conflict_resolve_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, conflicts_resolved: conflictedFiles.length, sha: newSha, tokens_used: resolveTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - conflictResolveStepStart, ...triggerField });
|
|
1086
1492
|
// Post a summary comment so the silent merge-commit push is visible on the
|
|
1087
1493
|
// timeline as a comment card. Best-effort — a failure here must not fail the run.
|
|
1088
1494
|
// Prefer the resolver's actual rewrite set; fall back to the originally-conflicted
|
|
@@ -1093,6 +1499,9 @@ export async function runWorkflow(ctx) {
|
|
|
1093
1499
|
owner, repo: repoName, sha: newSha,
|
|
1094
1500
|
conflictCount: conflictedFiles.length,
|
|
1095
1501
|
files: resolvedPaths.length > 0 ? resolvedPaths : conflictedFiles,
|
|
1502
|
+
model: conflictResolveModel,
|
|
1503
|
+
effort: resolveEffort,
|
|
1504
|
+
skills: activatedSkills,
|
|
1096
1505
|
});
|
|
1097
1506
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
1098
1507
|
fileLog({ level: 'info', event: 'conflict_resolved_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
|
|
@@ -1129,6 +1538,9 @@ export async function runWorkflow(ctx) {
|
|
|
1129
1538
|
throw err;
|
|
1130
1539
|
}
|
|
1131
1540
|
finally {
|
|
1541
|
+
for (const session of skillSessions.values())
|
|
1542
|
+
session.close();
|
|
1543
|
+
skillSessions.clear();
|
|
1132
1544
|
if (pushedShasNeedingRelease.length > 0 || fixPushedShaRequiresRecheck !== null) {
|
|
1133
1545
|
const lockOctokit = createGithubClient(token);
|
|
1134
1546
|
const outcome = workflowFailed ? 'failure' : 'success';
|
|
@@ -1180,7 +1592,11 @@ export async function runWorkflow(ctx) {
|
|
|
1180
1592
|
failedStep,
|
|
1181
1593
|
round: ctx.round,
|
|
1182
1594
|
trigger: ctx.trigger,
|
|
1183
|
-
|
|
1595
|
+
// The tier that actually ran, not the configured one and not the base
|
|
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,
|
|
1184
1600
|
}));
|
|
1185
1601
|
}
|
|
1186
1602
|
}
|