@doist/doistbot-cli 1.0.4 → 1.0.6

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 (82) hide show
  1. package/dist/actions/auth.js +62 -28
  2. package/dist/actions/doctor.js +16 -11
  3. package/dist/actions/review.js +77 -11
  4. package/dist/auth.js +53 -8
  5. package/dist/config.js +40 -6
  6. package/dist/env.js +1 -1
  7. package/dist/file-store.js +1 -1
  8. package/dist/runtime.js +1 -1
  9. package/dist/terminal.js +2 -7
  10. package/package.json +7 -7
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
  12. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
  13. package/sandbox/dist/core/bot-logins.js +0 -13
  14. package/sandbox/dist/core/check-run.js +16 -0
  15. package/sandbox/dist/core/datadog-metrics.js +6 -54
  16. package/sandbox/dist/core/logger-span-processor.js +78 -0
  17. package/sandbox/dist/core/pi.js +270 -186
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +71 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/config.js +1 -1
  22. package/sandbox/dist/providers/credentials.js +40 -0
  23. package/sandbox/dist/providers/gemini.js +8 -10
  24. package/sandbox/dist/providers/helpers.js +18 -38
  25. package/sandbox/dist/providers/index.js +4 -3
  26. package/sandbox/dist/providers/openai.js +1 -8
  27. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  28. package/sandbox/dist/providers/pi-review.js +28 -55
  29. package/sandbox/dist/providers/schemas.js +0 -39
  30. package/sandbox/dist/tasks/chat/chat.js +162 -101
  31. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  32. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  33. package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
  34. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  35. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  36. package/sandbox/dist/tasks/issue-triage/context.js +25 -2
  37. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
  38. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  39. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  40. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  41. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  42. package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
  43. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  44. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  45. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  46. package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
  47. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  48. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  49. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  50. package/sandbox/dist/tasks/review/check-run.js +2 -1
  51. package/sandbox/dist/tasks/review/conversation-context.js +15 -8
  52. package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
  53. package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
  54. package/sandbox/dist/tasks/review/engines/shared.js +15 -61
  55. package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
  56. package/sandbox/dist/tasks/review/local.js +2 -29
  57. package/sandbox/dist/tasks/review/prompt.js +2 -49
  58. package/sandbox/dist/tasks/review/review-summary.js +120 -0
  59. package/sandbox/dist/tasks/review/review.js +49 -69
  60. package/sandbox/dist/tasks/review/runner.js +57 -52
  61. package/sandbox/dist/tasks/review/summary-model.js +172 -16
  62. package/sandbox/dist/tasks/review/thinking-level.js +4 -2
  63. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
  64. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
  65. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  66. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  67. package/sandbox/dist/providers/anthropic.js +0 -30
  68. package/sandbox/dist/tasks/review/author-format.js +0 -14
  69. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  70. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  71. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  72. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  73. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  74. package/sandbox/dist/tasks/review/council/types.js +0 -4
  75. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  76. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  77. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  78. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  79. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  80. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  81. package/sandbox/review-prompt.md +0 -209
  82. package/sandbox/vote-prompt.md +0 -69
@@ -2,19 +2,10 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { load as yamlLoad } from 'js-yaml';
4
4
  export const CONFIG_FILE_PATH = '.github/doistbot.yml';
5
- export const DEFAULT_COUNCIL_MODELS = ['gemini', 'openai'];
5
+ export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai', 'deepseek', 'zai'];
6
6
  function parseRepoConfigContent(content) {
7
7
  return yamlLoad(content);
8
8
  }
9
- function resolveWorkspacePath(workspaceDir, repoPath) {
10
- const root = path.resolve(workspaceDir);
11
- const resolved = path.resolve(root, repoPath);
12
- const relative = path.relative(root, resolved);
13
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
14
- return null;
15
- }
16
- return resolved;
17
- }
18
9
  async function fetchFileContent({ octokit, owner, repo, path, ref, logger, }) {
19
10
  const params = {
20
11
  owner,
@@ -99,80 +90,13 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
99
90
  return null;
100
91
  }
101
92
  }
