limn-review 0.1.126 → 0.1.128

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 (99) hide show
  1. package/dist/api/plugins/rate-limit.js +35 -23
  2. package/dist/app.js +5 -0
  3. package/dist/config.js +3 -0
  4. package/dist/db/erase-account.js +25 -0
  5. package/dist/db/export-account.js +17 -0
  6. package/dist/db/migrations/0084_auto_fix_off_by_default.sql +7 -0
  7. package/dist/db/migrations/0085_auto_review_daily_cap.sql +6 -0
  8. package/dist/db/migrations/0086_ticket_review_pr_cards.sql +24 -0
  9. package/dist/db/migrations/0087_auto_post.sql +21 -0
  10. package/dist/db/migrations/0088_issue_tracker.sql +109 -0
  11. package/dist/db/migrations/0089_github_issue_links.sql +18 -0
  12. package/dist/db/migrations/0090_linear_links.sql +22 -0
  13. package/dist/db/migrations/meta/_journal.json +49 -0
  14. package/dist/db/migrations-pg/0071_auto_fix_off_by_default.sql +5 -0
  15. package/dist/db/migrations-pg/0072_auto_review_daily_cap.sql +3 -0
  16. package/dist/db/migrations-pg/0073_ticket_review_pr_cards.sql +17 -0
  17. package/dist/db/migrations-pg/0074_auto_post.sql +14 -0
  18. package/dist/db/migrations-pg/0075_issue_tracker.sql +77 -0
  19. package/dist/db/migrations-pg/0076_github_issue_links.sql +6 -0
  20. package/dist/db/migrations-pg/0077_linear_links.sql +8 -0
  21. package/dist/db/migrations-pg/meta/_journal.json +49 -0
  22. package/dist/db/queries.js +16 -2
  23. package/dist/db/retention.js +3 -0
  24. package/dist/db/schema.pg.js +125 -1
  25. package/dist/db/schema.sqlite.js +226 -1
  26. package/dist/db/ticket-review-prune.js +4 -1
  27. package/dist/github/rate-budget.js +15 -0
  28. package/dist/index.js +7 -0
  29. package/dist/pr/detail-enricher.js +8 -18
  30. package/dist/pro/bind.js +3 -14
  31. package/dist/pro/contract.js +0 -1
  32. package/dist/review/claude-review/auto-post.js +405 -0
  33. package/dist/review/claude-review/auto-settings.js +175 -11
  34. package/dist/review/claude-review/auto.js +3 -3
  35. package/dist/review/claude-review/manager.js +7 -4
  36. package/dist/review/claude-review/persist.js +29 -5
  37. package/dist/review/claude-review/routes.js +9 -0
  38. package/dist/review/clone-manager.js +48 -0
  39. package/dist/review/path-guard.js +1 -1
  40. package/dist/review/post-review.js +6 -2
  41. package/dist/review/post-seam.js +22 -13
  42. package/dist/review/ticket-review/auto-post.js +291 -0
  43. package/dist/review/ticket-review/cards.js +271 -0
  44. package/dist/review/ticket-review/fingerprint.js +2 -0
  45. package/dist/review/ticket-review/manager.js +169 -28
  46. package/dist/review/ticket-review/persist.js +44 -2
  47. package/dist/review/ticket-review/post-item.js +187 -0
  48. package/dist/review/ticket-review/prepare.js +34 -50
  49. package/dist/review/ticket-review/prepass.js +259 -0
  50. package/dist/review/ticket-review/prompts.js +72 -11
  51. package/dist/review/ticket-review/routes.js +36 -156
  52. package/dist/review/ticket-review/schema.js +43 -0
  53. package/dist/review/ticket-review/sweep.js +12 -12
  54. package/dist/shared/claude-review.js +173 -7
  55. package/dist/shared/types.js +49 -8
  56. package/dist/sync/scheduler.js +15 -0
  57. package/dist/tracker/ac.js +172 -0
  58. package/dist/tracker/calls.js +59 -0
  59. package/dist/tracker/context.js +45 -0
  60. package/dist/tracker/detect.js +172 -0
  61. package/dist/tracker/enricher.js +147 -0
  62. package/dist/tracker/github/ac.js +8 -0
  63. package/dist/tracker/github/adapter.js +47 -0
  64. package/dist/tracker/github/links.js +115 -0
  65. package/dist/tracker/github/reader.js +166 -0
  66. package/dist/tracker/index.js +39 -0
  67. package/dist/tracker/jira/adapter.js +23 -0
  68. package/dist/tracker/jira/candidates.js +122 -0
  69. package/dist/tracker/jira/client.js +158 -0
  70. package/dist/tracker/jira/fetch.js +238 -0
  71. package/dist/tracker/jira/text.js +329 -0
  72. package/dist/tracker/legacy-import.js +140 -0
  73. package/dist/tracker/linear/adapter.js +52 -0
  74. package/dist/tracker/linear/client.js +270 -0
  75. package/dist/tracker/linear/links.js +113 -0
  76. package/dist/tracker/links.js +104 -0
  77. package/dist/tracker/merged.js +163 -0
  78. package/dist/tracker/peers.js +117 -0
  79. package/dist/tracker/registry.js +35 -0
  80. package/dist/tracker/routes.js +329 -0
  81. package/dist/tracker/runtime.js +54 -0
  82. package/dist/tracker/secret.js +28 -0
  83. package/dist/tracker/settings.js +213 -0
  84. package/dist/tracker/store.js +267 -0
  85. package/dist/tracker/stories.js +144 -0
  86. package/dist/tracker/ticket-source.js +46 -0
  87. package/dist/tracker/types.js +2 -0
  88. package/dist/tracker/worker.js +814 -0
  89. package/package.json +1 -1
  90. package/public/assets/{AiFixTab-CzugrcNK.js → AiFixTab-Cq2Wdm2l.js} +1 -1
  91. package/public/assets/ClaudeReviewTab-C6WftwSM.js +16 -0
  92. package/public/assets/{PrBotBehaviourTab-Cc1F4O6z.js → PrBotBehaviourTab-CY_iLmFp.js} +1 -1
  93. package/public/assets/index-CB6bLAqG.js +77 -0
  94. package/public/assets/index-gwxOLty-.css +1 -0
  95. package/public/index.html +2 -2
  96. package/dist/review/plugin-providers.js +0 -12
  97. package/public/assets/ClaudeReviewTab-OGu_QicR.js +0 -16
  98. package/public/assets/index-1ieo_9fr.js +0 -77
  99. package/public/assets/index-C9m8ITkB.css +0 -1
