@doist/doistbot-cli 1.0.12 → 1.0.14

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 (27) hide show
  1. package/dist/config.js +1 -1
  2. package/package.json +5 -5
  3. package/sandbox/dist/core/pi.js +2 -2
  4. package/sandbox/dist/core/repository-map.js +1 -1
  5. package/sandbox/dist/tasks/chat/chat.js +1 -1
  6. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +5 -1
  7. package/sandbox/dist/tasks/issue-summarize/context.js +1 -0
  8. package/sandbox/dist/tasks/issue-summarize/model.js +24 -7
  9. package/sandbox/dist/tasks/issue-summarize/summarize.js +6 -4
  10. package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
  11. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +16 -11
  12. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +2 -0
  13. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +21 -19
  14. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +32 -31
  15. package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
  16. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +41 -3
  17. package/sandbox/dist/tasks/issue-triage/markers.js +1 -0
  18. package/sandbox/dist/tasks/issue-triage/pr-creator.js +59 -1
  19. package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
  20. package/sandbox/dist/tasks/issue-triage/resolution.js +3 -0
  21. package/sandbox/dist/tasks/issue-triage/triage.js +5 -1
  22. package/sandbox/dist/tasks/review/thinking-level.js +1 -1
  23. package/sandbox/src/review/prompts/ai-internal-tools.md +3 -3
  24. package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
  25. package/sandbox/src/review/prompts/outline-map.json +14 -10
  26. package/sandbox/src/review/prompts/service-production-readiness.md +2 -2
  27. package/sandbox/src/review/prompts/which-cloud-and-where-to-deploy.md +0 -344
@@ -1,5 +1,5 @@
1
1
  import { normalizeScopedLabelValue, scopedLabelValue } from '../../core/label-parsing.js';
