@agent-native/core 0.84.55 → 0.84.56

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.
@@ -26,6 +26,7 @@ import {
26
26
  type DomainMatchOrg,
27
27
  } from "@agent-native/core/client/org";
28
28
  import {
29
+ SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
29
30
  extractCommentMentions,
30
31
  formatPlanCommentAnchorForAgent,
31
32
  formatPlanCommentMentionToken,
@@ -374,7 +375,7 @@ function readDesktopPlanAutoSync(planId: string | undefined): boolean {
374
375
 
375
376
  type PlanAnnotationAnchor = PlanCommentAnchor & { x: number; y: number };
376
377
 
377
- type CommentDraft = {
378
+ export type CommentDraft = {
378
379
  message: string;
379
380
  mentions: PlanCommentMention[];
380
381
  resolutionTarget: PlanCommentResolutionTarget;
@@ -1245,6 +1246,110 @@ function displayNameForMention(email: string) {
1245
1246
  return emailToName(email).replace(/\s+/g, " ").trim() || email;
1246
1247
  }
1247
1248
 
1249
+ function elementForShortcutTarget(target: EventTarget | null) {
1250
+ if (target instanceof Element) return target;
1251
+ if (target instanceof Node) return target.parentElement;
1252
+ return null;
1253
+ }
1254
+
1255
+ export function isPlanCommentShortcutEditableTarget(
1256
+ target: EventTarget | null,
1257
+ ) {
1258
+ const element = elementForShortcutTarget(target);
1259
+ if (!element) return false;
1260
+ return Boolean(
1261
+ element.closest(
1262
+ "input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox']",
1263
+ ),
1264
+ );
1265
+ }
1266
+
1267
+ export function shouldHandlePlanCommentShortcut(
1268
+ event: Pick<
1269
+ KeyboardEvent,
1270
+ | "altKey"
1271
+ | "ctrlKey"
1272
+ | "defaultPrevented"
1273
+ | "key"
1274
+ | "metaKey"
1275
+ | "shiftKey"
1276
+ | "target"
1277
+ >,
1278
+ ) {
1279
+ if (event.defaultPrevented) return false;
1280
+ const activeElement =
1281
+ typeof document === "undefined" ? null : document.activeElement;
1282
+ if (
1283
+ isPlanCommentShortcutEditableTarget(activeElement) ||
1284
+ isPlanCommentShortcutEditableTarget(event.target)
1285
+ ) {
1286
+ return false;
1287
+ }
1288
+ const key = event.key.toLowerCase();
1289
+ if (
1290
+ key === "c" &&
1291
+ !event.metaKey &&
1292
+ !event.ctrlKey &&
1293
+ !event.altKey &&
1294
+ !event.shiftKey
1295
+ ) {
1296
+ return true;
1297
+ }
1298
+ return (
1299
+ key === "m" &&
1300
+ event.metaKey &&
1301
+ event.shiftKey &&
1302
+ !event.ctrlKey &&
1303
+ !event.altKey
1304
+ );
1305
+ }
1306
+
1307
+ export function defaultInlineCommentDraftForPlanContext(input: {
1308
+ planKind?: PlanKind | null;
1309
+ ownerEmail?: string | null;
1310
+ sourceAuthorName?: string | null;
1311
+ sourceAuthorLogin?: string | null;
1312
+ accessRole?: NonNullable<PlanBundle["access"]>["role"] | null;
1313
+ currentEmail?: string | null;
1314
+ }): CommentDraft {
1315
+ const currentEmail = normalizeCommentEmail(input.currentEmail);
1316
+ if (input.planKind === "recap") {
1317
+ const targetLabel =
1318
+ input.sourceAuthorName?.trim() || input.sourceAuthorLogin?.trim();
1319
+ if (!targetLabel || input.accessRole === "owner") {
1320
+ return { message: "", mentions: [], resolutionTarget: "agent" };
1321
+ }
1322
+ const mention: PlanCommentMention = {
1323
+ email: SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
1324
+ label: targetLabel,
1325
+ role: "source-author",
1326
+ };
1327
+ return {
1328
+ message: `${formatPlanCommentMentionToken(mention)} `,
1329
+ mentions: [mention],
1330
+ resolutionTarget: "human",
1331
+ };
1332
+ }
1333
+
1334
+ const targetEmail = normalizeCommentEmail(input.ownerEmail);
1335
+ if (
1336
+ !targetEmail ||
1337
+ input.accessRole === "owner" ||
1338
+ targetEmail === currentEmail
1339
+ ) {
1340
+ return { message: "", mentions: [], resolutionTarget: "agent" };
1341
+ }
1342
+ const mention = {
1343
+ email: targetEmail,
1344
+ label: displayNameForMention(targetEmail),
1345
+ };
1346
+ return {
1347
+ message: `${formatPlanCommentMentionToken(mention)} `,
1348
+ mentions: [mention],
1349
+ resolutionTarget: "human",
1350
+ };
1351
+ }
1352
+
1248
1353
  function safeDecodeURIComponent(value: string): string {
1249
1354
  try {
1250
1355
  return decodeURIComponent(value);
@@ -3282,25 +3387,22 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
3282
3387
  bundle && (localPlanMode || session || canEditPlanContent),
3283
3388
  );
3284
3389
  const defaultInlineCommentDraft = useMemo<CommentDraft>(() => {
3285
- const ownerEmail = normalizeCommentEmail(bundle?.access?.ownerEmail);
3286
- const currentEmail = normalizeCommentEmail(collabUser?.email);
3287
- if (
3288
- !ownerEmail ||
3289
- effectivePlanAccessRole === "owner" ||
3290
- ownerEmail === currentEmail
3291
- ) {
3292
- return { message: "", mentions: [], resolutionTarget: "agent" };
3293
- }
3294
- const mention = {
3295
- email: ownerEmail,
3296
- label: displayNameForMention(ownerEmail),
3297
- };
3298
- return {
3299
- message: `${formatPlanCommentMentionToken(mention)} `,
3300
- mentions: [mention],
3301
- resolutionTarget: "human",
3302
- };
3303
- }, [bundle?.access?.ownerEmail, collabUser?.email, effectivePlanAccessRole]);
3390
+ return defaultInlineCommentDraftForPlanContext({
3391
+ planKind: bundle?.plan.kind,
3392
+ ownerEmail: bundle?.access?.ownerEmail,
3393
+ sourceAuthorName: bundle?.plan.sourceAuthorName,
3394
+ sourceAuthorLogin: bundle?.plan.sourceAuthorLogin,
3395
+ accessRole: effectivePlanAccessRole,
3396
+ currentEmail: collabUser?.email,
3397
+ });
3398
+ }, [
3399
+ bundle?.access?.ownerEmail,
3400
+ bundle?.plan.kind,
3401
+ bundle?.plan.sourceAuthorName,
3402
+ bundle?.plan.sourceAuthorLogin,
3403
+ collabUser?.email,
3404
+ effectivePlanAccessRole,
3405
+ ]);
3304
3406
  const commentThreads = useMemo(
3305
3407
  () => buildCommentThreads(bundle?.comments ?? []),
3306
3408
  [bundle?.comments],
@@ -4547,13 +4649,13 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4547
4649
  });
4548
4650
  };
4549
4651
 
4550
- const startCommenting = () => {
4652
+ const startCommenting = useCallback(() => {
4551
4653
  setCanvasMarkupMode("none");
4552
4654
  setActiveAnnotation(null);
4553
4655
  setAnnotationsOpen(false);
4554
4656
  setCommentVisibility("open");
4555
4657
  setAnnotateMode(true);
4556
- };
4658
+ }, []);
4557
4659
 
4558
4660
  const selectReviewMode = (mode: CanvasMarkupMode) => {
4559
4661
  preservePlanReaderScroll(() => {
@@ -4609,96 +4711,135 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4609
4711
  scheduleNativeMarkerUpdate();
4610
4712
  };
4611
4713
 
4612
- const readNativeSelectionComment = (): NativeSelectionComment | null => {
4613
- const reader = nativeReaderRef.current;
4614
- const selection = window.getSelection();
4615
- if (!reader || !selection || selection.rangeCount === 0) return null;
4616
- if (selection.isCollapsed) return null;
4617
- const textQuote = selection.toString().replace(/\s+/g, " ").trim();
4618
- if (!textQuote) return null;
4619
- const range = selection.getRangeAt(0);
4620
- if (!reader.contains(range.commonAncestorContainer)) return null;
4621
-
4622
- const rects = Array.from(range.getClientRects()).filter(
4623
- (rect) => rect.width > 0 && rect.height > 0,
4624
- );
4625
- const selectionRect = rects[0] ?? range.getBoundingClientRect();
4626
- if (selectionRect.width <= 0 || selectionRect.height <= 0) return null;
4627
-
4628
- const readerRect = reader.getBoundingClientRect();
4629
- const pointX =
4630
- selectionRect.left + selectionRect.width / 2 - readerRect.left;
4631
- const pointY =
4632
- selectionRect.top + selectionRect.height / 2 - readerRect.top;
4633
- const startElement =
4634
- range.startContainer instanceof Element
4635
- ? range.startContainer
4636
- : range.startContainer.parentElement;
4637
- const blockElement = startElement?.closest<HTMLElement>("[data-block-id]");
4638
- const blockType = blockElement?.dataset.blockId
4639
- ? findPlanBlockById(
4640
- bundle?.plan.content?.blocks ?? [],
4641
- blockElement.dataset.blockId,
4642
- )?.type
4643
- : undefined;
4644
- const quoteContext = textQuoteContextForBlock({
4645
- block: blockElement,
4646
- quote: textQuote,
4647
- });
4648
- const snippet = textQuote.slice(0, 220);
4649
- const anchor = {
4650
- ...buildNativeAnchorFromElement({
4651
- reader,
4652
- target: startElement instanceof HTMLElement ? startElement : reader,
4653
- pointX,
4654
- pointY,
4655
- planTitle: bundle?.plan.title,
4656
- }),
4657
- snippet,
4658
- textQuote: snippet,
4659
- anchorKind: "text",
4660
- tagName: "selection",
4661
- blockType,
4662
- ...quoteContext,
4663
- } satisfies PlanAnnotationAnchor;
4664
- const toolbarWidth = 132;
4665
- const toolbarLeft = clamp(
4666
- pointX - toolbarWidth / 2,
4667
- 12,
4668
- Math.max(12, readerRect.width - toolbarWidth - 12),
4669
- );
4670
- const toolbarTop = clamp(
4671
- selectionRect.top - readerRect.top - 48,
4672
- 12,
4673
- Math.max(12, readerRect.height - 48),
4674
- );
4675
- return {
4676
- anchor,
4677
- toolbarLeft,
4678
- toolbarTop,
4679
- position:
4680
- getPositionFromAnchor(anchor) ??
4681
- resolveInlineCommentPosition({
4714
+ const readNativeSelectionComment =
4715
+ useCallback((): NativeSelectionComment | null => {
4716
+ const reader = nativeReaderRef.current;
4717
+ const selection = window.getSelection();
4718
+ if (!reader || !selection || selection.rangeCount === 0) return null;
4719
+ if (selection.isCollapsed) return null;
4720
+ const textQuote = selection.toString().replace(/\s+/g, " ").trim();
4721
+ if (!textQuote) return null;
4722
+ const range = selection.getRangeAt(0);
4723
+ if (!reader.contains(range.commonAncestorContainer)) return null;
4724
+
4725
+ const rects = Array.from(range.getClientRects()).filter(
4726
+ (rect) => rect.width > 0 && rect.height > 0,
4727
+ );
4728
+ const selectionRect = rects[0] ?? range.getBoundingClientRect();
4729
+ if (selectionRect.width <= 0 || selectionRect.height <= 0) return null;
4730
+
4731
+ const readerRect = reader.getBoundingClientRect();
4732
+ const pointX =
4733
+ selectionRect.left + selectionRect.width / 2 - readerRect.left;
4734
+ const pointY =
4735
+ selectionRect.top + selectionRect.height / 2 - readerRect.top;
4736
+ const startElement =
4737
+ range.startContainer instanceof Element
4738
+ ? range.startContainer
4739
+ : range.startContainer.parentElement;
4740
+ const blockElement =
4741
+ startElement?.closest<HTMLElement>("[data-block-id]");
4742
+ const blockType = blockElement?.dataset.blockId
4743
+ ? findPlanBlockById(
4744
+ bundle?.plan.content?.blocks ?? [],
4745
+ blockElement.dataset.blockId,
4746
+ )?.type
4747
+ : undefined;
4748
+ const quoteContext = textQuoteContextForBlock({
4749
+ block: blockElement,
4750
+ quote: textQuote,
4751
+ });
4752
+ const snippet = textQuote.slice(0, 220);
4753
+ const anchor = {
4754
+ ...buildNativeAnchorFromElement({
4755
+ reader,
4756
+ target: startElement instanceof HTMLElement ? startElement : reader,
4682
4757
  pointX,
4683
4758
  pointY,
4684
- viewportWidth: readerRect.width,
4685
- viewportHeight: readerRect.height,
4759
+ planTitle: bundle?.plan.title,
4686
4760
  }),
4687
- };
4688
- };
4761
+ snippet,
4762
+ textQuote: snippet,
4763
+ anchorKind: "text",
4764
+ tagName: "selection",
4765
+ blockType,
4766
+ ...quoteContext,
4767
+ } satisfies PlanAnnotationAnchor;
4768
+ const toolbarWidth = 132;
4769
+ const toolbarLeft = clamp(
4770
+ pointX - toolbarWidth / 2,
4771
+ 12,
4772
+ Math.max(12, readerRect.width - toolbarWidth - 12),
4773
+ );
4774
+ const toolbarTop = clamp(
4775
+ selectionRect.top - readerRect.top - 48,
4776
+ 12,
4777
+ Math.max(12, readerRect.height - 48),
4778
+ );
4779
+ return {
4780
+ anchor,
4781
+ toolbarLeft,
4782
+ toolbarTop,
4783
+ position:
4784
+ getPositionFromAnchor(anchor) ??
4785
+ resolveInlineCommentPosition({
4786
+ pointX,
4787
+ pointY,
4788
+ viewportWidth: readerRect.width,
4789
+ viewportHeight: readerRect.height,
4790
+ }),
4791
+ };
4792
+ }, [
4793
+ bundle?.plan.content?.blocks,
4794
+ bundle?.plan.title,
4795
+ getPositionFromAnchor,
4796
+ ]);
4797
+
4798
+ const openNativeSelectionComment = useCallback(
4799
+ (selectionComment: NativeSelectionComment) => {
4800
+ documentStateRef.current = readNativeDocumentState();
4801
+ setCanvasMarkupMode("none");
4802
+ setActiveAnnotation(null);
4803
+ setAnnotationsOpen(false);
4804
+ setCommentVisibility("open");
4805
+ setAnnotateMode(true);
4806
+ setPendingAnnotation(selectionComment.anchor);
4807
+ setInlineCommentPosition(selectionComment.position);
4808
+ setNativeSelectionComment(null);
4809
+ window.getSelection()?.removeAllRanges();
4810
+ },
4811
+ [readNativeDocumentState],
4812
+ );
4689
4813
 
4690
4814
  const beginNativeSelectionComment = () => {
4691
4815
  if (!nativeSelectionComment) return;
4692
- documentStateRef.current = readNativeDocumentState();
4693
- // Implicitly enter annotate mode when a selection comment is started
4694
- // outside of review mode so the inline comment popover renders correctly.
4695
- if (!annotateMode) setAnnotateMode(true);
4696
- setPendingAnnotation(nativeSelectionComment.anchor);
4697
- setInlineCommentPosition(nativeSelectionComment.position);
4698
- setNativeSelectionComment(null);
4699
- window.getSelection()?.removeAllRanges();
4816
+ openNativeSelectionComment(nativeSelectionComment);
4700
4817
  };
4701
4818
 
4819
+ useEffect(() => {
4820
+ if (!bundle) return;
4821
+ const handleCommentShortcut = (event: KeyboardEvent) => {
4822
+ if (!shouldHandlePlanCommentShortcut(event)) return;
4823
+ event.preventDefault();
4824
+ preservePlanReaderScroll(() => {
4825
+ const selectionComment = readNativeSelectionComment();
4826
+ if (selectionComment) {
4827
+ openNativeSelectionComment(selectionComment);
4828
+ return;
4829
+ }
4830
+ startCommenting();
4831
+ });
4832
+ };
4833
+ window.addEventListener("keydown", handleCommentShortcut);
4834
+ return () => window.removeEventListener("keydown", handleCommentShortcut);
4835
+ }, [
4836
+ bundle,
4837
+ openNativeSelectionComment,
4838
+ preservePlanReaderScroll,
4839
+ readNativeSelectionComment,
4840
+ startCommenting,
4841
+ ]);
4842
+
4702
4843
  const handleNativeReaderPointerDown = (
4703
4844
  event: PointerEvent<HTMLDivElement>,
4704
4845
  ) => {
@@ -9303,6 +9444,22 @@ function commentBodyText(message: string) {
9303
9444
  .trim();
9304
9445
  }
9305
9446
 
9447
+ export function canSubmitInlineCommentDraft(input: {
9448
+ draft: CommentDraft;
9449
+ isSubmitting?: boolean;
9450
+ lockToAgent?: boolean;
9451
+ }) {
9452
+ const needsHumanMention =
9453
+ !input.lockToAgent &&
9454
+ input.draft.resolutionTarget === "human" &&
9455
+ input.draft.mentions.length === 0;
9456
+ return (
9457
+ commentBodyText(input.draft.message).length > 0 &&
9458
+ !needsHumanMention &&
9459
+ !input.isSubmitting
9460
+ );
9461
+ }
9462
+
9306
9463
  export function mentionQueryAtCaret(root: HTMLElement) {
9307
9464
  const selection = window.getSelection();
9308
9465
  if (!selection || selection.rangeCount === 0) return null;
@@ -9600,7 +9757,11 @@ function InlineCommentPopover({
9600
9757
  mountedRef.current = false;
9601
9758
  };
9602
9759
  }, []);
9603
- const canSubmit = commentBodyText(draft.message).length > 0 && !isSubmitting;
9760
+ const canSubmit = canSubmitInlineCommentDraft({
9761
+ draft,
9762
+ isSubmitting,
9763
+ lockToAgent,
9764
+ });
9604
9765
  const submit = async () => {
9605
9766
  if (!canSubmit) return;
9606
9767
  setSubmitError(false);
@@ -9666,15 +9827,19 @@ function InlineCommentPopover({
9666
9827
  autoFocus
9667
9828
  onSubmitShortcut={submit}
9668
9829
  onChange={(value) =>
9669
- setDraft((current) => ({
9670
- ...current,
9671
- ...value,
9672
- resolutionTarget: resolverTouched
9673
- ? current.resolutionTarget
9674
- : value.mentions.length > 0
9675
- ? "human"
9676
- : "agent",
9677
- }))
9830
+ setDraft((current) => {
9831
+ const addedMention =
9832
+ value.mentions.length > current.mentions.length;
9833
+ return {
9834
+ ...current,
9835
+ ...value,
9836
+ resolutionTarget: resolverTouched
9837
+ ? current.resolutionTarget
9838
+ : addedMention
9839
+ ? "human"
9840
+ : current.resolutionTarget,
9841
+ };
9842
+ })
9678
9843
  }
9679
9844
  />
9680
9845
  <Button
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Plan comment shortcuts now open comment mode from the keyboard and PR recaps target the PR author for human feedback.
@@ -59,6 +59,9 @@ export const plans = table("plans", {
59
59
  sourcePrNumber: integer("source_pr_number"),
60
60
  sourcePrState: text("source_pr_state"),
61
61
  sourcePrMergedAt: text("source_pr_merged_at"),
62
+ sourceAuthorEmail: text("source_author_email"),
63
+ sourceAuthorName: text("source_author_name"),
64
+ sourceAuthorLogin: text("source_author_login"),
62
65
  // Stable key used by PR Visual Recap publish retries to replace the recap
63
66
  // created by an earlier attempt instead of creating duplicate recap rows.
64
67
  recapIdempotencyKey: text("recap_idempotency_key"),
@@ -7,7 +7,10 @@ import {
7
7
  } from "@agent-native/core/server";
8
8
  import { eq } from "drizzle-orm";
9
9
 
10
- import { extractCommentMentions } from "../../shared/comment-context.js";
10
+ import {
11
+ SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
12
+ extractCommentMentions,
13
+ } from "../../shared/comment-context.js";
11
14
  import type { PlanBundle, PlanComment } from "../../shared/types.js";
12
15
  import { getDb, schema } from "../db/index.js";
13
16
 
@@ -29,6 +32,7 @@ function normalizeEmail(email: string | null | undefined): string | null {
29
32
 
30
33
  function isSyntheticQaEmail(email: string): boolean {
31
34
  const trimmed = email.trim().toLowerCase();
35
+ if (trimmed === SOURCE_AUTHOR_COMMENT_MENTION_EMAIL) return true;
32
36
  const at = trimmed.lastIndexOf("@");
33
37
  if (at <= 0) return false;
34
38
  const local = trimmed.slice(0, at);
@@ -150,9 +154,11 @@ export function planCommentNotificationRecipients(input: {
150
154
  comment: PlanComment;
151
155
  comments: PlanComment[];
152
156
  planOwnerEmail?: string | null;
157
+ sourceAuthorEmail?: string | null;
153
158
  }): NotificationRecipient[] {
154
159
  if (input.comment.createdBy !== "human") return [];
155
160
  const actorEmail = normalizeEmail(input.comment.authorEmail);
161
+ const sourceAuthorEmail = normalizeEmail(input.sourceAuthorEmail);
156
162
  const recipients = new Map<string, NotificationRecipient>();
157
163
  const addRecipient = (
158
164
  email: string | null | undefined,
@@ -166,12 +172,22 @@ export function planCommentNotificationRecipients(input: {
166
172
  recipients.set(normalized, { email: normalized, reason });
167
173
  };
168
174
 
169
- addRecipient(input.planOwnerEmail, "plan-owner");
170
175
  const mentionedPeople =
171
176
  input.comment.mentions && input.comment.mentions.length > 0
172
177
  ? input.comment.mentions
173
178
  : extractCommentMentions(input.comment.message);
179
+ const mentionsSourceAuthor = mentionedPeople.some(
180
+ (mention) =>
181
+ normalizeEmail(mention.email) === SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
182
+ );
183
+ if (!(mentionsSourceAuthor && sourceAuthorEmail)) {
184
+ addRecipient(input.planOwnerEmail, "plan-owner");
185
+ }
174
186
  for (const mention of mentionedPeople) {
187
+ if (normalizeEmail(mention.email) === SOURCE_AUTHOR_COMMENT_MENTION_EMAIL) {
188
+ addRecipient(sourceAuthorEmail, "mention");
189
+ continue;
190
+ }
175
191
  addRecipient(mention.email, "mention");
176
192
  }
177
193
 
@@ -238,10 +254,12 @@ export async function notifyPlanCommentRecipients({
238
254
  id: schema.plans.id,
239
255
  title: schema.plans.title,
240
256
  ownerEmail: schema.plans.ownerEmail,
257
+ sourceAuthorEmail: schema.plans.sourceAuthorEmail,
241
258
  })
242
259
  .from(schema.plans)
243
260
  .where(eq(schema.plans.id, bundle.plan.id));
244
261
  const planOwnerEmail = planRow?.ownerEmail ?? null;
262
+ const sourceAuthorEmail = planRow?.sourceAuthorEmail ?? null;
245
263
  const planTitle = planRow?.title ?? bundle.plan.title;
246
264
  const insertedById = new Map(
247
265
  bundle.comments
@@ -261,6 +279,7 @@ export async function notifyPlanCommentRecipients({
261
279
  comment,
262
280
  comments: commentsForRecipients,
263
281
  planOwnerEmail,
282
+ sourceAuthorEmail,
264
283
  });
265
284
  for (const recipient of recipients) {
266
285
  try {
@@ -766,6 +766,8 @@ export async function loadPlanBundle(planId: string): Promise<PlanBundle> {
766
766
  hostedPlanId: plan.hostedPlanId,
767
767
  hostedPlanUrl: plan.hostedPlanUrl,
768
768
  sourceUrl: plan.sourceUrl,
769
+ sourceAuthorName: plan.sourceAuthorName,
770
+ sourceAuthorLogin: plan.sourceAuthorLogin,
769
771
  html: plan.html,
770
772
  markdown: plan.markdown,
771
773
  content: parsePlanContent(plan.content),
@@ -819,6 +821,8 @@ export async function summarizePlans(
819
821
  | "hostedPlanId"
820
822
  | "hostedPlanUrl"
821
823
  | "sourceUrl"
824
+ | "sourceAuthorName"
825
+ | "sourceAuthorLogin"
822
826
  | "createdAt"
823
827
  | "updatedAt"
824
828
  | "approvedAt"
@@ -877,6 +881,8 @@ export async function summarizePlans(
877
881
  hostedPlanId: plan.hostedPlanId,
878
882
  hostedPlanUrl: plan.hostedPlanUrl,
879
883
  sourceUrl: plan.sourceUrl,
884
+ sourceAuthorName: plan.sourceAuthorName,
885
+ sourceAuthorLogin: plan.sourceAuthorLogin,
880
886
  createdAt: plan.createdAt,
881
887
  updatedAt: plan.updatedAt,
882
888
  approvedAt: plan.approvedAt,
@@ -314,6 +314,17 @@ CREATE INDEX IF NOT EXISTS plans_recap_pr_merged_idx ON plans(kind, source_type,
314
314
  CREATE INDEX IF NOT EXISTS plans_source_pr_idx ON plans(source_repo, source_pr_number)`,
315
315
  },
316
316
  },
317
+ {
318
+ version: 33,
319
+ sql: {
320
+ postgres: `ALTER TABLE plans ADD COLUMN IF NOT EXISTS source_author_email TEXT;
321
+ ALTER TABLE plans ADD COLUMN IF NOT EXISTS source_author_name TEXT;
322
+ ALTER TABLE plans ADD COLUMN IF NOT EXISTS source_author_login TEXT`,
323
+ sqlite: `ALTER TABLE plans ADD COLUMN source_author_email TEXT;
324
+ ALTER TABLE plans ADD COLUMN source_author_name TEXT;
325
+ ALTER TABLE plans ADD COLUMN source_author_login TEXT`,
326
+ },
327
+ },
317
328
  ],
318
329
  { table: "plans_migrations" },
319
330
  );
@@ -1,4 +1,5 @@
1
1
  export const PLAN_COMMENT_RESOLUTION_TARGETS = ["agent", "human"] as const;
2
+ export const SOURCE_AUTHOR_COMMENT_MENTION_EMAIL = "source-author@plan.local";
2
3
 
3
4
  export type PlanCommentResolutionTarget =
4
5
  (typeof PLAN_COMMENT_RESOLUTION_TARGETS)[number];
@@ -98,6 +98,8 @@ export interface PlanSummary {
98
98
  hostedPlanId?: string | null;
99
99
  hostedPlanUrl?: string | null;
100
100
  sourceUrl?: string | null;
101
+ sourceAuthorName?: string | null;
102
+ sourceAuthorLogin?: string | null;
101
103
  createdAt: string;
102
104
  updatedAt: string;
103
105
  approvedAt?: string | null;
@@ -122,6 +124,8 @@ export interface Plan {
122
124
  hostedPlanId?: string | null;
123
125
  hostedPlanUrl?: string | null;
124
126
  sourceUrl?: string | null;
127
+ sourceAuthorName?: string | null;
128
+ sourceAuthorLogin?: string | null;
125
129
  html?: string | null;
126
130
  markdown?: string | null;
127
131
  content?: PlanContent | null;
@@ -241,6 +241,16 @@ type GitHubComment = {
241
241
  type?: string | null;
242
242
  } | null;
243
243
  };
244
+ export declare function resolveGitHubPullRequestAuthor(input: {
245
+ token: string;
246
+ repo: string;
247
+ pr: string;
248
+ fetchFn?: typeof fetch;
249
+ }): Promise<{
250
+ email?: string;
251
+ name?: string;
252
+ login?: string;
253
+ }>;
244
254
  export declare function isPullRequestHeadCurrent(input: {
245
255
  token: string;
246
256
  owner: string;
@@ -293,6 +303,7 @@ export declare function fetchRecapBlockReference(input: {
293
303
  export declare function publishRecapSource(input: {
294
304
  appUrl: string;
295
305
  token: string;
306
+ githubToken?: string;
296
307
  sourcePath?: string;
297
308
  out?: string;
298
309
  prevPlanId?: string;
@@ -304,6 +315,9 @@ export declare function publishRecapSource(input: {
304
315
  sourcePrNumber?: string;
305
316
  sourcePrState?: string;
306
317
  sourcePrMergedAt?: string;
318
+ sourceAuthorEmail?: string;
319
+ sourceAuthorName?: string;
320
+ sourceAuthorLogin?: string;
307
321
  fetchFn?: typeof fetch;
308
322
  cwd?: string;
309
323
  }): Promise<{