agent-dealer 1.2.13 → 1.2.15

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 (49) hide show
  1. package/bundle/server/dist/adapters/git-worktree.js +15 -0
  2. package/bundle/server/dist/adapters/git-worktree.test.js +30 -0
  3. package/bundle/server/dist/adapters/linear-inbox.js +97 -0
  4. package/bundle/server/dist/adapters/linear-inbox.test.js +299 -1
  5. package/bundle/server/dist/adapters/linear-sync.js +33 -10
  6. package/bundle/server/dist/capacity/claude-events.js +25 -5
  7. package/bundle/server/dist/capacity/claude-local-cache.js +159 -13
  8. package/bundle/server/dist/capacity/claude-local-cache.test.js +191 -3
  9. package/bundle/server/dist/coordinator/auto-merge.js +10 -0
  10. package/bundle/server/dist/coordinator/branch-progress.js +5 -4
  11. package/bundle/server/dist/coordinator/commands.js +37 -3
  12. package/bundle/server/dist/coordinator/delete-issue.js +473 -0
  13. package/bundle/server/dist/coordinator/developer-effect.js +33 -6
  14. package/bundle/server/dist/coordinator/linear-merge-verify.js +382 -0
  15. package/bundle/server/dist/coordinator/linear-merge-verify.test.js +376 -0
  16. package/bundle/server/dist/coordinator/prompts.js +9 -1
  17. package/bundle/server/dist/coordinator/prompts.test.js +93 -0
  18. package/bundle/server/dist/coordinator/source-attachments.js +496 -0
  19. package/bundle/server/dist/coordinator/source-attachments.test.js +396 -0
  20. package/bundle/server/dist/coordinator/source-reload.js +178 -0
  21. package/bundle/server/dist/db/index.js +4 -0
  22. package/bundle/server/dist/db/schema.sql +32 -0
  23. package/bundle/server/dist/docs-linear-attachments.test.js +36 -0
  24. package/bundle/server/dist/paths.js +12 -0
  25. package/bundle/server/dist/repository/runtime-capacity.js +19 -3
  26. package/bundle/server/dist/repository/source-attachments.js +56 -0
  27. package/bundle/server/dist/routes/human-actions.js +17 -0
  28. package/bundle/server/dist/routes/issues-delete.test.js +666 -0
  29. package/bundle/server/dist/routes/issues-execution-contract.test.js +296 -0
  30. package/bundle/server/dist/routes/issues-source-attachments.test.js +447 -0
  31. package/bundle/server/dist/routes/issues.js +109 -1
  32. package/bundle/server/dist/routes/runtime-capacity.test.js +35 -0
  33. package/bundle/server/package.json +2 -2
  34. package/bundle/server/static-ui/assets/index-DVyqblie.js +64 -0
  35. package/bundle/server/static-ui/index.html +1 -1
  36. package/bundle/shared/dist/index.d.ts +108 -2
  37. package/bundle/shared/dist/index.js +12 -1
  38. package/bundle/shared/dist/issues.d.ts +43 -2
  39. package/bundle/shared/dist/issues.js +10 -0
  40. package/bundle/shared/dist/linear-intake.d.ts +85 -0
  41. package/bundle/shared/dist/linear-intake.js +56 -0
  42. package/bundle/shared/dist/linear-intake.test.js +55 -1
  43. package/bundle/shared/dist/runtime-capacity.d.ts +147 -0
  44. package/bundle/shared/dist/runtime-capacity.js +15 -0
  45. package/bundle/shared/dist/workflow.d.ts +4 -4
  46. package/bundle/shared/dist/workflow.js +7 -0
  47. package/bundle/shared/package.json +1 -1
  48. package/package.json +1 -1
  49. package/bundle/server/static-ui/assets/index-BzLjMBI9.js +0 -64
@@ -8,7 +8,7 @@
8
8
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
9
  <meta name="theme-color" content="#0a0e14" />
10
10
  <link rel="preload" href="/fonts/Monaco.ttf" as="font" type="font/ttf" crossorigin />
11
- <script type="module" crossorigin src="/assets/index-BzLjMBI9.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-DVyqblie.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-COYJxD1y.css">
13
13
  </head>
14
14
  <body>
