@todoist/doistbot-cli 0.0.0-stage → 1.0.16
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/dist/actions/auth.js +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
|
|
2
|
+
import { logger } from '../../core/logging.js';
|
|
3
|
+
import { findOwningRoutes, findRepositoryTarget } from '../../core/repository-map.js';
|
|
4
|
+
import { withRetry } from '../../core/retry.js';
|
|
5
|
+
import { GITHUB_REQUEST_TIMEOUT_MS } from './github-request.js';
|
|
6
|
+
import { ASSIGNEE_REVIEW_MARKER } from './markers.js';
|
|
7
|
+
export async function resolveAssigneeReviewers({ owner, repo, assignees, membershipOctokit, }) {
|
|
8
|
+
const repository = findRepositoryTarget(owner, repo);
|
|
9
|
+
if (!repository || assignees.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const routes = findOwningRoutes(repository);
|
|
12
|
+
const teams = [...new Set(routes.map((route) => route.ownerTeam))];
|
|
13
|
+
const reviewers = [];
|
|
14
|
+
const logins = [...new Set(assignees.map((login) => login.toLowerCase()))];
|
|
15
|
+
for (const username of logins) {
|
|
16
|
+
if (isKnownDoistbotLogin(username))
|
|
17
|
+
continue;
|
|
18
|
+
for (const teamSlug of teams) {
|
|
19
|
+
try {
|
|
20
|
+
const { data } = await withRetry(() => membershipOctokit.rest.teams.getMembershipForUserInOrg({
|
|
21
|
+
org: owner,
|
|
22
|
+
team_slug: teamSlug,
|
|
23
|
+
username,
|
|
24
|
+
request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
|
|
25
|
+
}));
|
|
26
|
+
if (data.state === 'active') {
|
|
27
|
+
reviewers.push(username);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// A missing/invisible membership never qualifies an assignee.
|
|
33
|
+
if (error?.status !== 404) {
|
|
34
|
+
logger.warn('Unable to verify autofix assignee team membership', {
|
|
35
|
+
repository: `${owner}/${repo}`,
|
|
36
|
+
username,
|
|
37
|
+
team: teamSlug,
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return reviewers;
|
|
45
|
+
}
|
|
46
|
+
/** Best-effort early review request. Failure must not lose an already opened PR. */
|
|
47
|
+
export async function requestAssigneeReview({ assigneeReview, ...pr }) {
|
|
48
|
+
if (!assigneeReview)
|
|
49
|
+
return [];
|
|
50
|
+
const reviewers = await resolveAssigneeReviewers({ ...pr, ...assigneeReview });
|
|
51
|
+
if (reviewers.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
try {
|
|
54
|
+
await withRetry(() => pr.octokit.rest.pulls.requestReviewers({
|
|
55
|
+
owner: pr.owner,
|
|
56
|
+
repo: pr.repo,
|
|
57
|
+
pull_number: pr.prNumber,
|
|
58
|
+
reviewers,
|
|
59
|
+
request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.warn('Autofix assignee review request failed; retaining Hero fallback', {
|
|
64
|
+
repository: `${pr.owner}/${pr.repo}`,
|
|
65
|
+
pr_number: pr.prNumber,
|
|
66
|
+
reviewers,
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
});
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// A bot-authored receipt on the target PR survives the separate promotion
|
|
73
|
+
// process, completed/removed review requests, and edits to the PR body.
|
|
74
|
+
// Only write it AFTER GitHub accepts the request, never for candidates.
|
|
75
|
+
await withRetry(() => pr.octokit.rest.issues.createComment({
|
|
76
|
+
owner: pr.owner,
|
|
77
|
+
repo: pr.repo,
|
|
78
|
+
issue_number: pr.prNumber,
|
|
79
|
+
body: [
|
|
80
|
+
ASSIGNEE_REVIEW_MARKER,
|
|
81
|
+
'',
|
|
82
|
+
`Requested review from issue assignees: ${reviewers.map((login) => `@${login}`).join(', ')}.`,
|
|
83
|
+
].join('\n'),
|
|
84
|
+
request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
logger.warn('Failed to record autofix assignee review request; Hero fallback remains armed', {
|
|
89
|
+
repository: `${pr.owner}/${pr.repo}`,
|
|
90
|
+
pr_number: pr.prNumber,
|
|
91
|
+
reviewers,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return reviewers;
|
|
96
|
+
}
|
|
97
|
+
export async function hasAssigneeReviewRequest(pr) {
|
|
98
|
+
return withRetry(async () => {
|
|
99
|
+
const pages = pr.octokit.paginate.iterator(pr.octokit.rest.issues.listComments, {
|
|
100
|
+
owner: pr.owner,
|
|
101
|
+
repo: pr.repo,
|
|
102
|
+
issue_number: pr.prNumber,
|
|
103
|
+
per_page: 100,
|
|
104
|
+
request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
|
|
105
|
+
});
|
|
106
|
+
for await (const { data: comments } of pages) {
|
|
107
|
+
if (comments.some((comment) => isKnownDoistbotLogin(comment.user?.login) &&
|
|
108
|
+
comment.body?.split('\n')[0]?.trim() === ASSIGNEE_REVIEW_MARKER)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const REPOSITORY_CAPABILITIES = {
|
|
2
|
+
'todoist/todoist-web': ['github-packages-read'],
|
|
3
|
+
};
|
|
4
|
+
export function resolveAutofixCapabilities(repository) {
|
|
5
|
+
return REPOSITORY_CAPABILITIES[repository.toLowerCase()] ?? [];
|
|
6
|
+
}
|
|
7
|
+
export function resolveAutofixCapabilityEnv(repository, env = process.env) {
|
|
8
|
+
const capabilityEnv = {};
|
|
9
|
+
for (const capability of resolveAutofixCapabilities(repository)) {
|
|
10
|
+
if (capability === 'github-packages-read') {
|
|
11
|
+
const token = env.GH_PACKAGES_TOKEN?.trim();
|
|
12
|
+
if (!token) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
reason: `Missing GH_PACKAGES_TOKEN for required autofix capability "${capability}" on ${repository}.`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
capabilityEnv.GH_PACKAGES_TOKEN = token;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { ok: true, env: capabilityEnv };
|
|
22
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
6
|
+
import { Ajv } from 'ajv';
|
|
7
|
+
import { hashAutofixInputs, verificationStateSchema } from '../../core/autofix-verification.js';
|
|
8
|
+
import { githubHistoryIdentity } from '../../core/github-history.js';
|
|
9
|
+
import { logger } from '../../core/logging.js';
|
|
10
|
+
export const autofixCheckpointSchema = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
required: ['version', 'identity', 'baseCommit', 'patch', 'verification', 'status', 'savedAt'],
|
|
14
|
+
properties: {
|
|
15
|
+
version: { const: 1 },
|
|
16
|
+
identity: { type: 'string' },
|
|
17
|
+
baseCommit: { type: 'string' },
|
|
18
|
+
patch: { type: 'string', maxLength: 5 * 1024 * 1024 },
|
|
19
|
+
verification: verificationStateSchema,
|
|
20
|
+
status: { enum: ['timed-out', 'failed', 'completed'] },
|
|
21
|
+
savedAt: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const validateCheckpoint = new Ajv().compile(autofixCheckpointSchema);
|
|
25
|
+
export function checkpointIdentity(input) {
|
|
26
|
+
return hashAutofixInputs(JSON.stringify({
|
|
27
|
+
issueUrl: githubHistoryIdentity(input.issueUrl),
|
|
28
|
+
repository: githubHistoryIdentity(input.repository),
|
|
29
|
+
baseCommit: input.baseCommit,
|
|
30
|
+
triageOutput: input.triageOutput.trim(),
|
|
31
|
+
policy: input.policy,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
export class AutofixCheckpointStore {
|
|
35
|
+
identity;
|
|
36
|
+
bucket;
|
|
37
|
+
directory;
|
|
38
|
+
s3;
|
|
39
|
+
prefix;
|
|
40
|
+
constructor(identity, bucket, directory) {
|
|
41
|
+
this.identity = identity;
|
|
42
|
+
this.bucket = bucket;
|
|
43
|
+
this.directory = directory ?? path.join(tmpdir(), 'doistbot-autofix', randomUUID());
|
|
44
|
+
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
45
|
+
this.prefix = `autofix/${identity}`;
|
|
46
|
+
this.s3 = bucket ? new S3Client({ maxAttempts: 2 }) : undefined;
|
|
47
|
+
}
|
|
48
|
+
async load() {
|
|
49
|
+
if (!this.s3 || !this.bucket)
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const key = `${this.prefix}/checkpoint.json`;
|
|
53
|
+
const listing = await this.s3.send(new ListObjectsV2Command({ Bucket: this.bucket, Prefix: key, MaxKeys: 1 }), { abortSignal: AbortSignal.timeout(15_000) });
|
|
54
|
+
if (!listing.Contents?.some((object) => object.Key === key))
|
|
55
|
+
return null;
|
|
56
|
+
const object = await this.s3.send(new GetObjectCommand({
|
|
57
|
+
Bucket: this.bucket,
|
|
58
|
+
Key: key,
|
|
59
|
+
}), { abortSignal: AbortSignal.timeout(15_000) });
|
|
60
|
+
if (!object.Body || (object.ContentLength ?? 0) > 10 * 1024 * 1024)
|
|
61
|
+
throw new Error('Invalid checkpoint size');
|
|
62
|
+
const data = JSON.parse(await object.Body.transformToString());
|
|
63
|
+
if (!validateCheckpoint(data) || data.identity !== this.identity)
|
|
64
|
+
throw new Error('Invalid autofix checkpoint');
|
|
65
|
+
return data.status === 'timed-out' ? data : null;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof Error && error.name === 'NoSuchKey')
|
|
69
|
+
return null;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async save(checkpoint, report) {
|
|
74
|
+
const checkpointFile = {
|
|
75
|
+
name: 'checkpoint.json',
|
|
76
|
+
body: JSON.stringify(checkpoint),
|
|
77
|
+
contentType: 'application/json',
|
|
78
|
+
};
|
|
79
|
+
const files = [
|
|
80
|
+
{ name: 'fix.patch', body: checkpoint.patch, contentType: 'text/x-diff' },
|
|
81
|
+
{ name: 'run.json', body: report, contentType: 'application/json' },
|
|
82
|
+
checkpointFile,
|
|
83
|
+
];
|
|
84
|
+
for (const file of files)
|
|
85
|
+
writeFileSync(path.join(this.directory, file.name), file.body, { mode: 0o600 });
|
|
86
|
+
if (!this.s3 || !this.bucket) {
|
|
87
|
+
logger.warn('Autofix artifacts saved locally; cross-sandbox recovery is unavailable', {
|
|
88
|
+
directory: this.directory,
|
|
89
|
+
});
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// Archive each run separately. Update the resumable checkpoint last.
|
|
93
|
+
const runPrefix = `${this.prefix}/runs/${path.basename(this.directory)}`;
|
|
94
|
+
const uploads = [
|
|
95
|
+
...files.map((file) => ({ ...file, key: `${runPrefix}/${file.name}` })),
|
|
96
|
+
...(checkpoint.status === 'failed'
|
|
97
|
+
? []
|
|
98
|
+
: [{ ...checkpointFile, key: `${this.prefix}/checkpoint.json` }]),
|
|
99
|
+
];
|
|
100
|
+
try {
|
|
101
|
+
for (const file of uploads)
|
|
102
|
+
await this.s3.send(new PutObjectCommand({
|
|
103
|
+
Bucket: this.bucket,
|
|
104
|
+
Key: file.key,
|
|
105
|
+
Body: file.body,
|
|
106
|
+
ContentType: file.contentType,
|
|
107
|
+
}), { abortSignal: AbortSignal.timeout(15_000) });
|
|
108
|
+
logger.info('Autofix artifacts saved', {
|
|
109
|
+
directory: this.directory,
|
|
110
|
+
artifactPrefix: `s3://${this.bucket}/${runPrefix}`,
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.error('Autofix artifact upload failed', {
|
|
116
|
+
directory: this.directory,
|
|
117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
118
|
+
});
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CONFIG_FILE_PATH, getLocalRepoConfig } from 'doistbot-repo-config';
|
|
2
|
+
import { validateAutofixPolicy } from '../../core/autofix-verification.js';
|
|
3
|
+
import { logger } from '../../core/logging.js';
|
|
4
|
+
const NO_CHECKS_POLICY = { verification: { checks: [] } };
|
|
5
|
+
export function loadAutofixPolicy(repoPath) {
|
|
6
|
+
const config = getLocalRepoConfig({ workspaceDir: repoPath, logger, strict: true });
|
|
7
|
+
if (!config || typeof config !== 'object' || !('issue_triage' in config))
|
|
8
|
+
return NO_CHECKS_POLICY;
|
|
9
|
+
const section = config.issue_triage;
|
|
10
|
+
if (!section || typeof section !== 'object' || !('autofix' in section))
|
|
11
|
+
return NO_CHECKS_POLICY;
|
|
12
|
+
const policy = section.autofix;
|
|
13
|
+
if (!validateAutofixPolicy(policy)) {
|
|
14
|
+
throw new Error(`Invalid issue_triage.autofix in ${CONFIG_FILE_PATH}: ${JSON.stringify(validateAutofixPolicy.errors)}`);
|
|
15
|
+
}
|
|
16
|
+
const names = policy.verification.checks.map((check) => check.name);
|
|
17
|
+
if (new Set(names).size !== names.length)
|
|
18
|
+
throw new Error('Autofix check names must be unique');
|
|
19
|
+
return policy;
|
|
20
|
+
}
|
|
21
|
+
export function describeAutofixPolicy(policy) {
|
|
22
|
+
return [
|
|
23
|
+
'\n## Repository autofix policy',
|
|
24
|
+
`Source: ${CONFIG_FILE_PATH}. Verification shares the autofix time limit; there is no round-count limit.`,
|
|
25
|
+
policy.guidance ?? '',
|
|
26
|
+
'The first verification round runs every configured check. Later rounds can select check names. Run checks only through verify:',
|
|
27
|
+
...policy.verification.checks.map((check) => `- ${check.name}: ${check.command}${check.use_test_selectors ? '. Supply focused testSelectors for this check.' : ''}`),
|
|
28
|
+
...(policy.verification.checks.length
|
|
29
|
+
? []
|
|
30
|
+
: ['No local checks are configured. Report this limit and leave checks to CI.']),
|
|
31
|
+
'UI tests are forbidden. Do not create or modify UI tests, even if repository guidance asks for them.',
|
|
32
|
+
'Extra forbidden test paths: ' + (policy.forbidden_test_paths?.join(', ') || 'none'),
|
|
33
|
+
]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { logger } from '../../core/logging.js';
|
|
2
|
+
import { withPhase } from '../../core/tracing.js';
|
|
3
|
+
import { cloneRepository } from './shared.js';
|
|
4
|
+
export async function cloneTriageRepositories(params) {
|
|
5
|
+
return withPhase('triage.clone', { 'repos.count': params.repositories.length }, async (span) => {
|
|
6
|
+
const cloneFn = params.cloneRepositoryImpl ?? cloneRepository;
|
|
7
|
+
const clonedRepositories = [];
|
|
8
|
+
const failedRepositories = [];
|
|
9
|
+
for (const repository of params.repositories) {
|
|
10
|
+
try {
|
|
11
|
+
const cloneResult = await cloneFn({
|
|
12
|
+
app: params.app,
|
|
13
|
+
repository,
|
|
14
|
+
reposDir: params.reposDir,
|
|
15
|
+
fallbackInstallationId: params.fallbackInstallationId,
|
|
16
|
+
});
|
|
17
|
+
clonedRepositories.push(cloneResult);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
// Keep per-repo diagnostics (auth/git errors) visible even
|
|
22
|
+
// when EVERY clone fails and the orchestrator bails early.
|
|
23
|
+
logger.warn('Repository clone failed', {
|
|
24
|
+
repository: repository.fullName,
|
|
25
|
+
error: message,
|
|
26
|
+
});
|
|
27
|
+
failedRepositories.push({
|
|
28
|
+
repository: repository.fullName,
|
|
29
|
+
error: message,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
span.setAttributes({
|
|
34
|
+
'clone.success_count': clonedRepositories.length,
|
|
35
|
+
'clone.failed_count': failedRepositories.length,
|
|
36
|
+
'repos.count': params.repositories.length,
|
|
37
|
+
});
|
|
38
|
+
return { clonedRepositories, failedRepositories };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseSandboxVariant } from 'doistbot-issue-routing';
|
|
2
|
+
import { optionalPositiveInt, requiredEnv, requiredPositiveInt } from '../../core/environment.js';
|
|
3
|
+
import { withPhase } from '../../core/tracing.js';
|
|
4
|
+
import { extractLabelNames, fetchIssue, fetchIssueComments, } from './shared.js';
|
|
5
|
+
function parseOpenAIApiKey(env) {
|
|
6
|
+
const openaiKey = env.OPENAI_KEY?.trim();
|
|
7
|
+
if (!openaiKey) {
|
|
8
|
+
throw new Error('Missing required environment variable: OPENAI_KEY');
|
|
9
|
+
}
|
|
10
|
+
return openaiKey;
|
|
11
|
+
}
|
|
12
|
+
export function parseIssueTriageContext(env = process.env) {
|
|
13
|
+
const commentId = optionalPositiveInt(env.COMMENT_ID);
|
|
14
|
+
const reactionId = optionalPositiveInt(env.REACTION_ID);
|
|
15
|
+
const deliveryId = env.DELIVERY_ID?.trim() || 'unknown';
|
|
16
|
+
const triggerType = env.TRIGGER_TYPE?.toLowerCase() === 'auto' ? 'auto' : 'manual';
|
|
17
|
+
return {
|
|
18
|
+
owner: requiredEnv('REPO_OWNER', env),
|
|
19
|
+
repo: requiredEnv('REPO_NAME', env),
|
|
20
|
+
issueNumber: requiredPositiveInt('ISSUE_NUMBER', env),
|
|
21
|
+
installationId: requiredPositiveInt('INSTALLATION_ID', env),
|
|
22
|
+
appId: requiredEnv('APP_ID', env),
|
|
23
|
+
privateKey: requiredEnv('PRIVATE_KEY', env),
|
|
24
|
+
githubToken: requiredEnv('GITHUB_TOKEN', env),
|
|
25
|
+
openaiApiKey: parseOpenAIApiKey(env),
|
|
26
|
+
geminiApiKey: env.GEMINI_API_KEY?.trim() || undefined,
|
|
27
|
+
commentId,
|
|
28
|
+
reactionId,
|
|
29
|
+
placeholderCommentId: optionalPositiveInt(env.PLACEHOLDER_COMMENT_ID),
|
|
30
|
+
deliveryId,
|
|
31
|
+
dryRun: env.DRY_RUN?.toLowerCase() === 'true',
|
|
32
|
+
triggerType,
|
|
33
|
+
sandboxVariant: parseSandboxVariant(env.SANDBOX_VARIANT),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function loadTriageContext(params) {
|
|
37
|
+
return withPhase('triage.context', { 'issue.number': params.issueNumber }, async (span) => {
|
|
38
|
+
const [issue, comments] = await Promise.all([
|
|
39
|
+
fetchIssue(params.octokit, {
|
|
40
|
+
owner: params.owner,
|
|
41
|
+
repo: params.repo,
|
|
42
|
+
issueNumber: params.issueNumber,
|
|
43
|
+
}),
|
|
44
|
+
fetchIssueComments(params.octokit, {
|
|
45
|
+
owner: params.owner,
|
|
46
|
+
repo: params.repo,
|
|
47
|
+
issueNumber: params.issueNumber,
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
const labelNames = extractLabelNames(issue.labels);
|
|
51
|
+
span.setAttributes({
|
|
52
|
+
'comments.count': comments.length,
|
|
53
|
+
'labels.count': labelNames.length,
|
|
54
|
+
});
|
|
55
|
+
return { issue, comments, labelNames };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { isForbiddenAutofixTest } from '../../core/autofix-test-policy.js';
|
|
3
|
+
import { logger } from '../../core/logging.js';
|
|
4
|
+
const DEFAULT_MAX_CHANGED_LINES = 3000;
|
|
5
|
+
const BINARY_EXTENSIONS = new Set([
|
|
6
|
+
'.png',
|
|
7
|
+
'.jpg',
|
|
8
|
+
'.jpeg',
|
|
9
|
+
'.gif',
|
|
10
|
+
'.ico',
|
|
11
|
+
'.woff',
|
|
12
|
+
'.woff2',
|
|
13
|
+
'.ttf',
|
|
14
|
+
'.eot',
|
|
15
|
+
'.zip',
|
|
16
|
+
'.tar',
|
|
17
|
+
'.gz',
|
|
18
|
+
'.pdf',
|
|
19
|
+
'.exe',
|
|
20
|
+
'.dll',
|
|
21
|
+
'.so',
|
|
22
|
+
'.dylib',
|
|
23
|
+
]);
|
|
24
|
+
function parseDiffStat(diff) {
|
|
25
|
+
const files = [];
|
|
26
|
+
const diffSegments = diff.split(/^diff --git /m).filter((s) => s.trim().length > 0);
|
|
27
|
+
for (const segment of diffSegments) {
|
|
28
|
+
const headerMatch = segment.match(/^a\/(.+?) b\/(.+)/);
|
|
29
|
+
if (!headerMatch) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const sourcePath = headerMatch[1];
|
|
33
|
+
const filePath = headerMatch[2].split('\n')[0].trim();
|
|
34
|
+
let addedLines = 0;
|
|
35
|
+
let removedLines = 0;
|
|
36
|
+
const addedContent = [];
|
|
37
|
+
let inHunk = false;
|
|
38
|
+
const lines = segment.split('\n');
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
if (line.startsWith('@@')) {
|
|
41
|
+
inHunk = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!inHunk) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (line.startsWith('+')) {
|
|
48
|
+
addedLines++;
|
|
49
|
+
addedContent.push(line.slice(1));
|
|
50
|
+
}
|
|
51
|
+
else if (line.startsWith('-')) {
|
|
52
|
+
removedLines++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
files.push({
|
|
56
|
+
filePath,
|
|
57
|
+
sourcePath,
|
|
58
|
+
addedLines,
|
|
59
|
+
removedLines,
|
|
60
|
+
addedContent: addedContent.join('\n'),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
export function validateDiff(diff, limits) {
|
|
66
|
+
const maxChangedLines = limits?.maxChangedLines ?? DEFAULT_MAX_CHANGED_LINES;
|
|
67
|
+
if (!diff.trim()) {
|
|
68
|
+
return { valid: false, reason: 'Diff is empty — no changes were made.' };
|
|
69
|
+
}
|
|
70
|
+
const files = parseDiffStat(diff);
|
|
71
|
+
if (files.length === 0) {
|
|
72
|
+
return { valid: false, reason: 'Could not parse any file changes from the diff.' };
|
|
73
|
+
}
|
|
74
|
+
const totalChangedLines = files.reduce((sum, f) => sum + f.addedLines + f.removedLines, 0);
|
|
75
|
+
if (totalChangedLines > maxChangedLines) {
|
|
76
|
+
return {
|
|
77
|
+
valid: false,
|
|
78
|
+
reason: `Diff has ${totalChangedLines} changed lines (max ${maxChangedLines}).`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
for (const pathToCheck of [file.filePath, file.sourcePath]) {
|
|
83
|
+
if (isForbiddenAutofixTest(pathToCheck, limits?.forbiddenTestPaths, file.addedContent)) {
|
|
84
|
+
return { valid: false, reason: `Autofix cannot change UI tests: ${pathToCheck}` };
|
|
85
|
+
}
|
|
86
|
+
if (BINARY_EXTENSIONS.has(path.extname(pathToCheck).toLowerCase())) {
|
|
87
|
+
return { valid: false, reason: `Diff modifies binary file: ${pathToCheck}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
logger.info('Diff validation passed', {
|
|
92
|
+
task: 'issue-triage',
|
|
93
|
+
filesCount: files.length,
|
|
94
|
+
totalChangedLines,
|
|
95
|
+
files: files.map((f) => ({
|
|
96
|
+
path: f.filePath,
|
|
97
|
+
added: f.addedLines,
|
|
98
|
+
removed: f.removedLines,
|
|
99
|
+
})),
|
|
100
|
+
});
|
|
101
|
+
return { valid: true };
|
|
102
|
+
}
|