limn-review 0.1.126 → 0.1.127

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 (48) hide show
  1. package/dist/config.js +3 -0
  2. package/dist/db/erase-account.js +12 -0
  3. package/dist/db/migrations/0084_auto_fix_off_by_default.sql +7 -0
  4. package/dist/db/migrations/0085_auto_review_daily_cap.sql +6 -0
  5. package/dist/db/migrations/0086_ticket_review_pr_cards.sql +24 -0
  6. package/dist/db/migrations/0087_auto_post.sql +21 -0
  7. package/dist/db/migrations/meta/_journal.json +28 -0
  8. package/dist/db/migrations-pg/0071_auto_fix_off_by_default.sql +5 -0
  9. package/dist/db/migrations-pg/0072_auto_review_daily_cap.sql +3 -0
  10. package/dist/db/migrations-pg/0073_ticket_review_pr_cards.sql +17 -0
  11. package/dist/db/migrations-pg/0074_auto_post.sql +14 -0
  12. package/dist/db/migrations-pg/meta/_journal.json +28 -0
  13. package/dist/db/queries.js +13 -2
  14. package/dist/db/schema.pg.js +39 -1
  15. package/dist/db/schema.sqlite.js +66 -1
  16. package/dist/db/ticket-review-prune.js +4 -1
  17. package/dist/review/claude-review/auto-post.js +405 -0
  18. package/dist/review/claude-review/auto-settings.js +175 -11
  19. package/dist/review/claude-review/auto.js +3 -3
  20. package/dist/review/claude-review/manager.js +5 -0
  21. package/dist/review/claude-review/persist.js +29 -5
  22. package/dist/review/claude-review/routes.js +9 -0
  23. package/dist/review/clone-manager.js +48 -0
  24. package/dist/review/path-guard.js +1 -1
  25. package/dist/review/post-review.js +6 -2
  26. package/dist/review/post-seam.js +22 -13
  27. package/dist/review/ticket-review/auto-post.js +291 -0
  28. package/dist/review/ticket-review/cards.js +271 -0
  29. package/dist/review/ticket-review/fingerprint.js +2 -0
  30. package/dist/review/ticket-review/manager.js +167 -26
  31. package/dist/review/ticket-review/persist.js +44 -2
  32. package/dist/review/ticket-review/post-item.js +187 -0
  33. package/dist/review/ticket-review/prepare.js +18 -7
  34. package/dist/review/ticket-review/prepass.js +259 -0
  35. package/dist/review/ticket-review/prompts.js +72 -11
  36. package/dist/review/ticket-review/routes.js +30 -146
  37. package/dist/review/ticket-review/schema.js +43 -0
  38. package/dist/shared/types.js +29 -2
  39. package/package.json +1 -1
  40. package/public/assets/{AiFixTab-CzugrcNK.js → AiFixTab-D-Xph0Bw.js} +2 -2
  41. package/public/assets/ClaudeReviewTab-BwIpVwEa.js +16 -0
  42. package/public/assets/{PrBotBehaviourTab-Cc1F4O6z.js → PrBotBehaviourTab-DYNOt_5y.js} +1 -1
  43. package/public/assets/index-gwxOLty-.css +1 -0
  44. package/public/assets/index-rdFHnTfI.js +77 -0
  45. package/public/index.html +2 -2
  46. package/public/assets/ClaudeReviewTab-OGu_QicR.js +0 -16
  47. package/public/assets/index-1ieo_9fr.js +0 -77
  48. package/public/assets/index-C9m8ITkB.css +0 -1
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
@@ -218,6 +224,12 @@ export function accountScopedTables() {
218
224
  col: schema.ticketReviewItems.accountId,
219
225
  table: schema.ticketReviewItems,
220
226
  },
227
+ // Contribution cards (migration 0086 / pg 0073) — one per (PR, head); erased explicitly above.
228
+ {
229
+ name: 'ticketReviewPrCards',
230
+ col: schema.ticketReviewPrCards.accountId,
231
+ table: schema.ticketReviewPrCards,
232
+ },
221
233
  // CI reviews (migration 0082 / pg 0069) — the runs and their per-check items. Both carry their