@@ -593,16 +593,16 @@ export declare const Artifact: z.ZodObject<{
593
593
  createdAt: string;
594
594
  kind: "slack_draft" | "email_draft" | "service_draft" | "task_snapshot" | "draft_plan" | "approved_plan" | "plan_triage" | "plan_answers" | "acceptance_criteria" | "transcript" | "stream_trace" | "usage" | "agent_session" | "execution_result" | "diff" | "pr" | "document" | "research_brief" | "deliverable" | "feedback" | "playbook_patch" | "playbook_use_receipt" | "deck_feedback_signal" | "reflect_status" | "linear_sync" | "send_receipt" | "result_qa";
595
595
  runId: string;
596
- contentJson: string | null;
597
596
  blobPath: string | null;
597
+ contentJson: string | null;
598
598
  author: "human" | "system" | "agent";
599
599
  }, {
600
600
  id: string;
601
601
  createdAt: string;
602
602
  kind: "slack_draft" | "email_draft" | "service_draft" | "task_snapshot" | "draft_plan" | "approved_plan" | "plan_triage" | "plan_answers" | "acceptance_criteria" | "transcript" | "stream_trace" | "usage" | "agent_session" | "execution_result" | "diff" | "pr" | "document" | "research_brief" | "deliverable" | "feedback" | "playbook_patch" | "playbook_use_receipt" | "deck_feedback_signal" | "reflect_status" | "linear_sync" | "send_receipt" | "result_qa";
603
603
  runId: string;
604
- contentJson: string | null;
605
604
  blobPath: string | null;
605
+ contentJson: string | null;
606
606
  author: "human" | "system" | "agent";
607
607
  }>;
608
608
  export type Artifact = z.infer<typeof Artifact>;