@@ -117,6 +117,21 @@ function tierFor(method, path) {
117
117
  // `startsWith('/api/workspaces')` and emphatically not `startsWith('/api/workspace')`:
118
118
  // the `/api/workspace-metrics` family is a SIBLING vocabulary, and a loose prefix over two
119
119
  // sibling vocabularies is precisely how one silently swallows the other.
120
+ // ⚠ THREE WORKSPACE ROUTES SPEND THE CUSTOMER'S TRACKER QUOTA, so they are decided ABOVE the
121
+ // `read` line below, by exact shape (tracker/routes.ts):
122
+ // PUT /api/workspaces/:id/tracker saving a tracker or token KICKS the ticket
123
+ // worker for the account (up to 40 reads);
124
+ // GET /api/workspaces/:id/tracker/jira-fields the Settings connection check — one call to
125
+ // the customer's Jira with the saved token.
126
+ // GET /api/workspaces/:id/tracker/linear-check the Linear connection check — one call to
127
+ // Linear with the saved key.
128
+ // All three take the 60/min `search` bucket, the "one upstream call per request" tier. The plain
129
+ // `GET …/tracker` is DB-only and stays on `read` with the rest of the family.
130
+ if (method === 'PUT' && /^\/api\/workspaces\/\d+\/tracker$/.test(path))
131
+ return [TIERS.search, TIERS.read];
132
+ if (!mutating && /^\/api\/workspaces\/\d+\/tracker\/(jira-fields|linear-check)$/.test(path)) {
133
+ return [TIERS.search, TIERS.read];
134
+ }
120
135
  if (path === '/api/workspaces' || path.startsWith('/api/workspaces/'))
121
136
  return [TIERS.read];
122
137
  // ---- Per-account settings writes: `read`, RECORDED rather than inherited ----
@@ -407,35 +422,32 @@ function tierFor(method, path) {
407
422
  return [TIERS.read];
408
423
  if (mutating && path === '/api/pro/slack/test')
409
424
  return [TIERS.ai, TIERS.aiHourly];
410
- // ---- Jira API reads (must sit ABOVE the /api/pro/ AI-tier catch-all) ----
411
- // GET /api/pro/jira/fields and GET /api/pro/prs/:id/jira-ticket each make ONE outbound call to
412
- // the customer's own Jira with the workspace's saved token — a THIRD-PARTY quota (Jira Cloud rate
413
- // limits per user) and up to a 10s wait on this process. Following the token: it is theirs, not
414
- // ours, so the catch-all's 600/min GET→read branch is wrong; they take the 60/min `search`
415
- // bucket, the "one upstream call per request" tier. Anchored on exact paths (the ticket route by
416
- // a both-ends regex), so a sibling `/api/pro/prs/:id/*` route is not swept in.
417
- if (!mutating && path === '/api/pro/jira/fields')
425
+ // ---- The issue tracker's PR routes (CORE since apiVersion 23 — tracker/routes.ts) ----
426
+ // GET /api/prs/:id/tracker-ticket answers a STORED row, but a ticket the worker never reached is
427
+ // read ONCE from the customer's tracker with the workspace's saved token — a THIRD-PARTY quota
428
+ // (Jira Cloud rate limits per user) and up to a 10s wait. `POST …/refresh` re-reads it now and
429
+ // `PUT …/ac-field` saves the criteria field and re-reads it: one upstream call each. Following
430
+ // the token: theirs, not ours, so the 60/min `search` bucket. Anchored at BOTH ends, so a sibling
431
+ // `/api/prs/:id/*` route is not swept in — and the segment is `tracker-ticket`, never a prefix of
432
+ // `ticket-reviews` (the ticket review's own, `read`-tier family below).
433
+ if (!mutating && /^\/api\/prs\/\d+\/tracker-ticket$/.test(path))
418
434
  return [TIERS.search, TIERS.read];
419
- if (!mutating && /^\/api\/pro\/prs\/[^/]+\/jira-ticket$/.test(path)) {
435
+ if (method === 'POST' && /^\/api\/prs\/\d+\/tracker-ticket\/refresh$/.test(path)) {
420
436
  return [TIERS.search, TIERS.read];
421
437
  }
422
- // The story panel's two writes on a STORED ticket (plugin 0038): `POST …/jira-ticket/refresh` reads
423
- // that ticket from Jira again now, and `PUT …/jira-ticket/ac-field` saves the criteria field for
424
- // its issue type and re-reads it — one upstream call each, the customer's quota. Spelled by exact
425
- // verb + path: without these lines the catch-all parks a mutating /api/pro/ route on `ai`.
426
- if (method === 'POST' && /^\/api\/pro\/prs\/[^/]+\/jira-ticket\/refresh$/.test(path)) {
438
+ if (method === 'PUT' && /^\/api\/prs\/\d+\/tracker-ticket\/ac-field$/.test(path)) {
427
439
  return [TIERS.search, TIERS.read];
428
440
  }
429
- if (method === 'PUT' && /^\/api\/pro\/prs\/[^/]+\/jira-ticket\/ac-field$/.test(path)) {
430
- return [TIERS.search, TIERS.read];
431
- }
432
- // POST /api/pro/ticket-links — the Open PRs cards' ticket row, ONE request per board: DB-only
433
- // detection plus the STORED ticket rows (plugin 0038); a detected ticket with no row kicks the
434
- // background worker, which spends the customer's Jira quota — so `search`, not `read`. A POST
435
- // only because it carries a list of PR ids; it writes nothing itself. Without this line the catch-all would tier it
436
- // on the verb and park a read on the 20/min `ai` bucket.
437
- if (path === '/api/pro/ticket-links')
441
+ // POST /api/ticket-links — the Open PRs cards' ticket row, ONE request per board: DB-only
442
+ // detection plus the STORED ticket rows; a detected ticket with no row kicks the background
443
+ // worker, which spends the customer's tracker quota — so `search`, not `read`. A POST only
444
+ // because it carries a list of PR ids. EXACT `===`.
445
+ if (path === '/api/ticket-links')
438
446
  return [TIERS.search, TIERS.read];
447
+ // GET /api/ticket-merged-prs — the stacks' "Merged (n)" panel: DB-only (stored rows joined to
448
+ // pull_requests), no tracker call. `read`, DECIDED rather than inherited.
449
+ if (!mutating && path === '/api/ticket-merged-prs')
450
+ return [TIERS.read];
439
451
  // ---- AI generation ----
440
452
  // ⚠ RE-DECIDED, not inherited: `POST /api/pro/prs/:id/annotations/run` now spends GITHUB quota
441
453
  // as well as model tokens — one PR_DETAIL_QUERY per uncached PR for the anchor hunks
package/dist/app.js CHANGED
@@ -12,6 +12,7 @@ import { authRoutes } from './api/routes/auth.js';
12
12
  import { healthRoutes } from './api/routes/health.js';
13
13
  import { aiRuntimeRoutes } from './api/routes/ai-runtime.js';
14
14
  import { registerAgenticRoutes } from './review/agentic.js';
15
+ import { registerTracker } from './tracker/index.js';
15
16
  import { repoRoutes } from './api/routes/repos.js';
16
17
  import { workspaceRoutes } from './api/routes/workspaces.js';
17
18
  import { userRoutes } from './api/routes/users.js';
@@ -271,6 +272,10 @@ export async function buildApp() {
271
272
  // memory and AI Fix's fixer. CORE and FREE, LOCAL ONLY: `registerAgenticRoutes` registers
272
273
  // NOTHING in cloud (an explicit `isCloud` check) or under LIMN_AI_DISABLED. review/agentic.ts.
273
274
  registerAgenticRoutes(app);
275
+ // THE ISSUE TRACKER (CORE, FREE, BOTH MODES — apiVersion 23; docs/TRACKERS.md): the workspace's
276
+ // tracker settings, the stored-ticket routes behind the PR pane's story panel, the Open PRs ticket
277
+ // row and stacks. Unconditional: no capability, no plugin.
278
+ registerTracker(app);
274
279
  return app;
275
280
  }
276
281
  //# sourceMappingURL=app.js.map
package/dist/config.js CHANGED
@@ -536,6 +536,9 @@ export const config = {
536
536
  // every member's worktree.
537
537
  ticketReviewBudgetUsd: floatFromEnv('TICKET_REVIEW_BUDGET_USD', 4),
538
538
  ticketReviewMaxTurns: intFromEnv('TICKET_REVIEW_MAX_TURNS', 40),
539
+ // The per-card spend cap of the ticket review's CARD PRE-PASS (Sonnet 5, diff-only, one tool —
540
+ // review/ticket-review/prepass.ts). Its spend is added to the ticket run's cost. Env only.
541
+ ticketCardBudgetUsd: floatFromEnv('TICKET_CARD_BUDGET_USD', 0.4),
539
542
  // AUTOMATIC ticket reviews (trigger 'auto' / 'cascade') per workspace per UTC day. Not shared with
540
543
  // the PR review's own auto cap. Manual runs never count. Default = shared TICKET_REVIEW_DAILY_CAP.
541
544
  ticketReviewDailyCap: intFromEnv('TICKET_REVIEW_DAILY_CAP', 20),
@@ -128,6 +128,12 @@ export async function eraseAccountData(accountId) {
128
128
  .delete(schema.ticketReviews)
129
129
  .where(eq(schema.ticketReviews.accountId, accountId))
130
130
  .execute();
131
+ // Ticket review contribution cards (migration 0086 / pg 0073): model text describing the user's
132
+ // PRs. The repo loop has normally taken them with their PRs; explicit, like every agentic table.
133
+ await tx
134
+ .delete(schema.ticketReviewPrCards)
135
+ .where(eq(schema.ticketReviewPrCards.accountId, accountId))
136
+ .execute();
131
137
  // CI reviews (migration 0082 / pg 0069), items first. The repo loop has normally taken them with
132
138
  // their PRs; explicit, like every agentic table.
133
139
  await tx
@@ -138,6 +144,14 @@ export async function eraseAccountData(accountId) {
138
144
  // AI Fix runs (patches of the user's code + the prompts they were built from). Core since
139
145
  // migration 0074 / pg 0061 — the plugin's erasure hook used to own them.
140
146
  await tx.delete(schema.aiFixes).where(eq(schema.aiFixes.accountId, accountId)).execute();
147
+ // THE ISSUE TRACKER (core since migration 0088 / pg 0075 — the plugin's erasure hook used to own
148
+ // these): the stored tickets (story text from the customer's tracker), the acceptance-criteria
149
+ // field choices, and the workspace tracker rows — which hold the customer's internal tracker
150
+ // hostname AND the sealed API token. No FKs on the first two, so the explicit deletes are the
151
+ // whole guarantee; workspace_trackers cascades from workspaces too, and is explicit anyway.
152
+ await tx.delete(schema.trackerTickets).where(eq(schema.trackerTickets.accountId, accountId)).execute();
153
+ await tx.delete(schema.jiraAcFields).where(eq(schema.jiraAcFields.accountId, accountId)).execute();
154
+ await tx.delete(schema.workspaceTrackers).where(eq(schema.workspaceTrackers.accountId, accountId)).execute();
141
155
  // The AI spend ledger (token/credit counts — no prompt text).
142
156
  await tx.delete(aiUsage).where(eq(aiUsage.accountId, accountId)).execute();
143
157
  // Any aggregate rows contributed to the cross-org benchmark. Consent was the basis for
@@ -218,6 +232,12 @@ export function accountScopedTables() {
218
232
  col: schema.ticketReviewItems.accountId,
219
233
  table: schema.ticketReviewItems,
220
234
  },
235
+ // Contribution cards (migration 0086 / pg 0073) — one per (PR, head); erased explicitly above.
236
+ {
237
+ name: 'ticketReviewPrCards',
238
+ col: schema.ticketReviewPrCards.accountId,
239
+ table: schema.ticketReviewPrCards,
240
+ },
221
241
  // CI reviews (migration 0082 / pg 0069) — the runs and their per-check items. Both carry their
222
242
  // own accountId; both are erased explicitly above.
223
243
  { name: 'ciReviews', col: schema.ciReviews.accountId, table: schema.ciReviews },
@@ -227,6 +247,11 @@ export function accountScopedTables() {
227
247
  // cascades: the explicit delete above is the whole guarantee. (`reviewLearnings`, review
228
248
  // memory's table, sat here until migration 0075 / pg 0062 DROPPED it.)
229
249
  { name: 'aiFixes', col: schema.aiFixes.accountId, table: schema.aiFixes },
250
+ // The issue tracker (migration 0088 / pg 0075). All three carry their own accountId and are
251
+ // erased explicitly above; `workspaceTrackers` holds a credential.
252
+ { name: 'trackerTickets', col: schema.trackerTickets.accountId, table: schema.trackerTickets },
253
+ { name: 'jiraAcFields', col: schema.jiraAcFields.accountId, table: schema.jiraAcFields },
254
+ { name: 'workspaceTrackers', col: schema.workspaceTrackers.accountId, table: schema.workspaceTrackers },
230
255
  // `myTurnDismissals` sat here until migration 0060 / pg 0047 DROPPED the table. It is named
231
256
  // rather than silently absent because this list is a checklist, and a checklist that shortens
232
257
  // with no explanation reads as an omission — the exact failure this function guards against.
@@ -72,6 +72,21 @@ export async function exportAccountData(accountId) {
72
72
  // The child tables reach their account through prId. An account with no PRs short-circuits
73
73
  // to empty arrays rather than issuing an `IN ()`.
74
74
  const childRows = async (fn) => (prIds.length === 0 ? [] : fn(prIds));
75
+ // The tracker rows — explicit columns, the token reduced to its presence below.
76
+ const trackerRows = await db
77
+ .select({
78
+ workspaceId: schema.workspaceTrackers.workspaceId,
79
+ provider: schema.workspaceTrackers.provider,
80
+ baseUrl: schema.workspaceTrackers.baseUrl,
81
+ projectKeys: schema.workspaceTrackers.projectKeys,
82
+ matchScope: schema.workspaceTrackers.matchScope,
83
+ email: schema.workspaceTrackers.authEmail,
84
+ hasToken: schema.workspaceTrackers.authToken,
85
+ updatedAt: schema.workspaceTrackers.updatedAt,
86
+ })
87
+ .from(schema.workspaceTrackers)
88
+ .where(eq(schema.workspaceTrackers.accountId, accountId))
89
+ .execute();
75
90
  const [reviewRows, threadRows, reviewCommentRows, prCommentRows, eventRows, workspaceRows, workspaceRepoRows, usageRows, workspaceReviewerRows, benchmarkRows, autoMergeRows,] = await Promise.all([
76
91
  childRows((ids) => db.select().from(reviews).where(inArray(reviews.prId, ids)).execute()),
77
92
  childRows((ids) => db.select().from(reviewThreads).where(inArray(reviewThreads.prId, ids)).execute()),
@@ -184,6 +199,8 @@ export async function exportAccountData(accountId) {
184
199
  workspaceReviewers: workspaceReviewerRows,
185
200
  benchmarkContributions: benchmarkRows,
186
201
  autoMergeRequests: autoMergeRows,
202
+ // ⚠ PRESENCE ONLY — never the stored token, in any form.
203
+ workspaceTrackers: trackerRows.map((t) => ({ ...t, hasToken: t.hasToken != null && t.hasToken !== '' })),
187
204
  };
188
205
  }
189
206
  //# sourceMappingURL=export-account.js.map
@@ -0,0 +1,7 @@
1
+ -- AUTO AI FIX IS OFF BY DEFAULT. 0083 added `workspaces.auto_fix_enabled` DEFAULT 1 so nothing
2
+ -- changed on upgrade; the product decision since is OFF until someone switches it on. Every
3
+ -- existing workspace is switched off once here (the switch shipped one release earlier, so no one
4
+ -- has had time to choose it). SQLite cannot change a column default in place, so the 0083 DDL
5
+ -- default stays 1; both workspace inserts in db/queries.ts write `autoFixEnabled: false`
6
+ -- explicitly. The Postgres twin is migrations-pg/0071_auto_fix_off_by_default.sql.
7
+ UPDATE `workspaces` SET `auto_fix_enabled` = 0;
@@ -0,0 +1,6 @@
1
+ -- AUTO REVIEW DAILY CAP, PER WORKSPACE. How many auto Claude reviews a workspace may start per UTC
2
+ -- day. OVERRIDES ONLY: NULL = the product default (20, `AUTO_REVIEW_DAILY_CAP`), resolved through
3
+ -- `resolveAutoReviewDailyCap` (review/claude-review/auto-settings.ts); the PUT route bounds it
4
+ -- 1..500. Nullable with no default, so no workspace insert has to name it. The Postgres twin is
5
+ -- migrations-pg/0072_auto_review_daily_cap.sql.
6
+ ALTER TABLE `workspaces` ADD `auto_review_daily_cap` integer;
@@ -0,0 +1,24 @@
1
+ -- TICKET REVIEW CONTRIBUTION CARDS. One row per (account, PR, head commit): a model-written,
2
+ -- verdict-free description of what that PR's head does (summary, interfaces, the story criteria it
3
+ -- moves forward, loose ends) plus the PR's changed files at that head (server-written). A ticket
4
+ -- review reads a member whose CURRENT head has a card as the card, not as its diff; the run writes a
5
+ -- card for every member it read as a diff (source 'story_check') and a cheap per-PR pre-pass writes
6
+ -- the overflow (source 'prepass', with its own cost). Re-writing the same head replaces the card.
7
+ -- Tenancy: a composite FK against pull_requests(id, account_id). Both delete paths (deleteRepo,
8
+ -- retention's deletePrSubtree, via db/ticket-review-prune.ts) and eraseAccountData delete these rows
9
+ -- explicitly. The Postgres twin is migrations-pg/0073_ticket_review_pr_cards.sql.
10
+ CREATE TABLE IF NOT EXISTS `ticket_review_pr_cards` (
11
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
12
+ `account_id` integer NOT NULL,
13
+ `pr_id` integer NOT NULL,
14
+ `head_sha` text NOT NULL,
15
+ `card` text NOT NULL,
16
+ `source` text NOT NULL,
17
+ `model` text NOT NULL,
18
+ `cost_usd` real,
19
+ `created_at` integer DEFAULT (unixepoch()) NOT NULL,
20
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
21
+ CONSTRAINT `trpc_pr_account_fk` FOREIGN KEY (`pr_id`,`account_id`) REFERENCES `pull_requests`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
22
+ );
23
+ --> statement-breakpoint
24
+ CREATE UNIQUE INDEX IF NOT EXISTS `trpc_account_pr_head_ux` ON `ticket_review_pr_cards` (`account_id`,`pr_id`,`head_sha`);
@@ -0,0 +1,21 @@
1
+ -- AUTO-POSTING OF CLAUDE REVIEW OUTPUT (review/claude-review/auto-post.ts, review/ticket-review/
2
+ -- auto-post.ts). A per-workspace master switch, OFF for every workspace existing and new, plus the
3
+ -- per-run records of what was posted and the per-item "posted automatically" flag.
4
+ -- workspaces.auto_post_enabled NULL/false = off. Nullable, no default: no insert names it.
5
+ -- workspaces.auto_post_settings JSON, OVERRIDES ONLY ({ scope?, kinds? }); NULL = defaults.
6
+ -- claude_reviews.auto_post JSON ClaudeAutoPostRecord; NULL = never looked at.
7
+ -- claude_review_findings.posted_auto true = posted by auto-posting.
8
+ -- ticket_reviews.auto_post JSON TicketAutoPostRecord; NULL = never looked at.
9
+ -- ticket_review_items.posted_auto true = posted by auto-posting (carried with the posting).
10
+ -- The Postgres twin is migrations-pg/0074_auto_post.sql.
11
+ ALTER TABLE `workspaces` ADD `auto_post_enabled` integer;
12
+ --> statement-breakpoint
13
+ ALTER TABLE `workspaces` ADD `auto_post_settings` text;
14
+ --> statement-breakpoint
15
+ ALTER TABLE `claude_reviews` ADD `auto_post` text;
16
+ --> statement-breakpoint
17
+ ALTER TABLE `claude_review_findings` ADD `posted_auto` integer;
18
+ --> statement-breakpoint
19
+ ALTER TABLE `ticket_reviews` ADD `auto_post` text;
20
+ --> statement-breakpoint
21
+ ALTER TABLE `ticket_review_items` ADD `posted_auto` integer;
@@ -0,0 +1,109 @@
1
+ -- THE ISSUE TRACKER MOVES TO CORE (apiVersion 23). Ticket links, the stored tickets, the story panel
2
+ -- and the Open PRs ticket stacks are FREE and run in the public `npx limn-review` install, so their
3
+ -- tables leave the private plugin. The Postgres twin is migrations-pg/0075_issue_tracker.sql;
4
+ -- docs/TRACKERS.md has the contract and docs/MIGRATIONS.md the history.
5
+ --
6
+ -- workspace_trackers NEW. ONE tracker per workspace: provider, base URL, project-key allowlist,
7
+ -- match scope, and the Jira email + SEALED token. The values move here from
8
+ -- the plugin's `pro_workspace_settings` (plugin 0031/0035).
9
+ -- tracker_tickets NEW. The provider-aware generalisation of the plugin's
10
+ -- `pro_pr_jira_tickets` (plugin 0038/0039) — the same columns plus
11
+ -- `provider` (DEFAULT 'jira': every row that existed was a Jira row).
12
+ -- pro_jira_ac_fields ADOPTED IN PLACE, exactly the ai_fixes precedent (0074): same name, same
13
+ -- columns, same index name. Where the plugin created it this is a no-op and
14
+ -- its rows stay; on a fresh or plugin-less install it is created here.
15
+ --
16
+ -- ⚠ NO DATA IS COPIED BY THIS FILE, AND THAT IS DELIBERATE. The source rows live in PLUGIN tables,
17
+ -- which do not exist on a fresh install, on a plugin-less install, or (in the pg twin's case) may
18
+ -- not exist at all — and SQLite has no conditional DDL/DML to test for a table. The values are
19
+ -- MOVED by a guarded BOOT-TIME step instead (src/tracker/legacy-import.ts, run after the plugin
20
+ -- binds): it tests each source table and column for existence, copies with ON CONFLICT DO NOTHING,
21
+ -- and then CLEARS the source (deletes the ticket rows, NULLs the tracker columns) in the same
22
+ -- transaction — so a second boot finds nothing to move, a token is never left in two places, and a
23
+ -- value cleared in core is never resurrected from the old copy.
24
+ --
25
+ -- ⚠ WHY A NEW TICKETS TABLE RATHER THAN ADOPTING `pro_pr_jira_tickets` + ADD COLUMN `provider`.
26
+ -- Adopting needs `ALTER TABLE … ADD COLUMN provider` on a table this file may or may not have just
27
+ -- created, and `changed_at` (plugin 0039) may or may not already be on it — SQLite has no
28
+ -- ADD COLUMN IF NOT EXISTS, so one of the two install shapes would fail the boot. A new table has
29
+ -- one shape everywhere. Plugin 0038/0039 are stripped to no-ops in the same change, so a fresh
30
+ -- install never grows the old table at all.
31
+ --
32
+ -- Tenancy: `workspace_trackers.workspace_id` arrives in a request PATH, so its FK is the named
33
+ -- COMPOSITE one onto `workspaces (id, account_id)`. `tracker_tickets` and `pro_jira_ac_fields` are
34
+ -- written only by the server (the worker, and a route that resolves the PR's own workspace): no FKs,
35
+ -- like the tables they replace; every read predicates on account_id, both delete paths prune
36
+ -- tracker_tickets, and all three are in `accountScopedTables()`.
37
+ CREATE TABLE IF NOT EXISTS `workspace_trackers` (
38
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
39
+ `account_id` integer NOT NULL,
40
+ `workspace_id` integer NOT NULL,
41
+ `provider` text,
42
+ `base_url` text,
43
+ `project_keys` text,
44
+ `match_scope` text,
45
+ `auth_email` text,
46
+ `auth_token` text,
47
+ `updated_at` integer DEFAULT (unixepoch()) NOT NULL,
48
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
49
+ CONSTRAINT `workspace_trackers_workspace_account_fk` FOREIGN KEY (`workspace_id`,`account_id`) REFERENCES `workspaces`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
50
+ );
51
+ --> statement-breakpoint
52
+ CREATE UNIQUE INDEX IF NOT EXISTS `workspace_trackers_account_workspace` ON `workspace_trackers` (`account_id`,`workspace_id`);
53
+ --> statement-breakpoint
54
+ CREATE TABLE IF NOT EXISTS `tracker_tickets` (
55
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
56
+ `account_id` integer NOT NULL,
57
+ `workspace_id` integer NOT NULL,
58
+ `pr_id` integer NOT NULL,
59
+ `provider` text DEFAULT 'jira' NOT NULL,
60
+ `issue_key` text NOT NULL,
61
+ `detected_from` text NOT NULL,
62
+ `detect_order` integer NOT NULL,
63
+ `api_root` text NOT NULL,
64
+ `url` text NOT NULL,
65
+ `state` text NOT NULL,
66
+ `error_code` text,
67
+ `title` text,
68
+ `description` text,
69
+ `acceptance_criteria` text,
70
+ `ac_field_id` text,
71
+ `ac_field_name` text,
72
+ `ac_field_source` text,
73
+ `issue_type_id` text,
74
+ `issue_type_name` text,
75
+ `status_name` text,
76
+ `status_category` text,
77
+ `assignee_name` text,
78
+ `assignee_account_id` text,
79
+ `assignee_avatar_url` text,
80
+ `candidates_json` text,
81
+ `omitted_candidates` integer DEFAULT 0 NOT NULL,
82
+ `fetched_at` integer,
83
+ `checked_at` integer NOT NULL,
84
+ `next_check_at` integer NOT NULL,
85
+ `changed_at` integer
86
+ );
87
+ --> statement-breakpoint
88
+ CREATE UNIQUE INDEX IF NOT EXISTS `tracker_tickets_account_pr_key` ON `tracker_tickets` (`account_id`,`pr_id`,`issue_key`);
89
+ --> statement-breakpoint
90
+ CREATE INDEX IF NOT EXISTS `tracker_tickets_account_ws_type` ON `tracker_tickets` (`account_id`,`workspace_id`,`issue_type_id`);
91
+ --> statement-breakpoint
92
+ CREATE INDEX IF NOT EXISTS `tracker_tickets_account_site_key` ON `tracker_tickets` (`account_id`,`provider`,`api_root`,`issue_key`);
93
+ --> statement-breakpoint
94
+ CREATE INDEX IF NOT EXISTS `tracker_tickets_account_changed` ON `tracker_tickets` (`account_id`,`changed_at`);
95
+ --> statement-breakpoint
96
+ CREATE INDEX IF NOT EXISTS `tracker_tickets_pr` ON `tracker_tickets` (`pr_id`);
97
+ --> statement-breakpoint
98
+ CREATE TABLE IF NOT EXISTS `pro_jira_ac_fields` (
99
+ `id` integer PRIMARY KEY AUTOINCREMENT,
100
+ `account_id` integer NOT NULL,
101
+ `workspace_id` integer NOT NULL,
102
+ `api_root` text NOT NULL,
103
+ `issue_type_id` text NOT NULL,
104
+ `field_id` text NOT NULL,
105
+ `field_name` text NOT NULL,
106
+ `updated_at` integer NOT NULL
107
+ );
108
+ --> statement-breakpoint
109
+ CREATE UNIQUE INDEX IF NOT EXISTS `pro_jira_ac_fields_account_ws_site_type` ON `pro_jira_ac_fields` (`account_id`,`workspace_id`,`api_root`,`issue_type_id`);
@@ -0,0 +1,18 @@
1
+ -- GITHUB ISSUES — the tracker's second reading adapter (docs/TRACKERS.md § GitHub Issues). The
2
+ -- Postgres twin is migrations-pg/0076_github_issue_links.sql.
3
+ --
4
+ -- A GitHub Issues workspace links a pull request to the issues it CLOSES — GitHub's own
5
+ -- `closingIssuesReferences` ("Fixes #12", "Closes owner/repo#12", a Development-panel link), never
6
+ -- a bare "#12" mention. That list is a fact about the PR that the view paths (the PR pane's chips,
7
+ -- the Open PRs ticket row) must read without calling GitHub, so it is stored on the PR:
8
+ --
9
+ -- closing_issues JSON array of lower-cased `owner/repo#12` keys, in GitHub's order.
10
+ -- closing_issues_checked_at when it was last read.
11
+ --
12
+ -- ⚠ NULL IS "NEVER READ", NOT "CLOSES NOTHING" (`[]` is GitHub's positive statement). Both are
13
+ -- written ONLY by the tracker worker's targeted `nodes(ids:)` step, and only for PRs in a workspace
14
+ -- whose tracker is GitHub Issues — the repo walk's query is unchanged, so every other workspace pays
15
+ -- nothing for this. Additive; no data to move.
16
+ ALTER TABLE `pull_requests` ADD `closing_issues` text;
17
+ --> statement-breakpoint
18
+ ALTER TABLE `pull_requests` ADD `closing_issues_checked_at` integer;
@@ -0,0 +1,22 @@
1
+ -- LINEAR — the tracker's third reading adapter (docs/TRACKERS.md § Linear). The Postgres twin is
2
+ -- migrations-pg/0077_linear_links.sql.
3
+ --
4
+ -- A Linear workspace links a pull request to the Linear issues Linear's GitHub integration ATTACHED
5
+ -- it to (`attachmentsForURL` on the PR's GitHub URL — it sees the PR body, which lean storage never
6
+ -- keeps), in addition to the keys detection finds in the title and branch. That list is a fact the
7
+ -- view paths must read without calling Linear, so it is stored on the PR:
8
+ --
9
+ -- linear_links JSON array of upper-cased `ENG-123` keys, in Linear's order.
10
+ -- linear_links_root the Linear workspace they were read against (`https://linear.app/<key>`);
11
+ -- a different current root means "never read" for this workspace.
12
+ -- linear_links_checked_at when they were last read.
13
+ --
14
+ -- ⚠ NULL IS "NEVER READ", NOT "ATTACHED TO NOTHING" (`[]` is Linear's positive statement) — and for
15
+ -- Linear an unread PR still shows the tickets detection finds. Written ONLY by the tracker worker,
16
+ -- only for PRs in a workspace whose tracker is Linear with a saved key; the repo walk is unchanged,
17
+ -- so every other workspace pays nothing. Additive; no data to move.
18
+ ALTER TABLE `pull_requests` ADD `linear_links` text;
19
+ --> statement-breakpoint
20
+ ALTER TABLE `pull_requests` ADD `linear_links_root` text;
21
+ --> statement-breakpoint
22
+ ALTER TABLE `pull_requests` ADD `linear_links_checked_at` integer;
@@ -589,6 +589,55 @@
589
589
  "when": 1790665200000,
590
590
  "tag": "0083_workspace_auto_fix",
591
591
  "breakpoints": true
592
+ },
593
+ {
594
+ "idx": 84,
595
+ "version": "6",
596
+ "when": 1790751600000,
597
+ "tag": "0084_auto_fix_off_by_default",
598
+ "breakpoints": true
599
+ },
600
+ {
601
+ "idx": 85,
602
+ "version": "6",
603
+ "when": 1790838000000,
604
+ "tag": "0085_auto_review_daily_cap",
605
+ "breakpoints": true
606
+ },
607
+ {
608
+ "idx": 86,
609
+ "version": "6",
610
+ "when": 1790841600000,
611
+ "tag": "0086_ticket_review_pr_cards",
612
+ "breakpoints": true
613
+ },
614
+ {
615
+ "idx": 87,
616
+ "version": "6",
617
+ "when": 1790928000000,
618
+ "tag": "0087_auto_post",
619
+ "breakpoints": true
620
+ },
621
+ {
622
+ "idx": 88,
623
+ "version": "6",
624
+ "when": 1791014400000,
625
+ "tag": "0088_issue_tracker",
626
+ "breakpoints": true
627
+ },
628
+ {
629
+ "idx": 89,
630
+ "version": "6",
631
+ "when": 1791100800000,
632
+ "tag": "0089_github_issue_links",
633
+ "breakpoints": true
634
+ },
635
+ {
636
+ "idx": 90,
637
+ "version": "6",
638
+ "when": 1791187200000,
639
+ "tag": "0090_linear_links",
640
+ "breakpoints": true
592
641
  }
593
642
  ]
594
643
  }
@@ -0,0 +1,5 @@
1
+ -- Postgres twin of migrations/0084_auto_fix_off_by_default.sql: auto AI Fix is OFF by default,
2
+ -- and every existing workspace is switched off once.
3
+ ALTER TABLE "workspaces" ALTER COLUMN "auto_fix_enabled" SET DEFAULT false;
4
+ --> statement-breakpoint
5
+ UPDATE "workspaces" SET "auto_fix_enabled" = false;
@@ -0,0 +1,3 @@
1
+ -- Postgres twin of migrations/0085_auto_review_daily_cap.sql: the per-workspace auto review daily
2
+ -- cap. NULL = the default 20.
3
+ ALTER TABLE "workspaces" ADD COLUMN IF NOT EXISTS "auto_review_daily_cap" integer;
@@ -0,0 +1,17 @@
1
+ -- Postgres twin of migrations/0086_ticket_review_pr_cards.sql: ticket review contribution cards,
2
+ -- one row per (account, PR, head commit). The contract lives in schema.sqlite.ts.
3
+ CREATE TABLE IF NOT EXISTS "ticket_review_pr_cards" (
4
+ "id" serial PRIMARY KEY NOT NULL,
5
+ "account_id" integer NOT NULL,
6
+ "pr_id" integer NOT NULL,
7
+ "head_sha" text NOT NULL,
8
+ "card" jsonb NOT NULL,
9
+ "source" text NOT NULL,
10
+ "model" text NOT NULL,
11
+ "cost_usd" double precision,
12
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
13
+ CONSTRAINT "ticket_review_pr_cards_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE cascade,
14
+ CONSTRAINT "trpc_pr_account_fk" FOREIGN KEY ("pr_id","account_id") REFERENCES "pull_requests"("id","account_id") ON DELETE cascade
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE UNIQUE INDEX IF NOT EXISTS "trpc_account_pr_head_ux" ON "ticket_review_pr_cards" USING btree ("account_id","pr_id","head_sha");
@@ -0,0 +1,14 @@
1
+ -- Postgres twin of migrations/0087_auto_post.sql: auto-posting of Claude review output. A
2
+ -- per-workspace switch (NULL/false = off) + overrides-only settings, the per-run records and the
3
+ -- per-item "posted automatically" flags. The contract lives in schema.sqlite.ts.
4
+ ALTER TABLE "workspaces" ADD COLUMN IF NOT EXISTS "auto_post_enabled" boolean;
5
+ --> statement-breakpoint
6
+ ALTER TABLE "workspaces" ADD COLUMN IF NOT EXISTS "auto_post_settings" jsonb;
7
+ --> statement-breakpoint
8
+ ALTER TABLE "claude_reviews" ADD COLUMN IF NOT EXISTS "auto_post" jsonb;
9
+ --> statement-breakpoint
10
+ ALTER TABLE "claude_review_findings" ADD COLUMN IF NOT EXISTS "posted_auto" boolean;
11
+ --> statement-breakpoint
12
+ ALTER TABLE "ticket_reviews" ADD COLUMN IF NOT EXISTS "auto_post" jsonb;
13
+ --> statement-breakpoint
14
+ ALTER TABLE "ticket_review_items" ADD COLUMN IF NOT EXISTS "posted_auto" boolean;
@@ -0,0 +1,77 @@
1
+ -- Postgres twin of migrations/0088_issue_tracker.sql: the issue tracker moves to CORE
2
+ -- (apiVersion 23). `workspace_trackers` and `tracker_tickets` are new; `pro_jira_ac_fields` is
3
+ -- ADOPTED IN PLACE (the plugin's pg 0038 shape, same index name). No data is copied here — the
4
+ -- boot-time move (src/tracker/legacy-import.ts) does that, guarded on the plugin tables existing.
5
+ -- Rationale in the sqlite twin and docs/TRACKERS.md.
6
+ CREATE TABLE IF NOT EXISTS "workspace_trackers" (
7
+ "id" serial PRIMARY KEY NOT NULL,
8
+ "account_id" integer NOT NULL REFERENCES "accounts"("id") ON DELETE cascade,
9
+ "workspace_id" integer NOT NULL,
10
+ "provider" text,
11
+ "base_url" text,
12
+ "project_keys" text,
13
+ "match_scope" text,
14
+ "auth_email" text,
15
+ "auth_token" text,
16
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
17
+ CONSTRAINT "workspace_trackers_workspace_account_fk" FOREIGN KEY ("workspace_id","account_id") REFERENCES "workspaces"("id","account_id") ON DELETE cascade
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE UNIQUE INDEX IF NOT EXISTS "workspace_trackers_account_workspace" ON "workspace_trackers" USING btree ("account_id","workspace_id");
21
+ --> statement-breakpoint
22
+ CREATE TABLE IF NOT EXISTS "tracker_tickets" (
23
+ "id" serial PRIMARY KEY NOT NULL,
24
+ "account_id" integer NOT NULL,
25
+ "workspace_id" integer NOT NULL,
26
+ "pr_id" integer NOT NULL,
27
+ "provider" text DEFAULT 'jira' NOT NULL,
28
+ "issue_key" text NOT NULL,
29
+ "detected_from" text NOT NULL,
30
+ "detect_order" integer NOT NULL,
31
+ "api_root" text NOT NULL,
32
+ "url" text NOT NULL,
33
+ "state" text NOT NULL,
34
+ "error_code" text,
35
+ "title" text,
36
+ "description" text,
37
+ "acceptance_criteria" text,
38
+ "ac_field_id" text,
39
+ "ac_field_name" text,
40
+ "ac_field_source" text,
41
+ "issue_type_id" text,
42
+ "issue_type_name" text,
43
+ "status_name" text,
44
+ "status_category" text,
45
+ "assignee_name" text,
46
+ "assignee_account_id" text,
47
+ "assignee_avatar_url" text,
48
+ "candidates_json" text,
49
+ "omitted_candidates" integer DEFAULT 0 NOT NULL,
50
+ "fetched_at" timestamp with time zone,
51
+ "checked_at" timestamp with time zone NOT NULL,
52
+ "next_check_at" timestamp with time zone NOT NULL,
53
+ "changed_at" timestamp with time zone
54
+ );
55
+ --> statement-breakpoint
56
+ CREATE UNIQUE INDEX IF NOT EXISTS "tracker_tickets_account_pr_key" ON "tracker_tickets" USING btree ("account_id","pr_id","issue_key");
57
+ --> statement-breakpoint
58
+ CREATE INDEX IF NOT EXISTS "tracker_tickets_account_ws_type" ON "tracker_tickets" USING btree ("account_id","workspace_id","issue_type_id");
59
+ --> statement-breakpoint
60
+ CREATE INDEX IF NOT EXISTS "tracker_tickets_account_site_key" ON "tracker_tickets" USING btree ("account_id","provider","api_root","issue_key");
61
+ --> statement-breakpoint
62
+ CREATE INDEX IF NOT EXISTS "tracker_tickets_account_changed" ON "tracker_tickets" USING btree ("account_id","changed_at");
63
+ --> statement-breakpoint
64
+ CREATE INDEX IF NOT EXISTS "tracker_tickets_pr" ON "tracker_tickets" USING btree ("pr_id");
65
+ --> statement-breakpoint
66
+ CREATE TABLE IF NOT EXISTS "pro_jira_ac_fields" (
67
+ "id" serial PRIMARY KEY,
68
+ "account_id" integer NOT NULL,
69
+ "workspace_id" integer NOT NULL,
70
+ "api_root" text NOT NULL,
71
+ "issue_type_id" text NOT NULL,
72
+ "field_id" text NOT NULL,
73
+ "field_name" text NOT NULL,
74
+ "updated_at" timestamptz NOT NULL
75
+ );
76
+ --> statement-breakpoint
77
+ CREATE UNIQUE INDEX IF NOT EXISTS "pro_jira_ac_fields_account_ws_site_type" ON "pro_jira_ac_fields" ("account_id","workspace_id","api_root","issue_type_id");
@@ -0,0 +1,6 @@
1
+ -- GITHUB ISSUES LINKS — the Postgres twin of migrations/0089_github_issue_links.sql (read that file
2
+ -- for the rationale). The issues a PR closes, as GitHub states them, read only for GitHub Issues
3
+ -- workspaces; NULL is "never read", never "closes nothing". Additive.
4
+ ALTER TABLE "pull_requests" ADD COLUMN IF NOT EXISTS "closing_issues" jsonb;
5
+ --> statement-breakpoint
6
+ ALTER TABLE "pull_requests" ADD COLUMN IF NOT EXISTS "closing_issues_checked_at" timestamp with time zone;
@@ -0,0 +1,8 @@
1
+ -- LINEAR LINKS — the Postgres twin of migrations/0090_linear_links.sql (read that file for the
2
+ -- rationale). The Linear issues a PR is attached to, read only for Linear workspaces with a saved
3
+ -- key, and the Linear workspace they were read against; NULL is "never read". Additive.
4
+ ALTER TABLE "pull_requests" ADD COLUMN IF NOT EXISTS "linear_links" jsonb;
5
+ --> statement-breakpoint
6
+ ALTER TABLE "pull_requests" ADD COLUMN IF NOT EXISTS "linear_links_root" text;
7
+ --> statement-breakpoint
8
+ ALTER TABLE "pull_requests" ADD COLUMN IF NOT EXISTS "linear_links_checked_at" timestamp with time zone;