102
- /**
103
- * Fetches a custom prompt file from the repository.
104
- * Returns null if the file doesn't exist.
105
- */
106
- export async function getCustomPrompt({ octokit, owner, repo, path, ref, logger, }) {
107
- let content;
108
- try {
109
- content = await fetchFileContent({ octokit, owner, repo, path, ref, logger });
110
- }
111
- catch (error) {
112
- logger.warn('Error fetching custom prompt', {
113
- component: 'repo-config',
114
- repository: `${owner}/${repo}`,
115
- path,
116
- error: error instanceof Error ? error.message : String(error),
117
- });
118
- return null;
119
- }
120
- if (content !== null) {
121
- logger.info('Loaded custom prompt', {
122
- component: 'repo-config',
123
- repository: `${owner}/${repo}`,
124
- path,
125
- });
126
- }
127
- return content;
128
- }
129
- export function getLocalCustomPrompt({ workspaceDir, path: promptPath, logger, }) {
130
- const resolvedPath = resolveWorkspacePath(workspaceDir, promptPath);
131
- if (resolvedPath === null) {
132
- logger.warn('Custom prompt path escapes workspace', {
133
- component: 'repo-config',
134
- path: promptPath,
135
- });
136
- return null;
137
- }
138
- if (!fs.existsSync(resolvedPath)) {
139
- return null;
140
- }
141
- try {
142
- const content = fs.readFileSync(resolvedPath, 'utf8');
143
- logger.info('Loaded local custom prompt', {
144
- component: 'repo-config',
145
- path: promptPath,
146
- });
147
- return content;
148
- }
149
- catch (error) {
150
- logger.warn('Failed to load local custom prompt', {
151
- component: 'repo-config',
152
- path: promptPath,
153
- error: error instanceof Error ? error.message : String(error),
154
- });
155
- return null;
156
- }
157
- }
158
- export function parseCouncilModelsEnabled(raw, { defaultModels = DEFAULT_COUNCIL_MODELS } = {}) {
159
- if (!raw?.trim()) {
160
- return [...defaultModels];
161
- }
162
- const allowedModels = new Set(DEFAULT_COUNCIL_MODELS);
163
- const models = [];
164
- for (const token of raw.split(',')) {
165
- const model = token.trim().toLowerCase();
166
- if (!model || !allowedModels.has(model)) {
167
- continue;
168
- }
169
- if (!models.includes(model)) {
170
- models.push(model);
171
- }
172
- }
173
- return models.length > 0 ? models : [...defaultModels];
93
+ export function getDefaultReviewModels() {
94
+ return [...DEFAULT_REVIEW_MODELS];
174
95
  }
175
- export function getDefaultCouncilModels() {
176
- return [...DEFAULT_COUNCIL_MODELS];
96
+ export function parseReviewModelsEnabled(value) {
97
+ const models = value
98
+ ?.split(',')
99
+ .map((model) => model.trim())
100
+ .filter(Boolean) ?? [];
101
+ return models.length > 0 ? [...new Set(models)] : getDefaultReviewModels();
177
102
  }
178
- export { DEFAULT_REVIEW_ENGINE, parseReviewEngine, resolveReviewEngine } from './review-engine.js';
@@ -6,22 +6,9 @@ export const KNOWN_DOISTBOT_LOGINS = new Set([
6
6
  'doistbot-app',
7
7
  'doistbot-app[bot]',
8
8
  ]);
9
- export const KNOWN_AUTOMATION_LOGINS = new Set([
10
- ...KNOWN_DOISTBOT_LOGINS,
11
- 'dependabot',
12
- 'dependabot[bot]',
13
- 'renovate',
14
- 'renovate[bot]',
15
- ]);
16
9
  export function isKnownDoistbotLogin(login) {
17
10
  if (!login) {
18
11
  return false;
19
12
  }
20
13
  return KNOWN_DOISTBOT_LOGINS.has(login.toLowerCase());
21
14
  }
22
- export function isKnownAutomationLogin(login) {
23
- if (!login) {
24
- return false;
25
- }
26
- return KNOWN_AUTOMATION_LOGINS.has(login.toLowerCase());
27
- }
@@ -24,6 +24,7 @@ function truncateTitle(value) {
24
24
  */
25
25
  export async function postCheckRun(params) {
26
26
  const { octokit, owner, repo, sha, title, summary, detailsUrl, name, externalId } = params;
27
+ const checkRunId = params.checkRunId;
27
28
  const status = params.status ?? 'completed';
28
29
  const conclusion = status === 'completed' ? params.conclusion : undefined;
29
30
  const truncatedTitle = truncateTitle(title);
@@ -39,6 +40,21 @@ export async function postCheckRun(params) {
39
40
  output: { title: truncatedTitle, summary },
40
41
  };
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
+ }
42
58
  const existing = await octokit.rest.checks.listForRef({
43
59
  owner,
44
60
  repo,
@@ -1,17 +1,20 @@
1
+ import { createDatadogMetrics, resolveDatadogApiKey, resolveStandardObservabilityTags, } from 'doistbot-datadog-metrics';
1
2
  import { logger } from './logging.js';
2
3
  const TASK_COMPLETION_METRIC = 'doistbot.task.completion';
3
4
  const TASK_DURATION_METRIC = 'doistbot.task.duration_seconds';
4
- const DATADOG_REQUEST_TIMEOUT_MS = 5_000;
5
5
  let hasWarnedAboutMissingDatadogApiKey = false;
6
+ const { postDatadogMetric } = createDatadogMetrics({ logger });
6
7
  function toTaskMetricBaseModel(model) {
7
8
  switch (model) {
9
+ case 'deepseek':
10
+ return 'deepseek';
8
11
  case 'gemini':
9
12
  case 'google':
10
13
  return 'google';
14
+ case 'zai':
15
+ return 'zai';
11
16
  case 'openai':
12
17
  return 'openai';
13
- case 'anthropic':
14
- return 'anthropic';
15
18
  case 'none':
16
19
  return 'none';
17
20
  }
@@ -41,57 +44,6 @@ function buildTaskMetricTags({ type, repository, model, outcome, reason, }) {
41
44
  }
42
45
  return tags;
43
46
  }
44
- // Doist Observability Standard requires service/env/version on metrics submitted
45
- // directly to the API — the host agent would otherwise inject them.
46
- function resolveStandardObservabilityTags() {
47
- const tags = [];
48
- const service = process.env.DD_SERVICE?.trim();
49
- const env = process.env.DD_ENV?.trim();
50
- const version = process.env.DD_VERSION?.trim();
51
- if (service)
52
- tags.push(`service:${service}`);
53
- if (env)
54
- tags.push(`env:${env}`);
55
- if (version)
56
- tags.push(`version:${version}`);
57
- return tags;
58
- }
59
- function resolveDatadogApiKey() {
60
- const apiKey = process.env.DATADOG_API_KEY?.trim();
61
- return apiKey || null;
62
- }
63
- function resolveDatadogSite() {
64
- return process.env.DATADOG_SITE?.trim() || process.env.DD_SITE?.trim() || 'datadoghq.com';
65
- }
66
- async function postDatadogMetric({ apiKey, path, body, }) {
67
- const url = `https://api.${resolveDatadogSite()}${path}`;
68
- try {
69
- const response = await fetch(url, {
70
- method: 'POST',
71
- signal: AbortSignal.timeout(DATADOG_REQUEST_TIMEOUT_MS),
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- 'DD-API-KEY': apiKey,
75
- },
76
- body: JSON.stringify(body),
77
- });
78
- if (response.ok) {
79
- return;
80
- }
81
- const errorBody = (await response.text()).slice(0, 500);
82
- logger.warn('Failed to submit Datadog task metric', {
83
- path,
84
- status: response.status,
85
- errorBody,
86
- });
87
- }
88
- catch (error) {
89
- logger.warn('Failed to submit Datadog task metric', {
90
- path,
91
- error: error instanceof Error ? error.message : String(error),
92
- });
93
- }
94
- }
95
47
  export async function recordTaskMetrics({ type, repository, model, outcome, durationMs, reason, }) {
96
48
  const apiKey = resolveDatadogApiKey();
97
49
  if (!apiKey) {
@@ -0,0 +1,78 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { logger } from './logging.js';
3
+ const MAX_ATTR_SERIALIZED_LEN = 4096;
4
+ export function hrTimeToMs(hrTime) {
5
+ const [seconds, nanos] = hrTime;
6
+ return Math.round(seconds * 1000 + nanos / 1_000_000);
7
+ }
8
+ function capString(value) {
9
+ if (value.length <= MAX_ATTR_SERIALIZED_LEN)
10
+ return value;
11
+ // Reserve room for the marker so the FINAL emitted value stays <= the cap.
12
+ const marker = `…[truncated, ${value.length} chars]`;
13
+ return `${value.slice(0, Math.max(0, MAX_ATTR_SERIALIZED_LEN - marker.length))}${marker}`;
14
+ }
15
+ export function truncateAttrs(attrs) {
16
+ const out = {};
17
+ for (const [key, value] of Object.entries(attrs)) {
18
+ if (key === 'doistbot.kind')
19
+ continue; // internal marker, never emitted
20
+ if (typeof value === 'string') {
21
+ out[key] = capString(value);
22
+ }
23
+ else if (value !== null && typeof value === 'object') {
24
+ // Arrays/objects (e.g. decision paths, parsed-label arrays): keep
25
+ // the native value if its JSON serialization is under the cap;
26
+ // otherwise replace it with the truncated serialized string.
27
+ const json = JSON.stringify(value);
28
+ out[key] = json.length > MAX_ATTR_SERIALIZED_LEN ? capString(json) : value;
29
+ }
30
+ else {
31
+ out[key] = value; // number / boolean / null — inherently small
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ // Parent span id access on a finished span is SDK-version-specific. Prefer the
37
+ // newer `parentSpanContext`, fall back to the older `parentSpanId`. Only used
38
+ // in onEnd — onStart derives the parent from the parent Context instead.
39
+ function parentSpanIdOf(span) {
40
+ const withParentCtx = span;
41
+ if (withParentCtx.parentSpanContext?.spanId)
42
+ return withParentCtx.parentSpanContext.spanId;
43
+ const withParentId = span;
44
+ return withParentId.parentSpanId ?? null;
45
+ }
46
+ // Span attributes are spread FIRST so task attributes can never overwrite the
47
+ // envelope fields (event/name/span_id/parent_span_id/duration_ms/status/error)
48
+ // that the Datadog pipeline and the visualizer key on. Timestamps come from
49
+ // the logging system, never from meta.
50
+ export class LoggerSpanProcessor {
51
+ onStart(span, parentContext) {
52
+ const readable = span;
53
+ const isTool = readable.attributes['doistbot.kind'] === 'tool';
54
+ logger.info(`${isTool ? 'tool' : 'phase'}.start: ${readable.name}`, {
55
+ ...truncateAttrs(readable.attributes),
56
+ event: isTool ? 'tool.start' : 'phase.start',
57
+ name: readable.name,
58
+ span_id: readable.spanContext().spanId,
59
+ parent_span_id: trace.getSpanContext(parentContext)?.spanId ?? null,
60
+ });
61
+ }
62
+ onEnd(span) {
63
+ const isTool = span.attributes['doistbot.kind'] === 'tool';
64
+ const status = span.status.code === SpanStatusCode.ERROR ? 'error' : 'ok';
65
+ logger.info(`${isTool ? 'tool' : 'phase'}.end: ${span.name}`, {
66
+ ...truncateAttrs(span.attributes),
67
+ event: isTool ? 'tool.end' : 'phase.end',
68
+ name: span.name,
69
+ span_id: span.spanContext().spanId,
70
+ parent_span_id: parentSpanIdOf(span),
71
+ duration_ms: hrTimeToMs(span.duration),
72
+ status,
73
+ ...(status === 'error' && span.status.message ? { error: span.status.message } : {}),
74
+ });
75
+ }
76
+ async forceFlush() { }
77
+ async shutdown() { }
78
+ }