@@ -994,6 +994,12 @@ export declare const LinearCandidate: z.ZodObject<{
994
994
  state: z.ZodOptional<z.ZodString>;
995
995
  labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
996
996
  teamId: z.ZodOptional<z.ZodString>;
997
+ /**
998
+ * NOT-362: Linear's own suggested branch name for the issue. Intake persists
999
+ * it as the Dealer issue's branch so Linear's GitHub integration can link
1000
+ * the PR on repositories it covers.
1001
+ */
1002
+ branchName: z.ZodOptional<z.ZodString>;
997
1003
  /**
998
1004
  * NOT-242: server-resolved repository hint read from explicit `repo:` labels.
999
1005
  * Raw labels stay on the candidate; this is the deterministic read of them.
@@ -1017,6 +1023,29 @@ export declare const LinearCandidate: z.ZodObject<{
1017
1023
  labels?: string[] | undefined;
1018
1024
  error?: string | undefined;
1019
1025
  }>>;
1026
+ /**
1027
+ * NOT-364: the issue's own file/link attachments (never comment uploads).
1028
+ * Hosted files are snapshotted at import/reload; links stay metadata only.
1029
+ */
1030
+ attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
1031
+ id: z.ZodString;
1032
+ title: z.ZodString;
1033
+ url: z.ZodString;
1034
+ subtitle: z.ZodOptional<z.ZodString>;
1035
+ source: z.ZodOptional<z.ZodString>;
1036
+ }, "strip", z.ZodTypeAny, {
1037
+ id: string;
1038
+ title: string;
1039
+ url: string;
1040
+ subtitle?: string | undefined;
1041
+ source?: string | undefined;
1042
+ }, {
1043
+ id: string;
1044
+ title: string;
1045
+ url: string;
1046
+ subtitle?: string | undefined;
1047
+ source?: string | undefined;
1048
+ }>, "many">>;
1020
1049
  }, "strip", z.ZodTypeAny, {
1021
1050
  id: string;
1022
1051
  title: string;
@@ -1026,6 +1055,7 @@ export declare const LinearCandidate: z.ZodObject<{
1026
1055
  labels?: string[] | undefined;
1027
1056
  state?: string | undefined;
1028
1057
  teamId?: string | undefined;
1058
+ branchName?: string | undefined;
1029
1059
  repoResolution?: {
1030
1060
  status: "resolved" | "unresolved" | "conflict" | "invalid";
1031
1061
  repository?: string | undefined;
@@ -1033,6 +1063,13 @@ export declare const LinearCandidate: z.ZodObject<{
1033
1063
  labels?: string[] | undefined;
1034
1064
  error?: string | undefined;
1035
1065
  } | undefined;
1066
+ attachments?: {
1067
+ id: string;
1068
+ title: string;
1069
+ url: string;
1070
+ subtitle?: string | undefined;
1071
+ source?: string | undefined;
1072
+ }[] | undefined;
1036
1073
  }, {
1037
1074
  id: string;
1038
1075
  title: string;
@@ -1042,6 +1079,7 @@ export declare const LinearCandidate: z.ZodObject<{
1042
1079
  labels?: string[] | undefined;
1043
1080
  state?: string | undefined;
1044
1081
  teamId?: string | undefined;
1082
+ branchName?: string | undefined;
1045
1083
  repoResolution?: {
1046
1084
  status: "resolved" | "unresolved" | "conflict" | "invalid";
1047
1085
  repository?: string | undefined;
@@ -1049,6 +1087,13 @@ export declare const LinearCandidate: z.ZodObject<{
1049
1087
  labels?: string[] | undefined;
1050
1088
  error?: string | undefined;
1051
1089
  } | undefined;
1090
+ attachments?: {
1091
+ id: string;
1092
+ title: string;
1093
+ url: string;
1094
+ subtitle?: string | undefined;
1095
+ source?: string | undefined;
1096
+ }[] | undefined;
1052
1097
  }>;
1053
1098
  export type LinearCandidate = z.infer<typeof LinearCandidate>;
1054
1099
  export declare const LinearIntakeConfig: z.ZodObject<{
@@ -1274,6 +1319,12 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1274
1319
  state: z.ZodOptional<z.ZodString>;
1275
1320
  labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1276
1321
  teamId: z.ZodOptional<z.ZodString>;
1322
+ /**
1323
+ * NOT-362: Linear's own suggested branch name for the issue. Intake persists
1324
+ * it as the Dealer issue's branch so Linear's GitHub integration can link
1325
+ * the PR on repositories it covers.
1326
+ */
1327
+ branchName: z.ZodOptional<z.ZodString>;
1277
1328
  /**
1278
1329
  * NOT-242: server-resolved repository hint read from explicit `repo:` labels.
1279
1330
  * Raw labels stay on the candidate; this is the deterministic read of them.
@@ -1297,6 +1348,29 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1297
1348
  labels?: string[] | undefined;
1298
1349
  error?: string | undefined;
1299
1350
  }>>;
1351
+ /**
1352
+ * NOT-364: the issue's own file/link attachments (never comment uploads).
1353
+ * Hosted files are snapshotted at import/reload; links stay metadata only.
1354
+ */
1355
+ attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
1356
+ id: z.ZodString;
1357
+ title: z.ZodString;
1358
+ url: z.ZodString;
1359
+ subtitle: z.ZodOptional<z.ZodString>;
1360
+ source: z.ZodOptional<z.ZodString>;
1361
+ }, "strip", z.ZodTypeAny, {
1362
+ id: string;
1363
+ title: string;
1364
+ url: string;
1365
+ subtitle?: string | undefined;
1366
+ source?: string | undefined;
1367
+ }, {
1368
+ id: string;
1369
+ title: string;
1370
+ url: string;
1371
+ subtitle?: string | undefined;
1372
+ source?: string | undefined;
1373
+ }>, "many">>;
1300
1374
  }, "strip", z.ZodTypeAny, {
1301
1375
  id: string;
1302
1376
  title: string;
@@ -1306,6 +1380,7 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1306
1380
  labels?: string[] | undefined;
1307
1381
  state?: string | undefined;
1308
1382
  teamId?: string | undefined;
1383
+ branchName?: string | undefined;
1309
1384
  repoResolution?: {
1310
1385
  status: "resolved" | "unresolved" | "conflict" | "invalid";
1311
1386
  repository?: string | undefined;
@@ -1313,6 +1388,13 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1313
1388
  labels?: string[] | undefined;
1314
1389
  error?: string | undefined;
1315
1390
  } | undefined;
1391
+ attachments?: {
1392
+ id: string;
1393
+ title: string;
1394
+ url: string;
1395
+ subtitle?: string | undefined;
1396
+ source?: string | undefined;
1397
+ }[] | undefined;
1316
1398
  }, {
1317
1399
  id: string;
1318
1400
  title: string;
@@ -1322,6 +1404,7 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1322
1404
  labels?: string[] | undefined;
1323
1405
  state?: string | undefined;
1324
1406
  teamId?: string | undefined;
1407
+ branchName?: string | undefined;
1325
1408
  repoResolution?: {
1326
1409
  status: "resolved" | "unresolved" | "conflict" | "invalid";
1327
1410
  repository?: string | undefined;
@@ -1329,6 +1412,13 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1329
1412
  labels?: string[] | undefined;
1330
1413
  error?: string | undefined;
1331
1414
  } | undefined;
1415
+ attachments?: {
1416
+ id: string;
1417
+ title: string;
1418
+ url: string;
1419
+ subtitle?: string | undefined;
1420
+ source?: string | undefined;
1421
+ }[] | undefined;
1332
1422
  }>, "many">;
1333
1423
  hasMore: z.ZodBoolean;
1334
1424
  }, "strip", z.ZodTypeAny, {
@@ -1341,6 +1431,7 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1341
1431
  labels?: string[] | undefined;
1342
1432
  state?: string | undefined;
1343
1433
  teamId?: string | undefined;
1434
+ branchName?: string | undefined;
1344
1435
  repoResolution?: {
1345
1436
  status: "resolved" | "unresolved" | "conflict" | "invalid";
1346
1437
  repository?: string | undefined;
@@ -1348,6 +1439,13 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1348
1439
  labels?: string[] | undefined;
1349
1440
  error?: string | undefined;
1350
1441
  } | undefined;
1442
+ attachments?: {
1443
+ id: string;
1444
+ title: string;
1445
+ url: string;
1446
+ subtitle?: string | undefined;
1447
+ source?: string | undefined;
1448
+ }[] | undefined;
1351
1449
  }[];
1352
1450
  hasMore: boolean;
1353
1451
  }, {
@@ -1360,6 +1458,7 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1360
1458
  labels?: string[] | undefined;
1361
1459
  state?: string | undefined;
1362
1460
  teamId?: string | undefined;
1461
+ branchName?: string | undefined;
1363
1462
  repoResolution?: {
1364
1463
  status: "resolved" | "unresolved" | "conflict" | "invalid";
1365
1464
  repository?: string | undefined;
@@ -1367,6 +1466,13 @@ export declare const LinearCandidatesPage: z.ZodObject<{
1367
1466
  labels?: string[] | undefined;
1368
1467
  error?: string | undefined;
1369
1468
  } | undefined;
1469
+ attachments?: {
1470
+ id: string;
1471
+ title: string;
1472
+ url: string;
1473
+ subtitle?: string | undefined;
1474
+ source?: string | undefined;
1475
+ }[] | undefined;
1370
1476
  }[];
1371
1477
  hasMore: boolean;
1372
1478
  }>;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { LinearRepoResolution } from "./linear-intake.js";
2
+ import { LinearAttachment, LinearRepoResolution } from "./linear-intake.js";
3
3
  import { Runtime } from "./runtime.js";
4
4
  import { AgentWithHealth } from "./agents.js";
5
5
  import { PhaseBudget, RunBudget } from "./budget.js";
@@ -267,11 +267,22 @@ export const LinearCandidate = z.object({
267
267
  state: z.string().optional(),
268
268
  labels: z.array(z.string()).optional(),
269
269
  teamId: z.string().optional(),
270
+ /**
271
+ * NOT-362: Linear's own suggested branch name for the issue. Intake persists
272
+ * it as the Dealer issue's branch so Linear's GitHub integration can link
273
+ * the PR on repositories it covers.
274
+ */
275
+ branchName: z.string().optional(),
270
276
  /**
271
277
  * NOT-242: server-resolved repository hint read from explicit `repo:` labels.
272
278
  * Raw labels stay on the candidate; this is the deterministic read of them.
273
279
  */
274
280
  repoResolution: LinearRepoResolution.optional(),
281
+ /**
282
+ * NOT-364: the issue's own file/link attachments (never comment uploads).
283
+ * Hosted files are snapshotted at import/reload; links stay metadata only.
284
+ */
285
+ attachments: z.array(LinearAttachment).optional(),
275
286
  });
276
287
  // NOT-71: defaultAgentId and routingRules (label -> agent `autoAgent` routing) are gone
277
288
  // with intake/agent-routing.ts and the promote endpoint that consumed them. The persisted
@@ -209,6 +209,33 @@ export declare const CreateIssueInput: z.ZodObject<{
209
209
  * `false` creates a draft that sits outside the queue until someone adds or starts it.
210
210
  */
211
211
  enqueue: z.ZodDefault<z.ZodBoolean>;
212
+ /**
213
+ * NOT-364: Linear issue attachment metadata captured by the From-Linear form
214
+ * at lookup time. The server snapshots every Linear-hosted file before the
215
+ * issue/queue rows are written (atomic: any download/limit failure refuses
216
+ * the whole create); external links are stored as metadata only. Ignored
217
+ * for non-Linear sources. Never a stored issue field — rows live in
218
+ * `issue_source_attachments`.
219
+ */
220
+ linearAttachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
221
+ id: z.ZodString;
222
+ title: z.ZodString;
223
+ url: z.ZodString;
224
+ subtitle: z.ZodOptional<z.ZodString>;
225
+ source: z.ZodOptional<z.ZodString>;
226
+ }, "strip", z.ZodTypeAny, {
227
+ id: string;
228
+ title: string;
229
+ url: string;
230
+ subtitle?: string | undefined;
231
+ source?: string | undefined;
232
+ }, {
233
+ id: string;
234
+ title: string;
235
+ url: string;
236
+ subtitle?: string | undefined;
237
+ source?: string | undefined;
238
+ }>, "many">>;
212
239
  }, "strip", z.ZodTypeAny, {
213
240
  repo: string;
214
241
  title: string;
@@ -225,6 +252,13 @@ export declare const CreateIssueInput: z.ZodObject<{
225
252
  externalId?: string | undefined;
226
253
  externalLabel?: string | undefined;
227
254
  externalUrl?: string | undefined;
255
+ linearAttachments?: {
256
+ id: string;
257
+ title: string;
258
+ url: string;
259
+ subtitle?: string | undefined;
260
+ source?: string | undefined;
261
+ }[] | undefined;
228
262
  }, {
229
263
  repo: string;
230
264
  title: string;
@@ -241,6 +275,13 @@ export declare const CreateIssueInput: z.ZodObject<{
241
275
  maxInfraAttempts?: number | undefined;
242
276
  autoMerge?: boolean | undefined;
243
277
  enqueue?: boolean | undefined;
278
+ linearAttachments?: {
279
+ id: string;
280
+ title: string;
281
+ url: string;
282
+ subtitle?: string | undefined;
283
+ source?: string | undefined;
284
+ }[] | undefined;
244
285
  }>;
245
286
  /** Wire/API body shape — defaults applied by `CreateIssueInput.parse` / repository. */
246
287
  export type CreateIssueInput = z.input<typeof CreateIssueInput>;
@@ -528,18 +569,18 @@ export declare const IssueArtifact: z.ZodObject<{
528
569
  issueId: string | null;
529
570
  createdAt: string;
530
571
  kind: string;
572
+ blobPath: string | null;
531
573
  workerSessionId: string | null;
532
574
  contentJson: string | null;
533
- blobPath: string | null;
534
575
  author: "human" | "system" | "agent";
535
576
  }, {
536
577
  id: string;
537
578
  issueId: string | null;
538
579
  createdAt: string;
539
580
  kind: string;
581
+ blobPath: string | null;
540
582
  workerSessionId: string | null;
541
583
  contentJson: string | null;
542
- blobPath: string | null;
543
584
  author: "human" | "system" | "agent";
544
585
  }>;
545
586
  export type IssueArtifact = z.infer<typeof IssueArtifact>;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { GitHubRepoInput } from "./github-repo.js";
3
+ import { LinearAttachment } from "./linear-intake.js";
3
4
  import { ExecutionContractV1 } from "./execution-contract.js";
4
5
  export const IssueStatus = z.enum([
5
6
  "ready",
@@ -98,6 +99,15 @@ export const CreateIssueInput = z.object({
98
99
  * `false` creates a draft that sits outside the queue until someone adds or starts it.
99
100
  */
100
101
  enqueue: z.boolean().default(true),
102
+ /**
103
+ * NOT-364: Linear issue attachment metadata captured by the From-Linear form
104
+ * at lookup time. The server snapshots every Linear-hosted file before the
105
+ * issue/queue rows are written (atomic: any download/limit failure refuses
106
+ * the whole create); external links are stored as metadata only. Ignored
107
+ * for non-Linear sources. Never a stored issue field — rows live in
108
+ * `issue_source_attachments`.
109
+ */
110
+ linearAttachments: z.array(LinearAttachment).optional(),
101
111
  });
102
112
  /** Pre-start (or parked-on-`needs_human`) edits only — the route enforces no active
103
113
  * workflow instance; status/owner/intent/branch/SHA/PR stay coordinator-owned. */
@@ -133,3 +133,88 @@ export declare function resolveLinearRepoWithMappings(labels: readonly string[]
133
133
  * identity and a manually entered one cannot drift.
134
134
  */
135
135
  export declare function resolveLinearRepoLabels(labels: readonly string[] | undefined): LinearRepoResolution;
136
+ /**
137
+ * NOT-364: a Linear issue attachment as returned by the direct GraphQL query.
138
+ * `subtitle` and `source` are Linear's own optional metadata (present on link
139
+ * attachments that point at external documents, often absent on uploaded
140
+ * files). Files that exist only inside Linear *comments* never appear here —
141
+ * the query reads the issue's own `attachments` connection only.
142
+ */
143
+ export declare const LinearAttachment: z.ZodObject<{
144
+ /** Stable Linear attachment id — the reconciliation key across reloads. */
145
+ id: z.ZodString;
146
+ /** Display name (`repro.tar.gz`) or link title. */
147
+ title: z.ZodString;
148
+ /** Download URL for hosted files (short-lived) or the external target for links. */
149
+ url: z.ZodString;
150
+ subtitle: z.ZodOptional<z.ZodString>;
151
+ /** Linear's source metadata, coerced to a string by the adapter when needed. */
152
+ source: z.ZodOptional<z.ZodString>;
153
+ }, "strip", z.ZodTypeAny, {
154
+ id: string;
155
+ title: string;
156
+ url: string;
157
+ subtitle?: string | undefined;
158
+ source?: string | undefined;
159
+ }, {
160
+ id: string;
161
+ title: string;
162
+ url: string;
163
+ subtitle?: string | undefined;
164
+ source?: string | undefined;
165
+ }>;
166
+ export type LinearAttachment = z.infer<typeof LinearAttachment>;
167
+ /**
168
+ * NOT-364: Linear-hosted file uploads live under Linear's own upload hosts;
169
+ * everything else is an ordinary external link. Hosted files are snapshotted
170
+ * into Dealer-owned storage at import/reload so a queued Builder never needs
171
+ * the (expiring) Linear URL; links are metadata only and never downloaded.
172
+ */
173
+ export declare function isLinearHostedAttachmentUrl(rawUrl: string): boolean;
174
+ /**
175
+ * NOT-364: a durable Dealer-side record of one Linear issue attachment.
176
+ * `kind: "file"` rows carry a Dealer-owned `blobPath` plus integrity fields;
177
+ * `kind: "link"` rows carry source metadata only and must never be presented
178
+ * as offline-available files.
179
+ */
180
+ export declare const SourceAttachmentRecord: z.ZodObject<{
181
+ linearAttachmentId: z.ZodString;
182
+ kind: z.ZodEnum<["file", "link"]>;
183
+ title: z.ZodString;
184
+ /** Worktree-local safe basename for files — absent on links. */
185
+ safeFileName: z.ZodOptional<z.ZodString>;
186
+ /** Dealer-owned blob path for downloaded files — absent on links. */
187
+ blobPath: z.ZodOptional<z.ZodString>;
188
+ contentType: z.ZodOptional<z.ZodString>;
189
+ sizeBytes: z.ZodOptional<z.ZodNumber>;
190
+ sha256: z.ZodOptional<z.ZodString>;
191
+ /** The Linear (possibly expiring) URL for files, the external target for links. */
192
+ url: z.ZodString;
193
+ subtitle: z.ZodOptional<z.ZodString>;
194
+ source: z.ZodOptional<z.ZodString>;
195
+ }, "strip", z.ZodTypeAny, {
196
+ kind: "link" | "file";
197
+ title: string;
198
+ url: string;
199
+ linearAttachmentId: string;
200
+ subtitle?: string | undefined;
201
+ source?: string | undefined;
202
+ safeFileName?: string | undefined;
203
+ blobPath?: string | undefined;
204
+ contentType?: string | undefined;
205
+ sizeBytes?: number | undefined;
206
+ sha256?: string | undefined;
207
+ }, {
208
+ kind: "link" | "file";
209
+ title: string;
210
+ url: string;
211
+ linearAttachmentId: string;
212
+ subtitle?: string | undefined;
213
+ source?: string | undefined;
214
+ safeFileName?: string | undefined;
215
+ blobPath?: string | undefined;
216
+ contentType?: string | undefined;
217
+ sizeBytes?: number | undefined;
218
+ sha256?: string | undefined;
219
+ }>;
220
+ export type SourceAttachmentRecord = z.infer<typeof SourceAttachmentRecord>;
@@ -178,3 +178,59 @@ export function resolveLinearRepoLabels(labels) {
178
178
  return { status: "invalid", labels: [raw], error: err.message };
179
179
  }
180
180
  }
181
+ /**
182
+ * NOT-364: a Linear issue attachment as returned by the direct GraphQL query.
183
+ * `subtitle` and `source` are Linear's own optional metadata (present on link
184
+ * attachments that point at external documents, often absent on uploaded
185
+ * files). Files that exist only inside Linear *comments* never appear here —
186
+ * the query reads the issue's own `attachments` connection only.
187
+ */
188
+ export const LinearAttachment = z.object({
189
+ /** Stable Linear attachment id — the reconciliation key across reloads. */
190
+ id: z.string(),
191
+ /** Display name (`repro.tar.gz`) or link title. */
192
+ title: z.string(),
193
+ /** Download URL for hosted files (short-lived) or the external target for links. */
194
+ url: z.string(),
195
+ subtitle: z.string().optional(),
196
+ /** Linear's source metadata, coerced to a string by the adapter when needed. */
197
+ source: z.string().optional(),
198
+ });
199
+ /**
200
+ * NOT-364: Linear-hosted file uploads live under Linear's own upload hosts;
201
+ * everything else is an ordinary external link. Hosted files are snapshotted
202
+ * into Dealer-owned storage at import/reload so a queued Builder never needs
203
+ * the (expiring) Linear URL; links are metadata only and never downloaded.
204
+ */
205
+ export function isLinearHostedAttachmentUrl(rawUrl) {
206
+ let host;
207
+ try {
208
+ host = new URL(rawUrl).hostname.toLowerCase();
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ return host === "uploads.linear.app" || host.endsWith(".uploads.linear.app") || host === "linear.app" || host.endsWith(".linear.app");
214
+ }
215
+ /**
216
+ * NOT-364: a durable Dealer-side record of one Linear issue attachment.
217
+ * `kind: "file"` rows carry a Dealer-owned `blobPath` plus integrity fields;
218
+ * `kind: "link"` rows carry source metadata only and must never be presented
219
+ * as offline-available files.
220
+ */
221
+ export const SourceAttachmentRecord = z.object({
222
+ linearAttachmentId: z.string(),
223
+ kind: z.enum(["file", "link"]),
224
+ title: z.string(),
225
+ /** Worktree-local safe basename for files — absent on links. */
226
+ safeFileName: z.string().optional(),
227
+ /** Dealer-owned blob path for downloaded files — absent on links. */
228
+ blobPath: z.string().optional(),
229
+ contentType: z.string().optional(),
230
+ sizeBytes: z.number().int().nonnegative().optional(),
231
+ sha256: z.string().optional(),
232
+ /** The Linear (possibly expiring) URL for files, the external target for links. */
233
+ url: z.string(),
234
+ subtitle: z.string().optional(),
235
+ source: z.string().optional(),
236
+ });
@@ -2,7 +2,7 @@
2
2
  import { test } from "node:test";
3
3
  import assert from "node:assert/strict";
4
4
  import { LinearCandidate, LinearCandidatesPage, LinearIntakeConfigPatch, LinearIntakeConfigView, LinearIntakeMetadata, } from "./index.js";
5
- import { LinearRepoResolution, extractRepoLabels, normalizeLinearIntakePickerPatch, normalizeMappingLabel, normalizeRepositoryMappings, resolveLinearRepoLabels, resolveLinearRepoWithMappings, } from "./linear-intake.js";
5
+ import { LinearAttachment, LinearRepoResolution, SourceAttachmentRecord, extractRepoLabels, isLinearHostedAttachmentUrl, normalizeLinearIntakePickerPatch, normalizeMappingLabel, normalizeRepositoryMappings, resolveLinearRepoLabels, resolveLinearRepoWithMappings, } from "./linear-intake.js";
6
6
  test("one valid repo: label resolves to the canonical identity", () => {
7
7
  const r = resolveLinearRepoLabels(["agent-dealer", "repo:github.com/not-so-fat/agent-dealer"]);
8
8
  assert.equal(r.status, "resolved");
@@ -187,6 +187,60 @@ test("LinearIntakeConfigView and Patch reject deleted routing fields and empty s
187
187
  const patch = LinearIntakeConfigPatch.parse({ assigneeMe: true, teamId: "t-1" });
188
188
  assert.deepEqual(patch, { assigneeMe: true, teamId: "t-1" });
189
189
  });
190
+ // NOT-364: Linear-hosted files vs external links, and the durable record shape.
191
+ test("isLinearHostedAttachmentUrl distinguishes Linear uploads from external links", () => {
192
+ assert.equal(isLinearHostedAttachmentUrl("https://uploads.linear.app/abc/repro.tar.gz"), true);
193
+ assert.equal(isLinearHostedAttachmentUrl("https://files.uploads.linear.app/x.zip"), true);
194
+ assert.equal(isLinearHostedAttachmentUrl("https://linear.app/issue/attachment/x"), true);
195
+ assert.equal(isLinearHostedAttachmentUrl("https://docs.google.com/document/d/x"), false);
196
+ assert.equal(isLinearHostedAttachmentUrl("https://github.com/acme/app/archive/refs/heads/main.zip"), false);
197
+ assert.equal(isLinearHostedAttachmentUrl("https://evil-linear.app.evil.com/x.tar.gz"), false);
198
+ assert.equal(isLinearHostedAttachmentUrl("not a url"), false);
199
+ });
200
+ test("LinearAttachment and SourceAttachmentRecord parse the attachment contract", () => {
201
+ const file = LinearAttachment.parse({
202
+ id: "att-1",
203
+ title: "repro.tar.gz",
204
+ url: "https://uploads.linear.app/abc/repro.tar.gz",
205
+ });
206
+ assert.equal(file.subtitle, undefined);
207
+ const link = LinearAttachment.parse({
208
+ id: "att-2",
209
+ title: "Design doc",
210
+ url: "https://docs.example.com/x",
211
+ subtitle: "Spec",
212
+ source: "google-docs",
213
+ });
214
+ assert.equal(link.subtitle, "Spec");
215
+ const record = SourceAttachmentRecord.parse({
216
+ linearAttachmentId: "att-1",
217
+ kind: "file",
218
+ title: "repro.tar.gz",
219
+ safeFileName: "repro.tar.gz",
220
+ blobPath: "/data/source-attachments/issue-1/repro.tar.gz",
221
+ contentType: "application/gzip",
222
+ sizeBytes: 12,
223
+ sha256: "abc",
224
+ url: "https://uploads.linear.app/abc/repro.tar.gz",
225
+ });
226
+ assert.equal(record.kind, "file");
227
+ // Attachments stay optional so older candidates still parse.
228
+ const bare = LinearCandidate.parse({
229
+ id: "uuid-1",
230
+ identifier: "NOT-1",
231
+ title: "t",
232
+ url: "https://linear.app/x/issue/NOT-1/t",
233
+ });
234
+ assert.equal(bare.attachments, undefined);
235
+ const withAttachments = LinearCandidate.parse({
236
+ id: "uuid-1",
237
+ identifier: "NOT-1",
238
+ title: "t",
239
+ url: "https://linear.app/x/issue/NOT-1/t",
240
+ attachments: [file, link],
241
+ });
242
+ assert.equal(withAttachments.attachments?.length, 2);
243
+ });
190
244
  test("LinearCandidatesPage and LinearIntakeMetadata parse the picker contracts", () => {
191
245
  const page = LinearCandidatesPage.parse({
192
246
  candidates: [],