@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.
Files changed (185) hide show
  1. package/dist/actions/auth.js +225 -0
  2. package/dist/actions/doctor.js +39 -0
  3. package/dist/actions/review-usage.js +25 -0
  4. package/dist/actions/review.js +311 -0
  5. package/dist/actions/setup.js +14 -0
  6. package/dist/auth.js +202 -0
  7. package/dist/config.js +145 -0
  8. package/dist/env.js +10 -0
  9. package/dist/file-store.js +112 -0
  10. package/dist/index.js +164 -0
  11. package/dist/input.js +40 -0
  12. package/dist/runtime.js +105 -0
  13. package/dist/secure-store.js +148 -0
  14. package/dist/terminal.js +185 -0
  15. package/dist/test-utils/secure-store-mocks.js +29 -0
  16. package/dist/update-cache-entrypoint.js +16 -0
  17. package/dist/update-check.js +235 -0
  18. package/package.json +65 -5
  19. package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
  20. package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
  21. package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
  22. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
  23. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
  24. package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
  25. package/sandbox/dist/core/autofix-resources.js +58 -0
  26. package/sandbox/dist/core/autofix-service-query.js +69 -0
  27. package/sandbox/dist/core/autofix-test-policy.js +11 -0
  28. package/sandbox/dist/core/autofix-verification.js +355 -0
  29. package/sandbox/dist/core/bot-logins.js +14 -0
  30. package/sandbox/dist/core/check-run.js +158 -0
  31. package/sandbox/dist/core/datadog-metrics.js +113 -0
  32. package/sandbox/dist/core/environment.js +49 -0
  33. package/sandbox/dist/core/git-auth.js +17 -0
  34. package/sandbox/dist/core/github-history.js +4 -0
  35. package/sandbox/dist/core/github-issues.js +18 -0
  36. package/sandbox/dist/core/label-parsing.js +1 -0
  37. package/sandbox/dist/core/langfuse-observation.js +33 -0
  38. package/sandbox/dist/core/langfuse-session.js +39 -0
  39. package/sandbox/dist/core/logger-span-processor.js +90 -0
  40. package/sandbox/dist/core/logging.js +18 -0
  41. package/sandbox/dist/core/pi-autofix-extension.js +188 -0
  42. package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
  43. package/sandbox/dist/core/pi-progress-logging.js +51 -0
  44. package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
  45. package/sandbox/dist/core/pi.js +870 -0
  46. package/sandbox/dist/core/pii-scrubber.js +31 -0
  47. package/sandbox/dist/core/prompt-template.js +63 -0
  48. package/sandbox/dist/core/reactions.js +36 -0
  49. package/sandbox/dist/core/regexp.js +3 -0
  50. package/sandbox/dist/core/repository-map.js +1 -0
  51. package/sandbox/dist/core/repository-resolver.js +1 -0
  52. package/sandbox/dist/core/retry.js +29 -0
  53. package/sandbox/dist/core/s3-utils.js +42 -0
  54. package/sandbox/dist/core/shared.js +124 -0
  55. package/sandbox/dist/core/snapshot-helpers.js +17 -0
  56. package/sandbox/dist/core/span-test-helpers.js +13 -0
  57. package/sandbox/dist/core/tool-environment.js +83 -0
  58. package/sandbox/dist/core/tool-output.js +18 -0
  59. package/sandbox/dist/core/tracing-setup.js +92 -0
  60. package/sandbox/dist/core/tracing.js +81 -0
  61. package/sandbox/dist/main.js +51 -0
  62. package/sandbox/dist/providers/config.js +23 -0
  63. package/sandbox/dist/providers/credentials.js +40 -0
  64. package/sandbox/dist/providers/gemini.js +28 -0
  65. package/sandbox/dist/providers/helpers.js +253 -0
  66. package/sandbox/dist/providers/index.js +27 -0
  67. package/sandbox/dist/providers/openai.js +23 -0
  68. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  69. package/sandbox/dist/providers/pi-review.js +301 -0
  70. package/sandbox/dist/providers/process.js +34 -0
  71. package/sandbox/dist/providers/schemas.js +61 -0
  72. package/sandbox/dist/providers/types.js +4 -0
  73. package/sandbox/dist/tasks/chat/chat.js +440 -0
  74. package/sandbox/dist/tasks/chat/prompt.js +20 -0
  75. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
  76. package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
  77. package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
  78. package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
  79. package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
  80. package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
  81. package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
  82. package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
  83. package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
  84. package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
  85. package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
  86. package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
  87. package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
  88. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  89. package/sandbox/dist/tasks/issue-triage/context.js +57 -0
  90. package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
  91. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
  92. package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
  93. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
  94. package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
  95. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
  96. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
  97. package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
  98. package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
  99. package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
  100. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
  101. package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
  102. package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
  103. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
  104. package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
  105. package/sandbox/dist/tasks/issue-triage/model.js +124 -0
  106. package/sandbox/dist/tasks/issue-triage/output.js +164 -0
  107. package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
  108. package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
  109. package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
  110. package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
  111. package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
  112. package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
  113. package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
  114. package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
  115. package/sandbox/dist/tasks/issue-triage/report.js +111 -0
  116. package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
  117. package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
  118. package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
  119. package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
  120. package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
  121. package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
  122. package/sandbox/dist/tasks/issue-triage/types.js +1 -0
  123. package/sandbox/dist/tasks/persist-logs/context.js +19 -0
  124. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
  125. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
  126. package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
  127. package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
  128. package/sandbox/dist/tasks/review/check-run.js +326 -0
  129. package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
  130. package/sandbox/dist/tasks/review/conversation-context.js +477 -0
  131. package/sandbox/dist/tasks/review/diff.js +95 -0
  132. package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
  133. package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
  134. package/sandbox/dist/tasks/review/engines/shared.js +87 -0
  135. package/sandbox/dist/tasks/review/engines/types.js +1 -0
  136. package/sandbox/dist/tasks/review/finding-output.js +18 -0
  137. package/sandbox/dist/tasks/review/format.js +17 -0
  138. package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
  139. package/sandbox/dist/tasks/review/local-git.js +374 -0
  140. package/sandbox/dist/tasks/review/local.js +118 -0
  141. package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
  142. package/sandbox/dist/tasks/review/prompt.js +81 -0
  143. package/sandbox/dist/tasks/review/review-feedback.js +48 -0
  144. package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
  145. package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
  146. package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
  147. package/sandbox/dist/tasks/review/review-mode.js +296 -0
  148. package/sandbox/dist/tasks/review/review-summary.js +123 -0
  149. package/sandbox/dist/tasks/review/review.js +391 -0
  150. package/sandbox/dist/tasks/review/runner.js +255 -0
  151. package/sandbox/dist/tasks/review/severity.js +5 -0
  152. package/sandbox/dist/tasks/review/summary-model.js +271 -0
  153. package/sandbox/dist/tasks/review/thinking-level.js +14 -0
  154. package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
  155. package/sandbox/dist/tasks/review/types.js +1 -0
  156. package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
  157. package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
  158. package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
  159. package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
  160. package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
  161. package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
  162. package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
  163. package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
  164. package/sandbox/node_modules/doistbot-logging/package.json +12 -0
  165. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
  166. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
  167. package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
  168. package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
  169. package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
  170. package/sandbox/src/review/prompts/observability-standard.md +169 -0
  171. package/sandbox/src/review/prompts/outline-map.json +26 -0
  172. package/sandbox/src/review/prompts/secrets-management.md +286 -0
  173. package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
  174. package/sandbox/src/review/prompts/workload-placement.md +345 -0
  175. package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
  176. package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
  177. package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
  178. package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
  179. package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
  180. package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
  181. package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
  182. package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
  183. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
  184. package/scripts/restore-runtime-assets.mjs +21 -0
  185. package/README.md +0 -3
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "doistbot-repo-config",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from 'node:fs';
2
+ function readNumber(file) {
3
+ try {
4
+ const value = Number(readFileSync(file, 'utf8').trim());
5
+ return Number.isFinite(value) ? value : null;
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ function readCounter(file, key) {
12
+ try {
13
+ const match = readFileSync(file, 'utf8').match(new RegExp(`^${key}\\s+(\\d+)$`, 'm'));
14
+ return match?.[1] ? Number(match[1]) : null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export function sampleAutofixResources(elapsedMs) {
21
+ const cpuMicros = readCounter('/sys/fs/cgroup/cpu.stat', 'usage_usec');
22
+ const cpuNanos = cpuMicros === null ? readNumber('/sys/fs/cgroup/cpuacct/cpuacct.usage') : null;
23
+ return {
24
+ elapsedMs,
25
+ cpuSeconds: cpuMicros === null ? (cpuNanos === null ? null : cpuNanos / 1e9) : cpuMicros / 1e6,
26
+ memoryBytes: readNumber('/sys/fs/cgroup/memory.current') ??
27
+ readNumber('/sys/fs/cgroup/memory/memory.usage_in_bytes'),
28
+ memoryLimitBytes: readNumber('/sys/fs/cgroup/memory.max') ??
29
+ readNumber('/sys/fs/cgroup/memory/memory.limit_in_bytes'),
30
+ oomKills: readCounter('/sys/fs/cgroup/memory.events', 'oom_kill') ??
31
+ readCounter('/sys/fs/cgroup/memory/memory.oom_control', 'oom_kill'),
32
+ };
33
+ }
34
+ export function summarizeAutofixResources(samples) {
35
+ const first = samples[0];
36
+ const last = samples.at(-1);
37
+ const memory = samples.flatMap((sample) => sample.memoryBytes === null ? [] : [sample.memoryBytes]);
38
+ let peakCpuCores = null;
39
+ for (let index = 1; index < samples.length; index++) {
40
+ const previous = samples[index - 1];
41
+ const current = samples[index];
42
+ const seconds = (current.elapsedMs - previous.elapsedMs) / 1000;
43
+ if (seconds > 0 && current.cpuSeconds !== null && previous.cpuSeconds !== null) {
44
+ peakCpuCores = Math.max(peakCpuCores ?? 0, (current.cpuSeconds - previous.cpuSeconds) / seconds);
45
+ }
46
+ }
47
+ return {
48
+ sampledPeakMemoryBytes: memory.length ? Math.max(...memory) : null,
49
+ memoryLimitBytes: last?.memoryLimitBytes ?? null,
50
+ cpuSeconds: first?.cpuSeconds != null && last?.cpuSeconds != null
51
+ ? Math.max(0, last.cpuSeconds - first.cpuSeconds)
52
+ : null,
53
+ peakCpuCores,
54
+ oomKills: first?.oomKills != null && last?.oomKills != null
55
+ ? Math.max(0, last.oomKills - first.oomKills)
56
+ : null,
57
+ };
58
+ }
@@ -0,0 +1,69 @@
1
+ export function serviceQueryArgs(cli, args) {
2
+ if (!args.length || args.length > 100 || args.some((arg) => arg.includes('\0'))) {
3
+ throw new Error('Supply CLI arguments for a read-only evidence query');
4
+ }
5
+ if (!args.includes('--') &&
6
+ (args.at(-1) === '--help' || args.at(-1) === '-h' || args[0] === '--version'))
7
+ return args;
8
+ const [group, action] = args;
9
+ const route = `${group} ${action}`;
10
+ if (cli === 'gh') {
11
+ for (let index = 0; index < args.length; index++) {
12
+ const arg = args[index];
13
+ if (/^--hostname(?:=|$)/.test(arg))
14
+ throw new Error('GitHub evidence queries must use github.com');
15
+ if (/^https?:\/\//i.test(arg)) {
16
+ const url = new URL(arg);
17
+ if (url.protocol !== 'https:' ||
18
+ url.host !== 'github.com' ||
19
+ url.username ||
20
+ url.password)
21
+ throw new Error('GitHub evidence queries must use HTTPS on github.com');
22
+ }
23
+ const repo = arg === '-R' || arg === '--repo'
24
+ ? args[index + 1]
25
+ : arg.startsWith('--repo=')
26
+ ? arg.slice(7)
27
+ : arg.startsWith('-R')
28
+ ? arg.slice(2)
29
+ : route === 'repo view' && index === 2 && !arg.startsWith('-')
30
+ ? arg
31
+ : undefined;
32
+ if (repo &&
33
+ !repo.startsWith('https://github.com/') &&
34
+ repo.split('/').length > 2 &&
35
+ !repo.startsWith('github.com/'))
36
+ throw new Error('GitHub repository queries must use github.com');
37
+ }
38
+ if (group === 'api') {
39
+ if (args.some((arg) => /^-(?:X|f|F)|^--(?:method|field|raw-field|input)(?:=|$)/.test(arg))) {
40
+ throw new Error('GitHub API queries use GET and cannot supply a request body');
41
+ }
42
+ return [...args, '--method', 'GET', '--hostname', 'github.com'];
43
+ }
44
+ if ([
45
+ 'issue view',
46
+ 'issue list',
47
+ 'pr view',
48
+ 'pr diff',
49
+ 'pr list',
50
+ 'pr checks',
51
+ 'repo view',
52
+ 'run view',
53
+ 'run list',
54
+ 'search issues',
55
+ 'search prs',
56
+ 'search code',
57
+ 'search commits',
58
+ ].includes(route))
59
+ return args;
60
+ }
61
+ if (cli === 'pup' &&
62
+ ['logs search', 'logs query', 'logs list', 'logs aggregate'].includes(route))
63
+ return args;
64
+ if (cli === 'sentry' &&
65
+ ['issue', 'event', 'trace', 'span', 'project', 'org'].includes(group ?? '') &&
66
+ ['view', 'get', 'list', 'search'].includes(action ?? ''))
67
+ return args;
68
+ throw new Error('Only read-only GitHub, Datadog log, and Sentry evidence queries are supported');
69
+ }
@@ -0,0 +1,11 @@
1
+ import path from 'node:path';
2
+ const UI_TEST_PATH = /(?:^|\/)(?:androidTest|uiTests?|uitests?|e2e|cypress|\.maestro)(?:\/|$)|(?:^|\/)[^/]*(?:UITests|UiTest|\.ui\.test\.|\.e2e\.)/i;
3
+ const UI_TEST_FRAMEWORK = /^\s*(?:import\b.*|from\s+|}\s*from\s+).*(?:androidx\.(?:test\.(?:espresso|uiautomator)|compose\.ui\.test)|app\.cash\.paparazzi|AndroidJUnit4|@playwright\/test)|^\s*@RunWith\(AndroidJUnit4|^\s*(?:(?:let|var|val|const)\s+\w+\s*=\s*)?(?:XCUIApplication|Espresso|UiDevice|composeTestRule|browser\.url|cy\.(?:visit|get))\b/m;
4
+ export function isForbiddenAutofixTest(file, extraPaths = [], content = '') {
5
+ const normalized = file.replaceAll('\\', '/');
6
+ return (UI_TEST_PATH.test(normalized) ||
7
+ UI_TEST_FRAMEWORK.test(content) ||
8
+ extraPaths.some((pattern) => normalized === pattern ||
9
+ normalized.startsWith(`${pattern.replace(/\/$/, '')}/`) ||
10
+ path.matchesGlob(normalized, pattern)));
11
+ }
@@ -0,0 +1,355 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { Ajv } from 'ajv';
5
+ import { buildVerificationEnvironment } from './tool-environment.js';
6
+ import { safeToolOutput } from './tool-output.js';
7
+ export const AUTOFIX_RUNTIME_ENV = 'DOISTBOT_AUTOFIX_RUNTIME';
8
+ const OUTPUT_LIMIT = 64_000;
9
+ export const autofixPolicySchema = {
10
+ type: 'object',
11
+ additionalProperties: false,
12
+ required: ['verification'],
13
+ properties: {
14
+ guidance: { type: 'string', maxLength: 8000 },
15
+ forbidden_test_paths: { type: 'array', items: { type: 'string', minLength: 1 } },
16
+ verification: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ required: ['checks'],
20
+ properties: {
21
+ checks: {
22
+ type: 'array',
23
+ maxItems: 20,
24
+ items: {
25
+ type: 'object',
26
+ additionalProperties: false,
27
+ required: ['name', 'command'],
28
+ properties: {
29
+ name: { type: 'string', pattern: '^[a-zA-Z0-9_-]{1,64}$' },
30
+ command: { type: 'string', minLength: 1, maxLength: 4000 },
31
+ use_test_selectors: { type: 'boolean' },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ };
39
+ export const validateAutofixPolicy = new Ajv().compile(autofixPolicySchema);
40
+ const checkResultProperties = {
41
+ name: { type: 'string' },
42
+ command: { type: 'string' },
43
+ round: { type: 'integer', minimum: 1 },
44
+ fingerprint: { type: 'string' },
45
+ testSelectors: {
46
+ type: 'array',
47
+ maxItems: 30,
48
+ items: { type: 'string', maxLength: 200 },
49
+ },
50
+ startedAtMs: { type: 'integer' },
51
+ buildStartedAtMs: { type: ['integer', 'null'] },
52
+ buildDurationMs: { type: ['number', 'null'] },
53
+ setupDurationMs: { type: ['number', 'null'] },
54
+ durationMs: { type: 'number', minimum: 0 },
55
+ exitCode: { type: ['integer', 'null'] },
56
+ signal: { type: ['string', 'null'] },
57
+ timedOut: { type: 'boolean' },
58
+ output: { type: 'string', maxLength: OUTPUT_LIMIT },
59
+ outputTruncated: { type: 'boolean' },
60
+ status: { enum: ['running', 'finished'] },
61
+ };
62
+ export const verificationStateSchema = {
63
+ type: 'object',
64
+ additionalProperties: false,
65
+ required: ['roundsUsed', 'results'],
66
+ properties: {
67
+ roundsUsed: { type: 'integer', minimum: 0 },
68
+ testSelectors: checkResultProperties.testSelectors,
69
+ results: {
70
+ type: 'array',
71
+ items: {
72
+ type: 'object',
73
+ additionalProperties: false,
74
+ required: Object.keys(checkResultProperties),
75
+ properties: checkResultProperties,
76
+ },
77
+ },
78
+ },
79
+ };
80
+ const validateState = new Ajv().compile(verificationStateSchema);
81
+ const validateResult = new Ajv().compile({
82
+ type: 'object',
83
+ additionalProperties: false,
84
+ required: Object.keys(checkResultProperties),
85
+ properties: checkResultProperties,
86
+ });
87
+ const validateRuntime = new Ajv().compile({
88
+ type: 'object',
89
+ additionalProperties: false,
90
+ required: ['repoPath', 'statePath', 'deadlineMs', 'policy'],
91
+ properties: {
92
+ repoPath: { type: 'string', minLength: 1 },
93
+ statePath: { type: 'string', minLength: 1 },
94
+ deadlineMs: { type: 'integer', minimum: 1 },
95
+ policy: autofixPolicySchema,
96
+ },
97
+ });
98
+ export function readAutofixRuntime(file) {
99
+ const data = JSON.parse(readFileSync(file, 'utf8'));
100
+ if (!validateRuntime(data))
101
+ throw new Error('Invalid autofix runtime configuration');
102
+ return data;
103
+ }
104
+ export function readVerificationState(file) {
105
+ const data = JSON.parse(readFileSync(file, 'utf8'));
106
+ if (!validateState(data))
107
+ throw new Error('Invalid autofix verification state');
108
+ let partial;
109
+ try {
110
+ partial = JSON.parse(readFileSync(`${file}.running`, 'utf8'));
111
+ }
112
+ catch (error) {
113
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
114
+ throw error;
115
+ }
116
+ if (partial !== undefined) {
117
+ if (!validateResult(partial))
118
+ throw new Error('Invalid running verification result');
119
+ const index = data.results.findIndex((result) => result.status === 'running' &&
120
+ result.name === partial.name &&
121
+ result.round === partial.round &&
122
+ result.startedAtMs === partial.startedAtMs &&
123
+ result.command === partial.command &&
124
+ result.fingerprint === partial.fingerprint);
125
+ if (index >= 0)
126
+ data.results[index] = partial;
127
+ }
128
+ return data;
129
+ }
130
+ export function saveVerificationState(file, state) {
131
+ writeFileSync(`${file}.tmp`, JSON.stringify(state), { mode: 0o600 });
132
+ renameSync(`${file}.tmp`, file);
133
+ }
134
+ export function selectCheckResult(state, check, fingerprint) {
135
+ const results = [...state.results]
136
+ .reverse()
137
+ .filter((result) => result.name === check.name && result.command === check.command);
138
+ const current = results.filter((result) => isCurrentCheck(result, check, fingerprint, state));
139
+ return (current.find((result) => result.status === 'finished' && result.exitCode === 0 && !result.timedOut) ??
140
+ current[0] ??
141
+ results[0]);
142
+ }
143
+ export function isCurrentCheck(result, check, fingerprint, state) {
144
+ return (result.fingerprint === fingerprint &&
145
+ JSON.stringify(result.testSelectors) ===
146
+ JSON.stringify(check.use_test_selectors ? (state.testSelectors ?? []) : []));
147
+ }
148
+ export function summarizeVerification(state, policy, fingerprint) {
149
+ const summary = {
150
+ roundsUsed: state.roundsUsed,
151
+ results: policy.verification.checks.map((check) => {
152
+ const result = selectCheckResult(state, check, fingerprint);
153
+ return {
154
+ name: check.name,
155
+ status: result
156
+ ? isCurrentCheck(result, check, fingerprint, state)
157
+ ? result.status
158
+ : 'stale'
159
+ : 'not-run',
160
+ exitCode: result?.exitCode ?? null,
161
+ signal: result?.signal ?? null,
162
+ timedOut: result?.timedOut ?? false,
163
+ round: result?.round ?? null,
164
+ durationMs: result?.durationMs ?? null,
165
+ buildDurationMs: result?.buildDurationMs ?? null,
166
+ setupDurationMs: result?.setupDurationMs ?? null,
167
+ output: result?.output.slice(-6000) ?? '',
168
+ outputTruncated: result
169
+ ? result.outputTruncated || result.output.length > 6000
170
+ : false,
171
+ };
172
+ }),
173
+ };
174
+ let text = JSON.stringify(summary);
175
+ while (text.length > 24_000) {
176
+ for (const result of summary.results) {
177
+ if (result.output) {
178
+ result.output = result.output.slice(-Math.floor(result.output.length / 2));
179
+ if (result.output.length === 1)
180
+ result.output = '';
181
+ result.outputTruncated = true;
182
+ }
183
+ }
184
+ text = JSON.stringify(summary);
185
+ }
186
+ return text;
187
+ }
188
+ export class AutofixVerifier {
189
+ runtime;
190
+ fingerprint;
191
+ record;
192
+ busy = false;
193
+ constructor(runtime, fingerprint, record) {
194
+ this.runtime = runtime;
195
+ this.fingerprint = fingerprint;
196
+ this.record = record;
197
+ }
198
+ async verify(names, signal, testSelectors) {
199
+ if (this.busy)
200
+ throw new Error('A verification round is already running');
201
+ this.busy = true;
202
+ try {
203
+ const { policy, statePath, deadlineMs } = this.runtime;
204
+ const requested = new Set(names);
205
+ const selected = policy.verification.checks.filter((check) => requested.has(check.name));
206
+ if (requested.size === 0 || selected.length !== requested.size) {
207
+ throw new Error('Select one or more check names from the repository policy');
208
+ }
209
+ const state = readVerificationState(statePath);
210
+ const checks = state.roundsUsed === 0 ? policy.verification.checks : selected;
211
+ const fingerprint = this.fingerprint();
212
+ state.testSelectors = testSelectors ?? state.testSelectors ?? [];
213
+ const pending = checks.filter((check) => {
214
+ const result = selectCheckResult(state, check, fingerprint);
215
+ return (!result ||
216
+ !isCurrentCheck(result, check, fingerprint, state) ||
217
+ result.status !== 'finished' ||
218
+ result.exitCode !== 0 ||
219
+ result.timedOut);
220
+ });
221
+ saveVerificationState(statePath, state);
222
+ if (pending.length === 0)
223
+ return state;
224
+ if (signal?.aborted || Date.now() >= deadlineMs)
225
+ throw new Error('Autofix time limit reached');
226
+ state.roundsUsed++;
227
+ saveVerificationState(statePath, state);
228
+ for (const check of pending) {
229
+ if (signal?.aborted || Date.now() >= deadlineMs)
230
+ break;
231
+ const result = {
232
+ name: check.name,
233
+ command: check.command,
234
+ round: state.roundsUsed,
235
+ fingerprint,
236
+ testSelectors: check.use_test_selectors ? state.testSelectors : [],
237
+ startedAtMs: Date.now(),
238
+ buildStartedAtMs: null,
239
+ buildDurationMs: null,
240
+ setupDurationMs: null,
241
+ durationMs: 0,
242
+ exitCode: null,
243
+ signal: null,
244
+ timedOut: false,
245
+ output: '',
246
+ outputTruncated: false,
247
+ status: 'running',
248
+ };
249
+ state.results.push(result);
250
+ saveVerificationState(statePath, state);
251
+ await this.runCheck(result, state, signal);
252
+ }
253
+ return state;
254
+ }
255
+ finally {
256
+ this.busy = false;
257
+ }
258
+ }
259
+ async runCheck(result, state, signal) {
260
+ const startedAt = Date.now();
261
+ const child = spawn('/bin/sh', ['-c', result.command], {
262
+ cwd: this.runtime.repoPath,
263
+ detached: process.platform !== 'win32',
264
+ stdio: ['ignore', 'pipe', 'pipe'],
265
+ env: {
266
+ ...buildVerificationEnvironment(),
267
+ DOISTBOT_TEST_SELECTORS: result.testSelectors.join('\n'),
268
+ },
269
+ });
270
+ let rawTail = '';
271
+ const { deadlineMs, statePath } = this.runtime;
272
+ let stopping;
273
+ function stop() {
274
+ if (stopping)
275
+ return;
276
+ result.timedOut = Date.now() >= deadlineMs;
277
+ function kill(signalName) {
278
+ try {
279
+ if (child.pid && process.platform !== 'win32')
280
+ process.kill(-child.pid, signalName);
281
+ else
282
+ child.kill(signalName);
283
+ }
284
+ catch {
285
+ /* The process can finish before cancellation. */
286
+ }
287
+ }
288
+ kill('SIGTERM');
289
+ stopping = new Promise((resolve) => {
290
+ setTimeout(() => {
291
+ kill('SIGKILL');
292
+ resolve();
293
+ }, 1000);
294
+ });
295
+ }
296
+ const timer = setTimeout(stop, Math.max(1, this.runtime.deadlineMs - Date.now()));
297
+ signal?.addEventListener('abort', stop, { once: true });
298
+ if (signal?.aborted)
299
+ stop();
300
+ function saveOutput() {
301
+ try {
302
+ result.output = safeToolOutput(rawTail).slice(-OUTPUT_LIMIT);
303
+ }
304
+ catch {
305
+ result.output = 'Tool output withheld: filtering failed.';
306
+ }
307
+ result.durationMs = Date.now() - startedAt;
308
+ if (result.buildStartedAtMs !== null) {
309
+ result.buildDurationMs = Date.now() - result.buildStartedAtMs;
310
+ result.setupDurationMs = result.buildStartedAtMs - startedAt;
311
+ }
312
+ }
313
+ function saveRunning() {
314
+ saveOutput();
315
+ writeFileSync(`${statePath}.running.tmp`, JSON.stringify(result), { mode: 0o600 });
316
+ renameSync(`${statePath}.running.tmp`, `${statePath}.running`);
317
+ }
318
+ function onData(chunk) {
319
+ const next = rawTail + chunk;
320
+ if (result.buildStartedAtMs === null && /(?:^|\n)DOISTBOT_BUILD_START\r?\n/.test(next))
321
+ result.buildStartedAtMs = Date.now();
322
+ result.outputTruncated ||= next.length > OUTPUT_LIMIT;
323
+ rawTail = next.slice(-OUTPUT_LIMIT);
324
+ }
325
+ child.stdout.setEncoding('utf8');
326
+ child.stderr.setEncoding('utf8');
327
+ child.stdout.on('data', onData);
328
+ child.stderr.on('data', onData);
329
+ const outputTimer = setInterval(saveRunning, 1000);
330
+ await new Promise((resolve) => {
331
+ child.once('error', (error) => {
332
+ onData(error.message);
333
+ result.exitCode = -1;
334
+ resolve();
335
+ });
336
+ child.once('close', (code, childSignal) => {
337
+ result.exitCode = code;
338
+ result.signal = childSignal;
339
+ resolve();
340
+ });
341
+ });
342
+ clearTimeout(timer);
343
+ clearInterval(outputTimer);
344
+ await stopping;
345
+ signal?.removeEventListener('abort', stop);
346
+ result.status = 'finished';
347
+ saveOutput();
348
+ saveVerificationState(statePath, state);
349
+ rmSync(`${statePath}.running`, { force: true });
350
+ this.record(result);
351
+ }
352
+ }
353
+ export function hashAutofixInputs(value) {
354
+ return createHash('sha256').update(value).digest('hex');
355
+ }
@@ -0,0 +1,14 @@
1
+ export const KNOWN_DOISTBOT_LOGINS = new Set([
2
+ 'doistbot',
3
+ 'doistbot[bot]',
4
+ 'doist-bot',
5
+ 'doist-bot[bot]',
6
+ 'doistbot-app',
7
+ 'doistbot-app[bot]',
8
+ ]);
9
+ export function isKnownDoistbotLogin(login) {
10
+ if (!login) {
11
+ return false;
12
+ }
13
+ return KNOWN_DOISTBOT_LOGINS.has(login.toLowerCase());
14
+ }
@@ -0,0 +1,158 @@
1
+ import { logger } from './logging.js';
2
+ /**
3
+ * GitHub Check Run output titles render in the Checks tab list and in the PR
4
+ * conversation; we keep them short for at-a-glance scannability.
5
+ */
6
+ const MAX_TITLE_LENGTH = 100;
7
+ function truncateTitle(value) {
8
+ if (value.length <= MAX_TITLE_LENGTH)
9
+ return value;
10
+ return `${value.slice(0, MAX_TITLE_LENGTH - 1)}…`;
11
+ }
12
+ /**
13
+ * Generic GitHub Check Run poster. Failures (e.g., missing `checks:write`
14
+ * permission) are logged at `warn` and swallowed — callers have typically
15
+ * already completed their main work and we don't want a check-posting failure
16
+ * to mask the primary outcome (a review, a fix-retry, etc.).
17
+ *
18
+ * Upserts on (sha, name): if a check run with the same name already exists for
19
+ * the SHA (e.g. an earlier in-progress run from this delivery, or a previous
20
+ * `/review` invocation), updates that run in place rather than appending a
21
+ * fresh one. Matches the prior commit-status behavior of "single current
22
+ * result" per (SHA, context), and lets the in_progress→completed transition
23
+ * happen naturally on the second call.
24
+ */
25
+ export async function postCheckRun(params) {
26
+ const { octokit, owner, repo, sha, title, summary, detailsUrl, name, externalId } = params;
27
+ const checkRunId = params.checkRunId;
28
+ const status = params.status ?? 'completed';
29
+ const conclusion = status === 'completed' ? params.conclusion : undefined;
30
+ const truncatedTitle = truncateTitle(title);
31
+ // Fields shared between checks.create and checks.update — extracted so the
32
+ // two call sites stay in sync as fields are added.
33
+ const payload = {
34
+ owner,
35
+ repo,
36
+ status,
37
+ conclusion,
38
+ details_url: detailsUrl,
39
+ external_id: externalId,
40
+ output: { title: truncatedTitle, summary },
41
+ };
42
+ try {
43
+ if (checkRunId !== undefined) {
44
+ await octokit.rest.checks.update({
45
+ ...payload,
46
+ check_run_id: checkRunId,
47
+ });
48
+ logger.info('Updated known check run', {
49
+ component: 'check-run',
50
+ name,
51
+ sha,
52
+ status,
53
+ conclusion,
54
+ checkRunId,
55
+ });
56
+ return;
57
+ }
58
+ const existing = await octokit.rest.checks.listForRef({
59
+ owner,
60
+ repo,
61
+ ref: sha,
62
+ check_name: name,
63
+ per_page: 5,
64
+ });
65
+ // Prefer matching by external_id when we have one; otherwise fall back
66
+ // to the first run for the (sha, name) tuple. This keeps a delivery
67
+ // from accidentally clobbering another delivery's run if multiple
68
+ // exist (e.g. after a transient lock-lookup failure).
69
+ const existingRun = externalId
70
+ ? (existing.data.check_runs.find((run) => (run.external_id ?? undefined) === externalId) ?? existing.data.check_runs[0])
71
+ : existing.data.check_runs[0];
72
+ if (existingRun) {
73
+ await octokit.rest.checks.update({
74
+ ...payload,
75
+ check_run_id: existingRun.id,
76
+ });
77
+ logger.info('Updated existing check run', {
78
+ component: 'check-run',
79
+ name,
80
+ sha,
81
+ status,
82
+ conclusion,
83
+ checkRunId: existingRun.id,
84
+ });
85
+ return;
86
+ }
87
+ await octokit.rest.checks.create({
88
+ ...payload,
89
+ name,
90
+ head_sha: sha,
91
+ });
92
+ logger.info('Posted check run', {
93
+ component: 'check-run',
94
+ name,
95
+ sha,
96
+ status,
97
+ conclusion,
98
+ });
99
+ }
100
+ catch (error) {
101
+ logger.warn('Failed to post check run', {
102
+ component: 'check-run',
103
+ name,
104
+ sha,
105
+ status,
106
+ conclusion,
107
+ error: error instanceof Error ? error.message : String(error),
108
+ });
109
+ }
110
+ }
111
+ /**
112
+ * Concurrency lock: returns `true` if there's an `in_progress` check run for
113
+ * the given (sha, name) whose `external_id` is from a different delivery.
114
+ *
115
+ * Replaces the pre-Check-Runs PR-comment-based lock. Same semantics: if a
116
+ * different delivery is already mid-review on the same SHA, skip rather
117
+ * than double-bill the LLM. Same-delivery in-progress runs (e.g. a retry
118
+ * after a webhook redelivery) are not treated as locks.
119
+ *
120
+ * `per_page: 5` defends against the rare case where multiple in-progress
121
+ * runs slipped through earlier (e.g. transient lock-lookup failure); we'd
122
+ * still see them all and lock if any belongs to another delivery. The
123
+ * normal steady state is one run per (sha, name).
124
+ *
125
+ * Caveat — TOCTOU: there's a window between this check and the caller's
126
+ * subsequent in_progress post where a concurrent delivery can pass its
127
+ * own check too, leading both to proceed. The pre-Check-Runs comment lock
128
+ * had the same race; bound is the same (≤2× LLM per concurrent fire).
129
+ *
130
+ * Failures are logged at `warn` and treated as "no lock found" so a
131
+ * read-side outage can't permanently wedge the bot. The cost of a false
132
+ * negative is one duplicate review.
133
+ */
134
+ export async function isAnotherDeliveryInProgress({ octokit, owner, repo, sha, name, deliveryId, }) {
135
+ try {
136
+ const response = await octokit.rest.checks.listForRef({
137
+ owner,
138
+ repo,
139
+ ref: sha,
140
+ check_name: name,
141
+ per_page: 5,
142
+ });
143
+ return response.data.check_runs.some(
144
+ // GitHub returns `null` for `external_id` when it was unset on
145
+ // creation; normalise to `undefined` so it compares equal to a
146
+ // missing `deliveryId` (e.g. local CLI retries).
147
+ (run) => run.status === 'in_progress' && (run.external_id ?? undefined) !== deliveryId);
148
+ }
149
+ catch (error) {
150
+ logger.warn('Failed to look up in-progress check runs (lock check)', {
151
+ component: 'check-run',
152
+ name,
153
+ sha,
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ return false;
157
+ }
158
+ }