@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,108 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { parseBaseContext, requiredEnv } from '../../../core/environment.js';
|
|
3
|
+
import { logger } from '../../../core/logging.js';
|
|
4
|
+
import { createGitHubClients } from '../../../core/shared.js';
|
|
5
|
+
import { findRepositoryTarget } from '../hero-group-map.js';
|
|
6
|
+
import { DefaultLoopEffects } from './effects.js';
|
|
7
|
+
import { runPromotionFlow } from './runners.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parses the env vars dispatched by the webhook handler's
|
|
10
|
+
* promotion-ci-gate route (apps/webhook-handler/src/tasks/promotion-ci-gate/fargate.ts).
|
|
11
|
+
* Throws a descriptive error for any missing or malformed variable so the
|
|
12
|
+
* sandbox task fails fast instead of degrading mid-run.
|
|
13
|
+
*/
|
|
14
|
+
export function parsePromotionTaskInput() {
|
|
15
|
+
return {
|
|
16
|
+
base: parseBaseContext(),
|
|
17
|
+
headSha: requiredEnv('HEAD_SHA'),
|
|
18
|
+
dryRun: process.env.DRY_RUN?.toLowerCase() === 'true',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds the PromotionSession. Promotion is API-only (no codex / push /
|
|
23
|
+
* review), so the session carries only the fields the promotion-flow
|
|
24
|
+
* effects actually read, and cloning the repo is skipped — none of the
|
|
25
|
+
* effects reads `localPath`.
|
|
26
|
+
*/
|
|
27
|
+
export function buildPromotionSession({ input, octokit, }) {
|
|
28
|
+
const { owner, repo } = input.base;
|
|
29
|
+
// Use the issue-routing catalog when the target repository is supported.
|
|
30
|
+
// The review effect can then derive its configured route and hero team.
|
|
31
|
+
const repository = findRepositoryTarget(owner, repo) ?? {
|
|
32
|
+
owner,
|
|
33
|
+
repo,
|
|
34
|
+
fullName: `${owner}/${repo}`,
|
|
35
|
+
// Keep the existing Product type for an unknown target. The repository
|
|
36
|
+
// does not occur in that product's routes, so hero routing uses cx-hero.
|
|
37
|
+
product: 'Todoist',
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
kind: 'fix-loop-promotion',
|
|
41
|
+
ctx: {
|
|
42
|
+
owner,
|
|
43
|
+
repo,
|
|
44
|
+
installationId: input.base.installationId,
|
|
45
|
+
deliveryId: input.base.deliveryId ?? 'unknown',
|
|
46
|
+
dryRun: input.dryRun,
|
|
47
|
+
},
|
|
48
|
+
octokit,
|
|
49
|
+
targetRepo: {
|
|
50
|
+
repository,
|
|
51
|
+
localPath: '',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function runPromotionTask(input) {
|
|
56
|
+
const startedAt = performance.now();
|
|
57
|
+
const { base, headSha, dryRun } = input;
|
|
58
|
+
const repository = `${base.owner}/${base.repo}`;
|
|
59
|
+
logger.setBaseContext({
|
|
60
|
+
delivery_id: base.deliveryId ?? 'unknown',
|
|
61
|
+
repository,
|
|
62
|
+
});
|
|
63
|
+
logger.info('Fix-loop promotion task started', {
|
|
64
|
+
task: 'fix-loop-promotion',
|
|
65
|
+
repository,
|
|
66
|
+
pr_number: base.prNumber,
|
|
67
|
+
head_sha: headSha,
|
|
68
|
+
dry_run: dryRun,
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
const { writeOctokit } = await createGitHubClients(base);
|
|
72
|
+
const session = buildPromotionSession({ input, octokit: writeOctokit });
|
|
73
|
+
const effects = new DefaultLoopEffects(session, { dryRun });
|
|
74
|
+
const finalState = await runPromotionFlow(effects, {
|
|
75
|
+
prNumber: base.prNumber,
|
|
76
|
+
headSha,
|
|
77
|
+
});
|
|
78
|
+
logger.info('Fix-loop promotion task completed', {
|
|
79
|
+
task: 'fix-loop-promotion',
|
|
80
|
+
repository,
|
|
81
|
+
pr_number: base.prNumber,
|
|
82
|
+
terminal_state: finalState.kind,
|
|
83
|
+
duration_ms: performance.now() - startedAt,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.error('Fix-loop promotion task failed', {
|
|
88
|
+
task: 'fix-loop-promotion',
|
|
89
|
+
repository,
|
|
90
|
+
pr_number: base.prNumber,
|
|
91
|
+
head_sha: headSha,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
duration_ms: performance.now() - startedAt,
|
|
94
|
+
});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function main() {
|
|
99
|
+
try {
|
|
100
|
+
await runPromotionTask(parsePromotionTaskInput());
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (process.env.VITEST !== 'true' && process.env.TASK_TYPE === 'fix-loop-promotion') {
|
|
107
|
+
main();
|
|
108
|
+
}
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
7
|
+
import { Ajv } from 'ajv';
|
|
8
|
+
import { sentryArtifactsPrefix } from 'doistbot-issue-routing';
|
|
9
|
+
import { hashAutofixInputs } from '../../../core/autofix-verification.js';
|
|
10
|
+
import { gitAuthEnv, GITHUB_HTTPS_REWRITE_ARGS } from '../../../core/git-auth.js';
|
|
11
|
+
import { githubHistoryIdentity } from '../../../core/github-history.js';
|
|
12
|
+
import { logger } from '../../../core/logging.js';
|
|
13
|
+
import { autofixCheckpointSchema, AutofixCheckpointStore, checkpointIdentity, } from '../autofix-checkpoint.js';
|
|
14
|
+
import { loadAutofixPolicy } from '../autofix-policy.js';
|
|
15
|
+
import { validateDiff } from '../diff-validator.js';
|
|
16
|
+
import { generateBranchName, verifyCommitSignature } from '../pr-creator.js';
|
|
17
|
+
import { loadRepositoryWorkflow, serializeAutofixPolicy } from '../repo-prompts.js';
|
|
18
|
+
import { ISSUE_FIX_INITIAL_STATE } from './state-machine.js';
|
|
19
|
+
const sha = { type: 'string', pattern: '^([a-f0-9]{40}|[a-f0-9]{64})$' };
|
|
20
|
+
const branch = { type: 'string', pattern: '^doistbot/[A-Za-z0-9/_.-]+$', maxLength: 250 };
|
|
21
|
+
const header = {
|
|
22
|
+
kind: { type: 'string' },
|
|
23
|
+
intent: { const: ISSUE_FIX_INITIAL_STATE.intent },
|
|
24
|
+
promotionPolicy: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
required: ['kind'],
|
|
28
|
+
properties: { kind: { const: ISSUE_FIX_INITIAL_STATE.promotionPolicy.kind } },
|
|
29
|
+
},
|
|
30
|
+
maxIterations: { const: ISSUE_FIX_INITIAL_STATE.maxIterations },
|
|
31
|
+
};
|
|
32
|
+
const findings = {
|
|
33
|
+
type: 'array',
|
|
34
|
+
maxItems: 1000,
|
|
35
|
+
items: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
required: ['path', 'line', 'body'],
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: 'string', maxLength: 4096 },
|
|
41
|
+
line: { type: 'integer', minimum: 1 },
|
|
42
|
+
body: { type: 'string', maxLength: 100_000 },
|
|
43
|
+
severity: { enum: ['P0', 'P1', 'P2', 'P3', null] },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function stateSchema(kind, required, properties) {
|
|
48
|
+
return {
|
|
49
|
+
type: 'object',
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
required: ['kind', 'intent', 'promotionPolicy', 'maxIterations', ...required],
|
|
52
|
+
properties: { ...header, kind: { const: kind }, ...properties },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const published = {
|
|
56
|
+
branch,
|
|
57
|
+
prNumber: { type: 'integer', minimum: 1 },
|
|
58
|
+
iteration: { type: 'integer', minimum: 0, maximum: 1 },
|
|
59
|
+
};
|
|
60
|
+
const validateRecord = new Ajv().compile({
|
|
61
|
+
type: 'object',
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
required: [
|
|
64
|
+
'version',
|
|
65
|
+
'reportHash',
|
|
66
|
+
'policyHash',
|
|
67
|
+
'originalBase',
|
|
68
|
+
'branch',
|
|
69
|
+
'state',
|
|
70
|
+
'attempt',
|
|
71
|
+
'generated',
|
|
72
|
+
'runReport',
|
|
73
|
+
'savedAt',
|
|
74
|
+
],
|
|
75
|
+
properties: {
|
|
76
|
+
version: { const: 2 },
|
|
77
|
+
reportHash: sha,
|
|
78
|
+
policyHash: sha,
|
|
79
|
+
originalBase: sha,
|
|
80
|
+
branch,
|
|
81
|
+
savedAt: { type: 'string' },
|
|
82
|
+
generated: {
|
|
83
|
+
anyOf: [
|
|
84
|
+
{ type: 'null' },
|
|
85
|
+
{
|
|
86
|
+
type: 'object',
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
required: ['ok', 'codexOutput', 'generatedPRTitle', 'generatedPRBody'],
|
|
89
|
+
properties: {
|
|
90
|
+
ok: { const: true },
|
|
91
|
+
diff: { type: 'string', maxLength: 5 * 1024 * 1024 },
|
|
92
|
+
codexOutput: { type: 'string', maxLength: 2 * 1024 * 1024 },
|
|
93
|
+
generatedPRTitle: { type: ['string', 'null'] },
|
|
94
|
+
generatedPRBody: { type: ['string', 'null'] },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
runReport: { type: ['string', 'null'], maxLength: 5 * 1024 * 1024 },
|
|
100
|
+
state: {
|
|
101
|
+
anyOf: [
|
|
102
|
+
stateSchema('AWAITING_FIRST_FIX', [], {}),
|
|
103
|
+
stateSchema('ITERATING', ['iteration', 'findings', 'prevHeadSha', 'branch', 'prNumber'], { ...published, iteration: { const: 1 }, findings, prevHeadSha: sha }),
|
|
104
|
+
stateSchema('REVIEWING', ['iteration', 'headSha', 'branch', 'prNumber'], {
|
|
105
|
+
...published,
|
|
106
|
+
headSha: sha,
|
|
107
|
+
reviewBaseSha: sha,
|
|
108
|
+
}),
|
|
109
|
+
stateSchema('PUSHED_AWAITING_CI', ['headSha', 'prNumber'], {
|
|
110
|
+
...published,
|
|
111
|
+
headSha: sha,
|
|
112
|
+
}),
|
|
113
|
+
stateSchema('DONE_NEEDS_HUMAN', ['reason', 'findings'], {
|
|
114
|
+
branch,
|
|
115
|
+
prNumber: published.prNumber,
|
|
116
|
+
reason: { type: 'string' },
|
|
117
|
+
findings,
|
|
118
|
+
}),
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
attempt: {
|
|
122
|
+
anyOf: [
|
|
123
|
+
{ type: 'null' },
|
|
124
|
+
{
|
|
125
|
+
...autofixCheckpointSchema,
|
|
126
|
+
properties: {
|
|
127
|
+
...autofixCheckpointSchema.properties,
|
|
128
|
+
identity: sha,
|
|
129
|
+
baseCommit: sha,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
export class IssueFixRecovery {
|
|
137
|
+
input;
|
|
138
|
+
directory = path.join(tmpdir(), 'doistbot-fix-loop', randomUUID());
|
|
139
|
+
s3;
|
|
140
|
+
key;
|
|
141
|
+
policy;
|
|
142
|
+
policyIdentity;
|
|
143
|
+
record;
|
|
144
|
+
etag;
|
|
145
|
+
shared = false;
|
|
146
|
+
publishedHead;
|
|
147
|
+
existingPR;
|
|
148
|
+
constructor(input) {
|
|
149
|
+
this.input = input;
|
|
150
|
+
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
151
|
+
this.s3 = input.bucket ? new S3Client({ maxAttempts: 2 }) : undefined;
|
|
152
|
+
this.policy = loadAutofixPolicy(input.targetRepo.localPath);
|
|
153
|
+
this.policyIdentity = serializeAutofixPolicy(this.policy, loadRepositoryWorkflow(input.targetRepo.localPath, 'autofix'));
|
|
154
|
+
const identity = hashAutofixInputs(`${githubHistoryIdentity(input.issueUrl)}\n${githubHistoryIdentity(input.targetRepo.repository.fullName)}`);
|
|
155
|
+
const prefix = input.sentryIssue ? sentryArtifactsPrefix(input.sentryIssue) : 'autofix/';
|
|
156
|
+
this.key = `${prefix}workflow-${identity}/checkpoint.json`;
|
|
157
|
+
this.record = {
|
|
158
|
+
version: 2,
|
|
159
|
+
reportHash: hashAutofixInputs(input.triageOutput.trim()),
|
|
160
|
+
policyHash: hashAutofixInputs(this.policyIdentity),
|
|
161
|
+
originalBase: this.git(['rev-parse', 'HEAD']),
|
|
162
|
+
branch: generateBranchName(input),
|
|
163
|
+
state: ISSUE_FIX_INITIAL_STATE,
|
|
164
|
+
attempt: null,
|
|
165
|
+
generated: null,
|
|
166
|
+
runReport: null,
|
|
167
|
+
savedAt: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
get state() {
|
|
171
|
+
return this.record.state;
|
|
172
|
+
}
|
|
173
|
+
get publicationBranch() {
|
|
174
|
+
return this.record.branch;
|
|
175
|
+
}
|
|
176
|
+
get checkpoint() {
|
|
177
|
+
return this.record.attempt ?? undefined;
|
|
178
|
+
}
|
|
179
|
+
get originalBase() {
|
|
180
|
+
return this.record.originalBase;
|
|
181
|
+
}
|
|
182
|
+
get generated() {
|
|
183
|
+
return this.record.generated && this.record.attempt
|
|
184
|
+
? { ...this.record.generated, diff: this.record.attempt.patch }
|
|
185
|
+
: undefined;
|
|
186
|
+
}
|
|
187
|
+
git(args, input) {
|
|
188
|
+
return execFileSync('git', args, {
|
|
189
|
+
cwd: this.input.targetRepo.localPath,
|
|
190
|
+
encoding: 'utf8',
|
|
191
|
+
stdio: 'pipe',
|
|
192
|
+
input,
|
|
193
|
+
timeout: 120_000,
|
|
194
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
195
|
+
env: gitAuthEnv(this.input.githubToken),
|
|
196
|
+
}).trim();
|
|
197
|
+
}
|
|
198
|
+
checkpointIdentity(baseCommit) {
|
|
199
|
+
return checkpointIdentity({
|
|
200
|
+
issueUrl: this.input.issueUrl,
|
|
201
|
+
repository: this.input.targetRepo.repository.fullName,
|
|
202
|
+
baseCommit,
|
|
203
|
+
triageOutput: this.input.triageOutput,
|
|
204
|
+
policy: this.policyIdentity,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async findSavedPR(data) {
|
|
208
|
+
const { owner, repo } = this.input.targetRepo.repository;
|
|
209
|
+
const prs = await this.input.octokit.rest.pulls.list({
|
|
210
|
+
owner,
|
|
211
|
+
repo,
|
|
212
|
+
head: `${owner}:${data.branch}`,
|
|
213
|
+
state: 'all',
|
|
214
|
+
per_page: 100,
|
|
215
|
+
});
|
|
216
|
+
return prs.data.find((item) => item.head.ref === data.branch &&
|
|
217
|
+
// PR descriptions written before the rename still contain the old URL.
|
|
218
|
+
item.body
|
|
219
|
+
?.replaceAll('https://github.com/todoist/', 'https://github.com/Doist/')
|
|
220
|
+
.includes(githubHistoryIdentity(this.input.issueUrl)));
|
|
221
|
+
}
|
|
222
|
+
ensureCommit(commit) {
|
|
223
|
+
try {
|
|
224
|
+
this.git(['cat-file', '-e', `${commit}^{commit}`]);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
this.git(['fetch', '--depth=1', 'origin', commit]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async prepare(retry) {
|
|
231
|
+
if (!this.s3 || !this.input.bucket)
|
|
232
|
+
return undefined;
|
|
233
|
+
let object;
|
|
234
|
+
try {
|
|
235
|
+
if (this.input.sentryIssue) {
|
|
236
|
+
// A missing key returns 403 with our prefix-scoped ListBucket grant.
|
|
237
|
+
const listed = await this.s3.send(new ListObjectsV2Command({
|
|
238
|
+
Bucket: this.input.bucket,
|
|
239
|
+
Prefix: this.key,
|
|
240
|
+
MaxKeys: 1,
|
|
241
|
+
}), { abortSignal: AbortSignal.timeout(15_000) });
|
|
242
|
+
if (!listed.Contents?.some((object) => object.Key === this.key)) {
|
|
243
|
+
if (retry)
|
|
244
|
+
throw new Error('Saved Sentry fix checkpoint is missing');
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
object = await this.s3.send(new GetObjectCommand({ Bucket: this.input.bucket, Key: this.key }), { abortSignal: AbortSignal.timeout(15_000) });
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (!(error instanceof Error) || !['NoSuchKey', 'NotFound'].includes(error.name))
|
|
252
|
+
throw error;
|
|
253
|
+
// Expiry can race a retry after its report was loaded. Never reset PR identity.
|
|
254
|
+
if (this.input.sentryIssue && retry)
|
|
255
|
+
throw error;
|
|
256
|
+
if (retry && !this.input.sentryIssue) {
|
|
257
|
+
this.record.attempt = await new AutofixCheckpointStore(this.checkpointIdentity(this.record.originalBase), this.input.bucket, path.join(this.directory, 'legacy')).load();
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
if (!object.Body || (object.ContentLength ?? 0) > 10 * 1024 * 1024)
|
|
262
|
+
throw new Error('Invalid saved fix size');
|
|
263
|
+
const data = JSON.parse(await object.Body.transformToString());
|
|
264
|
+
if (!validateRecord(data))
|
|
265
|
+
throw new Error('Invalid saved fix state');
|
|
266
|
+
this.etag = object.ETag;
|
|
267
|
+
this.existingPR = 'prNumber' in data.state ? data.state.prNumber : undefined;
|
|
268
|
+
if ('branch' in data.state &&
|
|
269
|
+
data.state.branch !== undefined &&
|
|
270
|
+
data.state.branch !== data.branch)
|
|
271
|
+
throw new Error('Saved branch does not match the loop step');
|
|
272
|
+
if (data.reportHash !== this.record.reportHash ||
|
|
273
|
+
data.policyHash !== this.record.policyHash) {
|
|
274
|
+
if (data.reportHash !== this.record.reportHash ||
|
|
275
|
+
data.state.kind === 'DONE_NEEDS_HUMAN') {
|
|
276
|
+
const pr = await this.findSavedPR(data);
|
|
277
|
+
if (pr?.state === 'open' && !pr.merged_at) {
|
|
278
|
+
this.existingPR = pr.number;
|
|
279
|
+
throw new Error(`The saved fix has a different report, check policy, or workflow prompt. Close PR #${pr.number} before starting a new fix. The previous work is kept.`);
|
|
280
|
+
}
|
|
281
|
+
if (this.existingPR !== undefined && !pr)
|
|
282
|
+
throw new Error('The saved fix PR could not be found. The previous work is kept.');
|
|
283
|
+
this.existingPR = undefined;
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
throw new Error('The saved fix has a different report, check policy, or workflow prompt. It cannot be resumed.');
|
|
287
|
+
}
|
|
288
|
+
if (data.state.kind === 'DONE_NEEDS_HUMAN' && data.state.prNumber === undefined)
|
|
289
|
+
return undefined;
|
|
290
|
+
if (data.generated) {
|
|
291
|
+
if (!data.attempt)
|
|
292
|
+
throw new Error('Saved model output has no patch');
|
|
293
|
+
if ('diff' in data.generated &&
|
|
294
|
+
typeof data.generated.diff === 'string' &&
|
|
295
|
+
data.generated.diff.trim() !== data.attempt.patch.trim())
|
|
296
|
+
throw new Error('Saved model output does not match the patch');
|
|
297
|
+
const { ok, codexOutput, generatedPRTitle, generatedPRBody } = data.generated;
|
|
298
|
+
data.generated = { ok, codexOutput, generatedPRTitle, generatedPRBody };
|
|
299
|
+
}
|
|
300
|
+
this.record = data;
|
|
301
|
+
const state = data.state;
|
|
302
|
+
if (state.kind === 'DONE_NEEDS_HUMAN')
|
|
303
|
+
return state;
|
|
304
|
+
const pr = await this.findSavedPR(data);
|
|
305
|
+
if (pr) {
|
|
306
|
+
this.existingPR = pr.number;
|
|
307
|
+
if (pr.state !== 'open' || pr.merged_at)
|
|
308
|
+
throw new Error(`Saved PR #${pr.number} is closed. The fix cannot be resumed.`);
|
|
309
|
+
const expected = state.kind === 'ITERATING'
|
|
310
|
+
? state.prevHeadSha
|
|
311
|
+
: 'headSha' in state
|
|
312
|
+
? state.headSha
|
|
313
|
+
: undefined;
|
|
314
|
+
const generated = this.generated;
|
|
315
|
+
const pushedBeforeSave = expected !== pr.head.sha &&
|
|
316
|
+
generated !== undefined &&
|
|
317
|
+
(state.kind === 'ITERATING' || state.kind === 'AWAITING_FIRST_FIX');
|
|
318
|
+
if (expected && pr.head.sha !== expected && !pushedBeforeSave)
|
|
319
|
+
throw new Error(`Saved PR #${pr.number} has changed. The fix cannot be resumed.`);
|
|
320
|
+
if ('prNumber' in state && state.prNumber !== pr.number)
|
|
321
|
+
throw new Error('Saved PR identity does not match');
|
|
322
|
+
this.git(['fetch', '--depth=1', 'origin', data.branch]);
|
|
323
|
+
if (this.git(['rev-parse', 'FETCH_HEAD']) !== pr.head.sha)
|
|
324
|
+
throw new Error('The remote fix branch changed during restore');
|
|
325
|
+
await verifyCommitSignature({
|
|
326
|
+
octokit: this.input.octokit,
|
|
327
|
+
repoOwner: this.input.targetRepo.repository.owner,
|
|
328
|
+
repoName: this.input.targetRepo.repository.repo,
|
|
329
|
+
headSha: pr.head.sha,
|
|
330
|
+
});
|
|
331
|
+
this.ensureCommit(data.originalBase);
|
|
332
|
+
const reviewBase = state.kind === 'ITERATING'
|
|
333
|
+
? state.prevHeadSha
|
|
334
|
+
: state.kind === 'REVIEWING'
|
|
335
|
+
? (state.reviewBaseSha ?? data.originalBase)
|
|
336
|
+
: data.originalBase;
|
|
337
|
+
this.ensureCommit(reviewBase);
|
|
338
|
+
if (pushedBeforeSave && generated) {
|
|
339
|
+
if (this.git([
|
|
340
|
+
'diff',
|
|
341
|
+
'--no-ext-diff',
|
|
342
|
+
'--no-textconv',
|
|
343
|
+
'--full-index',
|
|
344
|
+
'--binary',
|
|
345
|
+
reviewBase,
|
|
346
|
+
pr.head.sha,
|
|
347
|
+
]) !== generated.diff.trim())
|
|
348
|
+
throw new Error('The published patch does not match the saved fix');
|
|
349
|
+
}
|
|
350
|
+
if (pushedBeforeSave || state.kind === 'AWAITING_FIRST_FIX') {
|
|
351
|
+
this.record.state = {
|
|
352
|
+
...ISSUE_FIX_INITIAL_STATE,
|
|
353
|
+
kind: 'REVIEWING',
|
|
354
|
+
iteration: state.kind === 'ITERATING' ? state.iteration : 0,
|
|
355
|
+
branch: data.branch,
|
|
356
|
+
prNumber: pr.number,
|
|
357
|
+
headSha: pr.head.sha,
|
|
358
|
+
reviewBaseSha: reviewBase,
|
|
359
|
+
};
|
|
360
|
+
this.record.attempt = null;
|
|
361
|
+
this.record.generated = null;
|
|
362
|
+
}
|
|
363
|
+
this.git(['checkout', '-B', data.branch, pr.head.sha]);
|
|
364
|
+
}
|
|
365
|
+
else if (state.kind !== 'AWAITING_FIRST_FIX') {
|
|
366
|
+
throw new Error('The saved fix PR could not be found. The fix cannot be resumed.');
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
this.ensureCommit(data.originalBase);
|
|
370
|
+
this.git(['checkout', '--detach', data.originalBase]);
|
|
371
|
+
}
|
|
372
|
+
if (!pr && this.generated) {
|
|
373
|
+
const generated = this.generated;
|
|
374
|
+
const remote = this.git(['ls-remote', '--heads', 'origin', data.branch]);
|
|
375
|
+
if (remote) {
|
|
376
|
+
this.git(['fetch', '--depth=1', 'origin', data.branch]);
|
|
377
|
+
this.publishedHead = this.git(['rev-parse', 'FETCH_HEAD']);
|
|
378
|
+
await verifyCommitSignature({
|
|
379
|
+
octokit: this.input.octokit,
|
|
380
|
+
repoOwner: this.input.targetRepo.repository.owner,
|
|
381
|
+
repoName: this.input.targetRepo.repository.repo,
|
|
382
|
+
headSha: this.publishedHead,
|
|
383
|
+
});
|
|
384
|
+
if (this.git([
|
|
385
|
+
'diff',
|
|
386
|
+
'--no-ext-diff',
|
|
387
|
+
'--no-textconv',
|
|
388
|
+
'--full-index',
|
|
389
|
+
'--binary',
|
|
390
|
+
data.originalBase,
|
|
391
|
+
this.publishedHead,
|
|
392
|
+
]) !== generated.diff.trim()) {
|
|
393
|
+
throw new Error('The published patch does not match the saved fix');
|
|
394
|
+
}
|
|
395
|
+
this.git(['checkout', '-B', data.branch, this.publishedHead]);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!pr) {
|
|
399
|
+
this.git(['submodule', 'sync', '--recursive']);
|
|
400
|
+
this.git([
|
|
401
|
+
...GITHUB_HTTPS_REWRITE_ARGS,
|
|
402
|
+
'submodule',
|
|
403
|
+
'update',
|
|
404
|
+
'--init',
|
|
405
|
+
'--recursive',
|
|
406
|
+
'--depth=1',
|
|
407
|
+
]);
|
|
408
|
+
}
|
|
409
|
+
const policy = loadAutofixPolicy(this.input.targetRepo.localPath);
|
|
410
|
+
const policyIdentity = serializeAutofixPolicy(policy, loadRepositoryWorkflow(this.input.targetRepo.localPath, 'autofix'));
|
|
411
|
+
if (hashAutofixInputs(policyIdentity) !== data.policyHash)
|
|
412
|
+
throw new Error('The check policy or workflow prompt on the saved branch has changed');
|
|
413
|
+
if (this.record.attempt) {
|
|
414
|
+
const expectedBase = this.record.state.kind === 'ITERATING'
|
|
415
|
+
? this.record.state.prevHeadSha
|
|
416
|
+
: data.originalBase;
|
|
417
|
+
const expectedIdentity = this.checkpointIdentity(expectedBase);
|
|
418
|
+
if (this.record.attempt.identity !== expectedIdentity)
|
|
419
|
+
throw new Error('Saved patch inputs do not match');
|
|
420
|
+
if (this.record.attempt.baseCommit !== expectedBase)
|
|
421
|
+
throw new Error('Saved patch base does not match the loop step');
|
|
422
|
+
const validation = validateDiff(this.record.attempt.patch, {
|
|
423
|
+
forbiddenTestPaths: policy.forbidden_test_paths,
|
|
424
|
+
});
|
|
425
|
+
if (this.record.attempt.patch && !validation.valid)
|
|
426
|
+
throw new Error('Saved patch does not meet the current fix rules');
|
|
427
|
+
if (this.record.generated) {
|
|
428
|
+
if (!this.publishedHead) {
|
|
429
|
+
this.git(['reset', '--hard', expectedBase]);
|
|
430
|
+
this.git(['clean', '-fd']);
|
|
431
|
+
this.git(['apply', '--index', '--whitespace=nowarn', '-'], this.record.attempt.patch);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
this.shared = true;
|
|
436
|
+
return this.record.state;
|
|
437
|
+
}
|
|
438
|
+
async beforeStep(state) {
|
|
439
|
+
if (this.record.state.kind !== state.kind ||
|
|
440
|
+
state.kind === 'REVIEWING' ||
|
|
441
|
+
state.kind === 'PUSHED_AWAITING_CI') {
|
|
442
|
+
this.record.attempt = null;
|
|
443
|
+
this.record.generated = null;
|
|
444
|
+
this.record.runReport = null;
|
|
445
|
+
}
|
|
446
|
+
this.record.state = state;
|
|
447
|
+
if (state.kind === 'REVIEWING' && state.reviewBaseSha === undefined)
|
|
448
|
+
this.record.state = { ...state, reviewBaseSha: this.record.originalBase };
|
|
449
|
+
await this.save();
|
|
450
|
+
}
|
|
451
|
+
async saveGenerated(result) {
|
|
452
|
+
const { ok, codexOutput, generatedPRTitle, generatedPRBody } = result;
|
|
453
|
+
this.record.generated = { ok, codexOutput, generatedPRTitle, generatedPRBody };
|
|
454
|
+
await this.save();
|
|
455
|
+
}
|
|
456
|
+
async saveAttempt(checkpoint, report) {
|
|
457
|
+
this.record.attempt = checkpoint;
|
|
458
|
+
this.record.runReport = report;
|
|
459
|
+
try {
|
|
460
|
+
writeFileSync(path.join(this.directory, 'run.json'), report, { mode: 0o600 });
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
logger.error('Could not save fix run data', { error: String(error) });
|
|
464
|
+
}
|
|
465
|
+
const canResume = checkpoint.status !== 'failed' &&
|
|
466
|
+
(!checkpoint.patch ||
|
|
467
|
+
validateDiff(checkpoint.patch, {
|
|
468
|
+
forbiddenTestPaths: this.policy.forbidden_test_paths,
|
|
469
|
+
}).valid);
|
|
470
|
+
return this.save(canResume);
|
|
471
|
+
}
|
|
472
|
+
async save(publish = true) {
|
|
473
|
+
this.record.savedAt = new Date().toISOString();
|
|
474
|
+
const body = JSON.stringify(this.record);
|
|
475
|
+
this.shared = false;
|
|
476
|
+
try {
|
|
477
|
+
writeFileSync(path.join(this.directory, 'checkpoint.json'), body, { mode: 0o600 });
|
|
478
|
+
if (!validateRecord(this.record) || Buffer.byteLength(body) > 10 * 1024 * 1024) {
|
|
479
|
+
logger.error('Fix loop state cannot be restored', { errors: validateRecord.errors });
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (!this.s3 || !this.input.bucket)
|
|
483
|
+
return false;
|
|
484
|
+
await this.s3.send(new PutObjectCommand({
|
|
485
|
+
Bucket: this.input.bucket,
|
|
486
|
+
Key: `${path.dirname(this.key)}/runs/${path.basename(this.directory)}/${randomUUID()}.json`,
|
|
487
|
+
Body: body,
|
|
488
|
+
ContentType: 'application/json',
|
|
489
|
+
}), { abortSignal: AbortSignal.timeout(15_000) });
|
|
490
|
+
if (!publish)
|
|
491
|
+
return false;
|
|
492
|
+
const result = await this.s3.send(new PutObjectCommand({
|
|
493
|
+
Bucket: this.input.bucket,
|
|
494
|
+
Key: this.key,
|
|
495
|
+
Body: body,
|
|
496
|
+
ContentType: 'application/json',
|
|
497
|
+
...(this.etag ? { IfMatch: this.etag } : { IfNoneMatch: '*' }),
|
|
498
|
+
}), { abortSignal: AbortSignal.timeout(15_000) });
|
|
499
|
+
this.etag = result.ETag;
|
|
500
|
+
this.shared = true;
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
if (error instanceof Error && error.name === 'PreconditionFailed')
|
|
504
|
+
throw new Error('Another fix task changed the saved work. This task stopped to keep that work.');
|
|
505
|
+
logger.error('Could not save fix loop in shared storage', {
|
|
506
|
+
directory: this.directory,
|
|
507
|
+
error: String(error),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return this.shared;
|
|
511
|
+
}
|
|
512
|
+
}
|