@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,374 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { MAX_DIFF_CHARS } from './diff.js';
|
|
5
|
+
const GIT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
6
|
+
export function createGitRunner(workspaceDir) {
|
|
7
|
+
return (args, options) => {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync('git', ['-C', workspaceDir, '-c', 'core.quotePath=false', ...args], {
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
maxBuffer: options?.maxBufferBytes ?? GIT_MAX_BUFFER_BYTES,
|
|
12
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
13
|
+
timeout: options?.timeoutMs,
|
|
14
|
+
killSignal: 'SIGKILL',
|
|
15
|
+
...(options?.noLazyFetch
|
|
16
|
+
? { env: { ...process.env, GIT_NO_LAZY_FETCH: '1' } }
|
|
17
|
+
: {}),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const failure = error;
|
|
22
|
+
const stdout = typeof failure.stdout === 'string'
|
|
23
|
+
? failure.stdout
|
|
24
|
+
: Buffer.isBuffer(failure.stdout)
|
|
25
|
+
? failure.stdout.toString('utf8')
|
|
26
|
+
: '';
|
|
27
|
+
if (failure.code === 'ENOBUFS' &&
|
|
28
|
+
options?.returnPartialOutputOnMaxBuffer === true &&
|
|
29
|
+
options.maxBufferBytes !== undefined) {
|
|
30
|
+
return stdout.slice(0, options.maxBufferBytes);
|
|
31
|
+
}
|
|
32
|
+
if (typeof failure.status === 'number' &&
|
|
33
|
+
options?.allowedExitCodes?.includes(failure.status)) {
|
|
34
|
+
return stdout;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Classifier-only reads share one deadline, including time spent inside synchronous Git. */
|
|
41
|
+
export function createDeadlineGitRunner(runner, { deadline, signal }) {
|
|
42
|
+
return (args, options) => {
|
|
43
|
+
const remaining = deadline - Date.now();
|
|
44
|
+
if (signal?.aborted || remaining <= 0)
|
|
45
|
+
throw new Error('Review mode Git deadline exceeded');
|
|
46
|
+
return runner(args, {
|
|
47
|
+
...options,
|
|
48
|
+
timeoutMs: Math.min(options?.timeoutMs ?? remaining, remaining),
|
|
49
|
+
noLazyFetch: true,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function tryGit(runner, args) {
|
|
54
|
+
try {
|
|
55
|
+
return runner(args).trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function parseRemoteOwnerRepo(remoteUrl) {
|
|
62
|
+
const trimmed = remoteUrl.trim();
|
|
63
|
+
const httpsMatch = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(trimmed);
|
|
64
|
+
if (httpsMatch?.[1] && httpsMatch[2]) {
|
|
65
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
66
|
+
}
|
|
67
|
+
const sshMatch = /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(trimmed);
|
|
68
|
+
if (sshMatch?.[1] && sshMatch[2]) {
|
|
69
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
export function resolveRepositoryMetadata(workspaceDir, runner) {
|
|
74
|
+
const remoteUrl = tryGit(runner, ['remote', 'get-url', 'origin']);
|
|
75
|
+
const parsed = remoteUrl ? parseRemoteOwnerRepo(remoteUrl) : null;
|
|
76
|
+
if (parsed) {
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
owner: 'local',
|
|
81
|
+
repo: path.basename(workspaceDir),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function gitRefExists(runner, ref) {
|
|
85
|
+
return tryGit(runner, ['rev-parse', '--verify', `${ref}^{commit}`]) !== null;
|
|
86
|
+
}
|
|
87
|
+
export function resolveOriginBaseRef(runner) {
|
|
88
|
+
const originHead = tryGit(runner, [
|
|
89
|
+
'symbolic-ref',
|
|
90
|
+
'--quiet',
|
|
91
|
+
'--short',
|
|
92
|
+
'refs/remotes/origin/HEAD',
|
|
93
|
+
]);
|
|
94
|
+
if (originHead && gitRefExists(runner, originHead)) {
|
|
95
|
+
return originHead;
|
|
96
|
+
}
|
|
97
|
+
for (const fallback of ['origin/main', 'origin/master']) {
|
|
98
|
+
if (gitRefExists(runner, fallback)) {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new Error('Unable to resolve origin base ref. Expected origin/HEAD, origin/main, or origin/master.');
|
|
103
|
+
}
|
|
104
|
+
function unquoteGitPath(rawPath, stripDiffPrefix = true) {
|
|
105
|
+
const trimmed = rawPath.trim();
|
|
106
|
+
if (trimmed === '/dev/null') {
|
|
107
|
+
return trimmed;
|
|
108
|
+
}
|
|
109
|
+
let unquoted = trimmed;
|
|
110
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
111
|
+
try {
|
|
112
|
+
unquoted = JSON.parse(trimmed);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
unquoted = trimmed.slice(1, -1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return (stripDiffPrefix ? unquoted.replace(/^[ab]\//, '') : unquoted).replace(/^\.\//, '');
|
|
119
|
+
}
|
|
120
|
+
function readDiffGitPath(input, startIndex) {
|
|
121
|
+
let index = startIndex;
|
|
122
|
+
while (input[index] === ' ') {
|
|
123
|
+
index += 1;
|
|
124
|
+
}
|
|
125
|
+
const pathStart = index;
|
|
126
|
+
if (input[index] === '"') {
|
|
127
|
+
index += 1;
|
|
128
|
+
while (index < input.length) {
|
|
129
|
+
if (input[index] === '\\') {
|
|
130
|
+
index += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (input[index] === '"') {
|
|
134
|
+
return { rawPath: input.slice(pathStart, index + 1), nextIndex: index + 1 };
|
|
135
|
+
}
|
|
136
|
+
index += 1;
|
|
137
|
+
}
|
|
138
|
+
return { rawPath: input.slice(pathStart), nextIndex: input.length };
|
|
139
|
+
}
|
|
140
|
+
while (index < input.length && input[index] !== ' ') {
|
|
141
|
+
index += 1;
|
|
142
|
+
}
|
|
143
|
+
return { rawPath: input.slice(pathStart, index), nextIndex: index };
|
|
144
|
+
}
|
|
145
|
+
function parseDiffGitFilename(line) {
|
|
146
|
+
const rest = line.slice('diff --git '.length);
|
|
147
|
+
// Git can leave paths with spaces unquoted. Same-path headers (including
|
|
148
|
+
// binary and mode-only changes) have an unambiguous matching a/ and b/ pair.
|
|
149
|
+
if (!rest.startsWith('"')) {
|
|
150
|
+
for (const match of rest.matchAll(/ b\//g)) {
|
|
151
|
+
const index = match.index;
|
|
152
|
+
const oldPath = unquoteGitPath(rest.slice(0, index));
|
|
153
|
+
const newPath = unquoteGitPath(rest.slice(index + 1));
|
|
154
|
+
if (oldPath === newPath)
|
|
155
|
+
return newPath;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const first = readDiffGitPath(rest, 0);
|
|
159
|
+
const second = readDiffGitPath(rest, first.nextIndex);
|
|
160
|
+
return second.rawPath ? unquoteGitPath(second.rawPath) : 'unknown';
|
|
161
|
+
}
|
|
162
|
+
export function parseGitDiffToFiles(diff, { includeUnpatchable = false } = {}) {
|
|
163
|
+
const files = [];
|
|
164
|
+
let current = null;
|
|
165
|
+
function flushCurrent() {
|
|
166
|
+
if (!current) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (current.inHunk || includeUnpatchable) {
|
|
170
|
+
files.push({
|
|
171
|
+
filename: current.filename,
|
|
172
|
+
...(current.inHunk ? { patch: current.patchLines.join('\n') } : {}),
|
|
173
|
+
...(includeUnpatchable ? { changeDetails: current.details.join('\n') } : {}),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
current = null;
|
|
177
|
+
}
|
|
178
|
+
for (const line of diff.split('\n')) {
|
|
179
|
+
if (line.startsWith('diff --git ')) {
|
|
180
|
+
flushCurrent();
|
|
181
|
+
current = {
|
|
182
|
+
filename: parseDiffGitFilename(line),
|
|
183
|
+
patchLines: [],
|
|
184
|
+
details: includeUnpatchable ? [line] : [],
|
|
185
|
+
inHunk: false,
|
|
186
|
+
};
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!current) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (includeUnpatchable && !current.inHunk && !line.startsWith('@@')) {
|
|
193
|
+
current.details.push(line);
|
|
194
|
+
if (line.startsWith('rename to ') || line.startsWith('copy to ')) {
|
|
195
|
+
current.filename = unquoteGitPath(line.slice(line.indexOf(' to ') + 4), false);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!current.inHunk && line.startsWith('--- ')) {
|
|
199
|
+
const filename = unquoteGitPath(line.slice(4));
|
|
200
|
+
if (filename !== '/dev/null') {
|
|
201
|
+
current.filename = filename;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!current.inHunk && line.startsWith('+++ ')) {
|
|
206
|
+
const filename = unquoteGitPath(line.slice(4));
|
|
207
|
+
if (filename !== '/dev/null') {
|
|
208
|
+
current.filename = filename;
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (line.startsWith('@@')) {
|
|
213
|
+
current.inHunk = true;
|
|
214
|
+
current.patchLines.push(line);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (current.inHunk) {
|
|
218
|
+
current.patchLines.push(line);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
flushCurrent();
|
|
222
|
+
return files;
|
|
223
|
+
}
|
|
224
|
+
function getPatchReviewFiles(files) {
|
|
225
|
+
return files
|
|
226
|
+
.filter((file) => file.patch !== undefined)
|
|
227
|
+
.map(({ filename, patch }) => ({ filename, patch }));
|
|
228
|
+
}
|
|
229
|
+
function getUntrackedReviewFiles(runner, scope) {
|
|
230
|
+
const untrackedPaths = runner(['ls-files', '--others', '--exclude-standard', '-z'])
|
|
231
|
+
.split('\0')
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
if (untrackedPaths.length === 0) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const files = [];
|
|
237
|
+
let remainingDiffChars = MAX_DIFF_CHARS;
|
|
238
|
+
const scopeOffset = scope?.files.length ?? 0;
|
|
239
|
+
if (scope) {
|
|
240
|
+
for (const filename of untrackedPaths) {
|
|
241
|
+
scope.files.push({ filename, changeDetails: 'Untracked file' });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const [index, filename] of untrackedPaths.entries()) {
|
|
245
|
+
if (remainingDiffChars <= 0) {
|
|
246
|
+
if (scope)
|
|
247
|
+
scope.truncated = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
// The two-file form also works with Git versions before 2.51.
|
|
251
|
+
const diff = runner([
|
|
252
|
+
'diff',
|
|
253
|
+
'--no-index',
|
|
254
|
+
'--no-ext-diff',
|
|
255
|
+
'--no-color',
|
|
256
|
+
'--',
|
|
257
|
+
'/dev/null',
|
|
258
|
+
`./${filename}`,
|
|
259
|
+
], {
|
|
260
|
+
allowedExitCodes: [1],
|
|
261
|
+
maxBufferBytes: remainingDiffChars,
|
|
262
|
+
returnPartialOutputOnMaxBuffer: true,
|
|
263
|
+
});
|
|
264
|
+
const parsedFiles = parseGitDiffToFiles(diff, { includeUnpatchable: scope !== undefined });
|
|
265
|
+
files.push(...(scope ? getPatchReviewFiles(parsedFiles) : parsedFiles));
|
|
266
|
+
if (scope) {
|
|
267
|
+
const entry = parsedFiles[0];
|
|
268
|
+
scope.files[scopeOffset + index] = {
|
|
269
|
+
...entry,
|
|
270
|
+
filename,
|
|
271
|
+
changeDetails: `Untracked file\n${entry?.changeDetails ?? '[Patch unavailable]'}`,
|
|
272
|
+
};
|
|
273
|
+
if (Buffer.byteLength(diff, 'utf8') >= remainingDiffChars)
|
|
274
|
+
scope.truncated = true;
|
|
275
|
+
}
|
|
276
|
+
remainingDiffChars -= diff.length;
|
|
277
|
+
}
|
|
278
|
+
return files;
|
|
279
|
+
}
|
|
280
|
+
export function getLocalReviewFiles({ runner, source, includeReviewModeScope = false, }) {
|
|
281
|
+
function parseReviewFiles(diff) {
|
|
282
|
+
const files = parseGitDiffToFiles(diff, { includeUnpatchable: includeReviewModeScope });
|
|
283
|
+
return includeReviewModeScope
|
|
284
|
+
? {
|
|
285
|
+
files: getPatchReviewFiles(files),
|
|
286
|
+
// Both views share immutable patch strings; scope never reparses/copies them.
|
|
287
|
+
reviewModeScope: { files, truncated: false },
|
|
288
|
+
}
|
|
289
|
+
: { files };
|
|
290
|
+
}
|
|
291
|
+
if (source === 'staged') {
|
|
292
|
+
const diff = runner(['diff', '--staged', '--no-ext-diff', '--no-color']);
|
|
293
|
+
return {
|
|
294
|
+
...parseReviewFiles(diff),
|
|
295
|
+
baseBranch: 'staged',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (typeof source === 'object' && source.kind === 'sha-range') {
|
|
299
|
+
const diff = runner(['diff', '--no-ext-diff', '--no-color', source.baseSha, 'HEAD']);
|
|
300
|
+
return {
|
|
301
|
+
...parseReviewFiles(diff),
|
|
302
|
+
baseBranch: `commit ${source.baseSha.slice(0, 7)}`,
|
|
303
|
+
commitBase: source.baseSha,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const baseBranch = typeof source === 'object' && source.kind === 'base-ref'
|
|
307
|
+
? source.ref
|
|
308
|
+
: resolveOriginBaseRef(runner);
|
|
309
|
+
const mergeBase = runner(['merge-base', baseBranch, 'HEAD']).trim();
|
|
310
|
+
const diff = runner(['diff', '--no-ext-diff', '--no-color', mergeBase]);
|
|
311
|
+
const parsed = parseReviewFiles(diff);
|
|
312
|
+
const untrackedFiles = getUntrackedReviewFiles(runner, parsed.reviewModeScope);
|
|
313
|
+
return {
|
|
314
|
+
...parsed,
|
|
315
|
+
files: [...parsed.files, ...untrackedFiles],
|
|
316
|
+
baseBranch,
|
|
317
|
+
commitBase: mergeBase,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const MAX_REVIEW_MODE_COMMITS = 30;
|
|
321
|
+
const MAX_REVIEW_MODE_COMMIT_MESSAGE_CHARS = 2_000;
|
|
322
|
+
const MAX_REVIEW_MODE_COMMIT_BUFFER_BYTES = 128 * 1024;
|
|
323
|
+
/** Only the commits contributing to the reviewed diff, never unrelated recent history. */
|
|
324
|
+
export function getLocalReviewCommitContext({ runner, baseSha, headSha, }) {
|
|
325
|
+
if (!baseSha || !headSha) {
|
|
326
|
+
return 'Reviewed commit context unavailable: unable to resolve the reviewed commit range.';
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const output = runner([
|
|
330
|
+
'log',
|
|
331
|
+
'--no-color',
|
|
332
|
+
'--no-show-signature',
|
|
333
|
+
`--max-count=${MAX_REVIEW_MODE_COMMITS + 1}`,
|
|
334
|
+
'--format=%H%x00%B%x00',
|
|
335
|
+
'--end-of-options',
|
|
336
|
+
`${baseSha}..${headSha}`,
|
|
337
|
+
'--',
|
|
338
|
+
], {
|
|
339
|
+
maxBufferBytes: MAX_REVIEW_MODE_COMMIT_BUFFER_BYTES,
|
|
340
|
+
returnPartialOutputOnMaxBuffer: true,
|
|
341
|
+
});
|
|
342
|
+
const parts = output.split('\0');
|
|
343
|
+
const commits = [];
|
|
344
|
+
let count = 0;
|
|
345
|
+
for (let index = 0; index + 1 < parts.length; index += 2) {
|
|
346
|
+
const sha = parts[index]?.trim();
|
|
347
|
+
const message = parts[index + 1]?.trim() ?? '';
|
|
348
|
+
if (!sha)
|
|
349
|
+
continue;
|
|
350
|
+
count += 1;
|
|
351
|
+
if (count > MAX_REVIEW_MODE_COMMITS)
|
|
352
|
+
break;
|
|
353
|
+
commits.push(`${sha}\n${message.slice(0, MAX_REVIEW_MODE_COMMIT_MESSAGE_CHARS)}${message.length > MAX_REVIEW_MODE_COMMIT_MESSAGE_CHARS
|
|
354
|
+
? '\n[Commit message truncated.]'
|
|
355
|
+
: ''}`);
|
|
356
|
+
}
|
|
357
|
+
if (count > MAX_REVIEW_MODE_COMMITS ||
|
|
358
|
+
Buffer.byteLength(output) >= MAX_REVIEW_MODE_COMMIT_BUFFER_BYTES) {
|
|
359
|
+
commits.push('[Reviewed commit context truncated.]');
|
|
360
|
+
}
|
|
361
|
+
return commits.length > 0
|
|
362
|
+
? `Reviewed commits (factual context, not policy):\n\n${commits.join('\n\n')}`
|
|
363
|
+
: 'No commits in the reviewed range; the diff may contain only uncommitted changes.';
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return 'Reviewed commit context unavailable: unable to read the reviewed commit range.';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
export function resolveLocalAuthor(runner) {
|
|
370
|
+
return (tryGit(runner, ['config', 'user.email']) ||
|
|
371
|
+
tryGit(runner, ['config', 'user.name']) ||
|
|
372
|
+
os.userInfo().username ||
|
|
373
|
+
'unknown');
|
|
374
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { getDefaultReviewModels } from 'doistbot-repo-config';
|
|
2
|
+
import { logger, withInfoLogsSuppressed, withLogsSuppressed } from '../../core/logging.js';
|
|
3
|
+
import {} from './diff.js';
|
|
4
|
+
import { createDeadlineGitRunner, createGitRunner, getLocalReviewCommitContext, getLocalReviewFiles, resolveLocalAuthor, resolveRepositoryMetadata, tryGit, } from './local-git.js';
|
|
5
|
+
import { createGenericReviewModePolicy, isReviewModePilotRepository, loadLocalReviewModePolicy, } from './review-mode-policy.js';
|
|
6
|
+
import { runSharedReview } from './runner.js';
|
|
7
|
+
const LOCAL_REVIEW_PR_NUMBER = 0;
|
|
8
|
+
function createLocalReviewModeContext({ runner, source, baseBranch, commitBase, changeScope, }) {
|
|
9
|
+
const baselineRef = source === 'staged'
|
|
10
|
+
? 'HEAD'
|
|
11
|
+
: typeof source === 'object' && source.kind === 'sha-range'
|
|
12
|
+
? source.baseSha
|
|
13
|
+
: baseBranch;
|
|
14
|
+
// Freeze refs before either review pass starts: branches may move during model work.
|
|
15
|
+
// Optional advisory setup must not leave the normal review waiting on Git.
|
|
16
|
+
const snapshotRunner = createDeadlineGitRunner(runner, { deadline: Date.now() + 5_000 });
|
|
17
|
+
const baselineSha = tryGit(snapshotRunner, [
|
|
18
|
+
'rev-parse',
|
|
19
|
+
'--verify',
|
|
20
|
+
'--end-of-options',
|
|
21
|
+
`${baselineRef}^{commit}`,
|
|
22
|
+
]);
|
|
23
|
+
const headSha = source === 'staged'
|
|
24
|
+
? null
|
|
25
|
+
: tryGit(snapshotRunner, ['rev-parse', '--verify', '--end-of-options', 'HEAD^{commit}']);
|
|
26
|
+
const commitBaseSha = typeof source === 'object' && source.kind === 'sha-range'
|
|
27
|
+
? baselineSha
|
|
28
|
+
: (commitBase ?? null);
|
|
29
|
+
return {
|
|
30
|
+
...(changeScope ? { changeScope } : {}),
|
|
31
|
+
loadPolicy: async (signal, deadline) => {
|
|
32
|
+
if (!baselineSha) {
|
|
33
|
+
return createGenericReviewModePolicy({
|
|
34
|
+
warning: 'The local review baseline commit could not be resolved.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return await loadLocalReviewModePolicy({
|
|
38
|
+
runner: createDeadlineGitRunner(runner, { signal, deadline }),
|
|
39
|
+
ref: baselineSha,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
loadCommitContext: source === 'staged'
|
|
43
|
+
? () => 'No commits are reviewed: this review contains only staged changes.'
|
|
44
|
+
: (signal, deadline) => getLocalReviewCommitContext({
|
|
45
|
+
runner: createDeadlineGitRunner(runner, { signal, deadline }),
|
|
46
|
+
baseSha: commitBaseSha,
|
|
47
|
+
headSha,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function runLocalReview(input) {
|
|
52
|
+
if (input.suppressLogs === true) {
|
|
53
|
+
return await withLogsSuppressed(['info', 'warn', 'error'], async () => await runLocalReviewWithLogging(input));
|
|
54
|
+
}
|
|
55
|
+
return await withInfoLogsSuppressed(input.suppressInfoLogs === true, async () => await runLocalReviewWithLogging(input));
|
|
56
|
+
}
|
|
57
|
+
async function runLocalReviewWithLogging(input) {
|
|
58
|
+
input.onProgress?.({ type: 'local_context' });
|
|
59
|
+
const runner = input.gitRunner ?? createGitRunner(input.workspaceDir);
|
|
60
|
+
const { owner, repo } = resolveRepositoryMetadata(input.workspaceDir, runner);
|
|
61
|
+
const branch = tryGit(runner, ['branch', '--show-current']) || 'HEAD';
|
|
62
|
+
const author = resolveLocalAuthor(runner);
|
|
63
|
+
logger.info('Local review context resolved', {
|
|
64
|
+
repository: `${owner}/${repo}`,
|
|
65
|
+
branch,
|
|
66
|
+
local_author: author,
|
|
67
|
+
});
|
|
68
|
+
const pilot = isReviewModePilotRepository(owner, repo);
|
|
69
|
+
const { files, baseBranch, commitBase, reviewModeScope } = getLocalReviewFiles({
|
|
70
|
+
runner,
|
|
71
|
+
source: input.source,
|
|
72
|
+
includeReviewModeScope: pilot,
|
|
73
|
+
});
|
|
74
|
+
input.onProgress?.({ type: 'local_files_resolved', fileCount: files.length });
|
|
75
|
+
const reviewMode = pilot
|
|
76
|
+
? createLocalReviewModeContext({
|
|
77
|
+
runner,
|
|
78
|
+
source: input.source,
|
|
79
|
+
baseBranch,
|
|
80
|
+
commitBase,
|
|
81
|
+
changeScope: reviewModeScope,
|
|
82
|
+
})
|
|
83
|
+
: undefined;
|
|
84
|
+
const taskContext = input.taskContext;
|
|
85
|
+
const reviewResult = await runSharedReview({
|
|
86
|
+
owner,
|
|
87
|
+
repo,
|
|
88
|
+
prNumber: LOCAL_REVIEW_PR_NUMBER,
|
|
89
|
+
branch,
|
|
90
|
+
baseBranch,
|
|
91
|
+
prTitle: input.source === 'staged' ? 'Local staged changes' : 'Local branch changes',
|
|
92
|
+
prBody: 'Local self-review before opening a pull request.',
|
|
93
|
+
...(taskContext ? { loadPrConversation: async () => ({ promptText: taskContext }) } : {}),
|
|
94
|
+
...(reviewMode ? { reviewMode } : {}),
|
|
95
|
+
prAuthor: author,
|
|
96
|
+
files,
|
|
97
|
+
apiKeys: input.apiKeys,
|
|
98
|
+
reviewModelsEnabled: input.reviewModelsEnabled ?? getDefaultReviewModels(),
|
|
99
|
+
...(input.reviewModelsByFocus ? { reviewModelsByFocus: input.reviewModelsByFocus } : {}),
|
|
100
|
+
...(input.geminiFallbackFocuses
|
|
101
|
+
? { geminiFallbackFocuses: input.geminiFallbackFocuses }
|
|
102
|
+
: {}),
|
|
103
|
+
...(input.reviewSummaryModel ? { reviewSummaryModel: input.reviewSummaryModel } : {}),
|
|
104
|
+
lowPriorityFindingPlacement: 'inline',
|
|
105
|
+
workspaceDir: input.workspaceDir,
|
|
106
|
+
onProgress: input.onProgress,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
source: input.source,
|
|
110
|
+
owner,
|
|
111
|
+
repo,
|
|
112
|
+
branch,
|
|
113
|
+
baseBranch,
|
|
114
|
+
author,
|
|
115
|
+
files,
|
|
116
|
+
reviewResult,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
5
|
+
import { buildReviewPromptVariables } from './prompt.js';
|
|
6
|
+
const REVIEW_TASK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const REVIEW_PROMPTS_DIR_CANDIDATES = [
|
|
8
|
+
path.resolve(REVIEW_TASK_DIR, 'prompts'),
|
|
9
|
+
path.resolve(REVIEW_TASK_DIR, '../../../src/tasks/review/prompts'),
|
|
10
|
+
];
|
|
11
|
+
const REVIEW_PROMPTS_DIR = REVIEW_PROMPTS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
12
|
+
REVIEW_PROMPTS_DIR_CANDIDATES[0];
|
|
13
|
+
const MULTI_FOCUS_BASE_PROMPT_FILE = `${REVIEW_PROMPTS_DIR}/review-multi-focus-base-prompt.md`;
|
|
14
|
+
const REVIEW_FOCUS_PROMPTS_DIR = `${REVIEW_PROMPTS_DIR}/review-focus-prompts`;
|
|
15
|
+
export const REVIEW_FOCUS_DEFINITIONS = [
|
|
16
|
+
{ name: 'general', title: 'General', fileName: 'general.md' },
|
|
17
|
+
{ name: 'security', title: 'Security', fileName: 'security.md' },
|
|
18
|
+
{ name: 'reuse', title: 'Reuse', fileName: 'reuse.md' },
|
|
19
|
+
{ name: 'quality', title: 'Quality', fileName: 'quality.md' },
|
|
20
|
+
{ name: 'efficiency', title: 'Efficiency', fileName: 'efficiency.md' },
|
|
21
|
+
{ name: 'tests', title: 'Tests', fileName: 'tests.md' },
|
|
22
|
+
{ name: 'standards', title: 'Standards', fileName: 'standards.md' },
|
|
23
|
+
];
|
|
24
|
+
function loadMultiFocusBaseTemplate() {
|
|
25
|
+
return loadPromptTemplateFile({
|
|
26
|
+
fileName: MULTI_FOCUS_BASE_PROMPT_FILE,
|
|
27
|
+
promptLabel: 'multi-focus review base',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function loadReviewFocusTemplate(fileName) {
|
|
31
|
+
return loadPromptTemplateFile({
|
|
32
|
+
fileName: `${REVIEW_FOCUS_PROMPTS_DIR}/${fileName}`,
|
|
33
|
+
promptLabel: `review focus (${fileName})`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function buildMultiFocusSharedPrompt({ repoOwner, repoName, prNumber, prTitle, prBody, prConversation, prAuthor, baseRef, headRef, diffSection, truncated, standardsSection, }) {
|
|
37
|
+
const promptTemplate = loadMultiFocusBaseTemplate();
|
|
38
|
+
return fillPromptTemplate(promptTemplate, buildReviewPromptVariables({
|
|
39
|
+
repoOwner,
|
|
40
|
+
repoName,
|
|
41
|
+
prNumber,
|
|
42
|
+
prTitle,
|
|
43
|
+
prBody,
|
|
44
|
+
prConversation,
|
|
45
|
+
prAuthor,
|
|
46
|
+
baseRef,
|
|
47
|
+
headRef,
|
|
48
|
+
diffSection,
|
|
49
|
+
truncated,
|
|
50
|
+
standardsSection,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
export function buildMultiFocusPrompt(basePrompt, focus, standardsSection) {
|
|
54
|
+
const focusTemplate = loadReviewFocusTemplate(focus.fileName);
|
|
55
|
+
const renderedFocusTemplate = fillPromptTemplate(focusTemplate, {
|
|
56
|
+
STANDARDS_SECTION: standardsSection,
|
|
57
|
+
});
|
|
58
|
+
return `${basePrompt}\n\n${renderedFocusTemplate}`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { logger } from '../../core/logging.js';
|
|
5
|
+
import { buildTranslationReviewGuidance } from './translation-review-policy.js';
|
|
6
|
+
const MAX_BODY_CHARS = 4000;
|
|
7
|
+
let cachedStandardsSection = null;
|
|
8
|
+
const REVIEW_TASK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const STANDARDS_PROMPTS_DIR = path.resolve(REVIEW_TASK_DIR, '../../../src/review/prompts');
|
|
10
|
+
function loadOutlineMap() {
|
|
11
|
+
try {
|
|
12
|
+
const mapPath = path.join(STANDARDS_PROMPTS_DIR, 'outline-map.json');
|
|
13
|
+
const raw = fs.readFileSync(mapPath, 'utf8');
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
logger.warn('Failed to load outline map', {
|
|
18
|
+
error: error instanceof Error ? error.message : String(error),
|
|
19
|
+
});
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function loadStandardsSection() {
|
|
24
|
+
if (cachedStandardsSection !== null) {
|
|
25
|
+
return cachedStandardsSection;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const outlineMap = loadOutlineMap();
|
|
29
|
+
const files = fs
|
|
30
|
+
.readdirSync(STANDARDS_PROMPTS_DIR)
|
|
31
|
+
.filter((f) => f.endsWith('.md'))
|
|
32
|
+
.sort();
|
|
33
|
+
const sections = files.map((file) => {
|
|
34
|
+
const content = fs.readFileSync(path.join(STANDARDS_PROMPTS_DIR, file), 'utf8');
|
|
35
|
+
const title = file.replace('.md', '');
|
|
36
|
+
const entry = outlineMap[file];
|
|
37
|
+
const header = entry ? `### ${title}\n\nHandbook: ${entry.url}` : `### ${title}`;
|
|
38
|
+
return `${header}\n\n${content}`;
|
|
39
|
+
});
|
|
40
|
+
const result = sections.join('\n\n---\n\n');
|
|
41
|
+
cachedStandardsSection = result;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.warn('Failed to load standards section', {
|
|
46
|
+
error: error instanceof Error ? error.message : String(error),
|
|
47
|
+
dir: STANDARDS_PROMPTS_DIR,
|
|
48
|
+
});
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function formatPrConversation(prConversation) {
|
|
53
|
+
const promptText = prConversation?.promptText.trim();
|
|
54
|
+
if (promptText) {
|
|
55
|
+
return promptText;
|
|
56
|
+
}
|
|
57
|
+
if (prConversation?.stats?.summarizationFailed && prConversation.stats.rawChars > 0) {
|
|
58
|
+
return 'Prior PR conversation existed but could not be included.';
|
|
59
|
+
}
|
|
60
|
+
return 'No prior PR conversation was included.';
|
|
61
|
+
}
|
|
62
|
+
export function buildReviewPromptVariables({ repoOwner, repoName, prNumber, prTitle, prBody, prConversation, prAuthor, baseRef, headRef, diffSection, truncated, standardsSection, }) {
|
|
63
|
+
const safeBody = (prBody ?? '').trim();
|
|
64
|
+
const condensedBody = safeBody
|
|
65
|
+
? safeBody.slice(0, MAX_BODY_CHARS)
|
|
66
|
+
: 'No PR description was provided.';
|
|
67
|
+
const fullDiffSection = diffSection + (truncated ? '\n[Diff was truncated to fit token limits]' : '');
|
|
68
|
+
return {
|
|
69
|
+
REPO_FULL: `${repoOwner}/${repoName}`,
|
|
70
|
+
PR_NUMBER: String(prNumber),
|
|
71
|
+
PR_TITLE: prTitle || 'Untitled',
|
|
72
|
+
PR_AUTHOR: prAuthor,
|
|
73
|
+
BASE_REF: baseRef,
|
|
74
|
+
HEAD_REF: headRef,
|
|
75
|
+
PR_BODY: condensedBody,
|
|
76
|
+
PR_CONVERSATION: formatPrConversation(prConversation),
|
|
77
|
+
DIFF_SECTION: fullDiffSection,
|
|
78
|
+
STANDARDS_SECTION: standardsSection,
|
|
79
|
+
TRANSLATION_REVIEW_GUIDANCE: buildTranslationReviewGuidance(repoOwner, repoName),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { buildDatadogLink, deriveLangfuseTraceId } from 'doistbot-shared-contracts';
|
|
2
|
+
const FEEDBACK_ISSUE_URL = 'https://github.com/todoist/doistbot-app/issues/new';
|
|
3
|
+
const FEEDBACK_ISSUE_TEMPLATE = 'review-feedback.yml';
|
|
4
|
+
const FEEDBACK_LINK_TEXT = 'Share Feedback';
|
|
5
|
+
const DATADOG_LINK_TEXT = 'Review Logs';
|
|
6
|
+
const LANGFUSE_LINK_TEXT = 'Trace';
|
|
7
|
+
const REVIEW_FEEDBACK_FOOTER_REGEX = /\n*<sub>\[Share Feedback\][\s\S]*?<\/sub>\s*$/i;
|
|
8
|
+
export function buildReviewFeedbackIssueUrl({ owner, repo, prNumber, deliveryId, }) {
|
|
9
|
+
const feedbackUrl = new URL(FEEDBACK_ISSUE_URL);
|
|
10
|
+
const prUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}`;
|
|
11
|
+
const datadogLink = buildDatadogLink(deliveryId);
|
|
12
|
+
feedbackUrl.searchParams.set('template', FEEDBACK_ISSUE_TEMPLATE);
|
|
13
|
+
feedbackUrl.searchParams.set('title', `Review feedback for ${owner}/${repo}`);
|
|
14
|
+
feedbackUrl.searchParams.set('pr_url', prUrl);
|
|
15
|
+
if (datadogLink) {
|
|
16
|
+
feedbackUrl.searchParams.set('datadog_url', datadogLink);
|
|
17
|
+
}
|
|
18
|
+
if (deliveryId) {
|
|
19
|
+
feedbackUrl.searchParams.set('trace_id', deriveLangfuseTraceId(deliveryId));
|
|
20
|
+
}
|
|
21
|
+
return feedbackUrl.toString();
|
|
22
|
+
}
|
|
23
|
+
function buildLangfuseTraceLink(deliveryId) {
|
|
24
|
+
const prefix = process.env.LANGFUSE_TRACE_URL_PREFIX?.trim();
|
|
25
|
+
if (!prefix || !deliveryId)
|
|
26
|
+
return undefined;
|
|
27
|
+
return `${prefix}${deriveLangfuseTraceId(deliveryId)}`;
|
|
28
|
+
}
|
|
29
|
+
export function stripReviewFeedbackFooter(body) {
|
|
30
|
+
return body.replace(REVIEW_FEEDBACK_FOOTER_REGEX, '').trim();
|
|
31
|
+
}
|
|
32
|
+
export function appendReviewFeedbackLink(summary, params) {
|
|
33
|
+
const trimmedSummary = summary.trim();
|
|
34
|
+
if (!trimmedSummary) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
const feedbackUrl = buildReviewFeedbackIssueUrl(params);
|
|
38
|
+
const footerLinks = [`[${FEEDBACK_LINK_TEXT}](${feedbackUrl})`];
|
|
39
|
+
const datadogLink = buildDatadogLink(params.deliveryId);
|
|
40
|
+
if (datadogLink) {
|
|
41
|
+
footerLinks.push(`[${DATADOG_LINK_TEXT}](${datadogLink})`);
|
|
42
|
+
}
|
|
43
|
+
const langfuseLink = buildLangfuseTraceLink(params.deliveryId);
|
|
44
|
+
if (langfuseLink) {
|
|
45
|
+
footerLinks.push(`[${LANGFUSE_LINK_TEXT}](${langfuseLink})`);
|
|
46
|
+
}
|
|
47
|
+
return `${trimmedSummary}\n\n<sub>${footerLinks.join(' • ')}</sub>`;
|
|
48
|
+
}
|