222
234
  // own accountId; both are erased explicitly above.
223
235
  { name: 'ciReviews', col: schema.ciReviews.accountId, table: schema.ciReviews },
@@ -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;
@@ -589,6 +589,34 @@
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
592
620
  }
593
621
  ]
594
622
  }
@@ -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;
@@ -498,6 +498,34 @@
498
498
  "when": 1790665200000,
499
499
  "tag": "0070_workspace_auto_fix",
500
500
  "breakpoints": true
501
+ },
502
+ {
503
+ "idx": 71,
504
+ "version": "7",
505
+ "when": 1790751600000,
506
+ "tag": "0071_auto_fix_off_by_default",
507
+ "breakpoints": true
508
+ },
509
+ {
510
+ "idx": 72,
511
+ "version": "7",
512
+ "when": 1790838000000,
513
+ "tag": "0072_auto_review_daily_cap",
514
+ "breakpoints": true
515
+ },
516
+ {
517
+ "idx": 73,
518
+ "version": "7",
519
+ "when": 1790841600000,
520
+ "tag": "0073_ticket_review_pr_cards",
521
+ "breakpoints": true
522
+ },
523
+ {
524
+ "idx": 74,
525
+ "version": "7",
526
+ "when": 1790928000000,
527
+ "tag": "0074_auto_post",
528
+ "breakpoints": true
501
529
  }
502
530
  ]
503
531
  }