2
- import { APP_LABEL_ALIASES, findIssueRoute, findRepositoryRoutes, findRepositoryTarget as findRoutingRepositoryTarget, PRODUCT_LABEL_ALIASES, PRODUCT_ROUTES, } from '../../core/repository-map.js';
2
+ import { APP_LABEL_ALIASES, findIntegrationRoute, findIssueRoute, findOwningRoutes, findRepositoryTarget as findRoutingRepositoryTarget, INTEGRATION_LABEL_ALIASES, PRODUCT_LABEL_ALIASES, PRODUCT_ROUTES, } from '../../core/repository-map.js';
3
3
  const HERO_GROUPS = {
4
4
  'android-hero': { key: 'android-hero', mention: '@Doist/android-hero' },
5
5
  'app-copy-hero': { key: 'app-copy-hero', mention: '@Doist/app-copy-hero' },
@@ -63,11 +63,18 @@ export function resolveHeroTeamSlug(target) {
63
63
  function resolveAppLabelAlias(value) {
64
64
  return APP_LABEL_ALIASES[normalizeScopedLabelValue(value)];
65
65
  }
66
+ function resolveIntegrationLabelAlias(value) {
67
+ return INTEGRATION_LABEL_ALIASES[normalizeScopedLabelValue(value)];
68
+ }
66
69
  export function isKnownHeroRoutingLabel(label) {
67
70
  const teamValue = scopedLabelValue(label, 'Team');
68
71
  if (teamValue && TEAM_LABEL_TO_HERO[normalizeScopedLabelValue(teamValue)]) {
69
72
  return true;
70
73
  }
74
+ const integrationValue = scopedLabelValue(label, 'Integration');
75
+ if (integrationValue && resolveIntegrationLabelAlias(integrationValue)) {
76
+ return true;
77
+ }
71
78
  const appValue = scopedLabelValue(label, 'App');
72
79
  if (!appValue) {
73
80
  return false;
@@ -91,6 +98,28 @@ function resolveProduct(labels) {
91
98
  }
92
99
  return products.size === 1 ? products.values().next().value : undefined;
93
100
  }
101
+ function resolveIntegrationHeroKeys(labels) {
102
+ const product = resolveProduct(labels);
103
+ const heroKeys = new Set();
104
+ for (const label of labels) {
105
+ const integrationValue = scopedLabelValue(label, 'Integration');
106
+ if (!integrationValue) {
107
+ continue;
108
+ }
109
+ const integration = resolveIntegrationLabelAlias(integrationValue);
110
+ if (!integration) {
111
+ continue;
112
+ }
113
+ const route = findIntegrationRoute(integration);
114
+ // An integration from another product is not in scope for this issue, so
115
+ // it must not contribute a hero the repository resolver would never pick.
116
+ if (product && route.product !== product) {
117
+ continue;
118
+ }
119
+ heroKeys.add(route.heroTeam);
120
+ }
121
+ return heroKeys;
122
+ }
94
123
  function resolveAppRouteHeroKeys(labels) {
95
124
  const routeIds = new Set();
96
125
  for (const label of labels) {
@@ -99,7 +128,9 @@ function resolveAppRouteHeroKeys(labels) {
99
128
  continue;
100
129
  }
101
130
  const appLabel = resolveAppLabelAlias(appValue);
102
- if (appLabel && appLabel !== 'all') {
131
+ // `all` spans every route and `integration` names no route of its own —
132
+ // the accompanying `Integration:*` label supplies that hero instead.
133
+ if (appLabel && appLabel !== 'all' && appLabel !== 'integration') {
103
134
  routeIds.add(appLabel);
104
135
  }
105
136
  }
@@ -135,6 +166,13 @@ export function resolveHeroGroup(labels, primaryRepo) {
135
166
  if (teamHeroKeys.size > 1) {
136
167
  return null;
137
168
  }
169
+ const integrationHeroKeys = resolveIntegrationHeroKeys(labels);
170
+ if (integrationHeroKeys.size === 1) {
171
+ return HERO_GROUPS[integrationHeroKeys.values().next().value];
172
+ }
173
+ if (integrationHeroKeys.size > 1) {
174
+ return null;
175
+ }
138
176
  const appHeroKeys = resolveAppRouteHeroKeys(labels);
139
177
  if (appHeroKeys.size === 1) {
140
178
  return HERO_GROUPS[appHeroKeys.values().next().value];
@@ -143,7 +181,7 @@ export function resolveHeroGroup(labels, primaryRepo) {
143
181
  return null;
144
182
  }
145
183
  if (primaryRepo) {
146
- const repositoryHeroKeys = new Set(findRepositoryRoutes(primaryRepo).map((route) => route.heroTeam));
184
+ const repositoryHeroKeys = new Set(findOwningRoutes(primaryRepo).map((route) => route.heroTeam));
147
185
  if (repositoryHeroKeys.size === 1) {
148
186
  return HERO_GROUPS[repositoryHeroKeys.values().next().value];
149
187
  }
@@ -1,3 +1,4 @@
1
1
  export const HERO_AUTOFIX_MARKER = '<!--doistbot-autofix-followup-->';
2
+ export const ASSIGNEE_REVIEW_MARKER = '<!--doistbot-assignee-review-requested-->';
2
3
  export const FIX_LOOP_START_MARKER = '<!--doistbot-fixloop-start-->';
3
4
  export const AUTOFIX_FAILURE_MARKER = '<!--doistbot-autofix-failure-->';
@@ -4,6 +4,10 @@ import path from 'path';
4
4
  import { DOISTBOT_AUTO_FIX_LABEL, DOISTBOT_SPECULATIVE_FIX_LABEL } from 'doistbot-shared-contracts';
5
5
  import { logger } from '../../core/logging.js';
6
6
  import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
7
+ import { withRetry } from '../../core/retry.js';
8
+ import { requestAssigneeReview } from './assignee-review.js';
9
+ import { GITHUB_REQUEST_TIMEOUT_MS } from './github-request.js';
10
+ import { markPullRequestReady } from './pr-readiness.js';
7
11
  const PR_TEMPLATE_PATHS = [
8
12
  '.github/pull_request_template.md',
9
13
  '.github/PULL_REQUEST_TEMPLATE.md',
@@ -241,7 +245,7 @@ export async function commitAndPushFix(input) {
241
245
  /**
242
246
  * Commits + pushes a second fix attempt on the EXISTING iter-0 branch. Unlike
243
247
  * commitAndPushFix, this does not create a new branch — callers (the fix loop's
244
- * iter-1 path) need the existing draft PR to pick up the new commit, which only
248
+ * iter-1 path) need the existing PR to pick up the new commit, which only
245
249
  * works if we push to the same branch the PR was opened from.
246
250
  *
247
251
  * Assumes the working tree already has the iter-1 changes staged (typically by
@@ -272,6 +276,7 @@ export async function commitAndPushIteration(input) {
272
276
  });
273
277
  return { headSha };
274
278
  }
279
+ /** Opens a draft, then marks it ready if an eligible assignee review was requested. */
275
280
  export async function openDraftFixPR(input) {
276
281
  const prBody = input.prBody
277
282
  ? stripEmptySections(input.prBody)
@@ -298,6 +303,13 @@ export async function openDraftFixPR(input) {
298
303
  base: repo.default_branch,
299
304
  draft: true,
300
305
  });
306
+ const requestedAssignees = await requestAssigneeReview({
307
+ owner: input.repoOwner,
308
+ repo: input.repoName,
309
+ prNumber: pr.number,
310
+ octokit: input.octokit,
311
+ assigneeReview: input.assigneeReview,
312
+ });
301
313
  try {
302
314
  const prLabel = input.fixAssessment?.classification === 'speculative'
303
315
  ? DOISTBOT_SPECULATIVE_FIX_LABEL
@@ -307,6 +319,7 @@ export async function openDraftFixPR(input) {
307
319
  repo: input.repoName,
308
320
  issue_number: pr.number,
309
321
  labels: [prLabel],
322
+ request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
310
323
  });
311
324
  }
312
325
  catch (error) {
@@ -317,6 +330,50 @@ export async function openDraftFixPR(input) {
317
330
  error: error instanceof Error ? error.message : String(error),
318
331
  });
319
332
  }
333
+ if (requestedAssignees.length > 0) {
334
+ // Apply the autofix label before emitting ready_for_review. An accepted
335
+ // assignee request is sufficient, even if writing its receipt failed.
336
+ try {
337
+ await markPullRequestReady({
338
+ octokit: input.octokit,
339
+ owner: input.repoOwner,
340
+ repo: input.repoName,
341
+ prNumber: pr.number,
342
+ });
343
+ logger.info('Assignee-reviewed fix PR marked ready', {
344
+ task: 'issue-triage',
345
+ repository: `${input.repoOwner}/${input.repoName}`,
346
+ prNumber: pr.number,
347
+ });
348
+ }
349
+ catch (error) {
350
+ // The PR and review request already exist. Preserve both and keep
351
+ // the fix loop running rather than reporting PR creation failure.
352
+ logger.warn('Failed to mark assignee-reviewed fix PR ready', {
353
+ task: 'issue-triage',
354
+ repository: `${input.repoOwner}/${input.repoName}`,
355
+ prNumber: pr.number,
356
+ error: error instanceof Error ? error.message : String(error),
357
+ });
358
+ try {
359
+ await withRetry(() => input.octokit.rest.issues.createComment({
360
+ owner: input.repoOwner,
361
+ repo: input.repoName,
362
+ issue_number: pr.number,
363
+ body: "Review was requested from the issue assignees, but I couldn't confirm that this PR was marked ready for review. Please check its status and mark it ready manually if needed.",
364
+ request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
365
+ }));
366
+ }
367
+ catch (commentError) {
368
+ logger.warn('Failed to report fix PR readiness failure', {
369
+ task: 'issue-triage',
370
+ repository: `${input.repoOwner}/${input.repoName}`,
371
+ prNumber: pr.number,
372
+ error: commentError instanceof Error ? commentError.message : String(commentError),
373
+ });
374
+ }
375
+ }
376
+ }
320
377
  logger.info('Fix PR created', {
321
378
  task: 'issue-triage',
322
379
  repository: `${input.repoOwner}/${input.repoName}`,
@@ -327,5 +384,6 @@ export async function openDraftFixPR(input) {
327
384
  return {
328
385
  prUrl: pr.html_url,
329
386
  prNumber: pr.number,
387
+ ...(requestedAssignees.length > 0 && { requestedAssignees }),
330
388
  };
331
389
  }
@@ -0,0 +1,25 @@
1
+ import { withRetry } from '../../core/retry.js';
2
+ import { GITHUB_REQUEST_TIMEOUT_MS } from './github-request.js';
3
+ /** Safe both at creation and when the later CI-driven promotion runs. */
4
+ export async function markPullRequestReady({ octokit, owner, repo, prNumber, }) {
5
+ // Re-read on every attempt: a mutation may have succeeded even if its
6
+ // response was lost. Repeating that mutation on a ready PR would fail.
7
+ await withRetry(async () => {
8
+ const { data: pr } = await octokit.rest.pulls.get({
9
+ owner,
10
+ repo,
11
+ pull_number: prNumber,
12
+ request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
13
+ });
14
+ if (!pr.draft)
15
+ return;
16
+ await octokit.graphql(`mutation($pullRequestId: ID!) {
17
+ markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {
18
+ pullRequest { isDraft }
19
+ }
20
+ }`, {
21
+ pullRequestId: pr.node_id,
22
+ request: { signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS) },
23
+ });
24
+ });
25
+ }
@@ -12,10 +12,13 @@ export async function resolveTriageScope(params) {
12
12
  parsed_products: result.parsedLabels.productLabels,
13
13
  parsed_apps: result.parsedLabels.appLabels,
14
14
  parsed_teams: result.parsedLabels.teamLabels,
15
+ parsed_integrations: result.parsedLabels.integrationLabels,
15
16
  matched_routes: result.matchedRoutes,
17
+ matched_integrations: result.matchedIntegrations,
16
18
  unknown_product_labels: result.parsedLabels.unknownProductLabels,
17
19
  unknown_app_labels: result.parsedLabels.unknownAppLabels,
18
20
  unknown_team_labels: result.parsedLabels.unknownTeamLabels,
21
+ unknown_integration_labels: result.parsedLabels.unknownIntegrationLabels,
19
22
  });
20
23
  span.setAttributes({
21
24
  'resolution.status': result.status,
@@ -254,7 +254,7 @@ async function runIssueTriagePipeline(ctx, mode) {
254
254
  extraEnv: {
255
255
  GH_TOKEN: issueInstallationAuth.token,
256
256
  },
257
- thinkingLevel: 'high',
257
+ thinkingLevel: 'xhigh',
258
258
  });
259
259
  if (!triageResult.ok) {
260
260
  await postFinalTriageComment(buildTriageFailureMessage(triageResult.error));
@@ -318,6 +318,10 @@ async function runIssueTriagePipeline(ctx, mode) {
318
318
  issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
319
319
  ghToken: issueInstallationAuth.token,
320
320
  loadRepoConfig: getCachedRepoConfig,
321
+ assigneeReview: {
322
+ assignees: (issue.assignees ?? []).map((assignee) => assignee.login),
323
+ membershipOctokit: readOctokit,
324
+ },
321
325
  });
322
326
  logger.info('triage.fix_attempted', {
323
327
  task: 'issue-triage',
@@ -3,7 +3,7 @@ export function getReviewThinkingLevel(model) {
3
3
  case 'deepseek':
4
4
  return 'high';
5
5
  case 'openai':
6
- return 'high';
6
+ return 'xhigh';
7
7
  case 'grok':
8
8
  return 'high';
9
9
  case 'zai':
@@ -202,7 +202,7 @@ flowchart TD
202
202
  Agent --> NonSensitive
203
203
  ```
204
204
 
205
- > Note: see [`examples/internal-ai-service/`](https://github.com/Doist/platform-standards/tree/main/examples/internal-ai-service/) for the server-side service and [`examples/local-cli/`](https://github.com/Doist/platform-standards/tree/main/examples/local-cli/) for the thin local CLI. The example implements Zendesk only; the same pattern applies to Sentry/Datadog.
205
+ > Note: see [`examples/internal-ai-service/`](https://github.com/Doist/standards/tree/main/examples/internal-ai-service/) for the server-side service and [`examples/local-cli/`](https://github.com/Doist/standards/tree/main/examples/local-cli/) for the thin local CLI. The example implements Zendesk only; the same pattern applies to Sentry/Datadog.
206
206
 
207
207
  ### How to read this diagram
208
208
 
@@ -236,9 +236,9 @@ Think of the diagram as three separate rooms, each with different access rules:
236
236
 
237
237
  1. **Remove** the direct connections to Zendesk, Sentry, and Datadog from your local AI setup.
238
238
 
239
- 2. **Set up an internal service** (the Platform team can help) that fetches and cleans data before passing it to the AI — see [`examples/internal-ai-service/main.go`](https://github.com/Doist/platform-standards/tree/main/examples/internal-ai-service/main.go) for a working reference. It should only return what's needed to answer the question — never raw personal information.
239
+ 2. **Set up an internal service** (the Platform team can help) that fetches and cleans data before passing it to the AI — see [`examples/internal-ai-service/main.go`](https://github.com/Doist/standards/tree/main/examples/internal-ai-service/main.go) for a working reference. It should only return what's needed to answer the question — never raw personal information.
240
240
 
241
- 3. **Point your local tool at that internal service** instead of at the third-party tools directly — see [`examples/local-cli/main.go`](https://github.com/Doist/platform-standards/tree/main/examples/local-cli/main.go) for a working reference.
241
+ 3. **Point your local tool at that internal service** instead of at the third-party tools directly — see [`examples/local-cli/main.go`](https://github.com/Doist/standards/tree/main/examples/local-cli/main.go) for a working reference.
242
242
 
243
243
  4. **Keep your Github, Twist, and Outline connections as-is** — those don't hold user personal data, so they're fine to use directly.
244
244
 
@@ -0,0 +1,102 @@
1
+ # Standard: Backend Repositories
2
+
3
+ | 📄 Document Type | Team Standard (Backend) |
4
+ | :--------------- | :------------------------------------------------------------------------------------------------------------------------ |
5
+ | 🎯 Status | Draft |
6
+ | 👤 Owner | Felipe Rodrigues |
7
+ | 📅 Last Reviewed | 2026-09-08 |
8
+ | 🗂️ References | [Standard: Service Production Readiness](https://handbook.doist.com/doc/standard-service-production-readiness-qgNRng8TJx) |
9
+ | 📌 Version | 0.2 |
10
+
11
+ This standard applies to **all backend repositories at Doist, regardless of language or current ownership.** Today that means `Todoist` and other repositories already owned by the Backend team as well as newer backend services such as `todoist-id` and `automations` - currently developed by squads, and expected to come under Backend team ownership over time.
12
+
13
+ Meeting this standard is part of what makes that handover possible.
14
+
15
+ ## Tests
16
+
17
+ Every repository must have automated tests. A repository with no tests is not production-ready.
18
+
19
+ - Tests must run in CI on every pull request.
20
+ - Tests must run in CI on every merge to `main`.
21
+ - Deployments must be gated on tests passing. In GitHub Actions, the deploy job must depend on the test job — via `needs: test` in the same workflow, or by invoking the test workflow as a reusable workflow and depending on that job. A deploy that races its own test run is not gated.
22
+
23
+ ## Branch Protection
24
+
25
+ Tests that exist but are never enforced provide false confidence. The `main` branch must be protected:
26
+
27
+ - Changes land via pull request — no direct pushes to `main`.
28
+ - Test, lint, and type-check jobs are configured as **required status checks**, so a PR cannot merge while they fail or haven't run.
29
+
30
+ ## Code Quality Hooks
31
+
32
+ - Pre-commit hooks must be used to enforce formatting, linting, and repo hygiene. The runner is language-specific: [prek](https://prek.j178.dev) in Python repositories, [husky](https://typicode.github.io/husky/) in JavaScript/TypeScript ones.
33
+ - The same checks must also run in CI on every pull request and merge to `main` (e.g. `prek run --all-files`, or the `npm` scripts the hooks call). Hooks that only run locally are advisory; developers can skip or misconfigure them.
34
+
35
+ ## README
36
+
37
+ Every repository must have a `README.md` with at least:
38
+
39
+ - Project description
40
+ - How to run the project locally
41
+ - How to run tests
42
+ - How to deploy the project
43
+
44
+ ## Dependency Updates
45
+
46
+ Automated dependency updates (Renovate or Dependabot) must be enabled. Update PRs go through the same CI gates as any other change.
47
+
48
+ ## Python Requirements
49
+
50
+ Python repositories must use the following stack (mirroring the main Todoist codebase):
51
+
52
+ | Concern | Tool |
53
+ | ------------------------------- | ---------------------------- |
54
+ | Project & dependency management | `uv` |
55
+ | Minimum Python version | 3.13 |
56
+ | Tests | `pytest` |
57
+ | Linting & formatting | `ruff check` + `ruff format` |
58
+ | Git hooks | `prek` |
59
+ | Type checking | `mypy` and `pyrefly` |
60
+ | Dead code detection | `vulture` |
61
+
62
+ - Dependencies in `pyproject.toml` must be pinned with upper bounds (`>=X.Y,<X+1`).
63
+ - Type hints are mandatory on production code.
64
+
65
+ ## JavaScript / TypeScript Requirements
66
+
67
+ JavaScript and TypeScript repositories must use the following stack (mirroring `automations`, our newest backend TS project):
68
+
69
+ | Concern | Tool |
70
+ | ------------------------------- | ----------------------------------------------- |
71
+ | Language | TypeScript |
72
+ | Runtime | Node.js — current LTS major |
73
+ | Project & dependency management | `npm` (workspaces for monorepos) |
74
+ | Tests | `vitest` |
75
+ | Linting & formatting | `oxlint` + `oxfmt` |
76
+ | Git hooks | `husky` |
77
+ | Type checking | `tsc --noEmit`, exposed as `npm run type-check` |
78
+
79
+ - Production code must be TypeScript. Plain JavaScript only where a tool requires it.
80
+ - The Node version must be pinned in `engines` and in a version file (`.node-version`, which `actions/setup-node` reads via `node-version-file`; `.nvmrc` alongside it if people use `nvm`). Version files pin Node only, so pin the npm major in `engines` as well.
81
+ - `engine-strict=true` must be set in `.npmrc`. Without it npm only warns on an `engines` mismatch, and the pins above do not actually stop local, CI and container builds from drifting apart.
82
+ - Dependencies in `package.json` must be pinned to exact versions, with the lockfile committed.
83
+ - The TypeScript config must extend the shared `@doist/tsconfig` base and keep `strict` on.
84
+
85
+ ## Related Documents
86
+
87
+ This standard complements the Doist-wide [Service Production Readiness](https://handbook.doist.com/doc/standard-service-production-readiness-qgNRng8TJx) standard: that one covers how a service must _behave_ in production while this one covers the baseline health a _repository_ must have before its code reaches production.
88
+
89
+ Related engineering-wide guidelines:
90
+
91
+ - [GitHub Repository Guidelines](https://handbook.doist.com/doc/github-repository-guidelines-iJNZ0D2vLN)
92
+ - [Automate linting and formatting](https://handbook.doist.com/doc/automate-linting-and-formatting-omSECLKiJI)
93
+ - [Keeping dependencies up-to-date](https://handbook.doist.com/doc/keeping-dependencies-up-to-date-Qf28E929N5)
94
+
95
+ ---
96
+
97
+ ## Open questions/Possible future expansions to the Standard
98
+
99
+ - Type checkers: Todoist currently runs `mypy`, `ty`, and `pyrefly` in parallel, but that is an evaluation state, not a recommendation (see [Choosing type checkers for Todoist](https://comms.todoist.com/69/ch/BH4VumxwScG1NAiomtcBq/t/CbVia3jW38koSRmHWXSeV/)). A June 2026 attempt to remove mypy was reverted after it left real gaps (pyrefly's legacy config disabled core checks; ty lacks Pydantic support). Current direction: pyrefly in strict mode is the candidate primary, mypy stays as the mature baseline, ty is a secondary bet pending Pydantic support. `mypy` + `pyrefly` therefore remains the right baseline for new repos; revisit when ty matures.
100
+ - Maybe create a [Copier](https://copier.readthedocs.io/en/stable/) template for Python repos — and an equivalent for TS ones — so new services start compliant? Or even a small template repo with “sane defaults” for our tooling.
101
+ - Dead code detection has no JS/TS entry yet. Python repos run `vulture`; `knip` is the closest equivalent, but nothing runs it today.
102
+ - Define the baseline ruff rule set / prek hook list based on the Todoist codebase (its `prek.toml` is a good starting point: ruff, standard pre-commit-hooks, shellcheck, hadolint, mypy/type checkers, vulture on pre-push).
@@ -1,22 +1,26 @@
1
1
  {
2
2
  "workload-placement.md": {
3
- "id": "26dac3b3-fc42-4e03-a559-ccff6b980557",
4
- "url": "https://handbook.doist.com/doc/standard-workload-placement-j2Xe36eQar"
3
+ "id": "",
4
+ "url": "https://handbook.doist.com/doc/standard-workload-placement-"
5
5
  },
6
6
  "secrets-management.md": {
7
- "id": "c0daf063-8120-4ace-a26c-fd6f64656324",
8
- "url": "https://handbook.doist.com/doc/standard-secrets-management-wbKmIfrtgr"
7
+ "id": "",
8
+ "url": "https://handbook.doist.com/doc/standard-secrets-management-"
9
9
  },
10
10
  "ai-internal-tools.md": {
11
- "id": "02f9d168-f91a-4632-88d9-4d25f4b33063",
12
- "url": "https://handbook.doist.com/doc/standard-internal-ai-tools-zU0fqnhpmC"
11
+ "id": "",
12
+ "url": "https://handbook.doist.com/doc/standard-internal-ai-tools-"
13
13
  },
14
14
  "service-production-readiness.md": {
15
- "id": "1f4fba5d-9ca6-43bb-97b5-cc0ee8aad3cc",
16
- "url": "https://handbook.doist.com/doc/standard-service-production-readiness-qgNRng8TJx"
15
+ "id": "",
16
+ "url": "https://handbook.doist.com/doc/standard-service-production-readiness-"
17
17
  },
18
18
  "observability-standard.md": {
19
- "id": "e9fe25ca-453d-40b2-b4b0-ef4cdcb5f030",
20
- "url": "https://handbook.doist.com/doc/standard-observability-vWRaOfitho"
19
+ "id": "",
20
+ "url": "https://handbook.doist.com/doc/standard-observability-"
21
+ },
22
+ "backend-repository-standards.md": {
23
+ "id": "",
24
+ "url": "https://handbook.doist.com/doc/standard-backend-repositories-"
21
25
  }
22
26
  }
@@ -49,7 +49,7 @@ All services must produce structured, meaningful logs.
49
49
 
50
50
  Logs should support operational debugging and monitoring.
51
51
 
52
- See the Observability standard for more details. TODO(artyom): add a link once it's finalized.
52
+ See [the Observability standard for more details](https://handbook.doist.com/doc/standard-observability-vWRaOfitho).
53
53
 
54
54
  ## Configuration Management
55
55
 
@@ -59,7 +59,7 @@ Configuration must be provided via environment variables.
59
59
  - Env files are allowed only if variables can be overridden by the environment.
60
60
  - Secrets must come from environment variables or read directly from a secrets management service (AWS Secrets Manager).
61
61
 
62
- See the Secrets Management standard for more details. TODO(artyom): add a link once it's finalized.
62
+ See [the Secrets Management standard for more details](https://handbook.doist.com/doc/standard-secrets-management-wbKmIfrtgr).
63
63
 
64
64
  ## State Persistence
65
65