@@ -253,7 +253,7 @@ export async function ensureDefaultWorkspace(accountId) {
253
253
  : `Default (workspace ${accountId})`;
254
254
  await db
255
255
  .insert(workspaces)
256
- .values({ accountId, name, isDefault: true })
256
+ .values({ accountId, name, isDefault: true, autoFixEnabled: false })
257
257
  .onConflictDoNothing()
258
258
  .execute();
259
259
  const after = await readDefault();
@@ -336,7 +336,7 @@ export async function listWorkspaces(accountId) {
336
336
  export async function createWorkspace(accountId, name) {
337
337
  const [row] = await db
338
338
  .insert(workspaces)
339
- .values({ accountId, name, isDefault: false })
339
+ .values({ accountId, name, isDefault: false, autoFixEnabled: false })
340
340
  .returning()
341
341
  .execute();
342
342
  // No repos yet, so no repo-grained mutes to intersect — and `pending_muted` defaults to false.
@@ -14792,4 +14792,15 @@ export async function updateAutoMergeState(id, opts) {
14792
14792
  .where(eq(autoMergeRequests.id, id))
14793
14793
  .execute();
14794
14794
  }
14795
+ /**
14796
+ * Is this user automation IN THIS WORKSPACE — the same union `hiddenBotUserIds` hides (a manual
14797
+ * "this is a human" wins both directions)? Read by auto-posting (review/claude-review/auto-post.ts),
14798
+ * which never posts on a bot-authored PR. A null user (an unmapped author) answers TRUE: a PR whose
14799
+ * author we cannot place is not one we write on.
14800
+ */
14801
+ export async function isWorkspaceAutomationUser(accountId, workspaceId, userId) {
14802
+ if (userId == null)
14803
+ return true;
14804
+ return (await hiddenBotUserIds(accountId, workspaceId)).includes(userId);
14805
+ }
14795
14806
  //# sourceMappingURL=queries.js.map
@@ -726,6 +726,8 @@ export const claudeReviews = pgTable('claude_reviews', {
726
726
  commentsThrough: timestamp('comments_through', { withTimezone: true, mode: 'date' }),
727
727
  // Failed CI on the reviewed head, diagnosed. Twin of schema.sqlite.ts. Migration pg 0064 (sqlite 0077).
728
728
  ciFailures: jsonb('ci_failures').$type(),
729
+ // What auto-posting did with this run (migration 0087 / pg 0074). Twin of schema.sqlite.ts.
730
+ autoPost: jsonb('auto_post').$type(),
729
731
  }, (t) => ({
730
732
  prIdx: index('cr_pr_idx').on(t.prId),
731
733
  prShaIdx: index('cr_pr_sha_idx').on(t.prId, t.headSha),
@@ -779,6 +781,8 @@ export const claudeReviewFindings = pgTable('claude_review_findings', {
779
781
  // 0066 (sqlite 0079).
780
782
  storyIndex: integer('story_index'),
781
783
  storyRef: text('story_ref'),
784
+ // Posted by auto-posting (migration 0087 / pg 0074). Twin of schema.sqlite.ts.
785
+ postedAuto: boolean('posted_auto'),
782
786
  }, (t) => ({ reviewIdx: index('crf_review_idx').on(t.reviewId) }));
783
787
  // Claude Review chat messages. Twin of schema.sqlite.ts claudeReviewChatMessages, where the
784
788
  // contract lives. Migration pg 0060 (sqlite 0073).
@@ -855,6 +859,8 @@ export const ticketReviews = pgTable('ticket_reviews', {
855
859
  summary: text('summary'),
856
860
  // The server-validated TicketAssessment. Null unless the run succeeded.
857
861
  assessment: jsonb('assessment').$type(),
862
+ // What auto-posting did with this run (migration 0087 / pg 0074). Twin of schema.sqlite.ts.
863
+ autoPost: jsonb('auto_post').$type(),
858
864
  }, (t) => ({
859
865
  accountIdentIdx: index('tr_account_ident_created_idx').on(t.accountId, t.ticketIdent, t.createdAt),
860
866
  // The per-workspace daily cap on automatic runs.
@@ -908,6 +914,8 @@ export const ticketReviewItems = pgTable('ticket_review_items', {
908
914
  postedPrId: integer('posted_pr_id'),
909
915
  postedCommentId: text('posted_comment_id'),
910
916
  postedAt: timestamp('posted_at', { withTimezone: true, mode: 'date' }),
917
+ // Posted by auto-posting (migration 0087 / pg 0074). Twin of schema.sqlite.ts.
918
+ postedAuto: boolean('posted_auto'),
911
919
  priorItemId: integer('prior_item_id'),
912
920
  createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' })
913
921
  .notNull()
@@ -922,6 +930,30 @@ export const ticketReviewItems = pgTable('ticket_review_items', {
922
930
  foreignColumns: [ticketReviews.id, ticketReviews.accountId],
923
931
  }).onDelete('cascade'),
924
932
  }));
933
+ // ---- Ticket review contribution cards (CORE; pg 0073 / sqlite 0086) ----
934
+ // Twin of schema.sqlite.ts ticketReviewPrCards, where the contract lives.
935
+ export const ticketReviewPrCards = pgTable('ticket_review_pr_cards', {
936
+ id: serial('id').primaryKey(),
937
+ accountId: integer('account_id')
938
+ .notNull()
939
+ .references(() => accounts.id, { onDelete: 'cascade' }),
940
+ prId: integer('pr_id').notNull(),
941
+ headSha: text('head_sha').notNull(),
942
+ card: jsonb('card').$type().notNull(),
943
+ source: text('source', { enum: ['story_check', 'prepass'] }).notNull(),
944
+ model: text('model').notNull(),
945
+ costUsd: doublePrecision('cost_usd'),
946
+ createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' })
947
+ .notNull()
948
+ .defaultNow(),
949
+ }, (t) => ({
950
+ accountPrHeadUx: uniqueIndex('trpc_account_pr_head_ux').on(t.accountId, t.prId, t.headSha),
951
+ prAccountFk: foreignKey({
952
+ name: 'trpc_pr_account_fk',
953
+ columns: [t.prId, t.accountId],
954
+ foreignColumns: [pullRequests.id, pullRequests.accountId],
955
+ }).onDelete('cascade'),
956
+ }));
925
957
  // ---- CI review (CORE; pg 0069 / sqlite 0082) ----
926
958
  // Twin of schema.sqlite.ts ciReviews / ciReviewItems, where the contract lives.
927
959
  export const ciReviews = pgTable('ci_reviews', {
@@ -1081,7 +1113,13 @@ export const workspaces = pgTable('workspaces', {
1081
1113
  autoReviewEnabled: boolean('auto_review_enabled'),
1082
1114
  autoReviewEnabledAt: timestamp('auto_review_enabled_at', { withTimezone: true, mode: 'date' }),
1083
1115
  // Auto AI Fix (migration 0083 / pg 0070) — the pg twin. DEFAULT true. Rationale in the sqlite twin.
1084
- autoFixEnabled: boolean('auto_fix_enabled').notNull().default(true),
1116
+ autoFixEnabled: boolean('auto_fix_enabled').notNull().default(false),
1117
+ // Auto review daily cap (migration 0085 / pg 0072) — the pg twin. NULL = the default 20.
1118
+ autoReviewDailyCap: integer('auto_review_daily_cap'),
1119
+ // Auto-posting (migration 0087 / pg 0074) — the pg twin. NULL/false = off; settings are
1120
+ // overrides only. Rationale in the sqlite twin.
1121
+ autoPostEnabled: boolean('auto_post_enabled'),
1122
+ autoPostSettings: jsonb('auto_post_settings').$type(),
1085
1123
  createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' })
1086
1124
  .notNull()
1087
1125
  .defaultNow(),
@@ -1007,6 +1007,11 @@ export const claudeReviews = sqliteTable('claude_reviews', {
1007
1007
  // the field, a skip, a run that did not succeed, or one whose checks could not be read.
1008
1008
  // Migration 0077 (pg 0064).
1009
1009
  ciFailures: text('ci_failures', { mode: 'json' }).$type(),
1010
+ // WHAT AUTO-POSTING DID WITH THIS RUN (migration 0087 / pg 0074): written once, by the claim in
1011
+ // review/claude-review/auto-post.ts (`status: 'posting'` BEFORE any GitHub call, so a crash
1012
+ // mid-post can never lead to a retry), then settled to posted / partial / failed / skipped.
1013
+ // NULL = auto-posting never looked at this run (a manual run, or the switch was off).
1014
+ autoPost: text('auto_post', { mode: 'json' }).$type(),
1010
1015
  }, (t) => ({
1011
1016
  prIdx: index('cr_pr_idx').on(t.prId),
1012
1017
  prShaIdx: index('cr_pr_sha_idx').on(t.prId, t.headSha),
@@ -1077,6 +1082,9 @@ export const claudeReviewFindings = sqliteTable('claude_review_findings', {
1077
1082
  // together: a row has both or neither.
1078
1083
  storyIndex: integer('story_index'),
1079
1084
  storyRef: text('story_ref'),
1085
+ // true = auto-posting put this finding on GitHub (migration 0087 / pg 0074); NULL/false = a
1086
+ // person did, or it is not posted. Display only ("Posted automatically").
1087
+ postedAuto: integer('posted_auto', { mode: 'boolean' }),
1080
1088
  }, (t) => ({ reviewIdx: index('crf_review_idx').on(t.reviewId) }));
1081
1089
  // ---- Claude Review chat (CORE table, written by the Pro plugin) ----
1082
1090
  // One row per chat MESSAGE about one succeeded review: a general thread (`finding_id` NULL) and
@@ -1185,6 +1193,9 @@ export const ticketReviews = sqliteTable('ticket_reviews', {
1185
1193
  summary: text('summary'),
1186
1194
  // The server-validated TicketAssessment. Null unless the run succeeded.
1187
1195
  assessment: text('assessment', { mode: 'json' }).$type(),
1196
+ // What auto-posting did with this run (migration 0087 / pg 0074) — review/ticket-review/
1197
+ // auto-post.ts. Also holds the posted "Not asked for" entries, which have no item row.
1198
+ autoPost: text('auto_post', { mode: 'json' }).$type(),
1188
1199
  }, (t) => ({
1189
1200
  accountIdentIdx: index('tr_account_ident_created_idx').on(t.accountId, t.ticketIdent, t.createdAt),
1190
1201
  // The per-workspace daily cap on automatic runs.
@@ -1238,6 +1249,8 @@ export const ticketReviewItems = sqliteTable('ticket_review_items', {
1238
1249
  postedPrId: integer('posted_pr_id'),
1239
1250
  postedCommentId: text('posted_comment_id'),
1240
1251
  postedAt: integer('posted_at', { mode: 'timestamp' }),
1252
+ // true = auto-posting posted it (migration 0087 / pg 0074); carried with the posting.
1253
+ postedAuto: integer('posted_auto', { mode: 'boolean' }),
1241
1254
  priorItemId: integer('prior_item_id'),
1242
1255
  createdAt: integer('created_at', { mode: 'timestamp' })
1243
1256
  .notNull()
@@ -1252,6 +1265,45 @@ export const ticketReviewItems = sqliteTable('ticket_review_items', {
1252
1265
  foreignColumns: [ticketReviews.id, ticketReviews.accountId],
1253
1266
  }).onDelete('cascade'),
1254
1267
  }));
1268
+ // ---- Ticket review CONTRIBUTION CARDS (migration 0086, pg 0073) ----
1269
+ // One row per (account, PR, head commit): a model-written, verdict-free description of what that
1270
+ // PR's head does (shared `TicketPrCard`: summary, interfaces, criteria it moves forward, loose
1271
+ // ends) plus the changed files at that head (server-written). Written by a ticket review — its own
1272
+ // run for every member it read as a diff (`source = 'story_check'`), or the cheap per-PR pre-pass
1273
+ // for the overflow beyond TICKET_REVIEW_MAX_DIFFS (`'prepass'`, with that card's own cost). A later
1274
+ // run reads a member whose current head has a card AS THE CARD. Currency is `head_sha` equality
1275
+ // with the PR's synced head: a merged PR's head never moves, so its card holds for good.
1276
+ // ⚠ Card availability NEVER enters the ticket fingerprint (review/ticket-review/fingerprint.ts).
1277
+ // Re-writing the same head replaces the card (upsert on the unique below).
1278
+ // Tenancy: a composite FK against pull_requests(id, account_id). ⚠ BOTH DELETE PATHS clear rows for
1279
+ // the deleted PRs (db/ticket-review-prune.ts), plus eraseAccountData + accountScopedTables().
1280
+ // Twin: schema.pg.ts.
1281
+ export const ticketReviewPrCards = sqliteTable('ticket_review_pr_cards', {
1282
+ id: integer('id').primaryKey({ autoIncrement: true }),
1283
+ accountId: integer('account_id')
1284
+ .notNull()
1285
+ .references(() => accounts.id, { onDelete: 'cascade' }),
1286
+ // Composite FK below (no single-column one).
1287
+ prId: integer('pr_id').notNull(),
1288
+ headSha: text('head_sha').notNull(),
1289
+ card: text('card', { mode: 'json' }).$type().notNull(),
1290
+ source: text('source', { enum: ['story_check', 'prepass'] }).notNull(),
1291
+ // Stored model id (no drizzle `enum:`, like ticket_reviews.model).
1292
+ model: text('model').notNull(),
1293
+ // A pre-pass card's own spend; null for a card written inside a ticket review's run (that run's
1294
+ // cost holds it).
1295
+ costUsd: real('cost_usd'),
1296
+ createdAt: integer('created_at', { mode: 'timestamp' })
1297
+ .notNull()
1298
+ .default(sql `(unixepoch())`),
1299
+ }, (t) => ({
1300
+ accountPrHeadUx: uniqueIndex('trpc_account_pr_head_ux').on(t.accountId, t.prId, t.headSha),
1301
+ prAccountFk: foreignKey({
1302
+ name: 'trpc_pr_account_fk',
1303
+ columns: [t.prId, t.accountId],
1304
+ foreignColumns: [pullRequests.id, pullRequests.accountId],
1305
+ }).onDelete('cascade'),
1306
+ }));
1255
1307
  // ---- CI review (CORE, local-only like the rest of Claude Review) ----
1256
1308
  // A separate Claude run that explains why checks FAILED on a PR's head — split out of the PR review's
1257
1309
  // own run (`claude_reviews.ci_failures`, now LEGACY and read-only). Two tables, migration 0082
@@ -1474,7 +1526,20 @@ export const workspaces = sqliteTable('workspaces', {
1474
1526
  // column, so existing workspaces keep that until switched off. Read by `maybeStartAutoFix`
1475
1527
  // (coding/ai-fix/auto-fix.ts); the ONE writer is `setWorkspaceAutoReview`. It only matters
1476
1528
  // while auto review is on.
1477
- autoFixEnabled: integer('auto_fix_enabled', { mode: 'boolean' }).notNull().default(true),
1529
+ autoFixEnabled: integer('auto_fix_enabled', { mode: 'boolean' }).notNull().default(false),
1530
+ // AUTO REVIEW DAILY CAP (migration 0085 / pg 0072): auto runs this workspace may start per UTC
1531
+ // day. OVERRIDES ONLY — NULL = the default `AUTO_REVIEW_DAILY_CAP` (20), resolved through
1532
+ // `resolveAutoReviewDailyCap`; the route bounds it 1..500. The ONE writer is
1533
+ // `setWorkspaceAutoReview`, the reader the sweeper's roster (`listAutoReviewWorkspaces`).
1534
+ autoReviewDailyCap: integer('auto_review_daily_cap'),
1535
+ // AUTO-POSTING (migration 0087 / pg 0074): may a SUCCEEDED AUTO review (and an auto ticket
1536
+ // review) post its output to GitHub without a click? NULL/false = OFF — the default for every
1537
+ // workspace, existing and new; nullable with no default, so no workspace insert has to name it.
1538
+ // `autoPostSettings` is OVERRIDES ONLY ({ scope?, kinds? }) — NULL = the product defaults,
1539
+ // resolved through `resolveAutoPostSettings` (review/claude-review/auto-settings.ts). The ONE
1540
+ // writer is `setWorkspaceAutoReview`; the reader is review/claude-review/auto-post.ts.
1541
+ autoPostEnabled: integer('auto_post_enabled', { mode: 'boolean' }),
1542
+ autoPostSettings: text('auto_post_settings', { mode: 'json' }).$type(),
1478
1543
  createdAt: integer('created_at', { mode: 'timestamp' })
1479
1544
  .notNull()
1480
1545
  .default(sql `(unixepoch())`),
@@ -6,7 +6,9 @@ import { schema } from './client.js';
6
6
  // • a run left with NO members is deleted with its items — it describes nothing any more. That
7
7
  // includes a run refused before it had members, which only its starting PR anchors;
8
8
  // • an item whose owner was a deleted PR keeps its history but loses the owner (`owner_pr_id`
9
- // null), so a Post can never target a PR that is gone.
9
+ // null), so a Post can never target a PR that is gone;
10
+ // • the deleted PRs' contribution cards (`ticket_review_pr_cards`, migration 0086 / pg 0073) go
11
+ // with them — a card describes one PR's head and nothing else.
10
12
  // Children before parents, explicitly — the composite FKs cascade, but these paths run parent-last
11
13
  // by hand on both dialects. Runs inside the caller's transaction.
12
14
  export async function pruneTicketReviewsForPrs(tx, prIds) {
@@ -29,6 +31,7 @@ export async function pruneTicketReviewsForPrs(tx, prIds) {
29
31
  .where(inArray(tr.originPrId, ids))
30
32
  .execute());
31
33
  await tx.delete(m).where(inArray(m.prId, ids)).execute();
34
+ await tx.delete(schema.ticketReviewPrCards).where(inArray(schema.ticketReviewPrCards.prId, ids)).execute();
32
35
  await tx.update(items).set({ ownerPrId: null }).where(inArray(items.ownerPrId, ids)).execute();
33
36
  const runIds = [...new Set([...touched, ...started].map((r) => r.id))];
34
37
  if (runIds.length === 0)